@ttt-productions/auth-core 1.6.9 → 1.6.10

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.
@@ -1 +1 @@
1
- {"version":3,"file":"acceptance.d.ts","sourceRoot":"","sources":["../src/acceptance.ts"],"names":[],"mappings":"AAQA,sFAAsF;AACtF,eAAO,MAAM,0BAA0B,EAAG,qBAA8B,CAAC;AAEzE;;;;GAIG;AACH,MAAM,WAAW,yBAAyB;IACxC,QAAQ,CAAC,MAAM,EAAE,OAAO,0BAA0B,CAAC;IACnD,gDAAgD;IAChD,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,4EAA4E;IAC5E,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;CAChC;AAED;;;;;GAKG;AACH,wBAAgB,wBAAwB,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAG/D;AAED,0EAA0E;AAC1E,wBAAgB,2BAA2B,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,IAAI,yBAAyB,CAQlG;AASD;;;;GAIG;AACH,wBAAgB,yBAAyB,CAAC,GAAG,EAAE,OAAO,GAAG,OAAO,CAI/D"}
1
+ {"version":3,"file":"acceptance.d.ts","sourceRoot":"","sources":["../src/acceptance.ts"],"names":[],"mappings":"AAUA,sFAAsF;AACtF,eAAO,MAAM,0BAA0B,EAAG,qBAA8B,CAAC;AAEzE;;;;GAIG;AACH,MAAM,WAAW,yBAAyB;IACxC,QAAQ,CAAC,MAAM,EAAE,OAAO,0BAA0B,CAAC;IACnD,gDAAgD;IAChD,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,4EAA4E;IAC5E,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;CAChC;AAED;;;;;GAKG;AACH,wBAAgB,wBAAwB,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAG/D;AAED,0EAA0E;AAC1E,wBAAgB,2BAA2B,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,IAAI,yBAAyB,CAQlG;AAED;;;;GAIG;AACH,wBAAgB,yBAAyB,CAAC,GAAG,EAAE,OAAO,GAAG,OAAO,CAE/D"}
@@ -5,6 +5,7 @@
5
5
  // monotonically increasing whole-number LEVEL: the app decides what raises the required level
6
6
  // and where the caller's accepted level is stored (a custom claim named by the app). The package
7
7
  // only compares the two numbers and reports a refusal in one recognisable shape.
8
+ import { failedPreconditionDetails } from "./refusal.js";
8
9
  /** The `details.reason` of the refusal a caller below the required level receives. */
9
10
  export const ACCEPTANCE_REQUIRED_REASON = "acceptance-required";
10
11
  /**
@@ -27,21 +28,12 @@ export function isAcceptanceRequiredDetails(details) {
27
28
  typeof d.requiredLevel === "number" &&
28
29
  typeof d.acceptedLevel === "number");
29
30
  }
30
- // The HttpsError code the refusal carries, as the server (`failed-precondition`) and the
31
- // Firebase client SDK (`functions/failed-precondition`) spell it.
32
- const ACCEPTANCE_REFUSAL_CODES = new Set([
33
- "failed-precondition",
34
- "functions/failed-precondition",
35
- ]);
36
31
  /**
37
32
  * True when `err` is the acceptance refusal — a `failed-precondition` whose details carry
38
33
  * `reason: 'acceptance-required'`. Works on the server-side error and on the client-side
39
34
  * callable error alike, so the app has one recognizer for "show the acceptance prompt".
40
35
  */
41
36
  export function isAcceptanceRequiredError(err) {
42
- if (!err || typeof err !== "object")
43
- return false;
44
- const e = err;
45
- return typeof e.code === "string" && ACCEPTANCE_REFUSAL_CODES.has(e.code) && isAcceptanceRequiredDetails(e.details);
37
+ return isAcceptanceRequiredDetails(failedPreconditionDetails(err));
46
38
  }
47
39
  //# sourceMappingURL=acceptance.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"acceptance.js","sourceRoot":"","sources":["../src/acceptance.ts"],"names":[],"mappings":"AAAA,yFAAyF;AACzF,8FAA8F;AAC9F,EAAE;AACF,wFAAwF;AACxF,8FAA8F;AAC9F,iGAAiG;AACjG,iFAAiF;AAEjF,sFAAsF;AACtF,MAAM,CAAC,MAAM,0BAA0B,GAAG,qBAA8B,CAAC;AAezE;;;;;GAKG;AACH,MAAM,UAAU,wBAAwB,CAAC,KAAc;IACrD,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,KAAK,IAAI,CAAC;QAAE,OAAO,CAAC,CAAC;IACjF,OAAO,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;AAC3B,CAAC;AAED,0EAA0E;AAC1E,MAAM,UAAU,2BAA2B,CAAC,OAAgB;IAC1D,IAAI,CAAC,OAAO,IAAI,OAAO,OAAO,KAAK,QAAQ;QAAE,OAAO,KAAK,CAAC;IAC1D,MAAM,CAAC,GAAG,OAAkC,CAAC;IAC7C,OAAO,CACL,CAAC,CAAC,MAAM,KAAK,0BAA0B;QACvC,OAAO,CAAC,CAAC,aAAa,KAAK,QAAQ;QACnC,OAAO,CAAC,CAAC,aAAa,KAAK,QAAQ,CACpC,CAAC;AACJ,CAAC;AAED,yFAAyF;AACzF,kEAAkE;AAClE,MAAM,wBAAwB,GAAwB,IAAI,GAAG,CAAC;IAC5D,qBAAqB;IACrB,+BAA+B;CAChC,CAAC,CAAC;AAEH;;;;GAIG;AACH,MAAM,UAAU,yBAAyB,CAAC,GAAY;IACpD,IAAI,CAAC,GAAG,IAAI,OAAO,GAAG,KAAK,QAAQ;QAAE,OAAO,KAAK,CAAC;IAClD,MAAM,CAAC,GAAG,GAA4C,CAAC;IACvD,OAAO,OAAO,CAAC,CAAC,IAAI,KAAK,QAAQ,IAAI,wBAAwB,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,2BAA2B,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC;AACtH,CAAC","sourcesContent":["// The acceptance-level contract shared by the server gate (`createAssertAuth`'s optional\n// `acceptance` config on ./server) and the client that answers it. Pure: no Firebase runtime.\n//\n// A consuming app publishes documents its users must accept, and tracks acceptance as a\n// monotonically increasing whole-number LEVEL: the app decides what raises the required level\n// and where the caller's accepted level is stored (a custom claim named by the app). The package\n// only compares the two numbers and reports a refusal in one recognisable shape.\n\n/** The `details.reason` of the refusal a caller below the required level receives. */\nexport const ACCEPTANCE_REQUIRED_REASON = \"acceptance-required\" as const;\n\n/**\n * Structured details carried by the acceptance refusal. Server side it rides\n * `AuthAssertionError.details`; the consuming app forwards it as the `HttpsError` details, so\n * the client receives it on the callable error's `details`.\n */\nexport interface AcceptanceRequiredDetails {\n readonly reason: typeof ACCEPTANCE_REQUIRED_REASON;\n /** The level the backend currently requires. */\n readonly requiredLevel: number;\n /** The level read from the caller's token (0 when absent or unreadable). */\n readonly acceptedLevel: number;\n}\n\n/**\n * Normalize an acceptance level: a finite number ≥ 0, floored to a whole number. Anything\n * else — absent, a string, NaN, a negative — is 0. Applied to BOTH sides of the comparison:\n * an unreadable accepted level can never satisfy a positive requirement, and an unreadable\n * required level requires nothing.\n */\nexport function normalizeAcceptanceLevel(value: unknown): number {\n if (typeof value !== \"number\" || !Number.isFinite(value) || value <= 0) return 0;\n return Math.floor(value);\n}\n\n/** True when `details` is the acceptance refusal's structured details. */\nexport function isAcceptanceRequiredDetails(details: unknown): details is AcceptanceRequiredDetails {\n if (!details || typeof details !== \"object\") return false;\n const d = details as Record<string, unknown>;\n return (\n d.reason === ACCEPTANCE_REQUIRED_REASON &&\n typeof d.requiredLevel === \"number\" &&\n typeof d.acceptedLevel === \"number\"\n );\n}\n\n// The HttpsError code the refusal carries, as the server (`failed-precondition`) and the\n// Firebase client SDK (`functions/failed-precondition`) spell it.\nconst ACCEPTANCE_REFUSAL_CODES: ReadonlySet<string> = new Set([\n \"failed-precondition\",\n \"functions/failed-precondition\",\n]);\n\n/**\n * True when `err` is the acceptance refusal — a `failed-precondition` whose details carry\n * `reason: 'acceptance-required'`. Works on the server-side error and on the client-side\n * callable error alike, so the app has one recognizer for \"show the acceptance prompt\".\n */\nexport function isAcceptanceRequiredError(err: unknown): boolean {\n if (!err || typeof err !== \"object\") return false;\n const e = err as { code?: unknown; details?: unknown };\n return typeof e.code === \"string\" && ACCEPTANCE_REFUSAL_CODES.has(e.code) && isAcceptanceRequiredDetails(e.details);\n}\n"]}
