@checkbox-site/node 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,57 @@
1
+ // GENERATED by scripts/generate-sdk-types from openapi/checkbox.v1.json (Checkbox API 2026-10-30). DO NOT EDIT.
2
+ // Regenerate with `pnpm generate:sdk` after `pnpm generate:openapi`; packages/sdk-node/src/generated.test.ts fails when stale.
3
+ /* eslint-disable */
4
+ /** The API version these types describe (sent as Checkbox-Version by default). */
5
+ export const API_VERSION = "2026-10-30";
6
+ /** Every API version the server knows, oldest first. */
7
+ export const API_VERSIONS = ["2026-10-30"];
8
+ /** Every webhook event type. */
9
+ export const WEBHOOK_EVENT_TYPES = ["plan.updated", "task.verified", "notice.due", "notice.overdue", "receipt.created", "test_run.completed", "test_run.failed", "site.score_changed", "site.insurable_changed", "finding.created", "finding.resolved", "scan.completed", "scan.failed", "law.changed", "certificate.issued", "certificate.lapsed", "certificate.revoked", "notice.sent", "consent.created", "site.compliant"];
10
+ // ---- Errors ----------------------------------------------------------------------------------------------------
11
+ /** Stable error codes (`error.code`), with HTTP status, type and meaning. Codes are never renamed. */
12
+ export const ERROR_CODES = {
13
+ parameter_invalid: { status: 400, type: "invalid_request_error", description: "A parameter has an invalid value; `param` names it." },
14
+ parameter_missing: { status: 400, type: "invalid_request_error", description: "A required parameter is missing; `param` names it." },
15
+ parameter_unknown: { status: 400, type: "invalid_request_error", description: "The request has a parameter this endpoint does not accept." },
16
+ body_invalid_json: { status: 400, type: "invalid_request_error", description: "The request body is not valid JSON." },
17
+ url_not_allowed: { status: 400, type: "invalid_request_error", description: "A URL points to a private, local or otherwise unreachable address." },
18
+ api_version_invalid: { status: 400, type: "invalid_request_error", description: "The Checkbox-Version header names an unknown API version." },
19
+ idempotency_key_invalid: { status: 400, type: "idempotency_error", description: "The Idempotency-Key header is empty or longer than 255 characters." },
20
+ idempotency_key_reused: { status: 400, type: "idempotency_error", description: "The Idempotency-Key was already used with different parameters within 24 hours." },
21
+ test_key_missing: { status: 400, type: "invalid_request_error", description: "A Stripe test run needs a Stripe TEST-mode key stored for the site (createTestCredential)." },
22
+ test_key_not_test_mode: { status: 400, type: "invalid_request_error", description: "The billing key is a live-mode (or malformed) key; test runs only accept test-mode keys (rk_test_… / sk_test_…)." },
23
+ test_key_rejected: { status: 400, type: "invalid_request_error", description: "Stripe rejected the test-mode key or it lacks a permission test runs need; `message` names it." },
24
+ authorization_pending: { status: 400, type: "invalid_request_error", description: "Device login: the human has not approved yet. Keep polling createDeviceToken every `interval` seconds." },
25
+ slow_down: { status: 400, type: "rate_limit_error", description: "Device login: polling too fast. Add 5 seconds to the interval and keep polling." },
26
+ expired_token: { status: 400, type: "invalid_request_error", description: "Device login: the device code expired (15 minutes) or was already used. Start again with createDeviceAuthorization." },
27
+ device_code_invalid: { status: 400, type: "invalid_request_error", description: "Device login: the device code is unknown." },
28
+ access_denied: { status: 403, type: "permission_error", description: "Device login: the human clicked Deny. Do not retry without asking them." },
29
+ api_key_missing: { status: 401, type: "authentication_error", description: "No API key was provided (Authorization: Bearer ck_…)." },
30
+ api_key_invalid: { status: 401, type: "authentication_error", description: "The API key is unknown, revoked or expired." },
31
+ publishable_key_not_allowed: { status: 403, type: "permission_error", description: "A publishable key (pk_…) cannot call this endpoint; use a secret key on your server." },
32
+ origin_not_allowed: { status: 403, type: "permission_error", description: "A publishable-key request came from a page (Origin) that is not on one of the site's domains (base URL or checkout URL)." },
33
+ insufficient_scope: { status: 403, type: "permission_error", description: "The restricted key lacks the scope this endpoint needs." },
34
+ insufficient_role: { status: 403, type: "permission_error", description: "The key (or the person who created it) does not have a role that allows this action." },
35
+ account_not_activated: { status: 403, type: "permission_error", description: "Live mode needs an activated account: verify the domain of one of your sites (or use a test key)." },
36
+ test_mode_domain_unverified: { status: 403, type: "permission_error", description: "Test mode only scans, monitors, fetches and test-runs localhost or domains the account has verified; verify the domain (createDomainVerification) or send the HTML instead." },
37
+ test_mode_scan_budget_exhausted: { status: 429, type: "rate_limit_error", description: "No longer returned (round 6): test mode has no scan budget for unverified domains any more, they are refused with test_mode_domain_unverified. Kept so older clients still recognise the code." },
38
+ domain_not_verified: { status: 403, type: "permission_error", description: "Checkbox only scans, monitors, fetches and test-runs websites the account has verified (DNS TXT record or /.well-known file): every target host (site, checkout, account and policy URLs) must be on a verified domain. `next_action` says which domain to verify." },
39
+ account_suspended: { status: 403, type: "permission_error", description: "The account was suspended by Checkbox staff; contact support." },
40
+ resource_missing: { status: 404, type: "invalid_request_error", description: "No object with this id exists in this mode of the account." },
41
+ route_not_found: { status: 404, type: "invalid_request_error", description: "No such endpoint. See /v1/openapi.json for the list." },
42
+ resource_conflict: { status: 409, type: "invalid_request_error", description: "The object is in a state that does not allow this action." },
43
+ scan_already_queued: { status: 409, type: "invalid_request_error", description: "A scan of this site is already queued or running." },
44
+ consent_reference_locked: { status: 409, type: "invalid_request_error", description: "The consent already carries a different customer_reference; references are set once and never changed." },
45
+ test_run_in_progress: { status: 409, type: "invalid_request_error", description: "The site already has a queued or running test run; wait for it (retrieveTestRun) or cancel it (cancelTestRun)." },
46
+ idempotency_key_in_use: { status: 409, type: "idempotency_error", description: "A request with this Idempotency-Key is still being processed; retry shortly." },
47
+ resource_archived: { status: 410, type: "invalid_request_error", description: "The object is archived; restore it before changing it." },
48
+ body_too_large: { status: 413, type: "invalid_request_error", description: "The request body is too large." },
49
+ content_type_unsupported: { status: 415, type: "invalid_request_error", description: "Send JSON bodies with Content-Type: application/json." },
50
+ site_limit_reached: { status: 403, type: "invalid_request_error", description: "The account reached its fair-use site limit (default 25); contact support to raise it." },
51
+ webhook_endpoint_limit_reached: { status: 403, type: "invalid_request_error", description: "The account reached the maximum number of webhook endpoints (16 per mode)." },
52
+ plan_required: { status: 402, type: "invalid_request_error", description: "Only when billing is enabled on this deployment: the feature needs a paid plan." },
53
+ rate_limit_exceeded: { status: 429, type: "rate_limit_error", description: "Too many requests; slow down and retry after the Retry-After header." },
54
+ receipts_disabled: { status: 403, type: "permission_error", description: "Consent receipts are turned off for this account (updateReceiptSettings enabled=true turns them on)." },
55
+ feature_unavailable: { status: 503, type: "api_error", description: "The feature is not configured on this deployment (for example the database migration is missing)." },
56
+ internal_error: { status: 500, type: "api_error", description: "Something went wrong on Checkbox's side. Retrying with the same Idempotency-Key is safe." },
57
+ };
@@ -0,0 +1,10 @@
1
+ import { Checkbox } from './client.ts';
2
+ export { Checkbox, modeOfKey, getLastResponse, backoff, DEFAULT_BASE_URL, DEFAULT_MAX_NETWORK_RETRIES, DEFAULT_TIMEOUT_MS, type CheckboxConfig, type Mode } from './client.ts';
3
+ export { CheckboxError, CheckboxInvalidRequestError, CheckboxAuthenticationError, CheckboxPermissionError, CheckboxRateLimitError, CheckboxIdempotencyError, CheckboxAPIError, CheckboxConnectionError, CheckboxSignatureVerificationError, type CheckboxErrorType, type CheckboxErrorRaw, type NextAction, } from './errors.ts';
4
+ export { Webhooks, webhooks, SIGNATURE_HEADER, DEFAULT_TOLERANCE, parseSignatureHeader, computeSignature, type WebhookPayload, type WebhookHeader, type VerifyOptions } from './webhooks.ts';
5
+ export { Resource, MAX_AUTO_PAGING, type ApiListPromise, type ListObject, type RequestOptions, type ResponseMeta } from './resource.ts';
6
+ export { OPERATIONS, RESOURCES, type OperationId, type OperationSpec, type OperationTypes } from './generated/operations.ts';
7
+ export { SDK_VERSION } from './version.ts';
8
+ export * from './generated/types.ts';
9
+ export type * as Types from './generated/types.ts';
10
+ export default Checkbox;
package/dist/index.js ADDED
@@ -0,0 +1,15 @@
1
+ // Checkbox Node SDK. Public surface:
2
+ // import Checkbox from '@checkbox-site/node'; // or: import { Checkbox } from '@checkbox-site/node';
3
+ // const checkbox = new Checkbox(process.env.CHECKBOX_API_KEY);
4
+ // await checkbox.checks.stripeCheckoutSession({ params });
5
+ // const event = checkbox.webhooks.constructEvent(rawBody, req.headers['checkbox-signature'], secret);
6
+ // Published to npm as @checkbox-site/node.
7
+ import { Checkbox } from "./client.js";
8
+ export { Checkbox, modeOfKey, getLastResponse, backoff, DEFAULT_BASE_URL, DEFAULT_MAX_NETWORK_RETRIES, DEFAULT_TIMEOUT_MS } from "./client.js";
9
+ export { CheckboxError, CheckboxInvalidRequestError, CheckboxAuthenticationError, CheckboxPermissionError, CheckboxRateLimitError, CheckboxIdempotencyError, CheckboxAPIError, CheckboxConnectionError, CheckboxSignatureVerificationError, } from "./errors.js";
10
+ export { Webhooks, webhooks, SIGNATURE_HEADER, DEFAULT_TOLERANCE, parseSignatureHeader, computeSignature } from "./webhooks.js";
11
+ export { Resource, MAX_AUTO_PAGING } from "./resource.js";
12
+ export { OPERATIONS, RESOURCES } from "./generated/operations.js";
13
+ export { SDK_VERSION } from "./version.js";
14
+ export * from "./generated/types.js";
15
+ export default Checkbox;
@@ -0,0 +1,59 @@
1
+ import type { OperationSpec } from './generated/operations.ts';
2
+ /** Per-request options (last argument of every method). */
3
+ export interface RequestOptions {
4
+ /** Idempotency-Key for this POST. Generated automatically (and reused across retries) when omitted. */
5
+ idempotencyKey?: string;
6
+ /** Checkbox-Version for this request (defaults to the client's). */
7
+ apiVersion?: string;
8
+ /** Use another API key for this request (e.g. a live key next to a test-mode client). */
9
+ apiKey?: string;
10
+ /** Timeout in milliseconds for each attempt (defaults to the client's, 80 s). */
11
+ timeout?: number;
12
+ /** Retries on network errors, 409 idempotency_key_in_use, 429 and 5xx (defaults to the client's, 2). */
13
+ maxNetworkRetries?: number;
14
+ /** Extra request headers. */
15
+ headers?: Record<string, string>;
16
+ /** Abort the request (and its retries). */
17
+ signal?: AbortSignal;
18
+ }
19
+ /** A page of a list (`{ object: 'list', data, has_more, url }`). */
20
+ export interface ListObject<T> {
21
+ object: 'list';
22
+ data: T[];
23
+ has_more: boolean;
24
+ url: string;
25
+ }
26
+ /**
27
+ * Result of a paginated `list()`: await it for one page, or iterate it with `for await` to walk every object across
28
+ * pages (cursor pagination with `starting_after`, or `ending_before` when you started with it).
29
+ */
30
+ export interface ApiListPromise<T> extends Promise<ListObject<T>>, AsyncIterable<T> {
31
+ /** Calls `fn` for every object across pages; return `false` from `fn` to stop early. */
32
+ autoPagingEach(fn: (item: T) => void | boolean | Promise<void | boolean>): Promise<void>;
33
+ /** Collects objects across pages, at most `limit` (required; max 10 000). */
34
+ autoPagingToArray(opts: {
35
+ limit: number;
36
+ }): Promise<T[]>;
37
+ }
38
+ /** Response metadata, attached (non-enumerable) to every returned object as `lastResponse`. */
39
+ export interface ResponseMeta {
40
+ statusCode: number;
41
+ requestId: string | undefined;
42
+ apiVersion: string | undefined;
43
+ idempotencyKey: string | undefined;
44
+ /** True when the response replayed an earlier request with the same Idempotency-Key. */
45
+ idempotentReplayed: boolean;
46
+ headers: Record<string, string>;
47
+ }
48
+ /** What a resource needs from the client. */
49
+ export interface RequestSender {
50
+ _send(op: OperationSpec, pathArgs: readonly unknown[], params: Record<string, unknown> | undefined, options: RequestOptions | undefined): Promise<unknown>;
51
+ }
52
+ export declare abstract class Resource {
53
+ protected readonly _client: RequestSender;
54
+ constructor(_client: RequestSender);
55
+ protected _request(op: OperationSpec, pathArgs: readonly unknown[], params: object | undefined, options: RequestOptions | undefined): Promise<unknown>;
56
+ protected _list<T>(op: OperationSpec, pathArgs: readonly unknown[], params: object | undefined, options: RequestOptions | undefined): ApiListPromise<T>;
57
+ }
58
+ export declare const MAX_AUTO_PAGING = 10000;
59
+ export declare function listPromise<T>(client: RequestSender, op: OperationSpec, pathArgs: readonly unknown[], params: Record<string, unknown>, options: RequestOptions | undefined): ApiListPromise<T>;
@@ -0,0 +1,63 @@
1
+ export class Resource {
2
+ _client;
3
+ constructor(_client) {
4
+ this._client = _client;
5
+ }
6
+ _request(op, pathArgs, params, options) {
7
+ return this._client._send(op, pathArgs, params, options);
8
+ }
9
+ _list(op, pathArgs, params, options) {
10
+ return listPromise(this._client, op, pathArgs, (params ?? {}), options);
11
+ }
12
+ }
13
+ export const MAX_AUTO_PAGING = 10_000;
14
+ export function listPromise(client, op, pathArgs, params, options) {
15
+ const fetchPage = (p) => client._send(op, pathArgs, p, options);
16
+ const first = fetchPage(params);
17
+ const backwards = params.ending_before !== undefined && params.starting_after === undefined;
18
+ async function* iterate() {
19
+ let page = await first;
20
+ for (;;) {
21
+ const items = backwards ? [...page.data].reverse() : page.data;
22
+ for (const item of items)
23
+ yield item;
24
+ if (!page.has_more || !page.data.length)
25
+ return;
26
+ const cursor = (backwards ? page.data[0] : page.data[page.data.length - 1]);
27
+ if (typeof cursor?.id !== 'string')
28
+ return;
29
+ const { starting_after: _s, ending_before: _e, ...rest } = params;
30
+ page = await fetchPage(backwards ? { ...rest, ending_before: cursor.id } : { ...rest, starting_after: cursor.id });
31
+ }
32
+ }
33
+ const promise = first;
34
+ // Keep `then` on the original promise; add the iteration helpers.
35
+ Object.defineProperties(promise, {
36
+ [Symbol.asyncIterator]: { value: () => iterate(), enumerable: false },
37
+ autoPagingEach: {
38
+ enumerable: false,
39
+ value: async (fn) => {
40
+ for await (const item of iterate())
41
+ if ((await fn(item)) === false)
42
+ return;
43
+ },
44
+ },
45
+ autoPagingToArray: {
46
+ enumerable: false,
47
+ value: async (opts) => {
48
+ const limit = opts?.limit;
49
+ if (!Number.isInteger(limit) || limit < 1 || limit > MAX_AUTO_PAGING) {
50
+ throw new RangeError(`autoPagingToArray needs { limit } between 1 and ${MAX_AUTO_PAGING}.`);
51
+ }
52
+ const out = [];
53
+ for await (const item of iterate()) {
54
+ out.push(item);
55
+ if (out.length >= limit)
56
+ break;
57
+ }
58
+ return out;
59
+ },
60
+ },
61
+ });
62
+ return promise;
63
+ }
@@ -0,0 +1,2 @@
1
+ /** Version of this SDK (kept equal to packages/sdk-node/package.json "version"; a test checks it). */
2
+ export declare const SDK_VERSION = "0.1.0";
@@ -0,0 +1,2 @@
1
+ /** Version of this SDK (kept equal to packages/sdk-node/package.json "version"; a test checks it). */
2
+ export const SDK_VERSION = '0.1.0';
@@ -0,0 +1,40 @@
1
+ import type { WebhookEvent } from './generated/types.ts';
2
+ export declare const SIGNATURE_HEADER = "Checkbox-Signature";
3
+ /** Default maximum age of a signature, in seconds (replay protection). */
4
+ export declare const DEFAULT_TOLERANCE = 300;
5
+ export type WebhookPayload = string | Uint8Array;
6
+ export type WebhookHeader = string | string[] | undefined | null;
7
+ export interface ParsedSignatureHeader {
8
+ timestamp: number;
9
+ signatures: string[];
10
+ }
11
+ export declare function parseSignatureHeader(header: string | undefined): ParsedSignatureHeader | null;
12
+ export declare function computeSignature(payload: string, secret: string, timestamp: number): string;
13
+ export interface VerifyOptions {
14
+ /** Maximum signature age in seconds (default 300). 0 disables the check (not recommended). */
15
+ tolerance?: number;
16
+ /** Current time in unix seconds (tests). */
17
+ now?: number;
18
+ }
19
+ export declare class Webhooks {
20
+ /**
21
+ * Verifies `Checkbox-Signature` and returns the parsed event. Throws CheckboxSignatureVerificationError when the
22
+ * signature does not match or is older than `tolerance` seconds.
23
+ *
24
+ * @param payload the RAW request body (string or Buffer)
25
+ * @param header the Checkbox-Signature header
26
+ * @param secret the endpoint secret (whsec_…)
27
+ */
28
+ constructEvent(payload: WebhookPayload, header: WebhookHeader, secret: string, tolerance?: number | VerifyOptions): WebhookEvent;
29
+ /** Same as constructEvent, using WebCrypto (edge runtimes: Vercel Edge, Cloudflare Workers, Deno). */
30
+ constructEventAsync(payload: WebhookPayload, header: WebhookHeader, secret: string, tolerance?: number | VerifyOptions): Promise<WebhookEvent>;
31
+ /** True when the signature is valid (no exception). */
32
+ verifySignature(payload: WebhookPayload, header: WebhookHeader, secret: string, opts?: VerifyOptions): boolean;
33
+ /** Builds a valid Checkbox-Signature header for a payload (for your tests). */
34
+ generateTestHeaderString(opts: {
35
+ payload: string;
36
+ secret: string;
37
+ timestamp?: number;
38
+ }): string;
39
+ }
40
+ export declare const webhooks: Webhooks;
@@ -0,0 +1,129 @@
1
+ // Webhook signature verification. Checkbox signs every delivery with
2
+ // Checkbox-Signature: t=<unix seconds>,v1=<hex HMAC-SHA256(endpoint secret, "<t>.<raw body>")>
3
+ // (several v1 entries during a secret roll). Verify against the RAW request body: re-serialised JSON will not match.
4
+ import { createHmac } from 'node:crypto';
5
+ import { CheckboxSignatureVerificationError } from "./errors.js";
6
+ export const SIGNATURE_HEADER = 'Checkbox-Signature';
7
+ /** Default maximum age of a signature, in seconds (replay protection). */
8
+ export const DEFAULT_TOLERANCE = 300;
9
+ const decoder = new TextDecoder();
10
+ function payloadText(payload) {
11
+ if (typeof payload === 'string')
12
+ return payload;
13
+ if (payload instanceof Uint8Array)
14
+ return decoder.decode(payload);
15
+ throw new CheckboxSignatureVerificationError('Webhook payload must be the raw request body as a string or Buffer (not parsed JSON). In Express use express.raw({ type: "application/json" }); in Next.js use await request.text().', undefined, String(payload));
16
+ }
17
+ function headerText(header) {
18
+ if (Array.isArray(header))
19
+ return header[0];
20
+ return header ?? undefined;
21
+ }
22
+ export function parseSignatureHeader(header) {
23
+ if (!header || header.length > 2000)
24
+ return null;
25
+ let timestamp = null;
26
+ const signatures = [];
27
+ for (const part of header.split(',')) {
28
+ const i = part.indexOf('=');
29
+ if (i < 0)
30
+ continue;
31
+ const k = part.slice(0, i).trim();
32
+ const v = part.slice(i + 1).trim();
33
+ if (k === 't' && /^\d{1,12}$/.test(v))
34
+ timestamp = Number(v);
35
+ else if (k === 'v1' && /^[0-9a-f]{64}$/i.test(v))
36
+ signatures.push(v.toLowerCase());
37
+ }
38
+ return timestamp === null || !signatures.length ? null : { timestamp, signatures };
39
+ }
40
+ export function computeSignature(payload, secret, timestamp) {
41
+ return createHmac('sha256', secret).update(`${timestamp}.${payload}`, 'utf8').digest('hex');
42
+ }
43
+ /** Constant-time comparison of two hex strings (no Buffer, so it also runs on edge runtimes). */
44
+ function safeEqual(a, b) {
45
+ if (a.length !== b.length || !a.length)
46
+ return false;
47
+ let diff = 0;
48
+ for (let i = 0; i < a.length; i++)
49
+ diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
50
+ return diff === 0;
51
+ }
52
+ function check(payload, header, secret, opts, hmac) {
53
+ if (!secret)
54
+ throw new CheckboxSignatureVerificationError('No webhook secret given. Use the endpoint secret (whsec_…) from createWebhookEndpoint.', header, payload);
55
+ const parsed = parseSignatureHeader(header);
56
+ if (!parsed) {
57
+ throw new CheckboxSignatureVerificationError(`Missing or malformed ${SIGNATURE_HEADER} header (expected "t=<timestamp>,v1=<signature>").`, header, payload);
58
+ }
59
+ const expected = hmac(parsed.timestamp);
60
+ const match = parsed.signatures.some((s) => safeEqual(s, expected));
61
+ if (!match) {
62
+ throw new CheckboxSignatureVerificationError('No signature matches the payload. Check that you pass the raw request body (not parsed JSON) and the secret of this endpoint.', header, payload);
63
+ }
64
+ const tolerance = opts.tolerance ?? DEFAULT_TOLERANCE;
65
+ const now = opts.now ?? Math.floor(Date.now() / 1000);
66
+ if (tolerance > 0 && Math.abs(now - parsed.timestamp) > tolerance) {
67
+ throw new CheckboxSignatureVerificationError(`Signature timestamp is outside the tolerance of ${tolerance} seconds (possible replay).`, header, payload);
68
+ }
69
+ }
70
+ function parseEvent(text, header) {
71
+ try {
72
+ return JSON.parse(text);
73
+ }
74
+ catch {
75
+ throw new CheckboxSignatureVerificationError('The payload is not valid JSON.', header, text);
76
+ }
77
+ }
78
+ export class Webhooks {
79
+ /**
80
+ * Verifies `Checkbox-Signature` and returns the parsed event. Throws CheckboxSignatureVerificationError when the
81
+ * signature does not match or is older than `tolerance` seconds.
82
+ *
83
+ * @param payload the RAW request body (string or Buffer)
84
+ * @param header the Checkbox-Signature header
85
+ * @param secret the endpoint secret (whsec_…)
86
+ */
87
+ constructEvent(payload, header, secret, tolerance) {
88
+ const text = payloadText(payload);
89
+ const h = headerText(header);
90
+ const opts = typeof tolerance === 'number' ? { tolerance } : (tolerance ?? {});
91
+ check(text, h, secret, opts, (t) => computeSignature(text, secret, t));
92
+ return parseEvent(text, h);
93
+ }
94
+ /** Same as constructEvent, using WebCrypto (edge runtimes: Vercel Edge, Cloudflare Workers, Deno). */
95
+ async constructEventAsync(payload, header, secret, tolerance) {
96
+ const text = payloadText(payload);
97
+ const h = headerText(header);
98
+ const opts = typeof tolerance === 'number' ? { tolerance } : (tolerance ?? {});
99
+ const parsed = parseSignatureHeader(h);
100
+ let hex = '';
101
+ if (parsed && secret) {
102
+ const enc = new TextEncoder();
103
+ const key = await globalThis.crypto.subtle.importKey('raw', enc.encode(secret), { name: 'HMAC', hash: 'SHA-256' }, false, ['sign']);
104
+ const sig = new Uint8Array(await globalThis.crypto.subtle.sign('HMAC', key, enc.encode(`${parsed.timestamp}.${text}`)));
105
+ hex = [...sig].map((b) => b.toString(16).padStart(2, '0')).join('');
106
+ }
107
+ check(text, h, secret, opts, () => hex);
108
+ return parseEvent(text, h);
109
+ }
110
+ /** True when the signature is valid (no exception). */
111
+ verifySignature(payload, header, secret, opts = {}) {
112
+ try {
113
+ const text = payloadText(payload);
114
+ check(text, headerText(header), secret, opts, (t) => computeSignature(text, secret, t));
115
+ return true;
116
+ }
117
+ catch (err) {
118
+ if (err instanceof CheckboxSignatureVerificationError)
119
+ return false;
120
+ throw err;
121
+ }
122
+ }
123
+ /** Builds a valid Checkbox-Signature header for a payload (for your tests). */
124
+ generateTestHeaderString(opts) {
125
+ const t = opts.timestamp ?? Math.floor(Date.now() / 1000);
126
+ return `t=${t},v1=${computeSignature(opts.payload, opts.secret, t)}`;
127
+ }
128
+ }
129
+ export const webhooks = new Webhooks();
package/package.json CHANGED
@@ -1,6 +1,59 @@
1
1
  {
2
2
  "name": "@checkbox-site/node",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0",
4
+ "description": "Checkbox Node.js SDK: automatic-renewal-law compliance (ROSCA, California ARL, New York and other state laws) for subscription businesses. Zero dependencies. Published to npm as @checkbox-site/node.",
5
+ "keywords": [
6
+ "subscription",
7
+ "auto-renewal",
8
+ "automatic-renewal",
9
+ "compliance",
10
+ "rosca",
11
+ "click-to-cancel",
12
+ "california-arl",
13
+ "stripe",
14
+ "stripe-checkout",
15
+ "checkout",
16
+ "cancellation"
17
+ ],
18
+ "license": "MIT",
19
+ "author": "Checkbox <support@checkbox.site>",
20
+ "homepage": "https://checkbox.site/docs",
21
+ "repository": {
22
+ "type": "git",
23
+ "url": "git+https://github.com/checkbox-site/checkbox.git",
24
+ "directory": "node"
25
+ },
26
+ "bugs": {
27
+ "url": "https://github.com/checkbox-site/checkbox/issues",
28
+ "email": "support@checkbox.site"
29
+ },
30
+ "type": "module",
31
+ "engines": {
32
+ "node": ">=20"
33
+ },
34
+ "main": "./dist/index.js",
35
+ "types": "./dist/index.d.ts",
36
+ "exports": {
37
+ ".": {
38
+ "types": "./dist/index.d.ts",
39
+ "import": "./dist/index.js",
40
+ "default": "./dist/index.js"
41
+ },
42
+ "./package.json": "./package.json"
43
+ },
44
+ "files": [
45
+ "dist",
46
+ "README.md",
47
+ "LICENSE"
48
+ ],
49
+ "publishConfig": {
50
+ "access": "public",
51
+ "provenance": true
52
+ },
53
+ "sideEffects": false,
54
+ "scripts": {
55
+ "clean": "node -e \"require('node:fs').rmSync('dist', { recursive: true, force: true })\"",
56
+ "build": "npm run clean && tsc -p tsconfig.build.json",
57
+ "prepack": "npm run build"
58
+ }
59
+ }