Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

- `verifyOfflineLicense(license, options)` (also `KeyMint.verifyOfflineLicense`): verifies a signed offline license (`.lic` file or bare token) locally with `node:crypto`, no new dependencies. Requires the trusted public keys, `productId` and `machineCode`; optional `now` and `clockSkewSeconds` (default 60, max 3600). Returns `OfflineLicense`; failures throw `OfflineLicenseError` with a `code` (`malformed`, `unsupported_algorithm`, `unknown_key`, `bad_signature`, `wrong_type`, `wrong_product`, `wrong_machine`, `expired`). Invalid options throw `TypeError`/`RangeError`.
- Tests against the shared Keymint offline-license test vectors (34 cases).
- `deleteKey(params)`: permanently deletes a license key via `DELETE /key` (use `blockKey` to stop it reversibly).

## 1.6.0 - 2026-10-26

Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ Keymint provides utilities to uniquely identify machines for node-locking:
| `updateKey` | Updates an existing license key. |
| `blockKey` | Blocks a license key. |
| `unblockKey` | Unblocks a previously blocked license key. |
| `deleteKey` | Permanently deletes a license key (use `blockKey` to stop it reversibly). |
| `signKey` | Signs a key for offline validation using an admin API key.|
| `floatingCheckout` | Checks out a floating license seat. |
| `floatingHeartbeat`| Sends a heartbeat to keep a session alive. |
Expand All @@ -78,7 +79,7 @@ Keymint provides utilities to uniquely identify machines for node-locking:
| `getCustomerWithKeys` | Gets a customer along with their license keys. |
| `updateCustomer` | Updates an existing customer's information. |
| `toggleCustomerStatus`| Toggles a customer's active status. |
| `deleteCustomer` | Permanently deletes a customer and their keys. |
| `deleteCustomer` | Deletes a customer. Their license keys are kept; use `deleteKey` to remove them. |

### Webhook Verification