1
+ {"version":3,"file":"acceptance.js","sourceRoot":"","sources":["../src/acceptance.ts"],"names":[],"mappings":"AAAA,yFAAyF;AACzF,8FAA8F;AAC9F,EAAE;AACF,wFAAwF;AACxF,8FAA8F;AAC9F,iGAAiG;AACjG,iFAAiF;AAEjF,OAAO,EAAE,yBAAyB,EAAE,MAAM,cAAc,CAAC;AAEzD,sFAAsF;AACtF,MAAM,CAAC,MAAM,0BAA0B,GAAG,qBAA8B,CAAC;AAezE;;;;;GAKG;AACH,MAAM,UAAU,wBAAwB,CAAC,KAAc;IACrD,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,KAAK,IAAI,CAAC;QAAE,OAAO,CAAC,CAAC;IACjF,OAAO,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;AAC3B,CAAC;AAED,0EAA0E;AAC1E,MAAM,UAAU,2BAA2B,CAAC,OAAgB;IAC1D,IAAI,CAAC,OAAO,IAAI,OAAO,OAAO,KAAK,QAAQ;QAAE,OAAO,KAAK,CAAC;IAC1D,MAAM,CAAC,GAAG,OAAkC,CAAC;IAC7C,OAAO,CACL,CAAC,CAAC,MAAM,KAAK,0BAA0B;QACvC,OAAO,CAAC,CAAC,aAAa,KAAK,QAAQ;QACnC,OAAO,CAAC,CAAC,aAAa,KAAK,QAAQ,CACpC,CAAC;AACJ,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,yBAAyB,CAAC,GAAY;IACpD,OAAO,2BAA2B,CAAC,yBAAyB,CAAC,GAAG,CAAC,CAAC,CAAC;AACrE,CAAC","sourcesContent":["// The acceptance-level contract shared by the server gate (`createAssertAuth`'s optional\n// `acceptance` config on ./server) and the client that answers it. Pure: no Firebase runtime.\n//\n// A consuming app publishes documents its users must accept, and tracks acceptance as a\n// monotonically increasing whole-number LEVEL: the app decides what raises the required level\n// and where the caller's accepted level is stored (a custom claim named by the app). The package\n// only compares the two numbers and reports a refusal in one recognisable shape.\n\nimport { failedPreconditionDetails } from \"./refusal.js\";\n\n/** The `details.reason` of the refusal a caller below the required level receives. */\nexport const ACCEPTANCE_REQUIRED_REASON = \"acceptance-required\" as const;\n\n/**\n * Structured details carried by the acceptance refusal. Server side it rides\n * `AuthAssertionError.details`; the consuming app forwards it as the `HttpsError` details, so\n * the client receives it on the callable error's `details`.\n */\nexport interface AcceptanceRequiredDetails {\n readonly reason: typeof ACCEPTANCE_REQUIRED_REASON;\n /** The level the backend currently requires. */\n readonly requiredLevel: number;\n /** The level read from the caller's token (0 when absent or unreadable). */\n readonly acceptedLevel: number;\n}\n\n/**\n * Normalize an acceptance level: a finite number ≥ 0, floored to a whole number. Anything\n * else — absent, a string, NaN, a negative — is 0. Applied to BOTH sides of the comparison:\n * an unreadable accepted level can never satisfy a positive requirement, and an unreadable\n * required level requires nothing.\n */\nexport function normalizeAcceptanceLevel(value: unknown): number {\n if (typeof value !== \"number\" || !Number.isFinite(value) || value <= 0) return 0;\n return Math.floor(value);\n}\n\n/** True when `details` is the acceptance refusal's structured details. */\nexport function isAcceptanceRequiredDetails(details: unknown): details is AcceptanceRequiredDetails {\n if (!details || typeof details !== \"object\") return false;\n const d = details as Record<string, unknown>;\n return (\n d.reason === ACCEPTANCE_REQUIRED_REASON &&\n typeof d.requiredLevel === \"number\" &&\n typeof d.acceptedLevel === \"number\"\n );\n}\n\n/**\n * True when `err` is the acceptance refusal — a `failed-precondition` whose details carry\n * `reason: 'acceptance-required'`. Works on the server-side error and on the client-side\n * callable error alike, so the app has one recognizer for \"show the acceptance prompt\".\n */\nexport function isAcceptanceRequiredError(err: unknown): boolean {\n return isAcceptanceRequiredDetails(failedPreconditionDetails(err));\n}\n"]}
@@ -0,0 +1,20 @@
1
+ /** The `details.reason` of the refusal a caller without a verified email receives. */
2
+ export declare const EMAIL_VERIFICATION_REQUIRED_REASON: "email-verification-required";
3
+ /**
4
+ * Structured details carried by the email-verification refusal. Server side it rides
5
+ * `AuthAssertionError.details`; the consuming app forwards it as the `HttpsError` details, so
6
+ * the client receives it on the callable error's `details`.
7
+ */
8
+ export interface EmailVerificationRequiredDetails {
9
+ readonly reason: typeof EMAIL_VERIFICATION_REQUIRED_REASON;
10
+ }
11
+ /** True when `details` is the email-verification refusal's structured details. */
12
+ export declare function isEmailVerificationRequiredDetails(details: unknown): details is EmailVerificationRequiredDetails;
13
+ /**
14
+ * True when `err` is the email-verification refusal — a `failed-precondition` whose details
15
+ * carry `reason: 'email-verification-required'`. Works on the server-side error and on the
16
+ * client-side callable error alike, so the app has one recognizer for "show the verify-email
17
+ * surface".
18
+ */
19
+ export declare function isEmailVerificationRequiredError(err: unknown): boolean;
20
+ //# sourceMappingURL=email-verification.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"email-verification.d.ts","sourceRoot":"","sources":["../src/email-verification.ts"],"names":[],"mappings":"AAWA,sFAAsF;AACtF,eAAO,MAAM,kCAAkC,EAAG,6BAAsC,CAAC;AAEzF;;;;GAIG;AACH,MAAM,WAAW,gCAAgC;IAC/C,QAAQ,CAAC,MAAM,EAAE,OAAO,kCAAkC,CAAC;CAC5D;AAED,kFAAkF;AAClF,wBAAgB,kCAAkC,CAChD,OAAO,EAAE,OAAO,GACf,OAAO,IAAI,gCAAgC,CAG7C;AAED;;;;;GAKG;AACH,wBAAgB,gCAAgC,CAAC,GAAG,EAAE,OAAO,GAAG,OAAO,CAEtE"}
@@ -0,0 +1,27 @@
1
+ // The email-verification refusal contract shared by the server gate (`createAssertAuth`'s
2
+ // `emailVerified` requirement on ./server) and the client that answers it. Pure: no Firebase
3
+ // runtime.
4
+ //
5
+ // A callable that requires a verified email refuses a caller whose ID token is not
6
+ // email-verified. The package owns that refusal, so it owns its one recognisable shape: a
7
+ // `failed-precondition` whose details name the reason. The client tells it apart from every
8
+ // other failed-precondition by that reason — never by the message text.
9
+ import { failedPreconditionDetails } from "./refusal.js";
10
+ /** The `details.reason` of the refusal a caller without a verified email receives. */
11
+ export const EMAIL_VERIFICATION_REQUIRED_REASON = "email-verification-required";
12
+ /** True when `details` is the email-verification refusal's structured details. */
13
+ export function isEmailVerificationRequiredDetails(details) {
14
+ if (!details || typeof details !== "object")
15
+ return false;
16
+ return details.reason === EMAIL_VERIFICATION_REQUIRED_REASON;
17
+ }
18
+ /**
19
+ * True when `err` is the email-verification refusal — a `failed-precondition` whose details
20
+ * carry `reason: 'email-verification-required'`. Works on the server-side error and on the
21
+ * client-side callable error alike, so the app has one recognizer for "show the verify-email
22
+ * surface".
23
+ */
24
+ export function isEmailVerificationRequiredError(err) {
25
+ return isEmailVerificationRequiredDetails(failedPreconditionDetails(err));
26
+ }
27
+ //# sourceMappingURL=email-verification.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"email-verification.js","sourceRoot":"","sources":["../src/email-verification.ts"],"names":[],"mappings":"AAAA,0FAA0F;AAC1F,6FAA6F;AAC7F,WAAW;AACX,EAAE;AACF,mFAAmF;AACnF,0FAA0F;AAC1F,4FAA4F;AAC5F,wEAAwE;AAExE,OAAO,EAAE,yBAAyB,EAAE,MAAM,cAAc,CAAC;AAEzD,sFAAsF;AACtF,MAAM,CAAC,MAAM,kCAAkC,GAAG,6BAAsC,CAAC;AAWzF,kFAAkF;AAClF,MAAM,UAAU,kCAAkC,CAChD,OAAgB;IAEhB,IAAI,CAAC,OAAO,IAAI,OAAO,OAAO,KAAK,QAAQ;QAAE,OAAO,KAAK,CAAC;IAC1D,OAAQ,OAAmC,CAAC,MAAM,KAAK,kCAAkC,CAAC;AAC5F,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,gCAAgC,CAAC,GAAY;IAC3D,OAAO,kCAAkC,CAAC,yBAAyB,CAAC,GAAG,CAAC,CAAC,CAAC;AAC5E,CAAC","sourcesContent":["// The email-verification refusal contract shared by the server gate (`createAssertAuth`'s\n// `emailVerified` requirement on ./server) and the client that answers it. Pure: no Firebase\n// runtime.\n//\n// A callable that requires a verified email refuses a caller whose ID token is not\n// email-verified. The package owns that refusal, so it owns its one recognisable shape: a\n// `failed-precondition` whose details name the reason. The client tells it apart from every\n// other failed-precondition by that reason — never by the message text.\n\nimport { failedPreconditionDetails } from \"./refusal.js\";\n\n/** The `details.reason` of the refusal a caller without a verified email receives. */\nexport const EMAIL_VERIFICATION_REQUIRED_REASON = \"email-verification-required\" as const;\n\n/**\n * Structured details carried by the email-verification refusal. Server side it rides\n * `AuthAssertionError.details`; the consuming app forwards it as the `HttpsError` details, so\n * the client receives it on the callable error's `details`.\n */\nexport interface EmailVerificationRequiredDetails {\n readonly reason: typeof EMAIL_VERIFICATION_REQUIRED_REASON;\n}\n\n/** True when `details` is the email-verification refusal's structured details. */\nexport function isEmailVerificationRequiredDetails(\n details: unknown\n): details is EmailVerificationRequiredDetails {\n if (!details || typeof details !== \"object\") return false;\n return (details as Record<string, unknown>).reason === EMAIL_VERIFICATION_REQUIRED_REASON;\n}\n\n/**\n * True when `err` is the email-verification refusal — a `failed-precondition` whose details\n * carry `reason: 'email-verification-required'`. Works on the server-side error and on the\n * client-side callable error alike, so the app has one recognizer for \"show the verify-email\n * surface\".\n */\nexport function isEmailVerificationRequiredError(err: unknown): boolean {\n return isEmailVerificationRequiredDetails(failedPreconditionDetails(err));\n}\n"]}
package/dist/index.d.ts CHANGED
@@ -2,4 +2,5 @@ export * from "./claims.js";
2
2
  export * from "./errors.js";
3
3
  export * from "./env.js";
4
4
  export * from "./acceptance.js";
5
+ export * from "./email-verification.js";
5
6
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAOA,cAAc,aAAa,CAAC;AAC5B,cAAc,aAAa,CAAC;AAC5B,cAAc,UAAU,CAAC;AACzB,cAAc,iBAAiB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAOA,cAAc,aAAa,CAAC;AAC5B,cAAc,aAAa,CAAC;AAC5B,cAAc,UAAU,CAAC;AACzB,cAAc,iBAAiB,CAAC;AAChC,cAAc,yBAAyB,CAAC"}
package/dist/index.js CHANGED
@@ -9,4 +9,5 @@ export * from "./claims.js";
9
9
  export * from "./errors.js";
10
10
  export * from "./env.js";
11
11
  export * from "./acceptance.js";
