@ttt-productions/auth-core 1.6.15 → 1.6.17

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.
Files changed (88) hide show
  1. package/dist/acceptance.d.ts +0 -20
  2. package/dist/acceptance.d.ts.map +1 -1
  3. package/dist/acceptance.js +6 -26
  4. package/dist/acceptance.js.map +1 -1
  5. package/dist/{auth.d.ts → client/auth.d.ts} +1 -6
  6. package/dist/client/auth.d.ts.map +1 -0
  7. package/dist/client/auth.js +8 -0
  8. package/dist/client/auth.js.map +1 -0
  9. package/dist/client/claims.d.ts +11 -0
  10. package/dist/client/claims.d.ts.map +1 -0
  11. package/dist/client/claims.js +30 -0
  12. package/dist/client/claims.js.map +1 -0
  13. package/dist/client/index.d.ts +3 -0
  14. package/dist/client/index.d.ts.map +1 -0
  15. package/dist/client/index.js +3 -0
  16. package/dist/client/index.js.map +1 -0
  17. package/dist/email-verification.d.ts +0 -13
  18. package/dist/email-verification.d.ts.map +1 -1
  19. package/dist/email-verification.js +3 -19
  20. package/dist/email-verification.js.map +1 -1
  21. package/dist/env.d.ts +1 -1
  22. package/dist/env.js +5 -5
  23. package/dist/env.js.map +1 -1
  24. package/dist/errors.d.ts +1 -3
  25. package/dist/errors.d.ts.map +1 -1
  26. package/dist/errors.js +79 -86
  27. package/dist/errors.js.map +1 -1
  28. package/dist/index.d.ts +4 -5
  29. package/dist/index.d.ts.map +1 -1
  30. package/dist/index.js +4 -12
  31. package/dist/index.js.map +1 -1
  32. package/dist/react/AuthProvider.d.ts +3 -3
  33. package/dist/react/AuthProvider.d.ts.map +1 -1
  34. package/dist/react/AuthProvider.js +18 -36
  35. package/dist/react/AuthProvider.js.map +1 -1
  36. package/dist/react/index.d.ts +8 -8
  37. package/dist/react/index.js +6 -6
  38. package/dist/react/index.js.map +1 -1
  39. package/dist/react/redirect-storage.d.ts.map +1 -1
  40. package/dist/react/redirect-storage.js +5 -14
  41. package/dist/react/redirect-storage.js.map +1 -1
  42. package/dist/react/types.d.ts +1 -22
  43. package/dist/react/types.d.ts.map +1 -1
  44. package/dist/react/types.js.map +1 -1
  45. package/dist/react/useAuth.d.ts +1 -5
  46. package/dist/react/useAuth.d.ts.map +1 -1
  47. package/dist/react/useAuth.js +4 -8
  48. package/dist/react/useAuth.js.map +1 -1
  49. package/dist/react/useAuthGuard.d.ts +0 -33
  50. package/dist/react/useAuthGuard.d.ts.map +1 -1
  51. package/dist/react/useAuthGuard.js +7 -37
  52. package/dist/react/useAuthGuard.js.map +1 -1
  53. package/dist/react/useAuthState.d.ts +1 -1
  54. package/dist/react/useAuthState.d.ts.map +1 -1
  55. package/dist/react/useAuthState.js +2 -6
  56. package/dist/react/useAuthState.js.map +1 -1
  57. package/dist/refusal.d.ts +0 -6
  58. package/dist/refusal.d.ts.map +1 -1
  59. package/dist/refusal.js +6 -16
  60. package/dist/refusal.js.map +1 -1
  61. package/dist/server/assertAuth.d.ts +2 -2
  62. package/dist/server/assertAuth.d.ts.map +1 -1
  63. package/dist/server/assertAuth.js +13 -38
  64. package/dist/server/assertAuth.js.map +1 -1
  65. package/dist/server/authError.d.ts +3 -51
  66. package/dist/server/authError.d.ts.map +1 -1
  67. package/dist/server/authError.js +1 -1
  68. package/dist/server/authError.js.map +1 -1
  69. package/dist/server/index.d.ts +4 -4
  70. package/dist/server/index.d.ts.map +1 -1
  71. package/dist/server/index.js +2 -3
  72. package/dist/server/index.js.map +1 -1
  73. package/dist/server/types.d.ts +9 -109
  74. package/dist/server/types.d.ts.map +1 -1
  75. package/dist/server/types.js +0 -2
  76. package/dist/server/types.js.map +1 -1
  77. package/package.json +3 -3
  78. package/dist/auth.d.ts.map +0 -1
  79. package/dist/auth.js +0 -13
  80. package/dist/auth.js.map +0 -1
  81. package/dist/claims.d.ts +0 -38
  82. package/dist/claims.d.ts.map +0 -1
  83. package/dist/claims.js +0 -55
  84. package/dist/claims.js.map +0 -1
  85. package/dist/client.d.ts +0 -2
  86. package/dist/client.d.ts.map +0 -1
  87. package/dist/client.js +0 -5
  88. package/dist/client.js.map +0 -1