Expand Down
23 changes: 19 additions & 4 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ import {
ActivateKeyParams, ActivateKeyResponse, DeactivateKeyParams,
DeactivateKeyResponse, GetKeyParams, GetKeyResponse,
BlockKeyParams, BlockKeyResponse, UnblockKeyParams,
UnblockKeyResponse, CreateCustomerParams, CreateCustomerResponse,
UnblockKeyResponse, DeleteKeyParams, DeleteKeyResponse,
CreateCustomerParams, CreateCustomerResponse,
GetAllCustomersResponse, GetCustomerWithKeysParams, GetCustomerWithKeysResponse,
UpdateCustomerParams, UpdateCustomerResponse, DeleteCustomerParams, DeleteCustomerResponse,
ToggleCustomerStatusParams, ToggleCustomerStatusResponse, GetCustomerByIdParams, GetCustomerByIdResponse,
Expand Down Expand Up @@ -273,12 +274,13 @@ export class KeyMint {
* @param endpoint - API endpoint
* @param params - Query parameters
* @param options - Custom request options (e.g. idempotency keys)
* @param body - Optional JSON request body
* @returns A promise that resolves with the API response
*/
private async handleDeleteRequest<T>(endpoint: string, params?: Record<string, string>, options?: RequestOptions): Promise<T> {
private async handleDeleteRequest<T>(endpoint: string, params?: Record<string, string>, options?: RequestOptions, body?: object): Promise<T> {
try {
const headers = options?.idempotencyKey ? { "Idempotency-Key": options.idempotencyKey } : undefined;
const response = await this.apiClient.delete<T>(endpoint, { params, headers });
const response = await this.apiClient.delete<T>(endpoint, { params, headers, data: body });
return response.data;
} catch (error) {
throw this.handleError(error as AxiosError<KeyMintApiError>);
Expand Down Expand Up @@ -416,6 +418,19 @@ export class KeyMint {
return this.handleRequest<UnblockKeyResponse>('/key/unblock', params, options);
}

/**
* Permanently deletes a license key (use {@link blockKey} to stop it reversibly).
* @param params - Parameters for deleting the key.
* @param options - Custom request options (e.g. idempotency keys)
* @returns A promise that resolves with the deletion confirmation or rejects with an error.
*/
async deleteKey(params: DeleteKeyParams, options?: RequestOptions): Promise<DeleteKeyResponse> {
return this.handleDeleteRequest<DeleteKeyResponse>('/key', undefined, options, {
productId: params.productId,
licenseKey: params.licenseKey
});
}

/**
* Updates an existing license key.
* @param params - Parameters for updating the key (productId + licenseKey required, all other fields optional).
Expand Down Expand Up @@ -478,7 +493,7 @@ export class KeyMint {
}

/**
* Deletes a customer and all associated license keys permanently.
* Deletes a customer. Their license keys are kept; use {@link deleteKey} to remove them.
* @param params - Parameters for deleting the customer.
* @param options - Custom request options (e.g. idempotency keys)
* @returns A promise that resolves with the deletion confirmation or rejects with an error.
Expand Down
16 changes: 16 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,22 @@ export interface UnblockKeyResponse {
code: number; // API response code (e.g., 0 for success)
}

/**
* Parameters for the deleteKey API endpoint.
*/
export interface DeleteKeyParams {
productId: string; // Required: The unique identifier of the product.
licenseKey: string; // Required: The license key to delete permanently.
}

/**
* Response structure for a successful deleteKey API call.
*/
export interface DeleteKeyResponse {
message: string; // Confirmation message (e.g., "Key deleted")
code: number; // API response code (e.g., 0 for success)
}

/**
* Parameters for the createCustomer API endpoint.
*/
Expand Down
46 changes: 46 additions & 0 deletions tests/contracts.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -115,3 +115,49 @@ test('activateKey tolerates older servers that omit expirationDate', async () =>
assert.equal(result.expirationDate, undefined);
});
});

function readJson(request) {
return new Promise((resolve, reject) => {
let raw = '';
request.on('data', chunk => { raw += chunk; });
request.on('end', () => {
try { resolve({ raw, body: JSON.parse(raw) }); } catch (err) { reject(err); }
});
request.on('error', reject);
});
}

test('deleteKey sends DELETE /key with the license in the JSON body, never the URL', async () => {
const licenseKey = 'secret/license+key';
await withServer(async (request, response) => {
assert.equal(request.method, 'DELETE');
assert.equal(request.url, '/key');
assert.ok(!decodeURIComponent(request.url).includes(licenseKey));
assert.equal(request.headers.authorization, 'Bearer admin_test');
assert.match(request.headers['content-type'], /^application\/json/);
const { body } = await readJson(request);
assert.deepEqual(body, { productId: 'product_123', licenseKey });
response.setHeader('content-type', 'application/json');
response.end(JSON.stringify({ message: 'Key deleted', code: 0 }));
}, async baseUrl => {
const result = await new KeyMint('admin_test', baseUrl).deleteKey({ productId: 'product_123', licenseKey });
assert.deepEqual(result, { message: 'Key deleted', code: 0 });
});
});

test('deleteKey rejects an unknown key with the SDK error shape', async () => {
await withServer((request, response) => {
request.resume();
response.statusCode = 404;
response.setHeader('content-type', 'application/json');
response.end(JSON.stringify({ message: 'Invalid license key', code: 1 }));
}, async baseUrl => {
await assert.rejects(
() => new KeyMint('admin_test', baseUrl).deleteKey({ productId: 'product_123', licenseKey: 'missing' }),
error => {
assert.deepEqual(error, { message: 'Invalid license key', code: 1, status: 404 });
return true;
}
);
});
});
15 changes: 13 additions & 2 deletions tests/live.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -118,9 +118,20 @@ test('current license and customer workflows work end to end', async () => {
assert.equal((await live(() => admin.toggleCustomerStatus({ customerId }))).status, true);
assert.equal((await live(() => admin.deleteCustomer({ customerId }))).status, true);
customerId = undefined;

const deleted = await live(() => admin.deleteKey({ productId, licenseKey: nodeKey }));
assert.equal(deleted.code, 0);
const deletedKey = nodeKey;
nodeKey = undefined;
await assert.rejects(
() => live(() => readOnly.getKey({ productId, licenseKey: deletedKey })),
error => error.status >= 400 && error.status < 500 && error.status !== 429
);
} finally {
// Delete (not just block) what this run created so the shared test workspace
// does not fill up its license limit.
if (customerId) await live(() => admin.deleteCustomer({ customerId })).catch(() => {});
if (nodeKey) await live(() => admin.blockKey({ productId, licenseKey: nodeKey })).catch(() => {});
if (floatingKey) await live(() => admin.blockKey({ productId, licenseKey: floatingKey })).catch(() => {});
if (nodeKey) await live(() => admin.deleteKey({ productId, licenseKey: nodeKey })).catch(() => {});
if (floatingKey) await live(() => admin.deleteKey({ productId, licenseKey: floatingKey })).catch(() => {});
}
});
Loading