12
+ export * from "./email-verification.js";
12
13
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,gFAAgF;AAChF,iDAAiD;AACjD,EAAE;AACF,6EAA6E;AAC7E,2EAA2E;AAC3E,+DAA+D;AAC/D,sCAAsC;AACtC,cAAc,aAAa,CAAC;AAC5B,cAAc,aAAa,CAAC;AAC5B,cAAc,UAAU,CAAC;AACzB,cAAc,iBAAiB,CAAC","sourcesContent":["// Pure root: contracts, claims parsing, normalized errors, environment helpers.\n// Nothing here imports firebase/auth at runtime.\n//\n// Client auth runtime (onAuthStateChanged wrapper, getAuthUser) lives behind\n// \"@ttt-productions/auth-core/client\". Admin/Functions helpers live behind\n// \"@ttt-productions/auth-core/server\". React hooks live behind\n// \"@ttt-productions/auth-core/react\".\nexport * from \"./claims.js\";\nexport * from \"./errors.js\";\nexport * from \"./env.js\";\nexport * from \"./acceptance.js\";\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,gFAAgF;AAChF,iDAAiD;AACjD,EAAE;AACF,6EAA6E;AAC7E,2EAA2E;AAC3E,+DAA+D;AAC/D,sCAAsC;AACtC,cAAc,aAAa,CAAC;AAC5B,cAAc,aAAa,CAAC;AAC5B,cAAc,UAAU,CAAC;AACzB,cAAc,iBAAiB,CAAC;AAChC,cAAc,yBAAyB,CAAC","sourcesContent":["// Pure root: contracts, claims parsing, normalized errors, environment helpers.\n// Nothing here imports firebase/auth at runtime.\n//\n// Client auth runtime (onAuthStateChanged wrapper, getAuthUser) lives behind\n// \"@ttt-productions/auth-core/client\". Admin/Functions helpers live behind\n// \"@ttt-productions/auth-core/server\". React hooks live behind\n// \"@ttt-productions/auth-core/react\".\nexport * from \"./claims.js\";\nexport * from \"./errors.js\";\nexport * from \"./env.js\";\nexport * from \"./acceptance.js\";\nexport * from \"./email-verification.js\";\n"]}
@@ -0,0 +1,8 @@
1
+ /**
2
+ * The `details` of `err` when it is a `failed-precondition` error in either spelling —
3
+ * server-side or client-side callable error alike — otherwise `undefined`. Each recognizer then
4
+ * asks whether those details are its own refusal's, so refusals are told apart by
5
+ * `details.reason`, never by message text.
6
+ */
7
+ export declare function failedPreconditionDetails(err: unknown): unknown;
8
+ //# sourceMappingURL=refusal.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"refusal.d.ts","sourceRoot":"","sources":["../src/refusal.ts"],"names":[],"mappings":"AAYA;;;;;GAKG;AACH,wBAAgB,yBAAyB,CAAC,GAAG,EAAE,OAAO,GAAG,OAAO,CAI/D"}
@@ -0,0 +1,23 @@
1
+ // The code both structured `assertAuth` refusals carry — the acceptance refusal
2
+ // (./acceptance.ts) and the email-verification refusal (./email-verification.ts) — and the one
3
+ // place their recognizers read it. Internal: the root exports the recognizers, not this.
4
+ // Pure: no Firebase runtime.
5
+ // The HttpsError code, as the server (`failed-precondition`) and the Firebase client SDK
6
+ // (`functions/failed-precondition`) spell it.
7
+ const FAILED_PRECONDITION_CODES = new Set([
8
+ "failed-precondition",
9
+ "functions/failed-precondition",
10
+ ]);
11
+ /**
12
+ * The `details` of `err` when it is a `failed-precondition` error in either spelling —
13
+ * server-side or client-side callable error alike — otherwise `undefined`. Each recognizer then
14
+ * asks whether those details are its own refusal's, so refusals are told apart by
15
+ * `details.reason`, never by message text.
16
+ */
17
+ export function failedPreconditionDetails(err) {
18
+ if (!err || typeof err !== "object")
19
+ return undefined;
20
+ const e = err;
21
+ return typeof e.code === "string" && FAILED_PRECONDITION_CODES.has(e.code) ? e.details : undefined;
22
+ }
23
+ //# sourceMappingURL=refusal.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"refusal.js","sourceRoot":"","sources":["../src/refusal.ts"],"names":[],"mappings":"AAAA,gFAAgF;AAChF,+FAA+F;AAC/F,yFAAyF;AACzF,6BAA6B;AAE7B,yFAAyF;AACzF,8CAA8C;AAC9C,MAAM,yBAAyB,GAAwB,IAAI,GAAG,CAAC;IAC7D,qBAAqB;IACrB,+BAA+B;CAChC,CAAC,CAAC;AAEH;;;;;GAKG;AACH,MAAM,UAAU,yBAAyB,CAAC,GAAY;IACpD,IAAI,CAAC,GAAG,IAAI,OAAO,GAAG,KAAK,QAAQ;QAAE,OAAO,SAAS,CAAC;IACtD,MAAM,CAAC,GAAG,GAA4C,CAAC;IACvD,OAAO,OAAO,CAAC,CAAC,IAAI,KAAK,QAAQ,IAAI,yBAAyB,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,SAAS,CAAC;AACrG,CAAC","sourcesContent":["// The code both structured `assertAuth` refusals carry — the acceptance refusal\n// (./acceptance.ts) and the email-verification refusal (./email-verification.ts) — and the one\n// place their recognizers read it. Internal: the root exports the recognizers, not this.\n// Pure: no Firebase runtime.\n\n// The HttpsError code, as the server (`failed-precondition`) and the Firebase client SDK\n// (`functions/failed-precondition`) spell it.\nconst FAILED_PRECONDITION_CODES: ReadonlySet<string> = new Set([\n \"failed-precondition\",\n \"functions/failed-precondition\",\n]);\n\n/**\n * The `details` of `err` when it is a `failed-precondition` error in either spelling —\n * server-side or client-side callable error alike — otherwise `undefined`. Each recognizer then\n * asks whether those details are its own refusal's, so refusals are told apart by\n * `details.reason`, never by message text.\n */\nexport function failedPreconditionDetails(err: unknown): unknown {\n if (!err || typeof err !== \"object\") return undefined;\n const e = err as { code?: unknown; details?: unknown };\n return typeof e.code === \"string\" && FAILED_PRECONDITION_CODES.has(e.code) ? e.details : undefined;\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"assertAuth.d.ts","sourceRoot":"","sources":["../../src/server/assertAuth.ts"],"names":[],"mappings":"AAeA,OAAO,KAAK,EACV,gBAAgB,EAChB,YAAY,EAGb,MAAM,YAAY,CAAC;AAEpB,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,EACnD,MAAM,EAAE,gBAAgB,CAAC,KAAK,EAAE,MAAM,CAAC,GACtC,YAAY,CAAC,KAAK,EAAE,MAAM,CAAC,CA0F7B"}
1
+ {"version":3,"file":"assertAuth.d.ts","sourceRoot":"","sources":["../../src/server/assertAuth.ts"],"names":[],"mappings":"AAgBA,OAAO,KAAK,EACV,gBAAgB,EAChB,YAAY,EAGb,MAAM,YAAY,CAAC;AAEpB,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,EACnD,MAAM,EAAE,gBAAgB,CAAC,KAAK,EAAE,MAAM,CAAC,GACtC,YAAY,CAAC,KAAK,EAAE,MAAM,CAAC,CA6F7B"}
@@ -10,6 +10,7 @@
10
10
  // 5. Acceptance-level gate (only when config.acceptance is set)
11
11
  // 6. Admin check (delegated to config.requireAdmin)
12
12
  import { ACCEPTANCE_REQUIRED_REASON, normalizeAcceptanceLevel } from "../acceptance.js";
13
+ import { EMAIL_VERIFICATION_REQUIRED_REASON } from "../email-verification.js";
13
14
  import { AuthAssertionError } from "./authError.js";