@@ -1,14 +1,10 @@
1
- import { useEffect, useState } from "react";
2
- import { onAuthStateChanged } from "../auth.js";
1
+ import { useEffect, useState } from 'react';
2
+ import { onAuthStateChanged } from '../client/auth.js';
3
3
  export function useAuthState(auth) {
4
- // Initialize state based on current auth status to avoid initial flash
5
4
  const [user, setUser] = useState(() => auth.currentUser);
6
5
  const [loading, setLoading] = useState(() => auth.currentUser === null);
7
6
  const [error, setError] = useState(null);
8
7
  useEffect(() => {
9
- // Only set loading if we don't have a user and haven't loaded yet
10
- // This prevents the "flash" when a user is already signed in (e.g. hydration)
11
- // or when simply refreshing the token.
12
8
  if (auth.currentUser === null) {
13
9
  setLoading(true);
14
10
  }
@@ -1 +1 @@
1
- {"version":3,"file":"useAuthState.js","sourceRoot":"","sources":["../../src/react/useAuthState.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAC;AAE5C,OAAO,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAQhD,MAAM,UAAU,YAAY,CAAC,IAAU;IACrC,uEAAuE;IACvE,MAAM,CAAC,IAAI,EAAE,OAAO,CAAC,GAAG,QAAQ,CAAc,GAAG,EAAE,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC;IACtE,MAAM,CAAC,OAAO,EAAE,UAAU,CAAC,GAAG,QAAQ,CAAU,GAAG,EAAE,CAAC,IAAI,CAAC,WAAW,KAAK,IAAI,CAAC,CAAC;IACjF,MAAM,CAAC,KAAK,EAAE,QAAQ,CAAC,GAAG,QAAQ,CAAiB,IAAI,CAAC,CAAC;IAEzD,SAAS,CAAC,GAAG,EAAE;QACb,kEAAkE;QAClE,8EAA8E;QAC9E,uCAAuC;QACvC,IAAI,IAAI,CAAC,WAAW,KAAK,IAAI,EAAE,CAAC;YAC9B,UAAU,CAAC,IAAI,CAAC,CAAC;QACnB,CAAC;QAED,MAAM,KAAK,GAAG,kBAAkB,CAC9B,IAAI,EACJ,CAAC,CAAC,EAAE,EAAE;YACJ,OAAO,CAAC,CAAC,CAAC,CAAC;YACX,QAAQ,CAAC,IAAI,CAAC,CAAC;YACf,UAAU,CAAC,KAAK,CAAC,CAAC;QACpB,CAAC,EACD,CAAC,CAAC,EAAE,EAAE;YACJ,QAAQ,CAAC,CAAC,CAAC,CAAC;YACZ,UAAU,CAAC,KAAK,CAAC,CAAC;QACpB,CAAC,CACF,CAAC;QACF,OAAO,KAAK,CAAC;IACf,CAAC,EAAE,CAAC,IAAI,CAAC,CAAC,CAAC;IAEX,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC;AAClC,CAAC","sourcesContent":["import { useEffect, useState } from \"react\";\nimport type { Auth, User } from \"firebase/auth\";\nimport { onAuthStateChanged } from \"../auth.js\";\n\nexport type AuthState = {\n user: User | null;\n loading: boolean;\n error: unknown | null;\n};\n\nexport function useAuthState(auth: Auth): AuthState {\n // Initialize state based on current auth status to avoid initial flash\n const [user, setUser] = useState<User | null>(() => auth.currentUser);\n const [loading, setLoading] = useState<boolean>(() => auth.currentUser === null);\n const [error, setError] = useState<unknown | null>(null);\n\n useEffect(() => {\n // Only set loading if we don't have a user and haven't loaded yet\n // This prevents the \"flash\" when a user is already signed in (e.g. hydration)\n // or when simply refreshing the token.\n if (auth.currentUser === null) {\n setLoading(true);\n }\n\n const unsub = onAuthStateChanged(\n auth,\n (u) => {\n setUser(u);\n setError(null);\n setLoading(false);\n },\n (e) => {\n setError(e);\n setLoading(false);\n }\n );\n return unsub;\n }, [auth]);\n\n return { user, loading, error };\n}"]}
1
+ {"version":3,"file":"useAuthState.js","sourceRoot":"","sources":["../../src/react/useAuthState.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAC;AAE5C,OAAO,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AAQvD,MAAM,UAAU,YAAY,CAAC,IAAU;IACrC,MAAM,CAAC,IAAI,EAAE,OAAO,CAAC,GAAG,QAAQ,CAAc,GAAG,EAAE,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC;IACtE,MAAM,CAAC,OAAO,EAAE,UAAU,CAAC,GAAG,QAAQ,CAAU,GAAG,EAAE,CAAC,IAAI,CAAC,WAAW,KAAK,IAAI,CAAC,CAAC;IACjF,MAAM,CAAC,KAAK,EAAE,QAAQ,CAAC,GAAG,QAAQ,CAAiB,IAAI,CAAC,CAAC;IAEzD,SAAS,CAAC,GAAG,EAAE;QACb,IAAI,IAAI,CAAC,WAAW,KAAK,IAAI,EAAE,CAAC;YAC9B,UAAU,CAAC,IAAI,CAAC,CAAC;QACnB,CAAC;QAED,MAAM,KAAK,GAAG,kBAAkB,CAC9B,IAAI,EACJ,CAAC,CAAC,EAAE,EAAE;YACJ,OAAO,CAAC,CAAC,CAAC,CAAC;YACX,QAAQ,CAAC,IAAI,CAAC,CAAC;YACf,UAAU,CAAC,KAAK,CAAC,CAAC;QACpB,CAAC,EACD,CAAC,CAAC,EAAE,EAAE;YACJ,QAAQ,CAAC,CAAC,CAAC,CAAC;YACZ,UAAU,CAAC,KAAK,CAAC,CAAC;QACpB,CAAC,CACF,CAAC;QACF,OAAO,KAAK,CAAC;IACf,CAAC,EAAE,CAAC,IAAI,CAAC,CAAC,CAAC;IAEX,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC;AAClC,CAAC","sourcesContent":["import { useEffect, useState } from 'react';\nimport type { Auth, User } from 'firebase/auth';\nimport { onAuthStateChanged } from '../client/auth.js';\n\nexport type AuthState = {\n user: User | null;\n loading: boolean;\n error: unknown | null;\n};\n\nexport function useAuthState(auth: Auth): AuthState {\n const [user, setUser] = useState<User | null>(() => auth.currentUser);\n const [loading, setLoading] = useState<boolean>(() => auth.currentUser === null);\n const [error, setError] = useState<unknown | null>(null);\n\n useEffect(() => {\n if (auth.currentUser === null) {\n setLoading(true);\n }\n\n const unsub = onAuthStateChanged(\n auth,\n (u) => {\n setUser(u);\n setError(null);\n setLoading(false);\n },\n (e) => {\n setError(e);\n setLoading(false);\n },\n );\n return unsub;\n }, [auth]);\n\n return { user, loading, error };\n}\n"]}
package/dist/refusal.d.ts CHANGED
@@ -1,8 +1,2 @@
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
1
  export declare function failedPreconditionDetails(err: unknown): unknown;
8
2
  //# sourceMappingURL=refusal.d.ts.map
@@ -1 +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"}
1
+ {"version":3,"file":"refusal.d.ts","sourceRoot":"","sources":["../src/refusal.ts"],"names":[],"mappings":"AAKA,wBAAgB,yBAAyB,CAAC,GAAG,EAAE,OAAO,GAAG,OAAO,CAM/D"}
package/dist/refusal.js CHANGED
@@ -1,23 +1,13 @@
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
1
  const FAILED_PRECONDITION_CODES = new Set([
8
- "failed-precondition",
9
- "functions/failed-precondition",
2
+ 'failed-precondition',
3
+ 'functions/failed-precondition',
10
4
  ]);
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
5
  export function failedPreconditionDetails(err) {
18
- if (!err || typeof err !== "object")
6
+ if (!err || typeof err !== 'object')
19
7
  return undefined;
20
8
  const e = err;
21
- return typeof e.code === "string" && FAILED_PRECONDITION_CODES.has(e.code) ? e.details : undefined;
9
+ return typeof e.code === 'string' && FAILED_PRECONDITION_CODES.has(e.code)
10
+ ? e.details
11
+ : undefined;
22
12
  }
23
13
  //# sourceMappingURL=refusal.js.map
@@ -1 +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
+ {"version":3,"file":"refusal.js","sourceRoot":"","sources":["../src/refusal.ts"],"names":[],"mappings":"AAAA,MAAM,yBAAyB,GAAwB,IAAI,GAAG,CAAC;IAC7D,qBAAqB;IACrB,+BAA+B;CAChC,CAAC,CAAC;AAEH,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;QACxE,CAAC,CAAC,CAAC,CAAC,OAAO;QACX,CAAC,CAAC,SAAS,CAAC;AAChB,CAAC","sourcesContent":["const FAILED_PRECONDITION_CODES: ReadonlySet<string> = new Set([\n 'failed-precondition',\n 'functions/failed-precondition',\n]);\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)\n ? e.details\n : undefined;\n}\n"]}
@@ -1,3 +1,3 @@
1
- import type { AssertAuthConfig, AssertAuthFn } from "./types.js";
2
- export declare function createAssertAuth<TUser, TAdmin = void>(config: AssertAuthConfig<TUser, TAdmin>): AssertAuthFn<TUser, TAdmin>;
1
+ import type { AssertAuthConfig, AssertAuthFn } from './types.js';
2
+ export declare function createAssertAuth<TUser, TAdmin = void, TAdminOptions = never>(config: AssertAuthConfig<TUser, TAdmin, TAdminOptions>): AssertAuthFn<TUser, TAdmin, TAdminOptions>;
3
3
  //# sourceMappingURL=assertAuth.d.ts.map
@@ -1 +1 @@
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"}
1
+ {"version":3,"file":"assertAuth.d.ts","sourceRoot":"","sources":["../../src/server/assertAuth.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,gBAAgB,EAAE,YAAY,EAAiC,MAAM,YAAY,CAAC;AAEhG,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,EAAE,aAAa,GAAG,KAAK,EAC1E,MAAM,EAAE,gBAAgB,CAAC,KAAK,EAAE,MAAM,EAAE,aAAa,CAAC,GACrD,YAAY,CAAC,KAAK,EAAE,MAAM,EAAE,aAAa,CAAC,CAuE5C"}
@@ -1,75 +1,50 @@
1
- // Factory that produces an assertAuth function bound to a consuming app's
2
- // Firestore paths, user shape, and admin check.
3
- //
4
- // Every Cloud Functions callable in the consuming app should invoke the
5
- // returned assertAuth as its first executable line. The check order is:
6
- // 1. Authentication
7
- // 2. Email verification
8
- // 3. User doc Firestore read (when status check is needed)
9
- // 4. Status check (suspended / banned)
10
- // 5. Acceptance-level gate (only when config.acceptance is set)
11
- // 6. Admin check (delegated to config.requireAdmin)
12
- import { ACCEPTANCE_REQUIRED_REASON, normalizeAcceptanceLevel } from "../acceptance.js";
13
- import { EMAIL_VERIFICATION_REQUIRED_REASON } from "../email-verification.js";
14
- import { AuthAssertionError } from "./authError.js";
1
+ import { ACCEPTANCE_REQUIRED_REASON, normalizeAcceptanceLevel } from '../acceptance.js';
2
+ import { EMAIL_VERIFICATION_REQUIRED_REASON } from '../email-verification.js';
3
+ import { AuthAssertionError } from './authError.js';
15
4
  export function createAssertAuth(config) {
16
5
  return async function assertAuth(request, requirements = {}) {
17
- // 1. Authentication check (always first)
18
6
  if (!request.auth) {
19
- throw new AuthAssertionError("unauthenticated", "You must be authenticated");
7
+ throw new AuthAssertionError('unauthenticated', 'You must be authenticated');
20
8
  }
21
9
  const uid = request.auth.uid;
22
10
  const token = request.auth.token;
23
- // 2. Email verification check. The refusal carries its reason in `details`, so the client
24
- // recognizes it structurally (`isEmailVerificationRequiredError`), never by the message.
25
- if (requirements.emailVerified === true &&
26
- requirements.allowUnverified !== true) {
11
+ if (requirements.emailVerified === true && requirements.allowUnverified !== true) {
27
12
  if (token.email_verified !== true) {
28
- throw new AuthAssertionError("failed-precondition", "Email must be verified", {
13
+ throw new AuthAssertionError('failed-precondition', 'Email must be verified', {
29
14
  reason: EMAIL_VERIFICATION_REQUIRED_REASON,
30
15
  });
31
16
  }
32
17
  }
33
- // 3. User doc Firestore read (skipped when allowAnyStatus is set)
34
18
  const db = config.firestore();
35
- const userSnap = requirements.allowAnyStatus !== true
36
- ? await db.doc(config.userProfilePath(uid)).get()
37
- : null;
19
+ const userSnap = requirements.allowAnyStatus !== true ? await db.doc(config.userProfilePath(uid)).get() : null;
38
20
  let userDoc;
39
- // 4. Status check (default: block suspended + banned; skipped if allowAnyStatus)
40
21
  if (requirements.allowAnyStatus !== true) {
41
22
  if (!userSnap || !userSnap.exists) {
42
- throw new AuthAssertionError("not-found", "User profile not found");
23
+ throw new AuthAssertionError('not-found', 'User profile not found');
43
24
  }
44
25
  const userData = userSnap.data();
45
26
  const status = config.getUserStatus(userData);
46
- if (status === "banned") {
47
- throw new AuthAssertionError("permission-denied", "Account is not in good standing");
27
+ if (status === 'banned') {
28
+ throw new AuthAssertionError('permission-denied', 'Account is not in good standing');
48
29
  }
49
- if (status === "suspended" && requirements.allowSuspended !== true) {
50
- throw new AuthAssertionError("permission-denied", "Account is not in good standing");
30
+ if (status === 'suspended' && requirements.allowSuspended !== true) {
31
+ throw new AuthAssertionError('permission-denied', 'Account is not in good standing');
51
32
  }
52
33
  userDoc = userData;
53
34
  }
54
- // 5. Acceptance-level gate. Runs only when the app configured one and the callable did
55
- // not opt out. The required level comes from the app's cheap (cached) reader and the
56
- // accepted level from the verified token, so the gate adds no per-call Firestore read.
57
- // It sits after the status check so a banned caller still gets the standing refusal.
58
35
  if (config.acceptance !== undefined && requirements.allowUnaccepted !== true) {
59
36
  const requiredLevel = normalizeAcceptanceLevel(await config.acceptance.requiredLevel());
60
37
  if (requiredLevel > 0) {
61
38
  const acceptedLevel = normalizeAcceptanceLevel(token[config.acceptance.claimKey]);
62
39
  if (acceptedLevel < requiredLevel) {
63
- throw new AuthAssertionError("failed-precondition", "The current documents must be accepted before continuing", { reason: ACCEPTANCE_REQUIRED_REASON, requiredLevel, acceptedLevel });
40
+ throw new AuthAssertionError('failed-precondition', 'The current documents must be accepted before continuing', { reason: ACCEPTANCE_REQUIRED_REASON, requiredLevel, acceptedLevel });
64
41
  }
65
42
  }
66
43
  }
67
- // 6. Admin check (delegates to config.requireAdmin)
68
44
  let adminResult;
69
45
  if (requirements.admin !== undefined) {
70
46
  adminResult = await config.requireAdmin(uid, token, requirements.admin);
71
47
  }
72
- // 7. Return context with whatever docs were fetched
73
48
  const ctx = { uid, token };
74
49
  if (userDoc !== undefined) {
75
50
  ctx.userDoc = userDoc;
@@ -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 { 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"]}
1
+ {"version":3,"file":"assertAuth.js","sourceRoot":"","sources":["../../src/server/assertAuth.ts"],"names":[],"mappings":"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","sourcesContent":["import 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 { AssertAuthConfig, AssertAuthFn, AuthContext, AuthRequirements } from './types.js';\n\nexport function createAssertAuth<TUser, TAdmin = void, TAdminOptions = never>(\n config: AssertAuthConfig<TUser, TAdmin, TAdminOptions>,\n): AssertAuthFn<TUser, TAdmin, TAdminOptions> {\n return async function assertAuth(\n request: CallableRequest,\n requirements: AuthRequirements<TAdminOptions> = {},\n ): Promise<AuthContext<TUser, TAdmin>> {\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 if (requirements.emailVerified === true && requirements.allowUnverified !== true) {\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 const db = config.firestore();\n const userSnap =\n requirements.allowAnyStatus !== true ? await db.doc(config.userProfilePath(uid)).get() : null;\n\n let userDoc: TUser | undefined;\n\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('permission-denied', 'Account is not in good standing');\n }\n if (status === 'suspended' && requirements.allowSuspended !== true) {\n throw new AuthAssertionError('permission-denied', 'Account is not in good standing');\n }\n userDoc = userData;\n }\n\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 let adminResult: TAdmin | undefined;\n if (requirements.admin !== undefined) {\n adminResult = await config.requireAdmin(uid, token, requirements.admin);\n }\n\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,54 +1,6 @@
1
- import type { AcceptanceRequiredDetails } from "../acceptance.js";
2
- import type { EmailVerificationRequiredDetails } from "../email-verification.js";
3
- /**
4
- * Typed failure channel for `createAssertAuth`.
5
- *
6
- * Every EXPECTED auth outcome (not signed in, email unverified, missing profile,
7
- * account not in good standing) throws an `AuthAssertionError` whose `code` is
8
- * deliberately the matching `HttpsError` code string, so a consuming Cloud
9
- * Function maps it 1:1 at the app boundary:
10
- *
11
- * catch (err) {
12
- * if (err instanceof AuthAssertionError) throw new HttpsError(err.code, err.message);
13
- * throw err;
14
- * }
15
- *
16
- * WHY THE PACKAGE MUST NOT BUILD THE HttpsError ITSELF: firebase-functions 7.2.5
17
- * ships TWO distinct `HttpsError` classes — the one
18
- * exported from the ESM entry `firebase-functions/v2/https`, and the one the CJS
19
- * callable runtime closes over and `instanceof`-checks at
20
- * `lib/common/providers/https.js`. An `HttpsError` constructed by this package's
21
- * ESM `dist` fails that identity check, so `onCall` logged "Unhandled error" and
22
- * replaced a correct 400 with `500 INTERNAL`. Measured:
23
- *
24
- * ESM class === CJS class ? false
25
- * esm-built error instanceof CJS class ? false
26
- * httpErrorCode.status: 400 (the error itself was correct)
27
- *
28
- * Because `assertAuth` is the first line of every callable, that turned EVERY auth
29
- * rejection — unauthenticated, unverified email, suspended, banned — into a generic
30
- * 500: users saw a generic error instead of the verify-email dialog, and monitoring
31
- * filled with 500s that were really expected answers. Constructing the HttpsError in
32
- * APP module space fixes the identity permanently and cannot regress when
33
- * firebase-functions repackages.
34
- *
35
- * This also brings auth-core in line with engineering-rules QUALITY-102 (generic
36
- * packages stay framework-agnostic; the consuming app's adapter binds the framework) and
37
- * mirrors `report-core`'s `ReportCoreTaskError`, which already works this way.
38
- */
39
- export type AuthAssertionErrorCode = "unauthenticated" | "failed-precondition" | "not-found" | "permission-denied";
40
- /**
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:
47
- *
48
- * throw new HttpsError(err.code, err.message, err.details);
49
- *
50
- * Every other rejection leaves `details` undefined.
51
- */
1
+ import type { AcceptanceRequiredDetails } from '../acceptance.js';
2
+ import type { EmailVerificationRequiredDetails } from '../email-verification.js';
3
+ export type AuthAssertionErrorCode = 'unauthenticated' | 'failed-precondition' | 'not-found' | 'permission-denied';
52
4
  export type AuthAssertionErrorDetails = AcceptanceRequiredDetails | EmailVerificationRequiredDetails;
53
5
  export declare class AuthAssertionError extends Error {
54
6
  readonly code: AuthAssertionErrorCode;
@@ -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;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
+ {"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;AAIjF,MAAM,MAAM,sBAAsB,GAChC,iBAAiB,GAAG,qBAAqB,GAAG,WAAW,GAAG,mBAAmB,CAAC;AAEhF,MAAM,MAAM,yBAAyB,GACnC,yBAAyB,GAAG,gCAAgC,CAAC;AAE/D,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"}
@@ -3,7 +3,7 @@ export class AuthAssertionError extends Error {
3
3
  details;
4
4
  constructor(code, message, details) {
5
5
  super(message);
6
- this.name = "AuthAssertionError";
6
+ this.name = 'AuthAssertionError';
7
7
  this.code = code;
8
8
  if (details !== undefined)
9
9
  this.details = details;
@@ -1 +1 @@
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"]}
1
+ {"version":3,"file":"authError.js","sourceRoot":"","sources":["../../src/server/authError.ts"],"names":[],"mappings":"AAWA,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// firebase-functions' callable runtime instanceof-checks its own HttpsError class, which one built\n// in this package's ESM dist fails (a 500 INTERNAL), so the consuming app builds the HttpsError.\nexport type AuthAssertionErrorCode =\n 'unauthenticated' | 'failed-precondition' | 'not-found' | 'permission-denied';\n\nexport type AuthAssertionErrorDetails =\n 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"]}
@@ -1,5 +1,5 @@
1
- export { createAssertAuth } from "./assertAuth.js";
2
- export { AuthAssertionError } from "./authError.js";
3
- export type { AuthAssertionErrorCode, AuthAssertionErrorDetails } from "./authError.js";
4
- export type { AcceptanceGateConfig, AssertAuthConfig, AssertAuthFn, AuthContext, AuthRequirements, AdminCheckOptions, UserStatus, } from "./types.js";
1
+ export { createAssertAuth } from './assertAuth.js';
2
+ export { AuthAssertionError } from './authError.js';
3
+ export type { AuthAssertionErrorCode, AuthAssertionErrorDetails } from './authError.js';
4
+ export type { AcceptanceGateConfig, AssertAuthConfig, AssertAuthFn, AuthContext, AuthRequirements, UserStatus, } from './types.js';
5
5
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/server/index.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AACnD,OAAO,EAAE,kBAAkB,EAAE,MAAM,gBAAgB,CAAC;AACpD,YAAY,EAAE,sBAAsB,EAAE,yBAAyB,EAAE,MAAM,gBAAgB,CAAC;AACxF,YAAY,EACV,oBAAoB,EACpB,gBAAgB,EAChB,YAAY,EACZ,WAAW,EACX,gBAAgB,EAChB,iBAAiB,EACjB,UAAU,GACX,MAAM,YAAY,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/server/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AACnD,OAAO,EAAE,kBAAkB,EAAE,MAAM,gBAAgB,CAAC;AACpD,YAAY,EAAE,sBAAsB,EAAE,yBAAyB,EAAE,MAAM,gBAAgB,CAAC;AACxF,YAAY,EACV,oBAAoB,EACpB,gBAAgB,EAChB,YAAY,EACZ,WAAW,EACX,gBAAgB,EAChB,UAAU,GACX,MAAM,YAAY,CAAC"}
@@ -1,4 +1,3 @@
1
- // Public surface of @ttt-productions/auth-core/server.
2
- export { createAssertAuth } from "./assertAuth.js";
3
- export { AuthAssertionError } from "./authError.js";
1
+ export { createAssertAuth } from './assertAuth.js';
2
+ export { AuthAssertionError } from './authError.js';
4
3
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/server/index.ts"],"names":[],"mappings":"AAAA,uDAAuD;AAEvD,OAAO,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AACnD,OAAO,EAAE,kBAAkB,EAAE,MAAM,gBAAgB,CAAC","sourcesContent":["// Public surface of @ttt-productions/auth-core/server.\n\nexport { createAssertAuth } from \"./assertAuth.js\";\nexport { AuthAssertionError } from \"./authError.js\";\nexport type { AuthAssertionErrorCode, AuthAssertionErrorDetails } from \"./authError.js\";\nexport type {\n AcceptanceGateConfig,\n AssertAuthConfig,\n AssertAuthFn,\n AuthContext,\n AuthRequirements,\n AdminCheckOptions,\n UserStatus,\n} from \"./types.js\";\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/server/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AACnD,OAAO,EAAE,kBAAkB,EAAE,MAAM,gBAAgB,CAAC","sourcesContent":["export { createAssertAuth } from './assertAuth.js';\nexport { AuthAssertionError } from './authError.js';\nexport type { AuthAssertionErrorCode, AuthAssertionErrorDetails } from './authError.js';\nexport type {\n AcceptanceGateConfig,\n AssertAuthConfig,\n AssertAuthFn,\n AuthContext,\n AuthRequirements,\n UserStatus,\n} from './types.js';\n"]}
@@ -1,131 +1,31 @@
1
- import type { CallableRequest } from "firebase-functions/v2/https";
2
- import type { DecodedIdToken } from "firebase-admin/auth";
3
- import type { Firestore } from "firebase-admin/firestore";
4
- /**
5
- * Per-callable requirements passed at every assertAuth call site.
6
- * These are application-agnostic.
7
- */
8
- export interface AuthRequirements {
9
- /**
10
- * Allow unverified email. Default: false.
11
- * If true, the emailVerified check is skipped entirely.
12
- * Special-case for system flows (e.g. user registration).
13
- */
1
+ import type { CallableRequest } from 'firebase-functions/v2/https';
2
+ import type { DecodedIdToken } from 'firebase-admin/auth';
3
+ import type { Firestore } from 'firebase-admin/firestore';
4
+ export interface AuthRequirements<TAdminOptions = never> {
14
5
  allowUnverified?: boolean;
15
- /**
16
- * Require email_verified=true on the auth token.
17
- * Default: false. Skipped when allowUnverified=true.
18
- */
19
6
  emailVerified?: boolean;
20
- /**
21
- * Skip the status check entirely. Special-case for flows where the
22
- * user doc may not exist yet (e.g. user registration).
23
- * Default: false.
24
- */
25
7
  allowAnyStatus?: boolean;
26
- /**
27
- * Allow suspended users to pass the status check. Banned users still fail.
28
- * Use ONLY for callables that are a suspended user's last recourse
29
- * (e.g. admin-support chat / ban appeal).
30
- * Default: false. Does NOT skip the existence check.
31
- */
32
8
  allowSuspended?: boolean;
33
- /**
34
- * Let a caller below the currently required acceptance level through. Meaningful only
35
- * when the factory was configured with `acceptance`; ignored otherwise.
36
- * Use ONLY for callables that must work before the caller accepts: the accept callable
37
- * itself, registration (a new account holds no accepted level yet), and last-recourse
38
- * paths such as sign-out, support, refunds, account deletion, and legally required
39
- * reporting. Default: false.
40
- */
41
9
  allowUnaccepted?: boolean;
42
- /**
43
- * Require admin status. Delegates to config.requireAdmin().
44
- * The shape of this object is forwarded verbatim to the consumer's
45
- * requireAdmin callback — the package does not interpret it.
46
- */
47
- admin?: AdminCheckOptions;
10
+ admin?: TAdminOptions;
48
11
  }
49
- /**
50
- * Options forwarded to the consumer's requireAdmin callback. Generic shape.
51
- * Consuming apps may add their own fields; the package does not validate.
52
- */
53
- export interface AdminCheckOptions {
54
- allowJrAdmin?: boolean;
55
- useFastCheck?: boolean;
56
- errorMessage?: string;
57
- [key: string]: unknown;
58
- }
59
- /**
60
- * Return value from assertAuth. Generic over the consuming app's user
61
- * document type.
62
- */
63
12
  export interface AuthContext<TUser = unknown, TAdmin = void> {
64
13
  uid: string;
65
14
  token: DecodedIdToken;
66
- /** Populated when the user doc was read (default not-banned check). */
67
15
  userDoc?: TUser;
68
- /** Result returned by config.requireAdmin. Populated only when an admin check ran (AuthRequirements.admin was set). Undefined otherwise. */
69
16
  admin?: TAdmin;
70
17
  }
71
- /**
72
- * Status the consuming app reports for a user doc. The package only
73
- * interprets these three values.
74
- */
75
- export type UserStatus = "active" | "suspended" | "banned";
76
- /**
77
- * Configuration for createAssertAuth. The consuming app supplies every
78
- * app-specific operation as a callback.
79
- */
80
- export interface AssertAuthConfig<TUser, TAdmin = void> {
81
- /**
82
- * Returns the admin Firestore instance. Called every assertAuth invocation
83
- * that needs a Firestore read. Implementations typically return
84
- * `admin.firestore()` from a memoized handle.
85
- */
18
+ export type UserStatus = 'active' | 'suspended' | 'banned';
19
+ export interface AssertAuthConfig<TUser, TAdmin = void, TAdminOptions = never> {
86
20
  firestore: () => Firestore;
87
- /**
88
- * Returns the Firestore document path for a user profile, given a uid.
89
- * Example: `userProfiles/${uid}`
90
- */
91
21
  userProfilePath: (uid: string) => string;
92
- /**
93
- * Returns the user's status given the user doc data. The package only
94
- * acts on 'active' | 'suspended' | 'banned'. Apps may store status in any
95
- * field; this callback interprets it.
96
- */
97
22
  getUserStatus: (user: TUser) => UserStatus;
98
- /**
99
- * Admin check. Called only when AuthRequirements.admin is set. The
100
- * package forwards uid, token, and the admin options verbatim. The
101
- * callback throws (typically HttpsError) on failure.
102
- */
103
- requireAdmin: (uid: string, token: DecodedIdToken, options: AdminCheckOptions) => Promise<TAdmin>;
104
- /**
105
- * Optional acceptance-level gate. When set, every assertAuth call that does not pass
106
- * `allowUnaccepted` refuses a caller whose accepted level (read from the token claim
107
- * `claimKey`) is below the level `requiredLevel()` returns. Absent = no gate, exactly the
108
- * behavior before the gate existed.
109
- */
23
+ requireAdmin: (uid: string, token: DecodedIdToken, options: TAdminOptions) => Promise<TAdmin>;
110
24
  acceptance?: AcceptanceGateConfig;
111
25
  }
112
- /**
113
- * The consuming app's acceptance-level wiring. The package holds no claim name and no
114
- * document knowledge: the app names the claim and supplies the current requirement.
115
- */
116
26
  export interface AcceptanceGateConfig {
117
- /** The custom claim on the verified ID token that holds the caller's accepted level. */
118
27
  claimKey: string;
119
- /**
120
- * The level currently required. Runs on every gated assertAuth, so it must be cheap —
121
- * read it from a cached snapshot, never a fresh Firestore read per call. A non-finite or
122
- * non-positive result requires nothing (see `normalizeAcceptanceLevel`); a throw rejects
123
- * the call.
124
- */
125
28
  requiredLevel: () => number | Promise<number>;
126
29
  }
127
- /**
128
- * The function returned by createAssertAuth.
129
- */
130
- export type AssertAuthFn<TUser, TAdmin = void> = (request: CallableRequest, requirements?: AuthRequirements) => Promise<AuthContext<TUser, TAdmin>>;
30
+ export type AssertAuthFn<TUser, TAdmin = void, TAdminOptions = never> = (request: CallableRequest, requirements?: AuthRequirements<TAdminOptions>) => Promise<AuthContext<TUser, TAdmin>>;
131
31
  //# sourceMappingURL=types.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/server/types.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,6BAA6B,CAAC;AACnE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAC1D,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,0BAA0B,CAAC;AAE1D;;;GAGG;AACH,MAAM,WAAW,gBAAgB;IAC/B;;;;OAIG;IACH,eAAe,CAAC,EAAE,OAAO,CAAC;IAE1B;;;OAGG;IACH,aAAa,CAAC,EAAE,OAAO,CAAC;IAExB;;;;OAIG;IACH,cAAc,CAAC,EAAE,OAAO,CAAC;IAEzB;;;;;OAKG;IACH,cAAc,CAAC,EAAE,OAAO,CAAC;IAEzB;;;;;;;OAOG;IACH,eAAe,CAAC,EAAE,OAAO,CAAC;IAE1B;;;;OAIG;IACH,KAAK,CAAC,EAAE,iBAAiB,CAAC;CAC3B;AAED;;;GAGG;AACH,MAAM,WAAW,iBAAiB;IAChC,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AAED;;;GAGG;AACH,MAAM,WAAW,WAAW,CAAC,KAAK,GAAG,OAAO,EAAE,MAAM,GAAG,IAAI;IACzD,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,cAAc,CAAC;IACtB,uEAAuE;IACvE,OAAO,CAAC,EAAE,KAAK,CAAC;IAChB,4IAA4I;IAC5I,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED;;;GAGG;AACH,MAAM,MAAM,UAAU,GAAG,QAAQ,GAAG,WAAW,GAAG,QAAQ,CAAC;AAE3D;;;GAGG;AACH,MAAM,WAAW,gBAAgB,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI;IACpD;;;;OAIG;IACH,SAAS,EAAE,MAAM,SAAS,CAAC;IAE3B;;;OAGG;IACH,eAAe,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,MAAM,CAAC;IAEzC;;;;OAIG;IACH,aAAa,EAAE,CAAC,IAAI,EAAE,KAAK,KAAK,UAAU,CAAC;IAE3C;;;;OAIG;IACH,YAAY,EAAE,CACZ,GAAG,EAAE,MAAM,EACX,KAAK,EAAE,cAAc,EACrB,OAAO,EAAE,iBAAiB,KACvB,OAAO,CAAC,MAAM,CAAC,CAAC;IAErB;;;;;OAKG;IACH,UAAU,CAAC,EAAE,oBAAoB,CAAC;CACnC;AAED;;;GAGG;AACH,MAAM,WAAW,oBAAoB;IACnC,wFAAwF;IACxF,QAAQ,EAAE,MAAM,CAAC;IAEjB;;;;;OAKG;IACH,aAAa,EAAE,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;CAC/C;AAED;;GAEG;AACH,MAAM,MAAM,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,IAAI,CAC/C,OAAO,EAAE,eAAe,EACxB,YAAY,CAAC,EAAE,gBAAgB,KAC5B,OAAO,CAAC,WAAW,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC,CAAC"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/server/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,6BAA6B,CAAC;AACnE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAC1D,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,0BAA0B,CAAC;AAE1D,MAAM,WAAW,gBAAgB,CAAC,aAAa,GAAG,KAAK;IACrD,eAAe,CAAC,EAAE,OAAO,CAAC;IAE1B,aAAa,CAAC,EAAE,OAAO,CAAC;IAExB,cAAc,CAAC,EAAE,OAAO,CAAC;IAEzB,cAAc,CAAC,EAAE,OAAO,CAAC;IAEzB,eAAe,CAAC,EAAE,OAAO,CAAC;IAE1B,KAAK,CAAC,EAAE,aAAa,CAAC;CACvB;AAED,MAAM,WAAW,WAAW,CAAC,KAAK,GAAG,OAAO,EAAE,MAAM,GAAG,IAAI;IACzD,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,cAAc,CAAC;IACtB,OAAO,CAAC,EAAE,KAAK,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,MAAM,UAAU,GAAG,QAAQ,GAAG,WAAW,GAAG,QAAQ,CAAC;AAE3D,MAAM,WAAW,gBAAgB,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,EAAE,aAAa,GAAG,KAAK;IAC3E,SAAS,EAAE,MAAM,SAAS,CAAC;IAE3B,eAAe,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,MAAM,CAAC;IAEzC,aAAa,EAAE,CAAC,IAAI,EAAE,KAAK,KAAK,UAAU,CAAC;IAE3C,YAAY,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,cAAc,EAAE,OAAO,EAAE,aAAa,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;IAE9F,UAAU,CAAC,EAAE,oBAAoB,CAAC;CACnC;AAED,MAAM,WAAW,oBAAoB;IACnC,QAAQ,EAAE,MAAM,CAAC;IAEjB,aAAa,EAAE,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;CAC/C;AAED,MAAM,MAAM,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,EAAE,aAAa,GAAG,KAAK,IAAI,CACtE,OAAO,EAAE,eAAe,EACxB,YAAY,CAAC,EAAE,gBAAgB,CAAC,aAAa,CAAC,KAC3C,OAAO,CAAC,WAAW,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC,CAAC"}
@@ -1,4 +1,2 @@
1
- // Types for the createAssertAuth factory. Fully generic over the consuming
2
- // app's user document shape. The package contains no app-specific knowledge.
3
1
  export {};
4
2
  //# sourceMappingURL=types.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/server/types.ts"],"names":[],"mappings":"AAAA,2EAA2E;AAC3E,6EAA6E","sourcesContent":["// Types for the createAssertAuth factory. Fully generic over the consuming\n// app's user document shape. The package contains no app-specific knowledge.\n\nimport type { CallableRequest } from \"firebase-functions/v2/https\";\nimport type { DecodedIdToken } from \"firebase-admin/auth\";\nimport type { Firestore } from \"firebase-admin/firestore\";\n\n/**\n * Per-callable requirements passed at every assertAuth call site.\n * These are application-agnostic.\n */\nexport interface AuthRequirements {\n /**\n * Allow unverified email. Default: false.\n * If true, the emailVerified check is skipped entirely.\n * Special-case for system flows (e.g. user registration).\n */\n allowUnverified?: boolean;\n\n /**\n * Require email_verified=true on the auth token.\n * Default: false. Skipped when allowUnverified=true.\n */\n emailVerified?: boolean;\n\n /**\n * Skip the status check entirely. Special-case for flows where the\n * user doc may not exist yet (e.g. user registration).\n * Default: false.\n */\n allowAnyStatus?: boolean;\n\n /**\n * Allow suspended users to pass the status check. Banned users still fail.\n * Use ONLY for callables that are a suspended user's last recourse\n * (e.g. admin-support chat / ban appeal).\n * Default: false. Does NOT skip the existence check.\n */\n allowSuspended?: boolean;\n\n /**\n * Let a caller below the currently required acceptance level through. Meaningful only\n * when the factory was configured with `acceptance`; ignored otherwise.\n * Use ONLY for callables that must work before the caller accepts: the accept callable\n * itself, registration (a new account holds no accepted level yet), and last-recourse\n * paths such as sign-out, support, refunds, account deletion, and legally required\n * reporting. Default: false.\n */\n allowUnaccepted?: boolean;\n\n /**\n * Require admin status. Delegates to config.requireAdmin().\n * The shape of this object is forwarded verbatim to the consumer's\n * requireAdmin callback — the package does not interpret it.\n */\n admin?: AdminCheckOptions;\n}\n\n/**\n * Options forwarded to the consumer's requireAdmin callback. Generic shape.\n * Consuming apps may add their own fields; the package does not validate.\n */\nexport interface AdminCheckOptions {\n allowJrAdmin?: boolean;\n useFastCheck?: boolean;\n errorMessage?: string;\n [key: string]: unknown;\n}\n\n/**\n * Return value from assertAuth. Generic over the consuming app's user\n * document type.\n */\nexport interface AuthContext<TUser = unknown, TAdmin = void> {\n uid: string;\n token: DecodedIdToken;\n /** Populated when the user doc was read (default not-banned check). */\n userDoc?: TUser;\n /** Result returned by config.requireAdmin. Populated only when an admin check ran (AuthRequirements.admin was set). Undefined otherwise. */\n admin?: TAdmin;\n}\n\n/**\n * Status the consuming app reports for a user doc. The package only\n * interprets these three values.\n */\nexport type UserStatus = \"active\" | \"suspended\" | \"banned\";\n\n/**\n * Configuration for createAssertAuth. The consuming app supplies every\n * app-specific operation as a callback.\n */\nexport interface AssertAuthConfig<TUser, TAdmin = void> {\n /**\n * Returns the admin Firestore instance. Called every assertAuth invocation\n * that needs a Firestore read. Implementations typically return\n * `admin.firestore()` from a memoized handle.\n */\n firestore: () => Firestore;\n\n /**\n * Returns the Firestore document path for a user profile, given a uid.\n * Example: `userProfiles/${uid}`\n */\n userProfilePath: (uid: string) => string;\n\n /**\n * Returns the user's status given the user doc data. The package only\n * acts on 'active' | 'suspended' | 'banned'. Apps may store status in any\n * field; this callback interprets it.\n */\n getUserStatus: (user: TUser) => UserStatus;\n\n /**\n * Admin check. Called only when AuthRequirements.admin is set. The\n * package forwards uid, token, and the admin options verbatim. The\n * callback throws (typically HttpsError) on failure.\n */\n requireAdmin: (\n uid: string,\n token: DecodedIdToken,\n options: AdminCheckOptions\n ) => Promise<TAdmin>;\n\n /**\n * Optional acceptance-level gate. When set, every assertAuth call that does not pass\n * `allowUnaccepted` refuses a caller whose accepted level (read from the token claim\n * `claimKey`) is below the level `requiredLevel()` returns. Absent = no gate, exactly the\n * behavior before the gate existed.\n */\n acceptance?: AcceptanceGateConfig;\n}\n\n/**\n * The consuming app's acceptance-level wiring. The package holds no claim name and no\n * document knowledge: the app names the claim and supplies the current requirement.\n */\nexport interface AcceptanceGateConfig {\n /** The custom claim on the verified ID token that holds the caller's accepted level. */\n claimKey: string;\n\n /**\n * The level currently required. Runs on every gated assertAuth, so it must be cheap —\n * read it from a cached snapshot, never a fresh Firestore read per call. A non-finite or\n * non-positive result requires nothing (see `normalizeAcceptanceLevel`); a throw rejects\n * the call.\n */\n requiredLevel: () => number | Promise<number>;\n}\n\n/**\n * The function returned by createAssertAuth.\n */\nexport type AssertAuthFn<TUser, TAdmin = void> = (\n request: CallableRequest,\n requirements?: AuthRequirements\n) => Promise<AuthContext<TUser, TAdmin>>;\n"]}
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/server/types.ts"],"names":[],"mappings":"","sourcesContent":["import type { CallableRequest } from 'firebase-functions/v2/https';\nimport type { DecodedIdToken } from 'firebase-admin/auth';\nimport type { Firestore } from 'firebase-admin/firestore';\n\nexport interface AuthRequirements<TAdminOptions = never> {\n allowUnverified?: boolean;\n\n emailVerified?: boolean;\n\n allowAnyStatus?: boolean;\n\n allowSuspended?: boolean;\n\n allowUnaccepted?: boolean;\n\n admin?: TAdminOptions;\n}\n\nexport interface AuthContext<TUser = unknown, TAdmin = void> {\n uid: string;\n token: DecodedIdToken;\n userDoc?: TUser;\n admin?: TAdmin;\n}\n\nexport type UserStatus = 'active' | 'suspended' | 'banned';\n\nexport interface AssertAuthConfig<TUser, TAdmin = void, TAdminOptions = never> {\n firestore: () => Firestore;\n\n userProfilePath: (uid: string) => string;\n\n getUserStatus: (user: TUser) => UserStatus;\n\n requireAdmin: (uid: string, token: DecodedIdToken, options: TAdminOptions) => Promise<TAdmin>;\n\n acceptance?: AcceptanceGateConfig;\n}\n\nexport interface AcceptanceGateConfig {\n claimKey: string;\n\n requiredLevel: () => number | Promise<number>;\n}\n\nexport type AssertAuthFn<TUser, TAdmin = void, TAdminOptions = never> = (\n request: CallableRequest,\n requirements?: AuthRequirements<TAdminOptions>,\n) => Promise<AuthContext<TUser, TAdmin>>;\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ttt-productions/auth-core",
3
- "version": "1.6.15",
3
+ "version": "1.6.17",
4
4
  "description": "Shared Firebase authentication utilities and React hooks for TTT Productions apps",
5
5
  "repository": {
6
6
  "type": "git",
@@ -21,8 +21,8 @@
21
21
  "default": "./dist/index.js"
22
22
  },
23
23
  "./client": {
24
- "types": "./dist/client.d.ts",
25
- "default": "./dist/client.js"
24
+ "types": "./dist/client/index.d.ts",
25
+ "default": "./dist/client/index.js"
26
26
  },
27
27
  "./react": {
28
28
  "types": "./dist/react/index.d.ts",
@@ -1 +0,0 @@
1
- {"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,eAAe,CAAC;AAGhD,MAAM,MAAM,WAAW,GAAG,MAAM,IAAI,CAAC;AAErC,wBAAgB,WAAW,CAAC,IAAI,EAAE,IAAI,GAAG,IAAI,GAAG,IAAI,CAEnD;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CAChC,IAAI,EAAE,IAAI,EACV,EAAE,EAAE,CAAC,IAAI,EAAE,IAAI,GAAG,IAAI,KAAK,IAAI,EAC/B,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,GACjC,WAAW,CAEb"}
package/dist/auth.js DELETED
@@ -1,13 +0,0 @@
1
- import { onAuthStateChanged as fbOnAuthStateChanged } from "firebase/auth";
2
- export function getAuthUser(auth) {
3
- return auth.currentUser;
4
- }
5
- /**
6
- * Thin wrapper around Firebase onAuthStateChanged.
7
- * - always returns the unsubscribe function
8
- * - preserves Firebase callback signature
9
- */
10
- export function onAuthStateChanged(auth, cb, onError) {
11
- return fbOnAuthStateChanged(auth, cb, onError);
12
- }
13
- //# sourceMappingURL=auth.js.map
package/dist/auth.js.map DELETED
@@ -1 +0,0 @@
1
- {"version":3,"file":"auth.js","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,kBAAkB,IAAI,oBAAoB,EAAE,MAAM,eAAe,CAAC;AAI3E,MAAM,UAAU,WAAW,CAAC,IAAU;IACpC,OAAO,IAAI,CAAC,WAAW,CAAC;AAC1B,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,kBAAkB,CAChC,IAAU,EACV,EAA+B,EAC/B,OAAkC;IAElC,OAAO,oBAAoB,CAAC,IAAI,EAAE,EAAE,EAAE,OAAO,CAAC,CAAC;AACjD,CAAC","sourcesContent":["import type { Auth, User } from \"firebase/auth\";\nimport { onAuthStateChanged as fbOnAuthStateChanged } from \"firebase/auth\";\n\nexport type Unsubscribe = () => void;\n\nexport function getAuthUser(auth: Auth): User | null {\n return auth.currentUser;\n}\n\n/**\n * Thin wrapper around Firebase onAuthStateChanged.\n * - always returns the unsubscribe function\n * - preserves Firebase callback signature\n */\nexport function onAuthStateChanged(\n auth: Auth,\n cb: (user: User | null) => void,\n onError?: (error: unknown) => void\n): Unsubscribe {\n return fbOnAuthStateChanged(auth, cb, onError);\n}\n"]}