@cboxdk/id-js 0.17.0 → 0.18.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.
- package/README.md +218 -2
- package/dist/{chunk-WYTGLCMF.js → chunk-2E25IPBA.js} +4 -3
- package/dist/chunk-2E25IPBA.js.map +1 -0
- package/dist/{chunk-G3CD7X4U.js → chunk-CXGKOJRJ.js} +57 -4
- package/dist/chunk-CXGKOJRJ.js.map +1 -0
- package/dist/chunk-QQW63PGU.js +25 -0
- package/dist/chunk-QQW63PGU.js.map +1 -0
- package/dist/{chunk-ASOQN47M.js → chunk-SEUD4TE4.js} +3 -23
- package/dist/chunk-SEUD4TE4.js.map +1 -0
- package/dist/{client-CdwdigzG.d.ts → client-BSZASrMo.d.ts} +29 -1
- package/dist/{client-CEXddiCR.d.cts → client-BxUHm5hX.d.cts} +29 -1
- package/dist/errors-D61xljZE.d.cts +87 -0
- package/dist/errors-D61xljZE.d.ts +87 -0
- package/dist/index.cjs +56 -3
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +3 -88
- package/dist/index.d.ts +3 -88
- package/dist/index.js +5 -4
- package/dist/index.js.map +1 -1
- package/dist/management.cjs +4372 -0
- package/dist/management.cjs.map +1 -0
- package/dist/management.d.cts +10923 -0
- package/dist/management.d.ts +10923 -0
- package/dist/management.js +4335 -0
- package/dist/management.js.map +1 -0
- package/dist/nextjs.cjs.map +1 -1
- package/dist/nextjs.d.cts +1 -1
- package/dist/nextjs.d.ts +1 -1
- package/dist/nextjs.js +4 -3
- package/dist/nextjs.js.map +1 -1
- package/dist/server.js +3 -2
- package/package.json +9 -2
- package/dist/chunk-ASOQN47M.js.map +0 -1
- package/dist/chunk-G3CD7X4U.js.map +0 -1
- package/dist/chunk-WYTGLCMF.js.map +0 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/issuer.ts"],"names":[],"mappings":";;;AAaO,SAAS,mBAAmB,MAAA,EAAsB;AACvD,EAAA,IAAI,GAAA;AAEJ,EAAA,IAAI;AACF,IAAA,GAAA,GAAM,IAAI,IAAI,MAAM,CAAA;AAAA,EACtB,CAAA,CAAA,MAAQ;AACN,IAAA,MAAM,IAAI,mBAAmB,6CAA6C,CAAA;AAAA,EAC5E;AAEA,EAAA,IAAI,GAAA,CAAI,aAAa,QAAA,EAAU;AAC7B,IAAA;AAAA,EACF;AAEA,EAAA,MAAM,QAAA,GAAW,GAAA,CAAI,QAAA,KAAa,WAAA,IAAe,GAAA,CAAI,QAAA,KAAa,WAAA,IAAe,GAAA,CAAI,QAAA,KAAa,OAAA,IAAW,GAAA,CAAI,QAAA,KAAa,KAAA;AAE9H,EAAA,IAAI,GAAA,CAAI,QAAA,KAAa,OAAA,IAAW,QAAA,EAAU;AACxC,IAAA;AAAA,EACF;AAEA,EAAA,MAAM,IAAI,kBAAA;AAAA,IACR,CAAA,6CAAA,EAAgD,GAAA,CAAI,QAAQ,CAAA,EAAA,EAAK,IAAI,QAAQ,CAAA,EAAA;AAAA,GAC/E;AACF","file":"chunk-QQW63PGU.js","sourcesContent":["import { ConfigurationError } from './errors.js';\n\n/**\n * An issuer must be HTTPS.\n *\n * Everything this SDK does with it carries a credential: the authorization code, the PKCE\n * verifier, the client secret, the refresh token. Over `http://` a network attacker reads\n * all of them — and, worse, replaces the discovery document and JWKS, at which point a\n * forged id_token verifies cleanly and the whole verification chain proves nothing.\n *\n * Loopback is allowed because a native app's own callback listener is loopback by\n * definition (RFC 8252) and a dev instance runs there.\n */\nexport function assertSecureIssuer(issuer: string): void {\n let url: URL;\n\n try {\n url = new URL(issuer);\n } catch {\n throw new ConfigurationError('Cbox ID config `issuer` is not a valid URL.');\n }\n\n if (url.protocol === 'https:') {\n return;\n }\n\n const loopback = url.hostname === 'localhost' || url.hostname === '127.0.0.1' || url.hostname === '[::1]' || url.hostname === '::1';\n\n if (url.protocol === 'http:' && loopback) {\n return;\n }\n\n throw new ConfigurationError(\n `Cbox ID config \\`issuer\\` must be https (got ${url.protocol}//${url.hostname}).`,\n );\n}\n"]}
|
|
@@ -56,26 +56,6 @@ var FrontendApiError = class extends CboxIdError {
|
|
|
56
56
|
retryAfter;
|
|
57
57
|
};
|
|
58
58
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
try {
|
|
63
|
-
url = new URL(issuer);
|
|
64
|
-
} catch {
|
|
65
|
-
throw new ConfigurationError("Cbox ID config `issuer` is not a valid URL.");
|
|
66
|
-
}
|
|
67
|
-
if (url.protocol === "https:") {
|
|
68
|
-
return;
|
|
69
|
-
}
|
|
70
|
-
const loopback = url.hostname === "localhost" || url.hostname === "127.0.0.1" || url.hostname === "[::1]" || url.hostname === "::1";
|
|
71
|
-
if (url.protocol === "http:" && loopback) {
|
|
72
|
-
return;
|
|
73
|
-
}
|
|
74
|
-
throw new ConfigurationError(
|
|
75
|
-
`Cbox ID config \`issuer\` must be https (got ${url.protocol}//${url.hostname}).`
|
|
76
|
-
);
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
export { AuthenticationError, CboxIdError, ConfigurationError, FrontendApiError, InvalidStateError, assertSecureIssuer, oauthError };
|
|
80
|
-
//# sourceMappingURL=chunk-ASOQN47M.js.map
|
|
81
|
-
//# sourceMappingURL=chunk-ASOQN47M.js.map
|
|
59
|
+
export { AuthenticationError, CboxIdError, ConfigurationError, FrontendApiError, InvalidStateError, oauthError };
|
|
60
|
+
//# sourceMappingURL=chunk-SEUD4TE4.js.map
|
|
61
|
+
//# sourceMappingURL=chunk-SEUD4TE4.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/errors.ts"],"names":[],"mappings":";AAIO,IAAM,WAAA,GAAN,cAA0B,KAAA,CAAM;AAAA,EACrC,YAAY,OAAA,EAAiB;AAC3B,IAAA,KAAA,CAAM,OAAO,CAAA;AACb,IAAA,IAAA,CAAK,OAAO,GAAA,CAAA,MAAA,CAAW,IAAA;AAAA,EACzB;AACF;AAGO,IAAM,kBAAA,GAAN,cAAiC,WAAA,CAAY;AAAC;AAM9C,IAAM,iBAAA,GAAN,cAAgC,WAAA,CAAY;AAAC;AAa7C,IAAM,mBAAA,GAAN,cAAkC,WAAA,CAAY;AAAA,EACnD,WAAA,CACE,OAAA,EAES,KAAA,EAEA,gBAAA,EAEA,QAUA,UAAA,EACT;AACA,IAAA,KAAA,CAAM,OAAO,CAAA;AAhBJ,IAAA,IAAA,CAAA,KAAA,GAAA,KAAA;AAEA,IAAA,IAAA,CAAA,gBAAA,GAAA,gBAAA;AAEA,IAAA,IAAA,CAAA,MAAA,GAAA,MAAA;AAUA,IAAA,IAAA,CAAA,UAAA,GAAA,UAAA;AAAA,EAGX;AAAA,EAjBW,KAAA;AAAA,EAEA,gBAAA;AAAA,EAEA,MAAA;AAAA,EAUA,UAAA;AAAA;AAAA,EAMX,IAAI,aAAA,GAAyB;AAC3B,IAAA,OAAO,KAAK,MAAA,KAAW,GAAA;AAAA,EACzB;AACF;AAUA,eAAsB,UAAA,CAAW,UAAoB,QAAA,EAAgD;AACnG,EAAA,MAAM,OAAO,MAAM,QAAA,CAAS,MAAK,CAAE,KAAA,CAAM,MAAM,EAAE,CAAA;AAEjD,EAAA,IAAI,IAAA;AACJ,EAAA,IAAI,WAAA;AAEJ,EAAA,IAAI;AACF,IAAA,MAAM,MAAA,GAAkB,IAAA,CAAK,KAAA,CAAM,IAAI,CAAA;AAEvC,IAAA,IAAI,OAAO,MAAA,KAAW,QAAA,IAAY,MAAA,KAAW,IAAA,EAAM;AACjD,MAAA,MAAM,EAAE,KAAA,EAAO,iBAAA,EAAmB,gBAAA,EAAiB,GAAI,MAAA;AACvD,MAAA,IAAA,GAAO,OAAO,KAAA,KAAU,QAAA,GAAW,KAAA,GAAQ,KAAA,CAAA;AAC3C,MAAA,WAAA,GAAc,OAAO,gBAAA,KAAqB,QAAA,GAAW,gBAAA,GAAmB,KAAA,CAAA;AAAA,IAC1E;AAAA,EACF,CAAA,CAAA,MAAQ;AAAA,EAER;AAOA,EAAA,MAAM,MAAA,GAAS,IAAA,IAAQ,CAAA,KAAA,EAAQ,QAAA,CAAS,MAAM,CAAA,CAAA;AAM9C,EAAA,MAAM,gBAAA,GAAmB,QAAA,CAAS,OAAA,CAAQ,GAAA,CAAI,aAAa,CAAA;AAC3D,EAAA,MAAM,UAAA,GAAa,gBAAA,KAAqB,IAAA,IAAQ,OAAA,CAAQ,IAAA,CAAK,gBAAA,CAAiB,IAAA,EAAM,CAAA,GAChF,MAAA,CAAO,gBAAA,CAAiB,IAAA,EAAM,CAAA,GAC9B,MAAA;AAEJ,EAAA,OAAO,IAAI,mBAAA,CAAoB,CAAA,EAAG,QAAQ,CAAA,EAAA,EAAK,MAAM,CAAA,CAAA,EAAI,IAAA,EAAM,WAAA,EAAa,QAAA,CAAS,MAAA,EAAQ,UAAU,CAAA;AACzG;AAiBO,IAAM,gBAAA,GAAN,cAA+B,WAAA,CAAY;AAAA,EAChD,WAAA,CACE,OAAA,EACS,IAAA,EACA,MAAA,EACA,UAAA,EACT;AACA,IAAA,KAAA,CAAM,OAAO,CAAA;AAJJ,IAAA,IAAA,CAAA,IAAA,GAAA,IAAA;AACA,IAAA,IAAA,CAAA,MAAA,GAAA,MAAA;AACA,IAAA,IAAA,CAAA,UAAA,GAAA,UAAA;AAAA,EAGX;AAAA,EALW,IAAA;AAAA,EACA,MAAA;AAAA,EACA,UAAA;AAIb","file":"chunk-SEUD4TE4.js","sourcesContent":["/**\n * Base class for every error this SDK throws, so callers can `catch (e) { if (e\n * instanceof CboxIdError) ... }` in one branch.\n */\nexport class CboxIdError extends Error {\n constructor(message: string) {\n super(message);\n this.name = new.target.name;\n }\n}\n\n/** The client is misconfigured (a required option is missing or empty). */\nexport class ConfigurationError extends CboxIdError {}\n\n/**\n * The login `state` did not match what was stored — the callback is forged or\n * stale. Treat as a fresh start, not an error to surface to the user.\n */\nexport class InvalidStateError extends CboxIdError {}\n\n/**\n * Login could not be completed: the provider returned an error, the callback was\n * missing a code, or a token / id_token failed verification.\n *\n * `error` carries the RFC 6749 §5.2 code the authorization server sent, when it sent one.\n * It used to be discarded at every back-channel boundary, leaving a single message string\n * for outcomes that need opposite responses: `invalid_grant` on a refresh means the\n * session is over and the person has to sign in again, while `temporarily_unavailable`\n * means try the same token in a moment. A caller reduced to matching on prose either\n * retries what can never succeed, or signs out somebody who did not need to be.\n */\nexport class AuthenticationError extends CboxIdError {\n constructor(\n message: string,\n /** RFC 6749 §5.2 error code, when the server sent a parseable one. */\n readonly error?: string,\n /** The server's `error_description`, verbatim. Not end-user copy. */\n readonly errorDescription?: string,\n /** HTTP status, for the cases where the body says nothing useful. */\n readonly status?: number,\n /**\n * Seconds to wait, off the `Retry-After` header — set only on a 429.\n *\n * The distinction it carries is the one a caller most needs and could least infer:\n * a 429 is the ONLY back-channel failure where the same request will succeed\n * unchanged if you simply wait. Every other one needs a different request, or a new\n * sign-in. Without this the SDK reported \"token refresh failed\" and a caller with a\n * retry loop hammered a limiter that was already telling it exactly how long to stop.\n */\n readonly retryAfter?: number,\n ) {\n super(message);\n }\n\n /** Whether waiting `retryAfter` seconds and repeating the same request is worth it. */\n get isRateLimited(): boolean {\n return this.status === 429;\n }\n}\n\n/**\n * Read an OAuth error out of a failed token-endpoint response.\n *\n * Best-effort by design: a 502 from a proxy is HTML, a captive portal is worse, and the\n * caller still needs an error rather than a parse exception. What it must never do is\n * invent a code — an absent or unparseable `error` stays undefined, so\n * `e.error === 'invalid_grant'` is true only because the server said so.\n */\nexport async function oauthError(response: Response, fallback: string): Promise<AuthenticationError> {\n const body = await response.text().catch(() => '');\n\n let code: string | undefined;\n let description: string | undefined;\n\n try {\n const parsed: unknown = JSON.parse(body);\n\n if (typeof parsed === 'object' && parsed !== null) {\n const { error, error_description: errorDescription } = parsed as Record<string, unknown>;\n code = typeof error === 'string' ? error : undefined;\n description = typeof errorDescription === 'string' ? errorDescription : undefined;\n }\n } catch {\n // Not JSON. The status and the truncated body below are all there is to report.\n }\n\n // THE BODY IS NEVER ECHOED. These calls carry `client_secret`, `code_verifier` and\n // refresh tokens in their REQUEST, and a debug proxy, a captive portal or an upstream\n // error page that reflects what it received puts those straight into a thrown message\n // — which production apps log. Only the server's own OAuth error code travels, and\n // failing that, the status alone.\n const detail = code ?? `HTTP ${response.status}`;\n\n // `Retry-After` is seconds here — Laravel's throttler writes the integer form. The\n // HTTP-date form is legal too and is deliberately NOT parsed: guessing at a clock\n // skew is worse than saying nothing, and `status === 429` still tells the caller to\n // back off.\n const retryAfterHeader = response.headers.get('retry-after');\n const retryAfter = retryAfterHeader !== null && /^\\d+$/.test(retryAfterHeader.trim())\n ? Number(retryAfterHeader.trim())\n : undefined;\n\n return new AuthenticationError(`${fallback}: ${detail}`, code, description, response.status, retryAfter);\n}\n\n/**\n * The Frontend API refused, or could not be reached.\n *\n * Carries a `code` because the four ways this goes wrong need four different responses\n * from the caller, and telling them apart from a message string is the kind of thing\n * that works until somebody rewords the message:\n *\n * - `origin_not_allowed` — the overwhelmingly likely cause of a refusal, and a\n * configuration problem the developer fixes once in the console.\n * - `rate_limited` — back off; `retryAfter` says for how long, when the server said.\n * - `unavailable` — the network, or a 5xx. Transient by assumption; already retried.\n * - `malformed` — a 200 whose body is not the document we expect, which means something\n * between the page and us is rewriting responses (a proxy, an extension, a captive\n * portal). Worth distinguishing, because retrying it never helps.\n */\nexport class FrontendApiError extends CboxIdError {\n constructor(\n message: string,\n readonly code: 'origin_not_allowed' | 'rate_limited' | 'unavailable' | 'malformed',\n readonly status?: number,\n readonly retryAfter?: number,\n ) {\n super(message);\n }\n}\n"]}
|
|
@@ -78,6 +78,34 @@ interface VerifyWebhookOptions {
|
|
|
78
78
|
}
|
|
79
79
|
/** Returns `true` only when the signature is present, fresh and valid. Never throws. */
|
|
80
80
|
declare function verifyWebhook(options: VerifyWebhookOptions): Promise<boolean>;
|
|
81
|
+
/** Headers as a `Headers` object or a plain record (Node's `IncomingHttpHeaders`, a framework's map). */
|
|
82
|
+
type WebhookHeaders = Headers | Record<string, string | readonly string[] | undefined>;
|
|
83
|
+
interface VerifyStandardWebhookOptions {
|
|
84
|
+
/** The RAW request body — the exact bytes received, not a re-encoded copy. */
|
|
85
|
+
payload: string;
|
|
86
|
+
/** The request headers; `webhook-id`, `webhook-timestamp` and `webhook-signature` are read. */
|
|
87
|
+
headers: WebhookHeaders;
|
|
88
|
+
/**
|
|
89
|
+
* The endpoint's secret: a `whsec_…` secret, or the 64-hex secret of an endpoint that
|
|
90
|
+
* moved from the `cbox` scheme (Cbox ID uses it as `whsec_` + base64 of the hex string,
|
|
91
|
+
* which keys the HMAC with the same bytes).
|
|
92
|
+
*/
|
|
93
|
+
secret: string;
|
|
94
|
+
/** Freshness window in seconds. Defaults to 300 (5 minutes). */
|
|
95
|
+
toleranceSeconds?: number;
|
|
96
|
+
/** Injectable clock (unix seconds), for tests. Defaults to `Date.now()`. */
|
|
97
|
+
now?: number;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Verify a delivery from an endpoint on the `standard_webhooks` signature scheme
|
|
101
|
+
* ([Standard Webhooks](https://www.standardwebhooks.com/)): `webhook-signature` carries one or
|
|
102
|
+
* more space-separated `v1,<base64 HMAC-SHA256 of "{webhook-id}.{webhook-timestamp}.{body}">`,
|
|
103
|
+
* and the timestamp must be within the freshness window. Use {@link verifyWebhook} for the
|
|
104
|
+
* default `cbox` scheme (`X-Cbox-Signature`).
|
|
105
|
+
*
|
|
106
|
+
* Returns `true` only when a signature is present, fresh and valid. Never throws.
|
|
107
|
+
*/
|
|
108
|
+
declare function verifyStandardWebhook(options: VerifyStandardWebhookOptions): Promise<boolean>;
|
|
81
109
|
|
|
82
110
|
/** Query parameters as they arrive on your callback route. */
|
|
83
111
|
interface CallbackParams {
|
|
@@ -310,4 +338,4 @@ declare class CboxIdClient {
|
|
|
310
338
|
private fetchWithTimeout;
|
|
311
339
|
}
|
|
312
340
|
|
|
313
|
-
export { CboxIdClient as C, type StoreSecretInput as S, VaultClient as V, type CallbackParams as a, type StoredAuthState as b, type VaultLease as c, type VaultSecretRef as d, type
|
|
341
|
+
export { CboxIdClient as C, type StoreSecretInput as S, VaultClient as V, type WebhookHeaders as W, type CallbackParams as a, type StoredAuthState as b, type VaultLease as c, type VaultSecretRef as d, type VerifyStandardWebhookOptions as e, type VerifyWebhookOptions as f, verifyWebhook as g, verifyStandardWebhook as v };
|
|
@@ -78,6 +78,34 @@ interface VerifyWebhookOptions {
|
|
|
78
78
|
}
|
|
79
79
|
/** Returns `true` only when the signature is present, fresh and valid. Never throws. */
|
|
80
80
|
declare function verifyWebhook(options: VerifyWebhookOptions): Promise<boolean>;
|
|
81
|
+
/** Headers as a `Headers` object or a plain record (Node's `IncomingHttpHeaders`, a framework's map). */
|
|
82
|
+
type WebhookHeaders = Headers | Record<string, string | readonly string[] | undefined>;
|
|
83
|
+
interface VerifyStandardWebhookOptions {
|
|
84
|
+
/** The RAW request body — the exact bytes received, not a re-encoded copy. */
|
|
85
|
+
payload: string;
|
|
86
|
+
/** The request headers; `webhook-id`, `webhook-timestamp` and `webhook-signature` are read. */
|
|
87
|
+
headers: WebhookHeaders;
|
|
88
|
+
/**
|
|
89
|
+
* The endpoint's secret: a `whsec_…` secret, or the 64-hex secret of an endpoint that
|
|
90
|
+
* moved from the `cbox` scheme (Cbox ID uses it as `whsec_` + base64 of the hex string,
|
|
91
|
+
* which keys the HMAC with the same bytes).
|
|
92
|
+
*/
|
|
93
|
+
secret: string;
|
|
94
|
+
/** Freshness window in seconds. Defaults to 300 (5 minutes). */
|
|
95
|
+
toleranceSeconds?: number;
|
|
96
|
+
/** Injectable clock (unix seconds), for tests. Defaults to `Date.now()`. */
|
|
97
|
+
now?: number;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Verify a delivery from an endpoint on the `standard_webhooks` signature scheme
|
|
101
|
+
* ([Standard Webhooks](https://www.standardwebhooks.com/)): `webhook-signature` carries one or
|
|
102
|
+
* more space-separated `v1,<base64 HMAC-SHA256 of "{webhook-id}.{webhook-timestamp}.{body}">`,
|
|
103
|
+
* and the timestamp must be within the freshness window. Use {@link verifyWebhook} for the
|
|
104
|
+
* default `cbox` scheme (`X-Cbox-Signature`).
|
|
105
|
+
*
|
|
106
|
+
* Returns `true` only when a signature is present, fresh and valid. Never throws.
|
|
107
|
+
*/
|
|
108
|
+
declare function verifyStandardWebhook(options: VerifyStandardWebhookOptions): Promise<boolean>;
|
|
81
109
|
|
|
82
110
|
/** Query parameters as they arrive on your callback route. */
|
|
83
111
|
interface CallbackParams {
|
|
@@ -310,4 +338,4 @@ declare class CboxIdClient {
|
|
|
310
338
|
private fetchWithTimeout;
|
|
311
339
|
}
|
|
312
340
|
|
|
313
|
-
export { CboxIdClient as C, type StoreSecretInput as S, VaultClient as V, type CallbackParams as a, type StoredAuthState as b, type VaultLease as c, type VaultSecretRef as d, type
|
|
341
|
+
export { CboxIdClient as C, type StoreSecretInput as S, VaultClient as V, type WebhookHeaders as W, type CallbackParams as a, type StoredAuthState as b, type VaultLease as c, type VaultSecretRef as d, type VerifyStandardWebhookOptions as e, type VerifyWebhookOptions as f, verifyWebhook as g, verifyStandardWebhook as v };
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Base class for every error this SDK throws, so callers can `catch (e) { if (e
|
|
3
|
+
* instanceof CboxIdError) ... }` in one branch.
|
|
4
|
+
*/
|
|
5
|
+
declare class CboxIdError extends Error {
|
|
6
|
+
constructor(message: string);
|
|
7
|
+
}
|
|
8
|
+
/** The client is misconfigured (a required option is missing or empty). */
|
|
9
|
+
declare class ConfigurationError extends CboxIdError {
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* The login `state` did not match what was stored — the callback is forged or
|
|
13
|
+
* stale. Treat as a fresh start, not an error to surface to the user.
|
|
14
|
+
*/
|
|
15
|
+
declare class InvalidStateError extends CboxIdError {
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Login could not be completed: the provider returned an error, the callback was
|
|
19
|
+
* missing a code, or a token / id_token failed verification.
|
|
20
|
+
*
|
|
21
|
+
* `error` carries the RFC 6749 §5.2 code the authorization server sent, when it sent one.
|
|
22
|
+
* It used to be discarded at every back-channel boundary, leaving a single message string
|
|
23
|
+
* for outcomes that need opposite responses: `invalid_grant` on a refresh means the
|
|
24
|
+
* session is over and the person has to sign in again, while `temporarily_unavailable`
|
|
25
|
+
* means try the same token in a moment. A caller reduced to matching on prose either
|
|
26
|
+
* retries what can never succeed, or signs out somebody who did not need to be.
|
|
27
|
+
*/
|
|
28
|
+
declare class AuthenticationError extends CboxIdError {
|
|
29
|
+
/** RFC 6749 §5.2 error code, when the server sent a parseable one. */
|
|
30
|
+
readonly error?: string | undefined;
|
|
31
|
+
/** The server's `error_description`, verbatim. Not end-user copy. */
|
|
32
|
+
readonly errorDescription?: string | undefined;
|
|
33
|
+
/** HTTP status, for the cases where the body says nothing useful. */
|
|
34
|
+
readonly status?: number | undefined;
|
|
35
|
+
/**
|
|
36
|
+
* Seconds to wait, off the `Retry-After` header — set only on a 429.
|
|
37
|
+
*
|
|
38
|
+
* The distinction it carries is the one a caller most needs and could least infer:
|
|
39
|
+
* a 429 is the ONLY back-channel failure where the same request will succeed
|
|
40
|
+
* unchanged if you simply wait. Every other one needs a different request, or a new
|
|
41
|
+
* sign-in. Without this the SDK reported "token refresh failed" and a caller with a
|
|
42
|
+
* retry loop hammered a limiter that was already telling it exactly how long to stop.
|
|
43
|
+
*/
|
|
44
|
+
readonly retryAfter?: number | undefined;
|
|
45
|
+
constructor(message: string,
|
|
46
|
+
/** RFC 6749 §5.2 error code, when the server sent a parseable one. */
|
|
47
|
+
error?: string | undefined,
|
|
48
|
+
/** The server's `error_description`, verbatim. Not end-user copy. */
|
|
49
|
+
errorDescription?: string | undefined,
|
|
50
|
+
/** HTTP status, for the cases where the body says nothing useful. */
|
|
51
|
+
status?: number | undefined,
|
|
52
|
+
/**
|
|
53
|
+
* Seconds to wait, off the `Retry-After` header — set only on a 429.
|
|
54
|
+
*
|
|
55
|
+
* The distinction it carries is the one a caller most needs and could least infer:
|
|
56
|
+
* a 429 is the ONLY back-channel failure where the same request will succeed
|
|
57
|
+
* unchanged if you simply wait. Every other one needs a different request, or a new
|
|
58
|
+
* sign-in. Without this the SDK reported "token refresh failed" and a caller with a
|
|
59
|
+
* retry loop hammered a limiter that was already telling it exactly how long to stop.
|
|
60
|
+
*/
|
|
61
|
+
retryAfter?: number | undefined);
|
|
62
|
+
/** Whether waiting `retryAfter` seconds and repeating the same request is worth it. */
|
|
63
|
+
get isRateLimited(): boolean;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* The Frontend API refused, or could not be reached.
|
|
67
|
+
*
|
|
68
|
+
* Carries a `code` because the four ways this goes wrong need four different responses
|
|
69
|
+
* from the caller, and telling them apart from a message string is the kind of thing
|
|
70
|
+
* that works until somebody rewords the message:
|
|
71
|
+
*
|
|
72
|
+
* - `origin_not_allowed` — the overwhelmingly likely cause of a refusal, and a
|
|
73
|
+
* configuration problem the developer fixes once in the console.
|
|
74
|
+
* - `rate_limited` — back off; `retryAfter` says for how long, when the server said.
|
|
75
|
+
* - `unavailable` — the network, or a 5xx. Transient by assumption; already retried.
|
|
76
|
+
* - `malformed` — a 200 whose body is not the document we expect, which means something
|
|
77
|
+
* between the page and us is rewriting responses (a proxy, an extension, a captive
|
|
78
|
+
* portal). Worth distinguishing, because retrying it never helps.
|
|
79
|
+
*/
|
|
80
|
+
declare class FrontendApiError extends CboxIdError {
|
|
81
|
+
readonly code: 'origin_not_allowed' | 'rate_limited' | 'unavailable' | 'malformed';
|
|
82
|
+
readonly status?: number | undefined;
|
|
83
|
+
readonly retryAfter?: number | undefined;
|
|
84
|
+
constructor(message: string, code: 'origin_not_allowed' | 'rate_limited' | 'unavailable' | 'malformed', status?: number | undefined, retryAfter?: number | undefined);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export { AuthenticationError as A, CboxIdError as C, FrontendApiError as F, InvalidStateError as I, ConfigurationError as a };
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Base class for every error this SDK throws, so callers can `catch (e) { if (e
|
|
3
|
+
* instanceof CboxIdError) ... }` in one branch.
|
|
4
|
+
*/
|
|
5
|
+
declare class CboxIdError extends Error {
|
|
6
|
+
constructor(message: string);
|
|
7
|
+
}
|
|
8
|
+
/** The client is misconfigured (a required option is missing or empty). */
|
|
9
|
+
declare class ConfigurationError extends CboxIdError {
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* The login `state` did not match what was stored — the callback is forged or
|
|
13
|
+
* stale. Treat as a fresh start, not an error to surface to the user.
|
|
14
|
+
*/
|
|
15
|
+
declare class InvalidStateError extends CboxIdError {
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Login could not be completed: the provider returned an error, the callback was
|
|
19
|
+
* missing a code, or a token / id_token failed verification.
|
|
20
|
+
*
|
|
21
|
+
* `error` carries the RFC 6749 §5.2 code the authorization server sent, when it sent one.
|
|
22
|
+
* It used to be discarded at every back-channel boundary, leaving a single message string
|
|
23
|
+
* for outcomes that need opposite responses: `invalid_grant` on a refresh means the
|
|
24
|
+
* session is over and the person has to sign in again, while `temporarily_unavailable`
|
|
25
|
+
* means try the same token in a moment. A caller reduced to matching on prose either
|
|
26
|
+
* retries what can never succeed, or signs out somebody who did not need to be.
|
|
27
|
+
*/
|
|
28
|
+
declare class AuthenticationError extends CboxIdError {
|
|
29
|
+
/** RFC 6749 §5.2 error code, when the server sent a parseable one. */
|
|
30
|
+
readonly error?: string | undefined;
|
|
31
|
+
/** The server's `error_description`, verbatim. Not end-user copy. */
|
|
32
|
+
readonly errorDescription?: string | undefined;
|
|
33
|
+
/** HTTP status, for the cases where the body says nothing useful. */
|
|
34
|
+
readonly status?: number | undefined;
|
|
35
|
+
/**
|
|
36
|
+
* Seconds to wait, off the `Retry-After` header — set only on a 429.
|
|
37
|
+
*
|
|
38
|
+
* The distinction it carries is the one a caller most needs and could least infer:
|
|
39
|
+
* a 429 is the ONLY back-channel failure where the same request will succeed
|
|
40
|
+
* unchanged if you simply wait. Every other one needs a different request, or a new
|
|
41
|
+
* sign-in. Without this the SDK reported "token refresh failed" and a caller with a
|
|
42
|
+
* retry loop hammered a limiter that was already telling it exactly how long to stop.
|
|
43
|
+
*/
|
|
44
|
+
readonly retryAfter?: number | undefined;
|
|
45
|
+
constructor(message: string,
|
|
46
|
+
/** RFC 6749 §5.2 error code, when the server sent a parseable one. */
|
|
47
|
+
error?: string | undefined,
|
|
48
|
+
/** The server's `error_description`, verbatim. Not end-user copy. */
|
|
49
|
+
errorDescription?: string | undefined,
|
|
50
|
+
/** HTTP status, for the cases where the body says nothing useful. */
|
|
51
|
+
status?: number | undefined,
|
|
52
|
+
/**
|
|
53
|
+
* Seconds to wait, off the `Retry-After` header — set only on a 429.
|
|
54
|
+
*
|
|
55
|
+
* The distinction it carries is the one a caller most needs and could least infer:
|
|
56
|
+
* a 429 is the ONLY back-channel failure where the same request will succeed
|
|
57
|
+
* unchanged if you simply wait. Every other one needs a different request, or a new
|
|
58
|
+
* sign-in. Without this the SDK reported "token refresh failed" and a caller with a
|
|
59
|
+
* retry loop hammered a limiter that was already telling it exactly how long to stop.
|
|
60
|
+
*/
|
|
61
|
+
retryAfter?: number | undefined);
|
|
62
|
+
/** Whether waiting `retryAfter` seconds and repeating the same request is worth it. */
|
|
63
|
+
get isRateLimited(): boolean;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* The Frontend API refused, or could not be reached.
|
|
67
|
+
*
|
|
68
|
+
* Carries a `code` because the four ways this goes wrong need four different responses
|
|
69
|
+
* from the caller, and telling them apart from a message string is the kind of thing
|
|
70
|
+
* that works until somebody rewords the message:
|
|
71
|
+
*
|
|
72
|
+
* - `origin_not_allowed` — the overwhelmingly likely cause of a refusal, and a
|
|
73
|
+
* configuration problem the developer fixes once in the console.
|
|
74
|
+
* - `rate_limited` — back off; `retryAfter` says for how long, when the server said.
|
|
75
|
+
* - `unavailable` — the network, or a 5xx. Transient by assumption; already retried.
|
|
76
|
+
* - `malformed` — a 200 whose body is not the document we expect, which means something
|
|
77
|
+
* between the page and us is rewriting responses (a proxy, an extension, a captive
|
|
78
|
+
* portal). Worth distinguishing, because retrying it never helps.
|
|
79
|
+
*/
|
|
80
|
+
declare class FrontendApiError extends CboxIdError {
|
|
81
|
+
readonly code: 'origin_not_allowed' | 'rate_limited' | 'unavailable' | 'malformed';
|
|
82
|
+
readonly status?: number | undefined;
|
|
83
|
+
readonly retryAfter?: number | undefined;
|
|
84
|
+
constructor(message: string, code: 'origin_not_allowed' | 'rate_limited' | 'unavailable' | 'malformed', status?: number | undefined, retryAfter?: number | undefined);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export { AuthenticationError as A, CboxIdError as C, FrontendApiError as F, InvalidStateError as I, ConfigurationError as a };
|
package/dist/index.cjs
CHANGED
|
@@ -106,12 +106,12 @@ function createLegacyVerifier(options) {
|
|
|
106
106
|
);
|
|
107
107
|
};
|
|
108
108
|
}
|
|
109
|
-
async function isSignedByCboxId(body,
|
|
110
|
-
if (!
|
|
109
|
+
async function isSignedByCboxId(body, header2, secret, toleranceSeconds) {
|
|
110
|
+
if (!header2) {
|
|
111
111
|
return false;
|
|
112
112
|
}
|
|
113
113
|
const parts = {};
|
|
114
|
-
for (const piece of
|
|
114
|
+
for (const piece of header2.split(",")) {
|
|
115
115
|
const [key2, value] = piece.split("=", 2);
|
|
116
116
|
if (key2 && value) {
|
|
117
117
|
parts[key2.trim()] = value.trim();
|
|
@@ -766,6 +766,58 @@ async function verifyWebhook(options) {
|
|
|
766
766
|
);
|
|
767
767
|
return timingSafeEqual2(expected, signature);
|
|
768
768
|
}
|
|
769
|
+
function header(headers, name) {
|
|
770
|
+
if (typeof Headers !== "undefined" && headers instanceof Headers) {
|
|
771
|
+
return headers.get(name) ?? void 0;
|
|
772
|
+
}
|
|
773
|
+
const record = headers;
|
|
774
|
+
const key = Object.keys(record).find((k) => k.toLowerCase() === name);
|
|
775
|
+
const value = key === void 0 ? void 0 : record[key];
|
|
776
|
+
return Array.isArray(value) ? value.join(" ") : value;
|
|
777
|
+
}
|
|
778
|
+
function base64ToBytes(value) {
|
|
779
|
+
try {
|
|
780
|
+
const binary = atob(value);
|
|
781
|
+
return Uint8Array.from(binary, (c) => c.charCodeAt(0));
|
|
782
|
+
} catch {
|
|
783
|
+
return null;
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
function toBase64(bytes) {
|
|
787
|
+
let binary = "";
|
|
788
|
+
for (const byte of new Uint8Array(bytes)) {
|
|
789
|
+
binary += String.fromCharCode(byte);
|
|
790
|
+
}
|
|
791
|
+
return btoa(binary);
|
|
792
|
+
}
|
|
793
|
+
async function verifyStandardWebhook(options) {
|
|
794
|
+
const { payload, headers, secret } = options;
|
|
795
|
+
const toleranceSeconds = options.toleranceSeconds ?? 300;
|
|
796
|
+
const now = options.now ?? Math.floor(Date.now() / 1e3);
|
|
797
|
+
const id = header(headers, "webhook-id");
|
|
798
|
+
const timestamp = header(headers, "webhook-timestamp");
|
|
799
|
+
const signatures = header(headers, "webhook-signature");
|
|
800
|
+
if (!id || !timestamp || !signatures || !/^\d+$/.test(timestamp)) {
|
|
801
|
+
return false;
|
|
802
|
+
}
|
|
803
|
+
if (Math.abs(now - Number(timestamp)) > toleranceSeconds) {
|
|
804
|
+
return false;
|
|
805
|
+
}
|
|
806
|
+
const keyBytes = secret.startsWith("whsec_") ? base64ToBytes(secret.slice("whsec_".length)) : new TextEncoder().encode(secret);
|
|
807
|
+
if (keyBytes === null || keyBytes.length === 0) {
|
|
808
|
+
return false;
|
|
809
|
+
}
|
|
810
|
+
const key = await crypto.subtle.importKey("raw", keyBytes, { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
|
|
811
|
+
const expected = toBase64(await crypto.subtle.sign("HMAC", key, new TextEncoder().encode(`${id}.${timestamp}.${payload}`)));
|
|
812
|
+
let valid = false;
|
|
813
|
+
for (const candidate of signatures.split(" ")) {
|
|
814
|
+
const [version, signature] = candidate.split(",", 2);
|
|
815
|
+
if (version === "v1" && signature !== void 0 && timingSafeEqual2(expected, signature)) {
|
|
816
|
+
valid = true;
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
return valid;
|
|
820
|
+
}
|
|
769
821
|
|
|
770
822
|
// src/client.ts
|
|
771
823
|
var CboxIdClient = class {
|
|
@@ -1659,6 +1711,7 @@ exports.publishManifest = publishManifest;
|
|
|
1659
1711
|
exports.randomToken = randomToken;
|
|
1660
1712
|
exports.roles = roles;
|
|
1661
1713
|
exports.sessionId = sessionId;
|
|
1714
|
+
exports.verifyStandardWebhook = verifyStandardWebhook;
|
|
1662
1715
|
exports.verifyWebhook = verifyWebhook;
|
|
1663
1716
|
//# sourceMappingURL=index.cjs.map
|
|
1664
1717
|
//# sourceMappingURL=index.cjs.map
|