14
15
  export function createAssertAuth(config) {
15
16
  return async function assertAuth(request, requirements = {}) {
@@ -19,11 +20,14 @@ export function createAssertAuth(config) {
19
20
  }
20
21
  const uid = request.auth.uid;
21
22
  const token = request.auth.token;
22
- // 2. Email verification check
23
+ // 2. Email verification check. The refusal carries its reason in `details`, so the client
24
+ // recognizes it structurally (`isEmailVerificationRequiredError`), never by the message.
23
25
  if (requirements.emailVerified === true &&
24
26
  requirements.allowUnverified !== true) {
25
27
  if (token.email_verified !== true) {
26
- throw new AuthAssertionError("failed-precondition", "Email must be verified");
28
+ throw new AuthAssertionError("failed-precondition", "Email must be verified", {
29
+ reason: EMAIL_VERIFICATION_REQUIRED_REASON,
30
+ });
27
31
  }
28
32
  }
29
33
  // 3. User doc Firestore read (skipped when allowAnyStatus is set)
@@ -1 +1 @@
1
- {"version":3,"file":"assertAuth.js","sourceRoot":"","sources":["../../src/server/assertAuth.ts"],"names":[],"mappings":"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","sourcesContent":["// Factory that produces an assertAuth function bound to a consuming app's\n// Firestore paths, user shape, and admin check.\n//\n// Every Cloud Functions callable in the consuming app should invoke the\n// returned assertAuth as its first executable line. The check order is:\n// 1. Authentication\n// 2. Email verification\n// 3. User doc Firestore read (when status check is needed)\n// 4. Status check (suspended / banned)\n// 5. Acceptance-level gate (only when config.acceptance is set)\n// 6. Admin check (delegated to config.requireAdmin)\n\nimport type { CallableRequest } from \"firebase-functions/v2/https\";\nimport { ACCEPTANCE_REQUIRED_REASON, normalizeAcceptanceLevel } from \"../acceptance.js\";\nimport { AuthAssertionError } from \"./authError.js\";\nimport type {\n AssertAuthConfig,\n AssertAuthFn,\n AuthContext,\n AuthRequirements,\n} from \"./types.js\";\n\nexport function createAssertAuth<TUser, TAdmin = void>(\n config: AssertAuthConfig<TUser, TAdmin>\n): AssertAuthFn<TUser, TAdmin> {\n return async function assertAuth(\n request: CallableRequest,\n requirements: AuthRequirements = {}\n ): Promise<AuthContext<TUser, TAdmin>> {\n // 1. Authentication check (always first)\n if (!request.auth) {\n throw new AuthAssertionError(\"unauthenticated\", \"You must be authenticated\");\n }\n\n const uid = request.auth.uid;\n const token = request.auth.token;\n\n // 2. Email verification check\n if (\n requirements.emailVerified === true &&\n requirements.allowUnverified !== true\n ) {\n if (token.email_verified !== true) {\n throw new AuthAssertionError(\"failed-precondition\", \"Email must be verified\");\n }\n }\n\n // 3. User doc Firestore read (skipped when allowAnyStatus is set)\n const db = config.firestore();\n const userSnap =\n requirements.allowAnyStatus !== true\n ? await db.doc(config.userProfilePath(uid)).get()\n : null;\n\n let userDoc: TUser | undefined;\n\n // 4. Status check (default: block suspended + banned; skipped if allowAnyStatus)\n if (requirements.allowAnyStatus !== true) {\n if (!userSnap || !userSnap.exists) {\n throw new AuthAssertionError(\"not-found\", \"User profile not found\");\n }\n const userData = userSnap.data() as TUser;\n const status = config.getUserStatus(userData);\n if (status === \"banned\") {\n throw new AuthAssertionError(\n \"permission-denied\",\n \"Account is not in good standing\"\n );\n }\n if (status === \"suspended\" && requirements.allowSuspended !== true) {\n throw new AuthAssertionError(\n \"permission-denied\",\n \"Account is not in good standing\"\n );\n }\n userDoc = userData;\n }\n\n // 5. Acceptance-level gate. Runs only when the app configured one and the callable did\n // not opt out. The required level comes from the app's cheap (cached) reader and the\n // accepted level from the verified token, so the gate adds no per-call Firestore read.\n // It sits after the status check so a banned caller still gets the standing refusal.\n if (config.acceptance !== undefined && requirements.allowUnaccepted !== true) {\n const requiredLevel = normalizeAcceptanceLevel(await config.acceptance.requiredLevel());\n if (requiredLevel > 0) {\n const acceptedLevel = normalizeAcceptanceLevel(\n (token as Record<string, unknown>)[config.acceptance.claimKey]\n );\n if (acceptedLevel < requiredLevel) {\n throw new AuthAssertionError(\n \"failed-precondition\",\n \"The current documents must be accepted before continuing\",\n { reason: ACCEPTANCE_REQUIRED_REASON, requiredLevel, acceptedLevel }\n );\n }\n }\n }\n\n // 6. Admin check (delegates to config.requireAdmin)\n let adminResult: TAdmin | undefined;\n if (requirements.admin !== undefined) {\n adminResult = await config.requireAdmin(uid, token, requirements.admin);\n }\n\n // 7. Return context with whatever docs were fetched\n const ctx: AuthContext<TUser, TAdmin> = { uid, token };\n if (userDoc !== undefined) {\n ctx.userDoc = userDoc;\n }\n if (requirements.admin !== undefined) {\n ctx.admin = adminResult as TAdmin;\n }\n return ctx;\n };\n}\n"]}
1
+ {"version":3,"file":"assertAuth.js","sourceRoot":"","sources":["../../src/server/assertAuth.ts"],"names":[],"mappings":"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","sourcesContent":["// Factory that produces an assertAuth function bound to a consuming app's\n// Firestore paths, user shape, and admin check.\n//\n// Every Cloud Functions callable in the consuming app should invoke the\n// returned assertAuth as its first executable line. The check order is:\n// 1. Authentication\n// 2. Email verification\n// 3. User doc Firestore read (when status check is needed)\n// 4. Status check (suspended / banned)\n// 5. Acceptance-level gate (only when config.acceptance is set)\n// 6. Admin check (delegated to config.requireAdmin)\n\nimport type { CallableRequest } from \"firebase-functions/v2/https\";\nimport { ACCEPTANCE_REQUIRED_REASON, normalizeAcceptanceLevel } from \"../acceptance.js\";\nimport { EMAIL_VERIFICATION_REQUIRED_REASON } from \"../email-verification.js\";\nimport { AuthAssertionError } from \"./authError.js\";\nimport type {\n AssertAuthConfig,\n AssertAuthFn,\n AuthContext,\n AuthRequirements,\n} from \"./types.js\";\n\nexport function createAssertAuth<TUser, TAdmin = void>(\n config: AssertAuthConfig<TUser, TAdmin>\n): AssertAuthFn<TUser, TAdmin> {\n return async function assertAuth(\n request: CallableRequest,\n requirements: AuthRequirements = {}\n ): Promise<AuthContext<TUser, TAdmin>> {\n // 1. Authentication check (always first)\n if (!request.auth) {\n throw new AuthAssertionError(\"unauthenticated\", \"You must be authenticated\");\n }\n\n const uid = request.auth.uid;\n const token = request.auth.token;\n\n // 2. Email verification check. The refusal carries its reason in `details`, so the client\n // recognizes it structurally (`isEmailVerificationRequiredError`), never by the message.\n if (\n requirements.emailVerified === true &&\n requirements.allowUnverified !== true\n ) {\n if (token.email_verified !== true) {\n throw new AuthAssertionError(\"failed-precondition\", \"Email must be verified\", {\n reason: EMAIL_VERIFICATION_REQUIRED_REASON,\n });\n }\n }\n\n // 3. User doc Firestore read (skipped when allowAnyStatus is set)\n const db = config.firestore();\n const userSnap =\n requirements.allowAnyStatus !== true\n ? await db.doc(config.userProfilePath(uid)).get()\n : null;\n\n let userDoc: TUser | undefined;\n\n // 4. Status check (default: block suspended + banned; skipped if allowAnyStatus)\n if (requirements.allowAnyStatus !== true) {\n if (!userSnap || !userSnap.exists) {\n throw new AuthAssertionError(\"not-found\", \"User profile not found\");\n }\n const userData = userSnap.data() as TUser;\n const status = config.getUserStatus(userData);\n if (status === \"banned\") {\n throw new AuthAssertionError(\n \"permission-denied\",\n \"Account is not in good standing\"\n );\n }\n if (status === \"suspended\" && requirements.allowSuspended !== true) {\n throw new AuthAssertionError(\n \"permission-denied\",\n \"Account is not in good standing\"\n );\n }\n userDoc = userData;\n }\n\n // 5. Acceptance-level gate. Runs only when the app configured one and the callable did\n // not opt out. The required level comes from the app's cheap (cached) reader and the\n // accepted level from the verified token, so the gate adds no per-call Firestore read.\n // It sits after the status check so a banned caller still gets the standing refusal.\n if (config.acceptance !== undefined && requirements.allowUnaccepted !== true) {\n const requiredLevel = normalizeAcceptanceLevel(await config.acceptance.requiredLevel());\n if (requiredLevel > 0) {\n const acceptedLevel = normalizeAcceptanceLevel(\n (token as Record<string, unknown>)[config.acceptance.claimKey]\n );\n if (acceptedLevel < requiredLevel) {\n throw new AuthAssertionError(\n \"failed-precondition\",\n \"The current documents must be accepted before continuing\",\n { reason: ACCEPTANCE_REQUIRED_REASON, requiredLevel, acceptedLevel }\n );\n }\n }\n }\n\n // 6. Admin check (delegates to config.requireAdmin)\n let adminResult: TAdmin | undefined;\n if (requirements.admin !== undefined) {\n adminResult = await config.requireAdmin(uid, token, requirements.admin);\n }\n\n // 7. Return context with whatever docs were fetched\n const ctx: AuthContext<TUser, TAdmin> = { uid, token };\n if (userDoc !== undefined) {\n ctx.userDoc = userDoc;\n }\n if (requirements.admin !== undefined) {\n ctx.admin = adminResult as TAdmin;\n }\n return ctx;\n };\n}\n"]}
@@ -6,6 +6,7 @@ import { describe, it, expect, vi, beforeEach } from "vitest";
6
6
  import { createAssertAuth } from "./assertAuth.js";
7
7
  import { AuthAssertionError } from "./authError.js";
8
8
  import { ACCEPTANCE_REQUIRED_REASON, isAcceptanceRequiredError } from "../acceptance.js";
9
+ import { EMAIL_VERIFICATION_REQUIRED_REASON, isEmailVerificationRequiredError, } from "../email-verification.js";
9
10
  function makeRequest(opts) {
10
11
  const uid = opts?.uid ?? "uid-123";
11
12
  return {
@@ -109,6 +110,20 @@ describe("Email verification", () => {
109
110
  allowAnyStatus: true,
110
111
  })).rejects.toMatchObject({ code: "failed-precondition" });
111
112
  });
113
+ it("carries the email-verification reason in details, with the message unchanged", async () => {
114
+ const err = await assertAuth(makeRequest({ emailVerified: false }), {
115
+ emailVerified: true,
116
+ allowAnyStatus: true,
117
+ }).catch((e) => e);
118
+ expect(err).toBeInstanceOf(AuthAssertionError);
119
+ expect(err).toMatchObject({
120
+ code: "failed-precondition",
121
+ message: "Email must be verified",
122
+ details: { reason: EMAIL_VERIFICATION_REQUIRED_REASON },
123
+ });
124
+ expect(isEmailVerificationRequiredError(err)).toBe(true);
125
+ expect(isAcceptanceRequiredError(err)).toBe(false);
126
+ });
112
127
  it("succeeds when emailVerified required and token email_verified is true", async () => {
113
128
  await expect(assertAuth(makeRequest({ emailVerified: true }), {
114
129
  emailVerified: true,
@@ -277,6 +292,7 @@ describe("Acceptance-level gate", () => {
277
292
  details: { reason: ACCEPTANCE_REQUIRED_REASON, requiredLevel: 3, acceptedLevel: 2 },
278
293
  });
279
294
  expect(isAcceptanceRequiredError(err)).toBe(true);
295
+ expect(isEmailVerificationRequiredError(err)).toBe(false);
280
296
  });
281
297
  it("treats a missing claim as accepted level 0", async () => {
282
298
  const { assert } = gatedAssertAuth(() => 1);
@@ -342,8 +358,16 @@ describe("Acceptance-level gate", () => {
342
358
  });
343
359
  });
344
360
  it("leaves `details` undefined on every other rejection", async () => {
345
- const err = (await assertAuth(makeUnauthRequest()).catch((e) => e));
346
- expect(err.details).toBeUndefined();
361
+ const unauthenticated = (await assertAuth(makeUnauthRequest()).catch((e) => e));
362
+ expect(unauthenticated.details).toBeUndefined();
363
+ mockUserGet.mockResolvedValue({ exists: false, data: () => null });
364
+ const missingProfile = (await assertAuth(makeRequest()).catch((e) => e));
365
+ expect(missingProfile.code).toBe("not-found");
366
+ expect(missingProfile.details).toBeUndefined();
367
+ mockUserGet.mockResolvedValue({ exists: true, data: () => ({ ...activeUserDoc, status: "banned" }) });
368
+ const banned = (await assertAuth(makeRequest()).catch((e) => e));
369
+ expect(banned.code).toBe("permission-denied");
370
+ expect(banned.details).toBeUndefined();
347
371
  });
348
372
  });
349
373
  //# sourceMappingURL=assertAuth.test.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"assertAuth.test.js","sourceRoot":"","sources":["../../src/server/assertAuth.test.ts"],"names":[],"mappings":"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","sourcesContent":["import { describe, it, expect, vi, beforeEach } from \"vitest\";\n\n// NOTE: there is deliberately NO `vi.mock(\"firebase-functions/v2/https\")` here.\n// assertAuth no longer constructs HttpsError — it throws AuthAssertionError and the\n// consuming app maps it (see authError.ts for why). The only remaining reference to\n// firebase-functions in assertAuth.ts is a TYPE import, which is erased at runtime.\nimport { createAssertAuth } from \"./assertAuth.js\";\nimport { AuthAssertionError } from \"./authError.js\";\nimport { ACCEPTANCE_REQUIRED_REASON, isAcceptanceRequiredError } from \"../acceptance.js\";\nimport type { AssertAuthConfig, UserStatus } from \"./types.js\";\n\ntype TestUser = {\n uid: string;\n displayName?: string;\n status?: \"active\" | \"suspended\" | \"banned\";\n};\n\n\nfunction makeRequest(opts?: { uid?: string; emailVerified?: boolean }): any {\n const uid = opts?.uid ?? \"uid-123\";\n return {\n data: {},\n rawRequest: {},\n auth: {\n uid,\n token: { uid, email_verified: opts?.emailVerified ?? true },\n },\n };\n}\n\nfunction makeUnauthRequest(): any {\n return { data: {}, rawRequest: {}, auth: undefined };\n}\n\nconst activeUserDoc: TestUser = {\n uid: \"uid-123\",\n displayName: \"Test User\",\n status: \"active\",\n};\n\n// Minimal Firestore stub returning the active user doc, for tests that build\n// their own config instead of using the shared beforeEach harness.\nfunction fakeDbWithUser(): any {\n const userDocRef = {\n get: vi.fn().mockResolvedValue({ exists: true, data: () => activeUserDoc }),\n };\n return { doc: vi.fn(() => userDocRef) };\n}\n\n// --- harness ---------------------------------------------------------------\n\nlet mockUserGet: ReturnType<typeof vi.fn>;\nlet mockRequireAdmin: ReturnType<typeof vi.fn>;\nlet mockDbDoc: ReturnType<typeof vi.fn>;\nlet assertAuth: ReturnType<typeof createAssertAuth<TestUser>>;\n\nbeforeEach(() => {\n mockUserGet = vi.fn().mockResolvedValue({ exists: true, data: () => activeUserDoc });\n mockRequireAdmin = vi.fn().mockResolvedValue(undefined);\n\n const userDocRef = { get: mockUserGet };\n\n mockDbDoc = vi.fn(() => userDocRef);\n\n const fakeDb = { doc: mockDbDoc } as any;\n\n const config: AssertAuthConfig<TestUser> = {\n firestore: () => fakeDb,\n userProfilePath: (uid) => `userProfiles/${uid}`,\n getUserStatus: (user): UserStatus => {\n if (user.status === \"banned\") return \"banned\";\n if (user.status === \"suspended\") return \"suspended\";\n return \"active\";\n },\n requireAdmin: mockRequireAdmin as any,\n };\n\n assertAuth = createAssertAuth<TestUser>(config);\n});\n\n// --- tests -----------------------------------------------------------------\n\n// REGRESSION GUARD (2026-07-20). assertAuth must throw a plain, framework-free\n// AuthAssertionError — never an HttpsError built inside this package. firebase-functions\n// 7.2.5 ships two distinct HttpsError classes (ESM entry vs the CJS callable runtime), so\n// a package-built HttpsError fails the runtime's `instanceof` check and onCall converts a\n// correct 400 into a 500 INTERNAL. Since assertAuth is the first line of every callable,\n// that silently broke EVERY auth rejection in deployed environments. If someone\n// reintroduces `new HttpsError(...)` here, these assertions fail.\ndescribe(\"throws a framework-free typed error (never a package-built HttpsError)\", () => {\n it(\"rejects with an AuthAssertionError instance carrying the HttpsError code string\", async () => {\n const err = await assertAuth(makeUnauthRequest()).catch((e: unknown) => e);\n expect(err).toBeInstanceOf(AuthAssertionError);\n expect((err as AuthAssertionError).name).toBe(\"AuthAssertionError\");\n expect((err as AuthAssertionError).code).toBe(\"unauthenticated\");\n });\n\n it(\"does not decorate the error with HttpsError's own shape\", async () => {\n const err = (await assertAuth(makeUnauthRequest()).catch(\n (e: unknown) => e\n )) as AuthAssertionError & { httpErrorCode?: unknown; toJSON?: unknown };\n // `httpErrorCode`/`toJSON` are HttpsError internals — their absence proves the\n // package is not constructing one, which is the whole point of the fix.\n expect(err.httpErrorCode).toBeUndefined();\n expect(err.toJSON).toBeUndefined();\n });\n\n it(\"uses AuthAssertionError for the email-verification rejection too\", async () => {\n const err = await assertAuth(makeRequest({ emailVerified: false }), {\n emailVerified: true,\n allowAnyStatus: true,\n }).catch((e: unknown) => e);\n expect(err).toBeInstanceOf(AuthAssertionError);\n expect((err as AuthAssertionError).code).toBe(\"failed-precondition\");\n });\n});\n\ndescribe(\"Authentication\", () => {\n it(\"throws unauthenticated when request.auth is absent\", async () => {\n await expect(assertAuth(makeUnauthRequest())).rejects.toMatchObject({\n code: \"unauthenticated\",\n });\n });\n\n it(\"succeeds with no Firestore reads when only allowAnyStatus is set\", async () => {\n const ctx = await assertAuth(makeRequest(), { allowAnyStatus: true });\n expect(ctx.uid).toBe(\"uid-123\");\n expect(ctx.userDoc).toBeUndefined();\n expect(mockDbDoc).not.toHaveBeenCalled();\n });\n});\n\ndescribe(\"Email verification\", () => {\n it(\"throws failed-precondition when emailVerified required and token email_verified is false\", async () => {\n await expect(\n assertAuth(makeRequest({ emailVerified: false }), {\n emailVerified: true,\n allowAnyStatus: true,\n })\n ).rejects.toMatchObject({ code: \"failed-precondition\" });\n });\n\n it(\"succeeds when emailVerified required and token email_verified is true\", async () => {\n await expect(\n assertAuth(makeRequest({ emailVerified: true }), {\n emailVerified: true,\n allowAnyStatus: true,\n })\n ).resolves.toMatchObject({ uid: \"uid-123\" });\n });\n\n it(\"succeeds when allowUnverified is true even if email_verified is false\", async () => {\n await expect(\n assertAuth(makeRequest({ emailVerified: false }), {\n emailVerified: true,\n allowUnverified: true,\n allowAnyStatus: true,\n })\n ).resolves.toMatchObject({ uid: \"uid-123\" });\n });\n});\n\ndescribe(\"Not-banned check (default behavior)\", () => {\n it(\"throws permission-denied when user status is banned\", async () => {\n mockUserGet.mockResolvedValue({\n exists: true,\n data: () => ({ ...activeUserDoc, status: \"banned\" }),\n });\n await expect(assertAuth(makeRequest())).rejects.toMatchObject({\n code: \"permission-denied\",\n });\n });\n\n it(\"throws permission-denied when user status is suspended\", async () => {\n mockUserGet.mockResolvedValue({\n exists: true,\n data: () => ({ ...activeUserDoc, status: \"suspended\" }),\n });\n await expect(assertAuth(makeRequest())).rejects.toMatchObject({\n code: \"permission-denied\",\n });\n });\n\n it(\"throws not-found when user doc does not exist\", async () => {\n mockUserGet.mockResolvedValue({ exists: false, data: () => null });\n await expect(assertAuth(makeRequest())).rejects.toMatchObject({\n code: \"not-found\",\n });\n });\n\n it(\"succeeds when user doc has no status field\", async () => {\n mockUserGet.mockResolvedValue({\n exists: true,\n data: () => ({ uid: \"uid-123\", displayName: \"Test\" }),\n });\n const ctx = await assertAuth(makeRequest());\n expect(ctx.uid).toBe(\"uid-123\");\n expect(ctx.userDoc).toBeDefined();\n });\n\n it(\"succeeds when user status is active\", async () => {\n const ctx = await assertAuth(makeRequest());\n expect(ctx.uid).toBe(\"uid-123\");\n expect(ctx.userDoc).toMatchObject({ status: \"active\" });\n });\n\n it(\"skips user doc read entirely when allowAnyStatus is true\", async () => {\n await assertAuth(makeRequest(), { allowAnyStatus: true });\n expect(mockUserGet).not.toHaveBeenCalled();\n });\n});\n\ndescribe(\"allowSuspended opt-in\", () => {\n it(\"allows a suspended user when allowSuspended is true\", async () => {\n mockUserGet.mockResolvedValue({\n exists: true,\n data: () => ({ ...activeUserDoc, status: \"suspended\" }),\n });\n const ctx = await assertAuth(makeRequest(), { allowSuspended: true });\n expect(ctx.uid).toBe(\"uid-123\");\n expect(ctx.userDoc).toMatchObject({ status: \"suspended\" });\n });\n\n it(\"still blocks a banned user even when allowSuspended is true\", async () => {\n mockUserGet.mockResolvedValue({\n exists: true,\n data: () => ({ ...activeUserDoc, status: \"banned\" }),\n });\n await expect(\n assertAuth(makeRequest(), { allowSuspended: true })\n ).rejects.toMatchObject({ code: \"permission-denied\" });\n });\n\n it(\"still requires the user doc to exist when allowSuspended is true\", async () => {\n mockUserGet.mockResolvedValue({ exists: false, data: () => null });\n await expect(\n assertAuth(makeRequest(), { allowSuspended: true })\n ).rejects.toMatchObject({ code: \"not-found\" });\n });\n});\n\n\ndescribe(\"Admin check\", () => {\n it(\"propagates the error when requireAdmin throws\", async () => {\n mockRequireAdmin.mockRejectedValue(\n Object.assign(new Error(\"Administrator access required\"), {\n code: \"permission-denied\",\n })\n );\n await expect(\n assertAuth(makeRequest(), {\n admin: { allowJrAdmin: false },\n allowAnyStatus: true,\n })\n ).rejects.toMatchObject({ code: \"permission-denied\" });\n });\n\n it(\"succeeds and returns ctx when requireAdmin resolves\", async () => {\n mockRequireAdmin.mockResolvedValue(undefined);\n const ctx = await assertAuth(makeRequest(), {\n admin: { allowJrAdmin: true },\n allowAnyStatus: true,\n });\n expect(ctx.uid).toBe(\"uid-123\");\n expect(mockRequireAdmin).toHaveBeenCalledWith(\n \"uid-123\",\n expect.any(Object),\n { allowJrAdmin: true }\n );\n });\n\n it(\"populates ctx.admin with the requireAdmin result when an admin check runs\", async () => {\n type AdminResult = { role: string };\n const adminMock = vi.fn().mockResolvedValue({ role: \"superadmin\" });\n const config: AssertAuthConfig<TestUser, AdminResult> = {\n firestore: () => fakeDbWithUser(),\n userProfilePath: (uid) => `userProfiles/${uid}`,\n getUserStatus: (user): UserStatus => {\n if (user.status === \"banned\") return \"banned\";\n if (user.status === \"suspended\") return \"suspended\";\n return \"active\";\n },\n requireAdmin: adminMock as any,\n };\n const typedAssertAuth = createAssertAuth<TestUser, AdminResult>(config);\n\n const ctx = await typedAssertAuth(makeRequest(), {\n admin: { allowJrAdmin: true },\n });\n\n expect(ctx.admin).toEqual({ role: \"superadmin\" });\n });\n\n it(\"leaves ctx.admin undefined when no admin requirement is set\", async () => {\n const ctx = await assertAuth(makeRequest());\n expect(ctx.admin).toBeUndefined();\n expect(mockRequireAdmin).not.toHaveBeenCalled();\n });\n});\n\ndescribe(\"Combined checks\", () => {\n it(\"returns fully populated ctx when all checks pass\", async () => {\n mockUserGet.mockResolvedValue({ exists: true, data: () => activeUserDoc });\n mockRequireAdmin.mockResolvedValue(undefined);\n\n const ctx = await assertAuth(makeRequest({ emailVerified: true }), {\n emailVerified: true,\n admin: { allowJrAdmin: true },\n });\n\n expect(ctx.uid).toBe(\"uid-123\");\n expect(ctx.userDoc).toMatchObject({ status: \"active\" });\n expect(mockRequireAdmin).toHaveBeenCalled();\n });\n});\n\n// Acceptance-level gate (optional `acceptance` config). Every case below that expects a\n// refusal passes on the pre-gate factory only if the gate is ignored — i.e. it fails there.\ndescribe(\"Acceptance-level gate\", () => {\n const CLAIM = \"acceptedLevelClaim\";\n\n function gatedAssertAuth(requiredLevel: () => number | Promise<number>) {\n const requireAdmin = vi.fn().mockResolvedValue(\"admin\");\n const config: AssertAuthConfig<TestUser, string> = {\n firestore: () => fakeDbWithUser(),\n userProfilePath: (uid) => `userProfiles/${uid}`,\n getUserStatus: (user): UserStatus => (user.status === \"banned\" ? \"banned\" : \"active\"),\n requireAdmin: requireAdmin as any,\n acceptance: { claimKey: CLAIM, requiredLevel },\n };\n return { assert: createAssertAuth<TestUser, string>(config), requireAdmin };\n }\n\n function requestWithClaim(claim: unknown): any {\n const req = makeRequest();\n if (claim !== undefined) req.auth.token[CLAIM] = claim;\n return req;\n }\n\n it(\"refuses a caller below the required level with a structured failed-precondition\", async () => {\n const { assert } = gatedAssertAuth(() => 3);\n const err = await assert(requestWithClaim(2)).catch((e: unknown) => e);\n expect(err).toBeInstanceOf(AuthAssertionError);\n expect(err).toMatchObject({\n code: \"failed-precondition\",\n details: { reason: ACCEPTANCE_REQUIRED_REASON, requiredLevel: 3, acceptedLevel: 2 },\n });\n expect(isAcceptanceRequiredError(err)).toBe(true);\n });\n\n it(\"treats a missing claim as accepted level 0\", async () => {\n const { assert } = gatedAssertAuth(() => 1);\n await expect(assert(requestWithClaim(undefined))).rejects.toMatchObject({\n details: { reason: ACCEPTANCE_REQUIRED_REASON, requiredLevel: 1, acceptedLevel: 0 },\n });\n });\n\n it.each([[\"a string\", \"5\"], [\"NaN\", Number.NaN], [\"a negative\", -4], [\"null\", null]])(\n \"treats %s claim as accepted level 0 (fails closed)\",\n async (_label, claim) => {\n const { assert } = gatedAssertAuth(() => 1);\n await expect(assert(requestWithClaim(claim))).rejects.toMatchObject({\n details: { acceptedLevel: 0 },\n });\n }\n );\n\n it(\"passes a caller at or above the required level\", async () => {\n const { assert } = gatedAssertAuth(() => 3);\n await expect(assert(requestWithClaim(3))).resolves.toMatchObject({ uid: \"uid-123\" });\n await expect(assert(requestWithClaim(7))).resolves.toMatchObject({ uid: \"uid-123\" });\n });\n\n it(\"awaits an async required-level reader\", async () => {\n const { assert } = gatedAssertAuth(async () => 2);\n await expect(assert(requestWithClaim(1))).rejects.toMatchObject({ code: \"failed-precondition\" });\n });\n\n it.each([[\"zero\", 0], [\"NaN\", Number.NaN], [\"a negative\", -1]])(\n \"requires nothing when the required level is %s\",\n async (_label, level) => {\n const { assert } = gatedAssertAuth(() => level);\n await expect(assert(requestWithClaim(undefined))).resolves.toMatchObject({ uid: \"uid-123\" });\n }\n );\n\n it(\"lets an allowUnaccepted callable through without reading the required level\", async () => {\n const requiredLevel = vi.fn(() => 9);\n const { assert } = gatedAssertAuth(requiredLevel);\n await expect(assert(requestWithClaim(undefined), { allowUnaccepted: true })).resolves.toMatchObject({\n uid: \"uid-123\",\n });\n expect(requiredLevel).not.toHaveBeenCalled();\n });\n\n it(\"propagates a throwing required-level reader\", async () => {\n const failure = new Error(\"config unavailable\");\n const { assert } = gatedAssertAuth(() => {\n throw failure;\n });\n await expect(assert(requestWithClaim(5))).rejects.toBe(failure);\n });\n\n it(\"refuses before the admin check runs\", async () => {\n const { assert, requireAdmin } = gatedAssertAuth(() => 2);\n await expect(\n assert(requestWithClaim(1), { admin: { allowJrAdmin: true }, allowAnyStatus: true })\n ).rejects.toMatchObject({ code: \"failed-precondition\" });\n expect(requireAdmin).not.toHaveBeenCalled();\n });\n\n it(\"still gates a callable that skips the status check (allowAnyStatus)\", async () => {\n const { assert } = gatedAssertAuth(() => 1);\n await expect(assert(requestWithClaim(0), { allowAnyStatus: true })).rejects.toMatchObject({\n code: \"failed-precondition\",\n });\n });\n\n it(\"keeps authentication first — an unauthenticated caller never reaches the gate\", async () => {\n const requiredLevel = vi.fn(() => 1);\n const { assert } = gatedAssertAuth(requiredLevel);\n await expect(assert(makeUnauthRequest())).rejects.toMatchObject({ code: \"unauthenticated\" });\n expect(requiredLevel).not.toHaveBeenCalled();\n });\n\n it(\"is absent without config: an app that never sets `acceptance` behaves exactly as before\", async () => {\n // The shared harness config has no `acceptance`; a token with no level claim still passes.\n await expect(assertAuth(makeRequest(), { allowUnaccepted: false })).resolves.toMatchObject({\n uid: \"uid-123\",\n });\n });\n\n it(\"leaves `details` undefined on every other rejection\", async () => {\n const err = (await assertAuth(makeUnauthRequest()).catch((e: unknown) => e)) as AuthAssertionError;\n expect(err.details).toBeUndefined();\n });\n});\n"]}
1
+ {"version":3,"file":"assertAuth.test.js","sourceRoot":"","sources":["../../src/server/assertAuth.test.ts"],"names":[],"mappings":"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","sourcesContent":["import { describe, it, expect, vi, beforeEach } from \"vitest\";\n\n// NOTE: there is deliberately NO `vi.mock(\"firebase-functions/v2/https\")` here.\n// assertAuth no longer constructs HttpsError — it throws AuthAssertionError and the\n// consuming app maps it (see authError.ts for why). The only remaining reference to\n// firebase-functions in assertAuth.ts is a TYPE import, which is erased at runtime.\nimport { createAssertAuth } from \"./assertAuth.js\";\nimport { AuthAssertionError } from \"./authError.js\";\nimport { ACCEPTANCE_REQUIRED_REASON, isAcceptanceRequiredError } from \"../acceptance.js\";\nimport {\n EMAIL_VERIFICATION_REQUIRED_REASON,\n isEmailVerificationRequiredError,\n} from \"../email-verification.js\";\nimport type { AssertAuthConfig, UserStatus } from \"./types.js\";\n\ntype TestUser = {\n uid: string;\n displayName?: string;\n status?: \"active\" | \"suspended\" | \"banned\";\n};\n\n\nfunction makeRequest(opts?: { uid?: string; emailVerified?: boolean }): any {\n const uid = opts?.uid ?? \"uid-123\";\n return {\n data: {},\n rawRequest: {},\n auth: {\n uid,\n token: { uid, email_verified: opts?.emailVerified ?? true },\n },\n };\n}\n\nfunction makeUnauthRequest(): any {\n return { data: {}, rawRequest: {}, auth: undefined };\n}\n\nconst activeUserDoc: TestUser = {\n uid: \"uid-123\",\n displayName: \"Test User\",\n status: \"active\",\n};\n\n// Minimal Firestore stub returning the active user doc, for tests that build\n// their own config instead of using the shared beforeEach harness.\nfunction fakeDbWithUser(): any {\n const userDocRef = {\n get: vi.fn().mockResolvedValue({ exists: true, data: () => activeUserDoc }),\n };\n return { doc: vi.fn(() => userDocRef) };\n}\n\n// --- harness ---------------------------------------------------------------\n\nlet mockUserGet: ReturnType<typeof vi.fn>;\nlet mockRequireAdmin: ReturnType<typeof vi.fn>;\nlet mockDbDoc: ReturnType<typeof vi.fn>;\nlet assertAuth: ReturnType<typeof createAssertAuth<TestUser>>;\n\nbeforeEach(() => {\n mockUserGet = vi.fn().mockResolvedValue({ exists: true, data: () => activeUserDoc });\n mockRequireAdmin = vi.fn().mockResolvedValue(undefined);\n\n const userDocRef = { get: mockUserGet };\n\n mockDbDoc = vi.fn(() => userDocRef);\n\n const fakeDb = { doc: mockDbDoc } as any;\n\n const config: AssertAuthConfig<TestUser> = {\n firestore: () => fakeDb,\n userProfilePath: (uid) => `userProfiles/${uid}`,\n getUserStatus: (user): UserStatus => {\n if (user.status === \"banned\") return \"banned\";\n if (user.status === \"suspended\") return \"suspended\";\n return \"active\";\n },\n requireAdmin: mockRequireAdmin as any,\n };\n\n assertAuth = createAssertAuth<TestUser>(config);\n});\n\n// --- tests -----------------------------------------------------------------\n\n// REGRESSION GUARD (2026-07-20). assertAuth must throw a plain, framework-free\n// AuthAssertionError — never an HttpsError built inside this package. firebase-functions\n// 7.2.5 ships two distinct HttpsError classes (ESM entry vs the CJS callable runtime), so\n// a package-built HttpsError fails the runtime's `instanceof` check and onCall converts a\n// correct 400 into a 500 INTERNAL. Since assertAuth is the first line of every callable,\n// that silently broke EVERY auth rejection in deployed environments. If someone\n// reintroduces `new HttpsError(...)` here, these assertions fail.\ndescribe(\"throws a framework-free typed error (never a package-built HttpsError)\", () => {\n it(\"rejects with an AuthAssertionError instance carrying the HttpsError code string\", async () => {\n const err = await assertAuth(makeUnauthRequest()).catch((e: unknown) => e);\n expect(err).toBeInstanceOf(AuthAssertionError);\n expect((err as AuthAssertionError).name).toBe(\"AuthAssertionError\");\n expect((err as AuthAssertionError).code).toBe(\"unauthenticated\");\n });\n\n it(\"does not decorate the error with HttpsError's own shape\", async () => {\n const err = (await assertAuth(makeUnauthRequest()).catch(\n (e: unknown) => e\n )) as AuthAssertionError & { httpErrorCode?: unknown; toJSON?: unknown };\n // `httpErrorCode`/`toJSON` are HttpsError internals — their absence proves the\n // package is not constructing one, which is the whole point of the fix.\n expect(err.httpErrorCode).toBeUndefined();\n expect(err.toJSON).toBeUndefined();\n });\n\n it(\"uses AuthAssertionError for the email-verification rejection too\", async () => {\n const err = await assertAuth(makeRequest({ emailVerified: false }), {\n emailVerified: true,\n allowAnyStatus: true,\n }).catch((e: unknown) => e);\n expect(err).toBeInstanceOf(AuthAssertionError);\n expect((err as AuthAssertionError).code).toBe(\"failed-precondition\");\n });\n});\n\ndescribe(\"Authentication\", () => {\n it(\"throws unauthenticated when request.auth is absent\", async () => {\n await expect(assertAuth(makeUnauthRequest())).rejects.toMatchObject({\n code: \"unauthenticated\",\n });\n });\n\n it(\"succeeds with no Firestore reads when only allowAnyStatus is set\", async () => {\n const ctx = await assertAuth(makeRequest(), { allowAnyStatus: true });\n expect(ctx.uid).toBe(\"uid-123\");\n expect(ctx.userDoc).toBeUndefined();\n expect(mockDbDoc).not.toHaveBeenCalled();\n });\n});\n\ndescribe(\"Email verification\", () => {\n it(\"throws failed-precondition when emailVerified required and token email_verified is false\", async () => {\n await expect(\n assertAuth(makeRequest({ emailVerified: false }), {\n emailVerified: true,\n allowAnyStatus: true,\n })\n ).rejects.toMatchObject({ code: \"failed-precondition\" });\n });\n\n it(\"carries the email-verification reason in details, with the message unchanged\", async () => {\n const err = await assertAuth(makeRequest({ emailVerified: false }), {\n emailVerified: true,\n allowAnyStatus: true,\n }).catch((e: unknown) => e);\n expect(err).toBeInstanceOf(AuthAssertionError);\n expect(err).toMatchObject({\n code: \"failed-precondition\",\n message: \"Email must be verified\",\n details: { reason: EMAIL_VERIFICATION_REQUIRED_REASON },\n });\n expect(isEmailVerificationRequiredError(err)).toBe(true);\n expect(isAcceptanceRequiredError(err)).toBe(false);\n });\n\n it(\"succeeds when emailVerified required and token email_verified is true\", async () => {\n await expect(\n assertAuth(makeRequest({ emailVerified: true }), {\n emailVerified: true,\n allowAnyStatus: true,\n })\n ).resolves.toMatchObject({ uid: \"uid-123\" });\n });\n\n it(\"succeeds when allowUnverified is true even if email_verified is false\", async () => {\n await expect(\n assertAuth(makeRequest({ emailVerified: false }), {\n emailVerified: true,\n allowUnverified: true,\n allowAnyStatus: true,\n })\n ).resolves.toMatchObject({ uid: \"uid-123\" });\n });\n});\n\ndescribe(\"Not-banned check (default behavior)\", () => {\n it(\"throws permission-denied when user status is banned\", async () => {\n mockUserGet.mockResolvedValue({\n exists: true,\n data: () => ({ ...activeUserDoc, status: \"banned\" }),\n });\n await expect(assertAuth(makeRequest())).rejects.toMatchObject({\n code: \"permission-denied\",\n });\n });\n\n it(\"throws permission-denied when user status is suspended\", async () => {\n mockUserGet.mockResolvedValue({\n exists: true,\n data: () => ({ ...activeUserDoc, status: \"suspended\" }),\n });\n await expect(assertAuth(makeRequest())).rejects.toMatchObject({\n code: \"permission-denied\",\n });\n });\n\n it(\"throws not-found when user doc does not exist\", async () => {\n mockUserGet.mockResolvedValue({ exists: false, data: () => null });\n await expect(assertAuth(makeRequest())).rejects.toMatchObject({\n code: \"not-found\",\n });\n });\n\n it(\"succeeds when user doc has no status field\", async () => {\n mockUserGet.mockResolvedValue({\n exists: true,\n data: () => ({ uid: \"uid-123\", displayName: \"Test\" }),\n });\n const ctx = await assertAuth(makeRequest());\n expect(ctx.uid).toBe(\"uid-123\");\n expect(ctx.userDoc).toBeDefined();\n });\n\n it(\"succeeds when user status is active\", async () => {\n const ctx = await assertAuth(makeRequest());\n expect(ctx.uid).toBe(\"uid-123\");\n expect(ctx.userDoc).toMatchObject({ status: \"active\" });\n });\n\n it(\"skips user doc read entirely when allowAnyStatus is true\", async () => {\n await assertAuth(makeRequest(), { allowAnyStatus: true });\n expect(mockUserGet).not.toHaveBeenCalled();\n });\n});\n\ndescribe(\"allowSuspended opt-in\", () => {\n it(\"allows a suspended user when allowSuspended is true\", async () => {\n mockUserGet.mockResolvedValue({\n exists: true,\n data: () => ({ ...activeUserDoc, status: \"suspended\" }),\n });\n const ctx = await assertAuth(makeRequest(), { allowSuspended: true });\n expect(ctx.uid).toBe(\"uid-123\");\n expect(ctx.userDoc).toMatchObject({ status: \"suspended\" });\n });\n\n it(\"still blocks a banned user even when allowSuspended is true\", async () => {\n mockUserGet.mockResolvedValue({\n exists: true,\n data: () => ({ ...activeUserDoc, status: \"banned\" }),\n });\n await expect(\n assertAuth(makeRequest(), { allowSuspended: true })\n ).rejects.toMatchObject({ code: \"permission-denied\" });\n });\n\n it(\"still requires the user doc to exist when allowSuspended is true\", async () => {\n mockUserGet.mockResolvedValue({ exists: false, data: () => null });\n await expect(\n assertAuth(makeRequest(), { allowSuspended: true })\n ).rejects.toMatchObject({ code: \"not-found\" });\n });\n});\n\n\ndescribe(\"Admin check\", () => {\n it(\"propagates the error when requireAdmin throws\", async () => {\n mockRequireAdmin.mockRejectedValue(\n Object.assign(new Error(\"Administrator access required\"), {\n code: \"permission-denied\",\n })\n );\n await expect(\n assertAuth(makeRequest(), {\n admin: { allowJrAdmin: false },\n allowAnyStatus: true,\n })\n ).rejects.toMatchObject({ code: \"permission-denied\" });\n });\n\n it(\"succeeds and returns ctx when requireAdmin resolves\", async () => {\n mockRequireAdmin.mockResolvedValue(undefined);\n const ctx = await assertAuth(makeRequest(), {\n admin: { allowJrAdmin: true },\n allowAnyStatus: true,\n });\n expect(ctx.uid).toBe(\"uid-123\");\n expect(mockRequireAdmin).toHaveBeenCalledWith(\n \"uid-123\",\n expect.any(Object),\n { allowJrAdmin: true }\n );\n });\n\n it(\"populates ctx.admin with the requireAdmin result when an admin check runs\", async () => {\n type AdminResult = { role: string };\n const adminMock = vi.fn().mockResolvedValue({ role: \"superadmin\" });\n const config: AssertAuthConfig<TestUser, AdminResult> = {\n firestore: () => fakeDbWithUser(),\n userProfilePath: (uid) => `userProfiles/${uid}`,\n getUserStatus: (user): UserStatus => {\n if (user.status === \"banned\") return \"banned\";\n if (user.status === \"suspended\") return \"suspended\";\n return \"active\";\n },\n requireAdmin: adminMock as any,\n };\n const typedAssertAuth = createAssertAuth<TestUser, AdminResult>(config);\n\n const ctx = await typedAssertAuth(makeRequest(), {\n admin: { allowJrAdmin: true },\n });\n\n expect(ctx.admin).toEqual({ role: \"superadmin\" });\n });\n\n it(\"leaves ctx.admin undefined when no admin requirement is set\", async () => {\n const ctx = await assertAuth(makeRequest());\n expect(ctx.admin).toBeUndefined();\n expect(mockRequireAdmin).not.toHaveBeenCalled();\n });\n});\n\ndescribe(\"Combined checks\", () => {\n it(\"returns fully populated ctx when all checks pass\", async () => {\n mockUserGet.mockResolvedValue({ exists: true, data: () => activeUserDoc });\n mockRequireAdmin.mockResolvedValue(undefined);\n\n const ctx = await assertAuth(makeRequest({ emailVerified: true }), {\n emailVerified: true,\n admin: { allowJrAdmin: true },\n });\n\n expect(ctx.uid).toBe(\"uid-123\");\n expect(ctx.userDoc).toMatchObject({ status: \"active\" });\n expect(mockRequireAdmin).toHaveBeenCalled();\n });\n});\n\n// Acceptance-level gate (optional `acceptance` config). Every case below that expects a\n// refusal passes on the pre-gate factory only if the gate is ignored — i.e. it fails there.\ndescribe(\"Acceptance-level gate\", () => {\n const CLAIM = \"acceptedLevelClaim\";\n\n function gatedAssertAuth(requiredLevel: () => number | Promise<number>) {\n const requireAdmin = vi.fn().mockResolvedValue(\"admin\");\n const config: AssertAuthConfig<TestUser, string> = {\n firestore: () => fakeDbWithUser(),\n userProfilePath: (uid) => `userProfiles/${uid}`,\n getUserStatus: (user): UserStatus => (user.status === \"banned\" ? \"banned\" : \"active\"),\n requireAdmin: requireAdmin as any,\n acceptance: { claimKey: CLAIM, requiredLevel },\n };\n return { assert: createAssertAuth<TestUser, string>(config), requireAdmin };\n }\n\n function requestWithClaim(claim: unknown): any {\n const req = makeRequest();\n if (claim !== undefined) req.auth.token[CLAIM] = claim;\n return req;\n }\n\n it(\"refuses a caller below the required level with a structured failed-precondition\", async () => {\n const { assert } = gatedAssertAuth(() => 3);\n const err = await assert(requestWithClaim(2)).catch((e: unknown) => e);\n expect(err).toBeInstanceOf(AuthAssertionError);\n expect(err).toMatchObject({\n code: \"failed-precondition\",\n details: { reason: ACCEPTANCE_REQUIRED_REASON, requiredLevel: 3, acceptedLevel: 2 },\n });\n expect(isAcceptanceRequiredError(err)).toBe(true);\n expect(isEmailVerificationRequiredError(err)).toBe(false);\n });\n\n it(\"treats a missing claim as accepted level 0\", async () => {\n const { assert } = gatedAssertAuth(() => 1);\n await expect(assert(requestWithClaim(undefined))).rejects.toMatchObject({\n details: { reason: ACCEPTANCE_REQUIRED_REASON, requiredLevel: 1, acceptedLevel: 0 },\n });\n });\n\n it.each([[\"a string\", \"5\"], [\"NaN\", Number.NaN], [\"a negative\", -4], [\"null\", null]])(\n \"treats %s claim as accepted level 0 (fails closed)\",\n async (_label, claim) => {\n const { assert } = gatedAssertAuth(() => 1);\n await expect(assert(requestWithClaim(claim))).rejects.toMatchObject({\n details: { acceptedLevel: 0 },\n });\n }\n );\n\n it(\"passes a caller at or above the required level\", async () => {\n const { assert } = gatedAssertAuth(() => 3);\n await expect(assert(requestWithClaim(3))).resolves.toMatchObject({ uid: \"uid-123\" });\n await expect(assert(requestWithClaim(7))).resolves.toMatchObject({ uid: \"uid-123\" });\n });\n\n it(\"awaits an async required-level reader\", async () => {\n const { assert } = gatedAssertAuth(async () => 2);\n await expect(assert(requestWithClaim(1))).rejects.toMatchObject({ code: \"failed-precondition\" });\n });\n\n it.each([[\"zero\", 0], [\"NaN\", Number.NaN], [\"a negative\", -1]])(\n \"requires nothing when the required level is %s\",\n async (_label, level) => {\n const { assert } = gatedAssertAuth(() => level);\n await expect(assert(requestWithClaim(undefined))).resolves.toMatchObject({ uid: \"uid-123\" });\n }\n );\n\n it(\"lets an allowUnaccepted callable through without reading the required level\", async () => {\n const requiredLevel = vi.fn(() => 9);\n const { assert } = gatedAssertAuth(requiredLevel);\n await expect(assert(requestWithClaim(undefined), { allowUnaccepted: true })).resolves.toMatchObject({\n uid: \"uid-123\",\n });\n expect(requiredLevel).not.toHaveBeenCalled();\n });\n\n it(\"propagates a throwing required-level reader\", async () => {\n const failure = new Error(\"config unavailable\");\n const { assert } = gatedAssertAuth(() => {\n throw failure;\n });\n await expect(assert(requestWithClaim(5))).rejects.toBe(failure);\n });\n\n it(\"refuses before the admin check runs\", async () => {\n const { assert, requireAdmin } = gatedAssertAuth(() => 2);\n await expect(\n assert(requestWithClaim(1), { admin: { allowJrAdmin: true }, allowAnyStatus: true })\n ).rejects.toMatchObject({ code: \"failed-precondition\" });\n expect(requireAdmin).not.toHaveBeenCalled();\n });\n\n it(\"still gates a callable that skips the status check (allowAnyStatus)\", async () => {\n const { assert } = gatedAssertAuth(() => 1);\n await expect(assert(requestWithClaim(0), { allowAnyStatus: true })).rejects.toMatchObject({\n code: \"failed-precondition\",\n });\n });\n\n it(\"keeps authentication first — an unauthenticated caller never reaches the gate\", async () => {\n const requiredLevel = vi.fn(() => 1);\n const { assert } = gatedAssertAuth(requiredLevel);\n await expect(assert(makeUnauthRequest())).rejects.toMatchObject({ code: \"unauthenticated\" });\n expect(requiredLevel).not.toHaveBeenCalled();\n });\n\n it(\"is absent without config: an app that never sets `acceptance` behaves exactly as before\", async () => {\n // The shared harness config has no `acceptance`; a token with no level claim still passes.\n await expect(assertAuth(makeRequest(), { allowUnaccepted: false })).resolves.toMatchObject({\n uid: \"uid-123\",\n });\n });\n\n it(\"leaves `details` undefined on every other rejection\", async () => {\n const unauthenticated = (await assertAuth(makeUnauthRequest()).catch((e: unknown) => e)) as AuthAssertionError;\n expect(unauthenticated.details).toBeUndefined();\n\n mockUserGet.mockResolvedValue({ exists: false, data: () => null });\n const missingProfile = (await assertAuth(makeRequest()).catch((e: unknown) => e)) as AuthAssertionError;\n expect(missingProfile.code).toBe(\"not-found\");\n expect(missingProfile.details).toBeUndefined();\n\n mockUserGet.mockResolvedValue({ exists: true, data: () => ({ ...activeUserDoc, status: \"banned\" }) });\n const banned = (await assertAuth(makeRequest()).catch((e: unknown) => e)) as AuthAssertionError;\n expect(banned.code).toBe(\"permission-denied\");\n expect(banned.details).toBeUndefined();\n });\n});\n"]}
@@ -1,4 +1,5 @@
1
1
  import type { AcceptanceRequiredDetails } from "../acceptance.js";
2
+ import type { EmailVerificationRequiredDetails } from "../email-verification.js";
2
3
  /**
3
4
  * Typed failure channel for `createAssertAuth`.
4
5
  *
@@ -37,15 +38,18 @@ import type { AcceptanceRequiredDetails } from "../acceptance.js";
37
38
  */
38
39
  export type AuthAssertionErrorCode = "unauthenticated" | "failed-precondition" | "not-found" | "permission-denied";
39
40
  /**
40
- * Structured details an `AuthAssertionError` may carry. Today the only one is the
41
- * acceptance refusal (`reason: 'acceptance-required'`), so the app forwards `details` as the
42
- * `HttpsError` details and the client recognizes the refusal with `isAcceptanceRequiredError`:
41
+ * Structured details an `AuthAssertionError` may carry — one per refusal the client answers
42
+ * with a surface of its own, each told apart by `reason`: the email-verification refusal
43
+ * (`reason: 'email-verification-required'`, recognized by `isEmailVerificationRequiredError`)
44
+ * and the acceptance refusal (`reason: 'acceptance-required'`, recognized by
45
+ * `isAcceptanceRequiredError`). The app forwards `details` as the `HttpsError` details so the
46
+ * client can recognize them:
43
47
  *
44
48
  * throw new HttpsError(err.code, err.message, err.details);
45
49
  *
46
50
  * Every other rejection leaves `details` undefined.
47
51
  */
48
- export type AuthAssertionErrorDetails = AcceptanceRequiredDetails;
52
+ export type AuthAssertionErrorDetails = AcceptanceRequiredDetails | EmailVerificationRequiredDetails;
49
53
  export declare class AuthAssertionError extends Error {
50
54
  readonly code: AuthAssertionErrorCode;
51
55
  readonly details?: AuthAssertionErrorDetails;
@@ -1 +1 @@
1
- {"version":3,"file":"authError.d.ts","sourceRoot":"","sources":["../../src/server/authError.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,yBAAyB,EAAE,MAAM,kBAAkB,CAAC;AAElE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AACH,MAAM,MAAM,sBAAsB,GAC9B,iBAAiB,GACjB,qBAAqB,GACrB,WAAW,GACX,mBAAmB,CAAC;AAExB;;;;;;;;GAQG;AACH,MAAM,MAAM,yBAAyB,GAAG,yBAAyB,CAAC;AAElE,qBAAa,kBAAmB,SAAQ,KAAK;IAC3C,QAAQ,CAAC,IAAI,EAAE,sBAAsB,CAAC;IACtC,QAAQ,CAAC,OAAO,CAAC,EAAE,yBAAyB,CAAC;gBAEjC,IAAI,EAAE,sBAAsB,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,yBAAyB;CAM/F"}
1
+ {"version":3,"file":"authError.d.ts","sourceRoot":"","sources":["../../src/server/authError.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,yBAAyB,EAAE,MAAM,kBAAkB,CAAC;AAClE,OAAO,KAAK,EAAE,gCAAgC,EAAE,MAAM,0BAA0B,CAAC;AAEjF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AACH,MAAM,MAAM,sBAAsB,GAC9B,iBAAiB,GACjB,qBAAqB,GACrB,WAAW,GACX,mBAAmB,CAAC;AAExB;;;;;;;;;;;GAWG;AACH,MAAM,MAAM,yBAAyB,GAAG,yBAAyB,GAAG,gCAAgC,CAAC;AAErG,qBAAa,kBAAmB,SAAQ,KAAK;IAC3C,QAAQ,CAAC,IAAI,EAAE,sBAAsB,CAAC;IACtC,QAAQ,CAAC,OAAO,CAAC,EAAE,yBAAyB,CAAC;gBAEjC,IAAI,EAAE,sBAAsB,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,yBAAyB;CAM/F"}
@@ -1 +1 @@
1
- {"version":3,"file":"authError.js","sourceRoot":"","sources":["../../src/server/authError.ts"],"names":[],"mappings":"AAuDA,MAAM,OAAO,kBAAmB,SAAQ,KAAK;IAClC,IAAI,CAAyB;IAC7B,OAAO,CAA6B;IAE7C,YAAY,IAA4B,EAAE,OAAe,EAAE,OAAmC;QAC5F,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,IAAI,GAAG,oBAAoB,CAAC;QACjC,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;QACjB,IAAI,OAAO,KAAK,SAAS;YAAE,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC;IACpD,CAAC;CACF","sourcesContent":["import type { AcceptanceRequiredDetails } from \"../acceptance.js\";\n\n/**\n * Typed failure channel for `createAssertAuth`.\n *\n * Every EXPECTED auth outcome (not signed in, email unverified, missing profile,\n * account not in good standing) throws an `AuthAssertionError` whose `code` is\n * deliberately the matching `HttpsError` code string, so a consuming Cloud\n * Function maps it 1:1 at the app boundary:\n *\n * catch (err) {\n * if (err instanceof AuthAssertionError) throw new HttpsError(err.code, err.message);\n * throw err;\n * }\n *\n * WHY THE PACKAGE MUST NOT BUILD THE HttpsError ITSELF: firebase-functions 7.2.5\n * ships TWO distinct `HttpsError` classes — the one\n * exported from the ESM entry `firebase-functions/v2/https`, and the one the CJS\n * callable runtime closes over and `instanceof`-checks at\n * `lib/common/providers/https.js`. An `HttpsError` constructed by this package's\n * ESM `dist` fails that identity check, so `onCall` logged \"Unhandled error\" and\n * replaced a correct 400 with `500 INTERNAL`. Measured:\n *\n * ESM class === CJS class ? false\n * esm-built error instanceof CJS class ? false\n * httpErrorCode.status: 400 (the error itself was correct)\n *\n * Because `assertAuth` is the first line of every callable, that turned EVERY auth\n * rejection — unauthenticated, unverified email, suspended, banned — into a generic\n * 500: users saw a generic error instead of the verify-email dialog, and monitoring\n * filled with 500s that were really expected answers. Constructing the HttpsError in\n * APP module space fixes the identity permanently and cannot regress when\n * firebase-functions repackages.\n *\n * This also brings auth-core in line with engineering-rules QUALITY-102 (generic\n * packages stay framework-agnostic; the consuming app's adapter binds the framework) and\n * mirrors `report-core`'s `ReportCoreTaskError`, which already works this way.\n */\nexport type AuthAssertionErrorCode =\n | \"unauthenticated\"\n | \"failed-precondition\"\n | \"not-found\"\n | \"permission-denied\";\n\n/**\n * Structured details an `AuthAssertionError` may carry. Today the only one is the\n * acceptance refusal (`reason: 'acceptance-required'`), so the app forwards `details` as the\n * `HttpsError` details and the client recognizes the refusal with `isAcceptanceRequiredError`:\n *\n * throw new HttpsError(err.code, err.message, err.details);\n *\n * Every other rejection leaves `details` undefined.\n */\nexport type AuthAssertionErrorDetails = AcceptanceRequiredDetails;\n\nexport class AuthAssertionError extends Error {\n readonly code: AuthAssertionErrorCode;\n readonly details?: AuthAssertionErrorDetails;\n\n constructor(code: AuthAssertionErrorCode, message: string, details?: AuthAssertionErrorDetails) {\n super(message);\n this.name = \"AuthAssertionError\";\n this.code = code;\n if (details !== undefined) this.details = details;\n }\n}\n"]}
1
+ {"version":3,"file":"authError.js","sourceRoot":"","sources":["../../src/server/authError.ts"],"names":[],"mappings":"AA2DA,MAAM,OAAO,kBAAmB,SAAQ,KAAK;IAClC,IAAI,CAAyB;IAC7B,OAAO,CAA6B;IAE7C,YAAY,IAA4B,EAAE,OAAe,EAAE,OAAmC;QAC5F,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,IAAI,GAAG,oBAAoB,CAAC;QACjC,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;QACjB,IAAI,OAAO,KAAK,SAAS;YAAE,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC;IACpD,CAAC;CACF","sourcesContent":["import type { AcceptanceRequiredDetails } from \"../acceptance.js\";\nimport type { EmailVerificationRequiredDetails } from \"../email-verification.js\";\n\n/**\n * Typed failure channel for `createAssertAuth`.\n *\n * Every EXPECTED auth outcome (not signed in, email unverified, missing profile,\n * account not in good standing) throws an `AuthAssertionError` whose `code` is\n * deliberately the matching `HttpsError` code string, so a consuming Cloud\n * Function maps it 1:1 at the app boundary:\n *\n * catch (err) {\n * if (err instanceof AuthAssertionError) throw new HttpsError(err.code, err.message);\n * throw err;\n * }\n *\n * WHY THE PACKAGE MUST NOT BUILD THE HttpsError ITSELF: firebase-functions 7.2.5\n * ships TWO distinct `HttpsError` classes — the one\n * exported from the ESM entry `firebase-functions/v2/https`, and the one the CJS\n * callable runtime closes over and `instanceof`-checks at\n * `lib/common/providers/https.js`. An `HttpsError` constructed by this package's\n * ESM `dist` fails that identity check, so `onCall` logged \"Unhandled error\" and\n * replaced a correct 400 with `500 INTERNAL`. Measured:\n *\n * ESM class === CJS class ? false\n * esm-built error instanceof CJS class ? false\n * httpErrorCode.status: 400 (the error itself was correct)\n *\n * Because `assertAuth` is the first line of every callable, that turned EVERY auth\n * rejection — unauthenticated, unverified email, suspended, banned — into a generic\n * 500: users saw a generic error instead of the verify-email dialog, and monitoring\n * filled with 500s that were really expected answers. Constructing the HttpsError in\n * APP module space fixes the identity permanently and cannot regress when\n * firebase-functions repackages.\n *\n * This also brings auth-core in line with engineering-rules QUALITY-102 (generic\n * packages stay framework-agnostic; the consuming app's adapter binds the framework) and\n * mirrors `report-core`'s `ReportCoreTaskError`, which already works this way.\n */\nexport type AuthAssertionErrorCode =\n | \"unauthenticated\"\n | \"failed-precondition\"\n | \"not-found\"\n | \"permission-denied\";\n\n/**\n * Structured details an `AuthAssertionError` may carry — one per refusal the client answers\n * with a surface of its own, each told apart by `reason`: the email-verification refusal\n * (`reason: 'email-verification-required'`, recognized by `isEmailVerificationRequiredError`)\n * and the acceptance refusal (`reason: 'acceptance-required'`, recognized by\n * `isAcceptanceRequiredError`). The app forwards `details` as the `HttpsError` details so the\n * client can recognize them:\n *\n * throw new HttpsError(err.code, err.message, err.details);\n *\n * Every other rejection leaves `details` undefined.\n */\nexport type AuthAssertionErrorDetails = AcceptanceRequiredDetails | EmailVerificationRequiredDetails;\n\nexport class AuthAssertionError extends Error {\n readonly code: AuthAssertionErrorCode;\n readonly details?: AuthAssertionErrorDetails;\n\n constructor(code: AuthAssertionErrorCode, message: string, details?: AuthAssertionErrorDetails) {\n super(message);\n this.name = \"AuthAssertionError\";\n this.code = code;\n if (details !== undefined) this.details = details;\n }\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ttt-productions/auth-core",
3
- "version": "1.6.9",
3
+ "version": "1.6.10",
4
4
  "description": "Shared Firebase authentication utilities and React hooks for TTT Productions apps",
5
5
  "repository": {
6
6
  "type": "git",