@ttt-productions/auth-core 1.6.14 → 1.6.16

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 (73) hide show
  1. package/dist/acceptance.d.ts.map +1 -1
  2. package/dist/acceptance.js +6 -6
  3. package/dist/acceptance.js.map +1 -1
  4. package/dist/{auth.d.ts → client/auth.d.ts} +1 -1
  5. package/dist/client/auth.d.ts.map +1 -0
  6. package/dist/{auth.js → client/auth.js} +1 -1
  7. package/dist/client/auth.js.map +1 -0
  8. package/dist/{claims.d.ts → client/claims.d.ts} +2 -2
  9. package/dist/client/claims.d.ts.map +1 -0
  10. package/dist/{claims.js → client/claims.js} +5 -3
  11. package/dist/client/claims.js.map +1 -0
  12. package/dist/client/index.d.ts +3 -0
  13. package/dist/client/index.d.ts.map +1 -0
  14. package/dist/{client.js → client/index.js} +3 -2
  15. package/dist/client/index.js.map +1 -0
  16. package/dist/email-verification.js +3 -3
  17. package/dist/email-verification.js.map +1 -1
  18. package/dist/env.d.ts +1 -1
  19. package/dist/env.js +5 -5
  20. package/dist/env.js.map +1 -1
  21. package/dist/errors.d.ts +1 -1
  22. package/dist/errors.d.ts.map +1 -1
  23. package/dist/errors.js +61 -61
  24. package/dist/errors.js.map +1 -1
  25. package/dist/index.d.ts +4 -5
  26. package/dist/index.d.ts.map +1 -1
  27. package/dist/index.js +6 -7
  28. package/dist/index.js.map +1 -1
  29. package/dist/react/AuthProvider.d.ts +3 -3
  30. package/dist/react/AuthProvider.d.ts.map +1 -1
  31. package/dist/react/AuthProvider.js +25 -15
  32. package/dist/react/AuthProvider.js.map +1 -1
  33. package/dist/react/index.d.ts +8 -8
  34. package/dist/react/index.js +6 -6
  35. package/dist/react/index.js.map +1 -1
  36. package/dist/react/redirect-storage.js +1 -1
  37. package/dist/react/redirect-storage.js.map +1 -1
  38. package/dist/react/types.d.ts +4 -2
  39. package/dist/react/types.d.ts.map +1 -1
  40. package/dist/react/types.js.map +1 -1
  41. package/dist/react/useAuth.d.ts +1 -1
  42. package/dist/react/useAuth.js +4 -4
  43. package/dist/react/useAuth.js.map +1 -1
  44. package/dist/react/useAuthGuard.js +5 -5
  45. package/dist/react/useAuthGuard.js.map +1 -1
  46. package/dist/react/useAuthState.d.ts +1 -1
  47. package/dist/react/useAuthState.js +2 -2
  48. package/dist/react/useAuthState.js.map +1 -1
  49. package/dist/refusal.d.ts.map +1 -1
  50. package/dist/refusal.js +6 -4
  51. package/dist/refusal.js.map +1 -1
  52. package/dist/server/assertAuth.d.ts +1 -1
  53. package/dist/server/assertAuth.d.ts.map +1 -1
  54. package/dist/server/assertAuth.js +13 -16
  55. package/dist/server/assertAuth.js.map +1 -1
  56. package/dist/server/authError.d.ts +3 -3
  57. package/dist/server/authError.d.ts.map +1 -1
  58. package/dist/server/authError.js +1 -1
  59. package/dist/server/authError.js.map +1 -1
  60. package/dist/server/index.d.ts +4 -4
  61. package/dist/server/index.js +2 -2
  62. package/dist/server/index.js.map +1 -1
  63. package/dist/server/types.d.ts +4 -4
  64. package/dist/server/types.d.ts.map +1 -1
  65. package/dist/server/types.js.map +1 -1
  66. package/package.json +3 -3
  67. package/dist/auth.d.ts.map +0 -1
  68. package/dist/auth.js.map +0 -1
  69. package/dist/claims.d.ts.map +0 -1
  70. package/dist/claims.js.map +0 -1
  71. package/dist/client.d.ts +0 -2
  72. package/dist/client.d.ts.map +0 -1
  73. package/dist/client.js.map +0 -1
@@ -1,10 +1,10 @@
1
- "use client";
1
+ 'use client';
2
2
  import { jsx as _jsx } from "react/jsx-runtime";
3
- import { createContext, useEffect, useRef, useState, useCallback, useMemo, } from "react";
4
- import { useAuthState } from "./useAuthState.js";
5
- import { getIdTokenClaims } from "../claims.js";
3
+ import { createContext, useEffect, useRef, useState, useCallback, useMemo, } from 'react';
4
+ import { useAuthState } from './useAuthState.js';
5
+ import { getIdTokenClaims } from '../client/claims.js';
6
6
  export const AuthContext = createContext(null);
7
- export function AuthProvider({ config, children }) {
7
+ export function AuthProvider({ config, children, }) {
8
8
  const { user, loading: authLoading } = useAuthState(config.auth);
9
9
  // Claims are held with the session they were read for and derived at render (below), so a
10
10
  // render can never pair one account's `user` with another account's claims, and a read that
@@ -29,7 +29,11 @@ export function AuthProvider({ config, children }) {
29
29
  if (currentUserRef.current !== forUser || generation <= appliedGenerationRef.current)
30
30
  return;
31
31
  appliedGenerationRef.current = generation;
32
- setResolved({ user: forUser, generation, claims: configRef.current.parseClaims(raw ?? {}) });
32
+ setResolved({
33
+ user: forUser,
34
+ generation,
35
+ claims: configRef.current.parseClaims(raw ?? {}),
36
+ });
33
37
  }
34
38
  catch (err) {
35
39
  if (currentUserRef.current !== forUser)
@@ -51,12 +55,14 @@ export function AuthProvider({ config, children }) {
51
55
  let cancelled = false;
52
56
  Promise.resolve()
53
57
  .then(() => gate())
54
- .catch((err) => configRef.current.onError?.(err, "readyGate"))
58
+ .catch((err) => configRef.current.onError?.(err, 'readyGate'))
55
59
  .finally(() => {
56
60
  if (!cancelled)
57
61
  setGateOpen(true);
58
62
  });
59
- return () => { cancelled = true; };
63
+ return () => {
64
+ cancelled = true;
65
+ };
60
66
  // Mount-only by design (reads only the stable configRef): the gate is a
61
67
  // one-time client readiness signal.
62
68
  }, []);
@@ -70,7 +76,7 @@ export function AuthProvider({ config, children }) {
70
76
  return;
71
77
  }
72
78
  configRef.current.onAuthStateChange?.(user);
73
- void readClaims(user, async () => ((await getIdTokenClaims(user)) ?? {}), "getIdTokenClaims");
79
+ void readClaims(user, async () => ((await getIdTokenClaims(user)) ?? {}), 'getIdTokenClaims');
74
80
  }, [user, authLoading, readClaims]);
75
81
  // --- refreshClaims ---
76
82
  const refreshClaims = useCallback(async () => {
@@ -81,23 +87,27 @@ export function AuthProvider({ config, children }) {
81
87
  const liveUser = configRef.current.auth.currentUser;
82
88
  if (!liveUser)
83
89
  return;
84
- await readClaims(liveUser, async () => ((await liveUser.getIdTokenResult(true)).claims ?? {}), "refreshClaims");
90
+ await readClaims(liveUser, async () => ((await liveUser.getIdTokenResult(true)).claims ?? {}), 'refreshClaims');
85
91
  }, [readClaims]);
86
92
  // --- Derived at render: only the current session's claims are ever exposed ---
87
93
  const sessionClaims = user && resolved?.user === user ? resolved : null;
88
- const claims = sessionClaims ? sessionClaims.claims : config.defaultClaims;
89
94
  const claimsLoading = authLoading || (user !== null && sessionClaims === null);
95
+ // While the gate is closed a restored session is held back from consumers: a uid-keyed
96
+ // subscription opened before the gate's readiness signal is rejected once and dies, so no
97
+ // consumer may see a uid (or that session's claims) until the gate opens or fails open.
98
+ const exposedUser = gateOpen ? user : null;
99
+ const claims = exposedUser && sessionClaims ? sessionClaims.claims : config.defaultClaims;
90
100
  // --- Context value ---
91
101
  const loading = authLoading || claimsLoading || !gateOpen;
92
102
  const value = useMemo(() => ({
93
- user,
103
+ user: exposedUser,
94
104
  claims,
95
105
  loading,
96
106
  authLoading,
97
107
  claimsLoading,
98
- isAuthenticated: !!user,
108
+ isAuthenticated: !!exposedUser,
99
109
  refreshClaims,
100
- }), [user, claims, loading, authLoading, claimsLoading, refreshClaims]);
101
- return (_jsx(AuthContext.Provider, { value: value, children: children }));
110
+ }), [exposedUser, claims, loading, authLoading, claimsLoading, refreshClaims]);
111
+ return _jsx(AuthContext.Provider, { value: value, children: children });
102
112
  }
103
113
  //# sourceMappingURL=AuthProvider.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"AuthProvider.js","sourceRoot":"","sources":["../../src/react/AuthProvider.tsx"],"names":[],"mappings":"AAAA,YAAY,CAAC;;AAEb,OAAO,EACL,aAAa,EACb,SAAS,EACT,MAAM,EACN,QAAQ,EACR,WAAW,EACX,OAAO,GAER,MAAM,OAAO,CAAC;AAEf,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AACjD,OAAO,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAchD,MAAM,CAAC,MAAM,WAAW,GAAG,aAAa,CAA+B,IAAI,CAAC,CAAC;AAO7E,MAAM,UAAU,YAAY,CAC1B,EAAE,MAAM,EAAE,QAAQ,EAA8B;IAEhD,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,WAAW,EAAE,GAAG,YAAY,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;IAEjE,0FAA0F;IAC1F,4FAA4F;IAC5F,sFAAsF;IACtF,MAAM,CAAC,QAAQ,EAAE,WAAW,CAAC,GAAG,QAAQ,CAAiC,IAAI,CAAC,CAAC;IAC/E,MAAM,aAAa,GAAG,MAAM,CAAC,CAAC,CAAC,CAAC;IAChC,MAAM,oBAAoB,GAAG,MAAM,CAAC,CAAC,CAAC,CAAC;IACvC,MAAM,cAAc,GAAG,MAAM,CAAc,IAAI,CAAC,CAAC;IACjD,cAAc,CAAC,OAAO,GAAG,IAAI,CAAC;IAE9B,+EAA+E;IAC/E,MAAM,SAAS,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC;IACjC,SAAS,CAAC,OAAO,GAAG,MAAM,CAAC;IAE3B;;;;OAIG;IACH,MAAM,UAAU,GAAG,WAAW,CAC5B,KAAK,EAAE,OAAa,EAAE,IAAmD,EAAE,OAAe,EAAE,EAAE;QAC5F,MAAM,UAAU,GAAG,EAAE,aAAa,CAAC,OAAO,CAAC;QAC3C,IAAI,CAAC;YACH,MAAM,GAAG,GAAG,MAAM,IAAI,EAAE,CAAC;YACzB,IAAI,cAAc,CAAC,OAAO,KAAK,OAAO,IAAI,UAAU,IAAI,oBAAoB,CAAC,OAAO;gBAAE,OAAO;YAC7F,oBAAoB,CAAC,OAAO,GAAG,UAAU,CAAC;YAC1C,WAAW,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,UAAU,EAAE,MAAM,EAAE,SAAS,CAAC,OAAO,CAAC,WAAW,CAAC,GAAG,IAAI,EAAE,CAAC,EAAE,CAAC,CAAC;QAC/F,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,IAAI,cAAc,CAAC,OAAO,KAAK,OAAO;gBAAE,OAAO;YAC/C,SAAS,CAAC,OAAO,CAAC,OAAO,EAAE,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC;YAC1C,WAAW,CAAC,CAAC,IAAI,EAAE,EAAE,CACnB,IAAI,IAAI,IAAI,CAAC,IAAI,KAAK,OAAO;gBAC3B,CAAC,CAAC,IAAI;gBACN,CAAC,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,UAAU,EAAE,MAAM,EAAE,SAAS,CAAC,OAAO,CAAC,aAAa,EAAE,CAC3E,CAAC;QACJ,CAAC;IACH,CAAC,EACD,EAAE,CACH,CAAC;IAEF,qEAAqE;IACrE,+EAA+E;IAC/E,iFAAiF;IACjF,MAAM,CAAC,QAAQ,EAAE,WAAW,CAAC,GAAG,QAAQ,CAAC,GAAG,EAAE,CAAC,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC;IAClE,SAAS,CAAC,GAAG,EAAE;QACb,MAAM,IAAI,GAAG,SAAS,CAAC,OAAO,CAAC,SAAS,CAAC;QACzC,IAAI,CAAC,IAAI;YAAE,OAAO;QAClB,IAAI,SAAS,GAAG,KAAK,CAAC;QACtB,OAAO,CAAC,OAAO,EAAE;aACd,IAAI,CAAC,GAAG,EAAE,CAAC,IAAI,EAAE,CAAC;aAClB,KAAK,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,SAAS,CAAC,OAAO,CAAC,OAAO,EAAE,CAAC,GAAG,EAAE,WAAW,CAAC,CAAC;aAC7D,OAAO,CAAC,GAAG,EAAE;YACZ,IAAI,CAAC,SAAS;gBAAE,WAAW,CAAC,IAAI,CAAC,CAAC;QACpC,CAAC,CAAC,CAAC;QACL,OAAO,GAAG,EAAE,GAAG,SAAS,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC;QACnC,wEAAwE;QACxE,oCAAoC;IACtC,CAAC,EAAE,EAAE,CAAC,CAAC;IAEP,0BAA0B;IAC1B,SAAS,CAAC,GAAG,EAAE;QACb,IAAI,WAAW;YAAE,OAAO;QAExB,IAAI,CAAC,IAAI,EAAE,CAAC;YACV,WAAW,CAAC,IAAI,CAAC,CAAC;YAClB,SAAS,CAAC,OAAO,CAAC,iBAAiB,EAAE,CAAC,IAAI,CAAC,CAAC;YAC5C,OAAO;QACT,CAAC;QAED,SAAS,CAAC,OAAO,CAAC,iBAAiB,EAAE,CAAC,IAAI,CAAC,CAAC;QAC5C,KAAK,UAAU,CACb,IAAI,EACJ,KAAK,IAAI,EAAE,CAAC,CAAC,CAAC,MAAM,gBAAgB,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,CAA4B,EAC7E,kBAAkB,CACnB,CAAC;IACJ,CAAC,EAAE,CAAC,IAAI,EAAE,WAAW,EAAE,UAAU,CAAC,CAAC,CAAC;IAEpC,wBAAwB;IACxB,MAAM,aAAa,GAAG,WAAW,CAAC,KAAK,IAAI,EAAE;QAC3C,+FAA+F;QAC/F,gGAAgG;QAChG,iGAAiG;QACjG,2EAA2E;QAC3E,MAAM,QAAQ,GAAG,SAAS,CAAC,OAAO,CAAC,IAAI,CAAC,WAAW,CAAC;QACpD,IAAI,CAAC,QAAQ;YAAE,OAAO;QACtB,MAAM,UAAU,CACd,QAAQ,EACR,KAAK,IAAI,EAAE,CAAC,CAAC,CAAC,MAAM,QAAQ,CAAC,gBAAgB,CAAC,IAAI,CAAC,CAAC,CAAC,MAAM,IAAI,EAAE,CAA4B,EAC7F,eAAe,CAChB,CAAC;IACJ,CAAC,EAAE,CAAC,UAAU,CAAC,CAAC,CAAC;IAEjB,gFAAgF;IAChF,MAAM,aAAa,GAAG,IAAI,IAAI,QAAQ,EAAE,IAAI,KAAK,IAAI,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC;IACxE,MAAM,MAAM,GAAG,aAAa,CAAC,CAAC,CAAC,aAAa,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,aAAa,CAAC;IAC3E,MAAM,aAAa,GAAG,WAAW,IAAI,CAAC,IAAI,KAAK,IAAI,IAAI,aAAa,KAAK,IAAI,CAAC,CAAC;IAE/E,wBAAwB;IACxB,MAAM,OAAO,GAAG,WAAW,IAAI,aAAa,IAAI,CAAC,QAAQ,CAAC;IAE1D,MAAM,KAAK,GAAG,OAAO,CACnB,GAAG,EAAE,CAAC,CAAC;QACL,IAAI;QACJ,MAAM;QACN,OAAO;QACP,WAAW;QACX,aAAa;QACb,eAAe,EAAE,CAAC,CAAC,IAAI;QACvB,aAAa;KACd,CAAC,EACF,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,WAAW,EAAE,aAAa,EAAE,aAAa,CAAC,CACnE,CAAC;IAEF,OAAO,CACL,KAAC,WAAW,CAAC,QAAQ,IAAC,KAAK,EAAE,KAAK,YAC/B,QAAQ,GACY,CACxB,CAAC;AACJ,CAAC","sourcesContent":["\"use client\";\n\nimport {\n createContext,\n useEffect,\n useRef,\n useState,\n useCallback,\n useMemo,\n type ReactNode,\n} from \"react\";\nimport type { User } from \"firebase/auth\";\nimport { useAuthState } from \"./useAuthState.js\";\nimport { getIdTokenClaims } from \"../claims.js\";\nimport type { AuthProviderConfig, AuthContextValue } from \"./types.js\";\n\n/**\n * Claims resolved for one signed-in session. `user` is the Firebase `User` instance (it carries\n * the uid and is replaced on every sign-in), and `generation` orders the reads that produced it.\n */\ninterface ResolvedClaims<TClaims> {\n user: User;\n generation: number;\n claims: TClaims;\n}\n\n \nexport const AuthContext = createContext<AuthContextValue<any> | null>(null);\n\ninterface AuthProviderProps<TClaims> {\n config: AuthProviderConfig<TClaims>;\n children: ReactNode;\n}\n\nexport function AuthProvider<TClaims = Record<string, unknown>>(\n { config, children }: AuthProviderProps<TClaims>,\n) {\n const { user, loading: authLoading } = useAuthState(config.auth);\n\n // Claims are held with the session they were read for and derived at render (below), so a\n // render can never pair one account's `user` with another account's claims, and a read that\n // settles after the session changed (a late refresh, a slow initial read) is dropped.\n const [resolved, setResolved] = useState<ResolvedClaims<TClaims> | null>(null);\n const generationRef = useRef(0);\n const appliedGenerationRef = useRef(0);\n const currentUserRef = useRef<User | null>(user);\n currentUserRef.current = user;\n\n // Stable config ref so effects don't re-fire on config object identity changes\n const configRef = useRef(config);\n configRef.current = config;\n\n /**\n * Read `forUser`'s claims under a new generation and apply them only if `forUser` is still\n * the signed-in session and no newer read has applied. A failed read leaves the session's\n * claims as they are, or settles an unread session on the default claims.\n */\n const readClaims = useCallback(\n async (forUser: User, read: () => Promise<Record<string, unknown> | null>, context: string) => {\n const generation = ++generationRef.current;\n try {\n const raw = await read();\n if (currentUserRef.current !== forUser || generation <= appliedGenerationRef.current) return;\n appliedGenerationRef.current = generation;\n setResolved({ user: forUser, generation, claims: configRef.current.parseClaims(raw ?? {}) });\n } catch (err) {\n if (currentUserRef.current !== forUser) return;\n configRef.current.onError?.(err, context);\n setResolved((prev) =>\n prev && prev.user === forUser\n ? prev\n : { user: forUser, generation, claims: configRef.current.defaultClaims },\n );\n }\n },\n [],\n );\n\n // --- Optional readiness gate (see AuthProviderConfig.readyGate) ---\n // Open immediately when no gate is configured; otherwise held closed until the\n // gate settles. Fail-open: a rejected gate is reported and then treated as open.\n const [gateOpen, setGateOpen] = useState(() => !config.readyGate);\n useEffect(() => {\n const gate = configRef.current.readyGate;\n if (!gate) return;\n let cancelled = false;\n Promise.resolve()\n .then(() => gate())\n .catch((err) => configRef.current.onError?.(err, \"readyGate\"))\n .finally(() => {\n if (!cancelled) setGateOpen(true);\n });\n return () => { cancelled = true; };\n // Mount-only by design (reads only the stable configRef): the gate is a\n // one-time client readiness signal.\n }, []);\n\n // --- Claims fetching ---\n useEffect(() => {\n if (authLoading) return;\n\n if (!user) {\n setResolved(null);\n configRef.current.onAuthStateChange?.(null);\n return;\n }\n\n configRef.current.onAuthStateChange?.(user);\n void readClaims(\n user,\n async () => ((await getIdTokenClaims(user)) ?? {}) as Record<string, unknown>,\n \"getIdTokenClaims\",\n );\n }, [user, authLoading, readClaims]);\n\n // --- refreshClaims ---\n const refreshClaims = useCallback(async () => {\n // The LIVE signed-in user, never the one this render saw: a caller can hold this callback from\n // before its own sign-in (a registration's submit handler creates the account and then asks for\n // the claims the server just set), and a refresh of that render's `null` user would do nothing —\n // leaving the new account on its pre-claim token until the hourly refresh.\n const liveUser = configRef.current.auth.currentUser;\n if (!liveUser) return;\n await readClaims(\n liveUser,\n async () => ((await liveUser.getIdTokenResult(true)).claims ?? {}) as Record<string, unknown>,\n \"refreshClaims\",\n );\n }, [readClaims]);\n\n // --- Derived at render: only the current session's claims are ever exposed ---\n const sessionClaims = user && resolved?.user === user ? resolved : null;\n const claims = sessionClaims ? sessionClaims.claims : config.defaultClaims;\n const claimsLoading = authLoading || (user !== null && sessionClaims === null);\n\n // --- Context value ---\n const loading = authLoading || claimsLoading || !gateOpen;\n\n const value = useMemo<AuthContextValue<TClaims>>(\n () => ({\n user,\n claims,\n loading,\n authLoading,\n claimsLoading,\n isAuthenticated: !!user,\n refreshClaims,\n }),\n [user, claims, loading, authLoading, claimsLoading, refreshClaims],\n );\n\n return (\n <AuthContext.Provider value={value}>\n {children}\n </AuthContext.Provider>\n );\n}\n"]}
1
+ {"version":3,"file":"AuthProvider.js","sourceRoot":"","sources":["../../src/react/AuthProvider.tsx"],"names":[],"mappings":"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","sourcesContent":["'use client';\n\nimport {\n createContext,\n useEffect,\n useRef,\n useState,\n useCallback,\n useMemo,\n type ReactNode,\n} from 'react';\nimport type { User } from 'firebase/auth';\nimport { useAuthState } from './useAuthState.js';\nimport { getIdTokenClaims } from '../client/claims.js';\nimport type { AuthProviderConfig, AuthContextValue } from './types.js';\n\n/**\n * Claims resolved for one signed-in session. `user` is the Firebase `User` instance (it carries\n * the uid and is replaced on every sign-in), and `generation` orders the reads that produced it.\n */\ninterface ResolvedClaims<TClaims> {\n user: User;\n generation: number;\n claims: TClaims;\n}\n\nexport const AuthContext = createContext<AuthContextValue<any> | null>(null);\n\ninterface AuthProviderProps<TClaims> {\n config: AuthProviderConfig<TClaims>;\n children: ReactNode;\n}\n\nexport function AuthProvider<TClaims = Record<string, unknown>>({\n config,\n children,\n}: AuthProviderProps<TClaims>) {\n const { user, loading: authLoading } = useAuthState(config.auth);\n\n // Claims are held with the session they were read for and derived at render (below), so a\n // render can never pair one account's `user` with another account's claims, and a read that\n // settles after the session changed (a late refresh, a slow initial read) is dropped.\n const [resolved, setResolved] = useState<ResolvedClaims<TClaims> | null>(null);\n const generationRef = useRef(0);\n const appliedGenerationRef = useRef(0);\n const currentUserRef = useRef<User | null>(user);\n currentUserRef.current = user;\n\n // Stable config ref so effects don't re-fire on config object identity changes\n const configRef = useRef(config);\n configRef.current = config;\n\n /**\n * Read `forUser`'s claims under a new generation and apply them only if `forUser` is still\n * the signed-in session and no newer read has applied. A failed read leaves the session's\n * claims as they are, or settles an unread session on the default claims.\n */\n const readClaims = useCallback(\n async (forUser: User, read: () => Promise<Record<string, unknown> | null>, context: string) => {\n const generation = ++generationRef.current;\n try {\n const raw = await read();\n if (currentUserRef.current !== forUser || generation <= appliedGenerationRef.current)\n return;\n appliedGenerationRef.current = generation;\n setResolved({\n user: forUser,\n generation,\n claims: configRef.current.parseClaims(raw ?? {}),\n });\n } catch (err) {\n if (currentUserRef.current !== forUser) return;\n configRef.current.onError?.(err, context);\n setResolved((prev) =>\n prev && prev.user === forUser\n ? prev\n : { user: forUser, generation, claims: configRef.current.defaultClaims },\n );\n }\n },\n [],\n );\n\n // --- Optional readiness gate (see AuthProviderConfig.readyGate) ---\n // Open immediately when no gate is configured; otherwise held closed until the\n // gate settles. Fail-open: a rejected gate is reported and then treated as open.\n const [gateOpen, setGateOpen] = useState(() => !config.readyGate);\n useEffect(() => {\n const gate = configRef.current.readyGate;\n if (!gate) return;\n let cancelled = false;\n Promise.resolve()\n .then(() => gate())\n .catch((err) => configRef.current.onError?.(err, 'readyGate'))\n .finally(() => {\n if (!cancelled) setGateOpen(true);\n });\n return () => {\n cancelled = true;\n };\n // Mount-only by design (reads only the stable configRef): the gate is a\n // one-time client readiness signal.\n }, []);\n\n // --- Claims fetching ---\n useEffect(() => {\n if (authLoading) return;\n\n if (!user) {\n setResolved(null);\n configRef.current.onAuthStateChange?.(null);\n return;\n }\n\n configRef.current.onAuthStateChange?.(user);\n void readClaims(\n user,\n async () => ((await getIdTokenClaims(user)) ?? {}) as Record<string, unknown>,\n 'getIdTokenClaims',\n );\n }, [user, authLoading, readClaims]);\n\n // --- refreshClaims ---\n const refreshClaims = useCallback(async () => {\n // The LIVE signed-in user, never the one this render saw: a caller can hold this callback from\n // before its own sign-in (a registration's submit handler creates the account and then asks for\n // the claims the server just set), and a refresh of that render's `null` user would do nothing —\n // leaving the new account on its pre-claim token until the hourly refresh.\n const liveUser = configRef.current.auth.currentUser;\n if (!liveUser) return;\n await readClaims(\n liveUser,\n async () => ((await liveUser.getIdTokenResult(true)).claims ?? {}) as Record<string, unknown>,\n 'refreshClaims',\n );\n }, [readClaims]);\n\n // --- Derived at render: only the current session's claims are ever exposed ---\n const sessionClaims = user && resolved?.user === user ? resolved : null;\n const claimsLoading = authLoading || (user !== null && sessionClaims === null);\n\n // While the gate is closed a restored session is held back from consumers: a uid-keyed\n // subscription opened before the gate's readiness signal is rejected once and dies, so no\n // consumer may see a uid (or that session's claims) until the gate opens or fails open.\n const exposedUser = gateOpen ? user : null;\n const claims = exposedUser && sessionClaims ? sessionClaims.claims : config.defaultClaims;\n\n // --- Context value ---\n const loading = authLoading || claimsLoading || !gateOpen;\n\n const value = useMemo<AuthContextValue<TClaims>>(\n () => ({\n user: exposedUser,\n claims,\n loading,\n authLoading,\n claimsLoading,\n isAuthenticated: !!exposedUser,\n refreshClaims,\n }),\n [exposedUser, claims, loading, authLoading, claimsLoading, refreshClaims],\n );\n\n return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;\n}\n"]}
@@ -1,9 +1,9 @@
1
- export type { AuthState } from "./useAuthState.js";
2
- export { useAuthState } from "./useAuthState.js";
3
- export type { AuthProviderConfig, AuthContextValue } from "./types.js";
4
- export { AuthProvider } from "./AuthProvider.js";
5
- export { AuthContext } from "./AuthProvider.js";
6
- export { useAuth } from "./useAuth.js";
7
- export { useAuthGuard } from "./useAuthGuard.js";
8
- export type { AuthGuardConfig } from "./useAuthGuard.js";
1
+ export type { AuthState } from './useAuthState.js';
2
+ export { useAuthState } from './useAuthState.js';
3
+ export type { AuthProviderConfig, AuthContextValue } from './types.js';
4
+ export { AuthProvider } from './AuthProvider.js';
5
+ export { AuthContext } from './AuthProvider.js';
6
+ export { useAuth } from './useAuth.js';
7
+ export { useAuthGuard } from './useAuthGuard.js';
8
+ export type { AuthGuardConfig } from './useAuthGuard.js';
9
9
  //# sourceMappingURL=index.d.ts.map
@@ -1,7 +1,7 @@
1
- "use client";
2
- export { useAuthState } from "./useAuthState.js";
3
- export { AuthProvider } from "./AuthProvider.js";
4
- export { AuthContext } from "./AuthProvider.js";
5
- export { useAuth } from "./useAuth.js";
6
- export { useAuthGuard } from "./useAuthGuard.js";
1
+ 'use client';
2
+ export { useAuthState } from './useAuthState.js';
3
+ export { AuthProvider } from './AuthProvider.js';
4
+ export { AuthContext } from './AuthProvider.js';
5
+ export { useAuth } from './useAuth.js';
6
+ export { useAuthGuard } from './useAuthGuard.js';
7
7
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/react/index.ts"],"names":[],"mappings":"AAAA,YAAY,CAAC;AAGb,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAEjD,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AACjD,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAChD,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AACvC,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC","sourcesContent":["\"use client\";\n\nexport type { AuthState } from \"./useAuthState.js\";\nexport { useAuthState } from \"./useAuthState.js\";\nexport type { AuthProviderConfig, AuthContextValue } from \"./types.js\";\nexport { AuthProvider } from \"./AuthProvider.js\";\nexport { AuthContext } from \"./AuthProvider.js\";\nexport { useAuth } from \"./useAuth.js\";\nexport { useAuthGuard } from \"./useAuthGuard.js\";\nexport type { AuthGuardConfig } from \"./useAuthGuard.js\";\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/react/index.ts"],"names":[],"mappings":"AAAA,YAAY,CAAC;AAGb,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAEjD,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AACjD,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAChD,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AACvC,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC","sourcesContent":["'use client';\n\nexport type { AuthState } from './useAuthState.js';\nexport { useAuthState } from './useAuthState.js';\nexport type { AuthProviderConfig, AuthContextValue } from './types.js';\nexport { AuthProvider } from './AuthProvider.js';\nexport { AuthContext } from './AuthProvider.js';\nexport { useAuth } from './useAuth.js';\nexport { useAuthGuard } from './useAuthGuard.js';\nexport type { AuthGuardConfig } from './useAuthGuard.js';\n"]}
@@ -4,7 +4,7 @@
4
4
  // same-page sign-in still lands on it. Outside a browser every call is a no-op.
5
5
  const memory = new Map();
6
6
  function inBrowser() {
7
- return typeof window !== "undefined";
7
+ return typeof window !== 'undefined';
8
8
  }
9
9
  function storage() {
10
10
  try {
@@ -1 +1 @@
1
- {"version":3,"file":"redirect-storage.js","sourceRoot":"","sources":["../../src/react/redirect-storage.ts"],"names":[],"mappings":"AAAA,6FAA6F;AAC7F,+FAA+F;AAC/F,6FAA6F;AAC7F,gFAAgF;AAEhF,MAAM,MAAM,GAAG,IAAI,GAAG,EAAkB,CAAC;AAEzC,SAAS,SAAS;IAChB,OAAO,OAAO,MAAM,KAAK,WAAW,CAAC;AACvC,CAAC;AAED,SAAS,OAAO;IACd,IAAI,CAAC;QACH,OAAO,MAAM,CAAC,YAAY,CAAC;IAC7B,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC;AAED,MAAM,UAAU,gBAAgB,CAAC,GAAW;IAC1C,IAAI,CAAC,SAAS,EAAE;QAAE,OAAO,IAAI,CAAC;IAC9B,IAAI,CAAC;QACH,MAAM,KAAK,GAAG,OAAO,EAAE,EAAE,OAAO,CAAC,GAAG,CAAC,IAAI,IAAI,CAAC;QAC9C,IAAI,KAAK,KAAK,IAAI;YAAE,OAAO,KAAK,CAAC;IACnC,CAAC;IAAC,MAAM,CAAC;QACP,qDAAqD;IACvD,CAAC;IACD,OAAO,MAAM,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,IAAI,CAAC;AACjC,CAAC;AAED,MAAM,UAAU,gBAAgB,CAAC,GAAW,EAAE,IAAY;IACxD,IAAI,CAAC,SAAS,EAAE;QAAE,OAAO;IACzB,MAAM,KAAK,GAAG,OAAO,EAAE,CAAC;IACxB,IAAI,KAAK,EAAE,CAAC;QACV,IAAI,CAAC;YACH,KAAK,CAAC,OAAO,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;YACzB,MAAM,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;YACnB,OAAO;QACT,CAAC;QAAC,MAAM,CAAC;YACP,gDAAgD;QAClD,CAAC;IACH,CAAC;IACD,MAAM,CAAC,GAAG,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;AACxB,CAAC;AAED,MAAM,UAAU,iBAAiB,CAAC,GAAW;IAC3C,IAAI,CAAC,SAAS,EAAE;QAAE,OAAO;IACzB,MAAM,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;IACnB,IAAI,CAAC;QACH,OAAO,EAAE,EAAE,UAAU,CAAC,GAAG,CAAC,CAAC;IAC7B,CAAC;IAAC,MAAM,CAAC;QACP,8CAA8C;IAChD,CAAC;AACH,CAAC","sourcesContent":["// The post-login redirect path lives in localStorage. A browser that blocks site data throws\n// on the storage access itself (FRONTEND-010), so every access here is guarded: a blocked read\n// is \"nothing saved\", and a blocked write keeps the value in memory for the page's life so a\n// same-page sign-in still lands on it. Outside a browser every call is a no-op.\n\nconst memory = new Map<string, string>();\n\nfunction inBrowser(): boolean {\n return typeof window !== \"undefined\";\n}\n\nfunction storage(): Storage | null {\n try {\n return window.localStorage;\n } catch {\n return null;\n }\n}\n\nexport function readRedirectPath(key: string): string | null {\n if (!inBrowser()) return null;\n try {\n const saved = storage()?.getItem(key) ?? null;\n if (saved !== null) return saved;\n } catch {\n // Blocked store: fall through to the page-life copy.\n }\n return memory.get(key) ?? null;\n}\n\nexport function saveRedirectPath(key: string, path: string): void {\n if (!inBrowser()) return;\n const store = storage();\n if (store) {\n try {\n store.setItem(key, path);\n memory.delete(key);\n return;\n } catch {\n // Blocked store: keep the page-life copy below.\n }\n }\n memory.set(key, path);\n}\n\nexport function clearRedirectPath(key: string): void {\n if (!inBrowser()) return;\n memory.delete(key);\n try {\n storage()?.removeItem(key);\n } catch {\n // Blocked store: nothing was persisted there.\n }\n}\n"]}
1
+ {"version":3,"file":"redirect-storage.js","sourceRoot":"","sources":["../../src/react/redirect-storage.ts"],"names":[],"mappings":"AAAA,6FAA6F;AAC7F,+FAA+F;AAC/F,6FAA6F;AAC7F,gFAAgF;AAEhF,MAAM,MAAM,GAAG,IAAI,GAAG,EAAkB,CAAC;AAEzC,SAAS,SAAS;IAChB,OAAO,OAAO,MAAM,KAAK,WAAW,CAAC;AACvC,CAAC;AAED,SAAS,OAAO;IACd,IAAI,CAAC;QACH,OAAO,MAAM,CAAC,YAAY,CAAC;IAC7B,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC;AAED,MAAM,UAAU,gBAAgB,CAAC,GAAW;IAC1C,IAAI,CAAC,SAAS,EAAE;QAAE,OAAO,IAAI,CAAC;IAC9B,IAAI,CAAC;QACH,MAAM,KAAK,GAAG,OAAO,EAAE,EAAE,OAAO,CAAC,GAAG,CAAC,IAAI,IAAI,CAAC;QAC9C,IAAI,KAAK,KAAK,IAAI;YAAE,OAAO,KAAK,CAAC;IACnC,CAAC;IAAC,MAAM,CAAC;QACP,qDAAqD;IACvD,CAAC;IACD,OAAO,MAAM,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,IAAI,CAAC;AACjC,CAAC;AAED,MAAM,UAAU,gBAAgB,CAAC,GAAW,EAAE,IAAY;IACxD,IAAI,CAAC,SAAS,EAAE;QAAE,OAAO;IACzB,MAAM,KAAK,GAAG,OAAO,EAAE,CAAC;IACxB,IAAI,KAAK,EAAE,CAAC;QACV,IAAI,CAAC;YACH,KAAK,CAAC,OAAO,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;YACzB,MAAM,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;YACnB,OAAO;QACT,CAAC;QAAC,MAAM,CAAC;YACP,gDAAgD;QAClD,CAAC;IACH,CAAC;IACD,MAAM,CAAC,GAAG,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;AACxB,CAAC;AAED,MAAM,UAAU,iBAAiB,CAAC,GAAW;IAC3C,IAAI,CAAC,SAAS,EAAE;QAAE,OAAO;IACzB,MAAM,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;IACnB,IAAI,CAAC;QACH,OAAO,EAAE,EAAE,UAAU,CAAC,GAAG,CAAC,CAAC;IAC7B,CAAC;IAAC,MAAM,CAAC;QACP,8CAA8C;IAChD,CAAC;AACH,CAAC","sourcesContent":["// The post-login redirect path lives in localStorage. A browser that blocks site data throws\n// on the storage access itself (FRONTEND-010), so every access here is guarded: a blocked read\n// is \"nothing saved\", and a blocked write keeps the value in memory for the page's life so a\n// same-page sign-in still lands on it. Outside a browser every call is a no-op.\n\nconst memory = new Map<string, string>();\n\nfunction inBrowser(): boolean {\n return typeof window !== 'undefined';\n}\n\nfunction storage(): Storage | null {\n try {\n return window.localStorage;\n } catch {\n return null;\n }\n}\n\nexport function readRedirectPath(key: string): string | null {\n if (!inBrowser()) return null;\n try {\n const saved = storage()?.getItem(key) ?? null;\n if (saved !== null) return saved;\n } catch {\n // Blocked store: fall through to the page-life copy.\n }\n return memory.get(key) ?? null;\n}\n\nexport function saveRedirectPath(key: string, path: string): void {\n if (!inBrowser()) return;\n const store = storage();\n if (store) {\n try {\n store.setItem(key, path);\n memory.delete(key);\n return;\n } catch {\n // Blocked store: keep the page-life copy below.\n }\n }\n memory.set(key, path);\n}\n\nexport function clearRedirectPath(key: string): void {\n if (!inBrowser()) return;\n memory.delete(key);\n try {\n storage()?.removeItem(key);\n } catch {\n // Blocked store: nothing was persisted there.\n }\n}\n"]}
@@ -1,4 +1,4 @@
1
- import type { Auth, User } from "firebase/auth";
1
+ import type { Auth, User } from 'firebase/auth';
2
2
  export interface AuthProviderConfig<TClaims = Record<string, unknown>> {
3
3
  auth: Auth;
4
4
  /** Transform raw ID token claims into app claims shape */
@@ -14,7 +14,9 @@ export interface AuthProviderConfig<TClaims = Record<string, unknown>> {
14
14
  * Use when data consumers keyed on `user` must not mount until some async
15
15
  * client readiness completes first — e.g. an App Check / attestation token,
16
16
  * without which every data subscription opened at mount is rejected once and
17
- * dies. The gate FAILS OPEN: a rejection is reported via `onError('readyGate')`
17
+ * dies. While it is closed the context exposes `user: null`,
18
+ * `isAuthenticated: false` and the default claims, so no uid-keyed consumer can
19
+ * mount early. The gate FAILS OPEN: a rejection is reported via `onError('readyGate')`
18
20
  * and the provider proceeds, so a broken gate degrades to today's behavior
19
21
  * rather than bricking the app. Called once on mount (client only).
20
22
  */
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/react/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,eAAe,CAAC;AAEhD,MAAM,WAAW,kBAAkB,CAAC,OAAO,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;IACnE,IAAI,EAAE,IAAI,CAAC;IAEX,0DAA0D;IAC1D,WAAW,EAAE,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,OAAO,CAAC;IAEvD,wDAAwD;IACxD,aAAa,EAAE,OAAO,CAAC;IAEvB,uEAAuE;IACvE,iBAAiB,CAAC,EAAE,CAAC,IAAI,EAAE,IAAI,GAAG,IAAI,KAAK,IAAI,CAAC;IAEhD,oCAAoC;IACpC,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IAEpD;;;;;;;;OAQG;IACH,SAAS,CAAC,EAAE,MAAM,OAAO,CAAC,OAAO,CAAC,CAAC;CACpC;AAED,MAAM,WAAW,gBAAgB,CAAC,OAAO,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;IACjE,oBAAoB;IACpB,IAAI,EAAE,IAAI,GAAG,IAAI,CAAC;IAElB,2BAA2B;IAC3B,MAAM,EAAE,OAAO,CAAC;IAEhB,wCAAwC;IACxC,OAAO,EAAE,OAAO,CAAC;IAEjB,gCAAgC;IAChC,WAAW,EAAE,OAAO,CAAC;IACrB,aAAa,EAAE,OAAO,CAAC;IAEvB,0BAA0B;IAC1B,eAAe,EAAE,OAAO,CAAC;IAEzB,yCAAyC;IACzC,aAAa,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CACpC"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/react/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,eAAe,CAAC;AAEhD,MAAM,WAAW,kBAAkB,CAAC,OAAO,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;IACnE,IAAI,EAAE,IAAI,CAAC;IAEX,0DAA0D;IAC1D,WAAW,EAAE,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,OAAO,CAAC;IAEvD,wDAAwD;IACxD,aAAa,EAAE,OAAO,CAAC;IAEvB,uEAAuE;IACvE,iBAAiB,CAAC,EAAE,CAAC,IAAI,EAAE,IAAI,GAAG,IAAI,KAAK,IAAI,CAAC;IAEhD,oCAAoC;IACpC,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IAEpD;;;;;;;;;;OAUG;IACH,SAAS,CAAC,EAAE,MAAM,OAAO,CAAC,OAAO,CAAC,CAAC;CACpC;AAED,MAAM,WAAW,gBAAgB,CAAC,OAAO,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;IACjE,oBAAoB;IACpB,IAAI,EAAE,IAAI,GAAG,IAAI,CAAC;IAElB,2BAA2B;IAC3B,MAAM,EAAE,OAAO,CAAC;IAEhB,wCAAwC;IACxC,OAAO,EAAE,OAAO,CAAC;IAEjB,gCAAgC;IAChC,WAAW,EAAE,OAAO,CAAC;IACrB,aAAa,EAAE,OAAO,CAAC;IAEvB,0BAA0B;IAC1B,eAAe,EAAE,OAAO,CAAC;IAEzB,yCAAyC;IACzC,aAAa,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CACpC"}
@@ -1 +1 @@
1
- {"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/react/types.ts"],"names":[],"mappings":"","sourcesContent":["import type { Auth, User } from \"firebase/auth\";\n\nexport interface AuthProviderConfig<TClaims = Record<string, unknown>> {\n auth: Auth;\n\n /** Transform raw ID token claims into app claims shape */\n parseClaims: (raw: Record<string, unknown>) => TClaims;\n\n /** Default claims when no user or claims unavailable */\n defaultClaims: TClaims;\n\n /** Called on Firebase auth state change (for monitoring, analytics) */\n onAuthStateChange?: (user: User | null) => void;\n\n /** Called on errors (for Sentry) */\n onError?: (error: unknown, context: string) => void;\n\n /**\n * Optional readiness gate awaited before the provider reports `loading: false`.\n * Use when data consumers keyed on `user` must not mount until some async\n * client readiness completes first — e.g. an App Check / attestation token,\n * without which every data subscription opened at mount is rejected once and\n * dies. The gate FAILS OPEN: a rejection is reported via `onError('readyGate')`\n * and the provider proceeds, so a broken gate degrades to today's behavior\n * rather than bricking the app. Called once on mount (client only).\n */\n readyGate?: () => Promise<unknown>;\n}\n\nexport interface AuthContextValue<TClaims = Record<string, unknown>> {\n /** Firebase User */\n user: User | null;\n\n /** Parsed custom claims */\n claims: TClaims;\n\n /** True until auth + claims resolved */\n loading: boolean;\n\n /** Individual loading states */\n authLoading: boolean;\n claimsLoading: boolean;\n\n /** Convenience: !!user */\n isAuthenticated: boolean;\n\n /** Force refresh claims from Firebase */\n refreshClaims: () => Promise<void>;\n}\n"]}
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/react/types.ts"],"names":[],"mappings":"","sourcesContent":["import type { Auth, User } from 'firebase/auth';\n\nexport interface AuthProviderConfig<TClaims = Record<string, unknown>> {\n auth: Auth;\n\n /** Transform raw ID token claims into app claims shape */\n parseClaims: (raw: Record<string, unknown>) => TClaims;\n\n /** Default claims when no user or claims unavailable */\n defaultClaims: TClaims;\n\n /** Called on Firebase auth state change (for monitoring, analytics) */\n onAuthStateChange?: (user: User | null) => void;\n\n /** Called on errors (for Sentry) */\n onError?: (error: unknown, context: string) => void;\n\n /**\n * Optional readiness gate awaited before the provider reports `loading: false`.\n * Use when data consumers keyed on `user` must not mount until some async\n * client readiness completes first — e.g. an App Check / attestation token,\n * without which every data subscription opened at mount is rejected once and\n * dies. While it is closed the context exposes `user: null`,\n * `isAuthenticated: false` and the default claims, so no uid-keyed consumer can\n * mount early. The gate FAILS OPEN: a rejection is reported via `onError('readyGate')`\n * and the provider proceeds, so a broken gate degrades to today's behavior\n * rather than bricking the app. Called once on mount (client only).\n */\n readyGate?: () => Promise<unknown>;\n}\n\nexport interface AuthContextValue<TClaims = Record<string, unknown>> {\n /** Firebase User */\n user: User | null;\n\n /** Parsed custom claims */\n claims: TClaims;\n\n /** True until auth + claims resolved */\n loading: boolean;\n\n /** Individual loading states */\n authLoading: boolean;\n claimsLoading: boolean;\n\n /** Convenience: !!user */\n isAuthenticated: boolean;\n\n /** Force refresh claims from Firebase */\n refreshClaims: () => Promise<void>;\n}\n"]}
@@ -1,4 +1,4 @@
1
- import type { AuthContextValue } from "./types.js";
1
+ import type { AuthContextValue } from './types.js';
2
2
  /**
3
3
  * Access the auth context. Must be used within an AuthProvider.
4
4
  * Generic param lets each app specify its own claims type.
@@ -1,6 +1,6 @@
1
- "use client";
2
- import { useContext } from "react";
3
- import { AuthContext } from "./AuthProvider.js";
1
+ 'use client';
2
+ import { useContext } from 'react';
3
+ import { AuthContext } from './AuthProvider.js';
4
4
  /**
5
5
  * Access the auth context. Must be used within an AuthProvider.
6
6
  * Generic param lets each app specify its own claims type.
@@ -8,7 +8,7 @@ import { AuthContext } from "./AuthProvider.js";
8
8
  export function useAuth() {
9
9
  const ctx = useContext(AuthContext);
10
10
  if (!ctx) {
11
- throw new Error("useAuth must be used within an AuthProvider");
11
+ throw new Error('useAuth must be used within an AuthProvider');
12
12
  }
13
13
  return ctx;
14
14
  }
@@ -1 +1 @@
1
- {"version":3,"file":"useAuth.js","sourceRoot":"","sources":["../../src/react/useAuth.ts"],"names":[],"mappings":"AAAA,YAAY,CAAC;AAEb,OAAO,EAAE,UAAU,EAAE,MAAM,OAAO,CAAC;AACnC,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAGhD;;;GAGG;AACH,MAAM,UAAU,OAAO;IACrB,MAAM,GAAG,GAAG,UAAU,CAAC,WAAW,CAAC,CAAC;IACpC,IAAI,CAAC,GAAG,EAAE,CAAC;QACT,MAAM,IAAI,KAAK,CAAC,6CAA6C,CAAC,CAAC;IACjE,CAAC;IACD,OAAO,GAAgC,CAAC;AAC1C,CAAC","sourcesContent":["\"use client\";\n\nimport { useContext } from \"react\";\nimport { AuthContext } from \"./AuthProvider.js\";\nimport type { AuthContextValue } from \"./types.js\";\n\n/**\n * Access the auth context. Must be used within an AuthProvider.\n * Generic param lets each app specify its own claims type.\n */\nexport function useAuth<TClaims = Record<string, unknown>>(): AuthContextValue<TClaims> {\n const ctx = useContext(AuthContext);\n if (!ctx) {\n throw new Error(\"useAuth must be used within an AuthProvider\");\n }\n return ctx as AuthContextValue<TClaims>;\n}\n"]}
1
+ {"version":3,"file":"useAuth.js","sourceRoot":"","sources":["../../src/react/useAuth.ts"],"names":[],"mappings":"AAAA,YAAY,CAAC;AAEb,OAAO,EAAE,UAAU,EAAE,MAAM,OAAO,CAAC;AACnC,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAGhD;;;GAGG;AACH,MAAM,UAAU,OAAO;IACrB,MAAM,GAAG,GAAG,UAAU,CAAC,WAAW,CAAC,CAAC;IACpC,IAAI,CAAC,GAAG,EAAE,CAAC;QACT,MAAM,IAAI,KAAK,CAAC,6CAA6C,CAAC,CAAC;IACjE,CAAC;IACD,OAAO,GAAgC,CAAC;AAC1C,CAAC","sourcesContent":["'use client';\n\nimport { useContext } from 'react';\nimport { AuthContext } from './AuthProvider.js';\nimport type { AuthContextValue } from './types.js';\n\n/**\n * Access the auth context. Must be used within an AuthProvider.\n * Generic param lets each app specify its own claims type.\n */\nexport function useAuth<TClaims = Record<string, unknown>>(): AuthContextValue<TClaims> {\n const ctx = useContext(AuthContext);\n if (!ctx) {\n throw new Error('useAuth must be used within an AuthProvider');\n }\n return ctx as AuthContextValue<TClaims>;\n}\n"]}
@@ -1,7 +1,7 @@
1
- "use client";
2
- import { useEffect, useRef } from "react";
3
- import { clearRedirectPath, readRedirectPath, saveRedirectPath } from "./redirect-storage.js";
4
- const DEFAULT_REDIRECT_KEY = "auth_redirect_path";
1
+ 'use client';
2
+ import { useEffect, useRef } from 'react';
3
+ import { clearRedirectPath, readRedirectPath, saveRedirectPath } from './redirect-storage.js';
4
+ const DEFAULT_REDIRECT_KEY = 'auth_redirect_path';
5
5
  /**
6
6
  * Handles route protection. Call in app shell component.
7
7
  *
@@ -51,7 +51,7 @@ export function useAuthGuard(config) {
51
51
  authRouteRedirected.current = false;
52
52
  }
53
53
  // Root path special handling
54
- if (rootRedirect && pathname === "/") {
54
+ if (rootRedirect && pathname === '/') {
55
55
  replace(isAuthenticated ? rootRedirect.authenticated : rootRedirect.unauthenticated);
56
56
  return;
57
57
  }
@@ -1 +1 @@
1
- {"version":3,"file":"useAuthGuard.js","sourceRoot":"","sources":["../../src/react/useAuthGuard.ts"],"names":[],"mappings":"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","sourcesContent":["\"use client\";\n\nimport { useEffect, useRef } from \"react\";\nimport { clearRedirectPath, readRedirectPath, saveRedirectPath } from \"./redirect-storage.js\";\n\nexport interface AuthGuardConfig {\n /**\n * Routes accessible without authentication, matched by PREFIX\n * (`pathname.startsWith(entry)`), so `'/share/'` covers every `/share/:id`\n * beneath it and `'/terms'` covers `/terms/anything`. This is the SOLE source of public\n * routes — the hook has no built-in defaults and no app-specific literals, so\n * a consumer that needs a dynamic public section must list its prefix here.\n */\n publicRoutes: string[];\n /** Routes that redirect authenticated users away (login, register) */\n authRedirectRoutes: string[];\n /** Where unauthenticated users go */\n loginRoute: string;\n /** Where authenticated users go from authRedirectRoutes */\n defaultRoute: string;\n /** Special root path handling */\n rootRedirect?: { authenticated: string; unauthenticated: string };\n /** localStorage key for post-login redirect */\n redirectKey?: string;\n /** Navigation function (e.g. router.replace) */\n replace: (path: string) => void;\n /** Current path */\n pathname: string;\n /** From useAuth() */\n loading: boolean;\n /** From useAuth() */\n isAuthenticated: boolean;\n}\n\nconst DEFAULT_REDIRECT_KEY = \"auth_redirect_path\";\n\n/**\n * Handles route protection. Call in app shell component.\n *\n * - Does nothing while loading.\n * - Redirects unauthenticated users from protected routes to loginRoute.\n * - Redirects authenticated users from authRedirectRoutes to defaultRoute.\n * - Saves attempted path for post-login redirect — a visitor's deep link only. When a\n * session ends (an observed signed-in → signed-out transition: a sign-out, a forced\n * sign-out, an expiry, another tab), the path it ended on is NOT saved and any saved\n * path is cleared, so the next account to sign in on this device never lands on the\n * previous account's page.\n *\n * \"Public\" is decided ENTIRELY by `config.publicRoutes` (prefix match). The hook\n * carries no built-in or implicit public route: every route a consumer wants\n * reachable while signed out — including a dynamic section like `/share/:id` — is\n * listed by that consumer as a prefix.\n */\nexport function useAuthGuard(config: AuthGuardConfig): void {\n const {\n publicRoutes,\n authRedirectRoutes,\n loginRoute,\n defaultRoute,\n rootRedirect,\n redirectKey = DEFAULT_REDIRECT_KEY,\n replace,\n pathname,\n loading,\n isAuthenticated,\n } = config;\n\n // The auth-route redirect below is SINGLE-SHOT per auth-route visit. That branch\n // CONSUMES redirectKey and issues a navigation, but the navigation commits\n // asynchronously (the router fetches the target route first), so this effect can\n // legitimately re-run while pathname still reads the auth route (an auth-state\n // re-render, a loading flap). A re-run then finds the key already gone and\n // re-replaces to defaultRoute, CLOBBERING the in-flight saved-path navigation —\n // a deep link that routed through login then landed on defaultRoute instead of\n // the saved path. The latch resets whenever the pathname is off the auth-route set.\n const authRouteRedirected = useRef(false);\n\n // The last settled authentication state this guard observed (null before the first), and\n // the pathname a session ended on. The effect can re-run on that same pathname before the\n // login navigation commits, so the \"do not save\" decision is held for that pathname rather\n // than for one run.\n const lastAuthenticated = useRef<boolean | null>(null);\n const sessionEndedOn = useRef<string | null>(null);\n\n useEffect(() => {\n if (loading) return;\n\n if (lastAuthenticated.current === true && !isAuthenticated) {\n sessionEndedOn.current = pathname;\n clearRedirectPath(redirectKey);\n } else if (isAuthenticated || sessionEndedOn.current !== pathname) {\n sessionEndedOn.current = null;\n }\n lastAuthenticated.current = isAuthenticated;\n\n if (!authRedirectRoutes.includes(pathname)) {\n authRouteRedirected.current = false;\n }\n\n // Root path special handling\n if (rootRedirect && pathname === \"/\") {\n replace(isAuthenticated ? rootRedirect.authenticated : rootRedirect.unauthenticated);\n return;\n }\n\n const isPublic = publicRoutes.some((r) => pathname.startsWith(r));\n const isAuthRedirect = authRedirectRoutes.includes(pathname);\n\n // Unauthenticated on protected route -> save a visitor's path, go to login\n if (!isAuthenticated && !isPublic) {\n if (sessionEndedOn.current === null) saveRedirectPath(redirectKey, pathname);\n replace(loginRoute);\n return;\n }\n\n // Authenticated user on auth-only route (login, register, etc.)\n if (isAuthenticated && isAuthRedirect) {\n if (authRouteRedirected.current) return;\n authRouteRedirected.current = true;\n let target = defaultRoute;\n const saved = readRedirectPath(redirectKey);\n if (saved && !authRedirectRoutes.includes(saved)) {\n target = saved;\n }\n clearRedirectPath(redirectKey);\n replace(target);\n }\n }, [\n loading,\n isAuthenticated,\n pathname,\n publicRoutes,\n authRedirectRoutes,\n loginRoute,\n defaultRoute,\n rootRedirect,\n redirectKey,\n replace,\n ]);\n}\n"]}
1
+ {"version":3,"file":"useAuthGuard.js","sourceRoot":"","sources":["../../src/react/useAuthGuard.ts"],"names":[],"mappings":"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","sourcesContent":["'use client';\n\nimport { useEffect, useRef } from 'react';\nimport { clearRedirectPath, readRedirectPath, saveRedirectPath } from './redirect-storage.js';\n\nexport interface AuthGuardConfig {\n /**\n * Routes accessible without authentication, matched by PREFIX\n * (`pathname.startsWith(entry)`), so `'/share/'` covers every `/share/:id`\n * beneath it and `'/terms'` covers `/terms/anything`. This is the SOLE source of public\n * routes — the hook has no built-in defaults and no app-specific literals, so\n * a consumer that needs a dynamic public section must list its prefix here.\n */\n publicRoutes: string[];\n /** Routes that redirect authenticated users away (login, register) */\n authRedirectRoutes: string[];\n /** Where unauthenticated users go */\n loginRoute: string;\n /** Where authenticated users go from authRedirectRoutes */\n defaultRoute: string;\n /** Special root path handling */\n rootRedirect?: { authenticated: string; unauthenticated: string };\n /** localStorage key for post-login redirect */\n redirectKey?: string;\n /** Navigation function (e.g. router.replace) */\n replace: (path: string) => void;\n /** Current path */\n pathname: string;\n /** From useAuth() */\n loading: boolean;\n /** From useAuth() */\n isAuthenticated: boolean;\n}\n\nconst DEFAULT_REDIRECT_KEY = 'auth_redirect_path';\n\n/**\n * Handles route protection. Call in app shell component.\n *\n * - Does nothing while loading.\n * - Redirects unauthenticated users from protected routes to loginRoute.\n * - Redirects authenticated users from authRedirectRoutes to defaultRoute.\n * - Saves attempted path for post-login redirect — a visitor's deep link only. When a\n * session ends (an observed signed-in → signed-out transition: a sign-out, a forced\n * sign-out, an expiry, another tab), the path it ended on is NOT saved and any saved\n * path is cleared, so the next account to sign in on this device never lands on the\n * previous account's page.\n *\n * \"Public\" is decided ENTIRELY by `config.publicRoutes` (prefix match). The hook\n * carries no built-in or implicit public route: every route a consumer wants\n * reachable while signed out — including a dynamic section like `/share/:id` — is\n * listed by that consumer as a prefix.\n */\nexport function useAuthGuard(config: AuthGuardConfig): void {\n const {\n publicRoutes,\n authRedirectRoutes,\n loginRoute,\n defaultRoute,\n rootRedirect,\n redirectKey = DEFAULT_REDIRECT_KEY,\n replace,\n pathname,\n loading,\n isAuthenticated,\n } = config;\n\n // The auth-route redirect below is SINGLE-SHOT per auth-route visit. That branch\n // CONSUMES redirectKey and issues a navigation, but the navigation commits\n // asynchronously (the router fetches the target route first), so this effect can\n // legitimately re-run while pathname still reads the auth route (an auth-state\n // re-render, a loading flap). A re-run then finds the key already gone and\n // re-replaces to defaultRoute, CLOBBERING the in-flight saved-path navigation —\n // a deep link that routed through login then landed on defaultRoute instead of\n // the saved path. The latch resets whenever the pathname is off the auth-route set.\n const authRouteRedirected = useRef(false);\n\n // The last settled authentication state this guard observed (null before the first), and\n // the pathname a session ended on. The effect can re-run on that same pathname before the\n // login navigation commits, so the \"do not save\" decision is held for that pathname rather\n // than for one run.\n const lastAuthenticated = useRef<boolean | null>(null);\n const sessionEndedOn = useRef<string | null>(null);\n\n useEffect(() => {\n if (loading) return;\n\n if (lastAuthenticated.current === true && !isAuthenticated) {\n sessionEndedOn.current = pathname;\n clearRedirectPath(redirectKey);\n } else if (isAuthenticated || sessionEndedOn.current !== pathname) {\n sessionEndedOn.current = null;\n }\n lastAuthenticated.current = isAuthenticated;\n\n if (!authRedirectRoutes.includes(pathname)) {\n authRouteRedirected.current = false;\n }\n\n // Root path special handling\n if (rootRedirect && pathname === '/') {\n replace(isAuthenticated ? rootRedirect.authenticated : rootRedirect.unauthenticated);\n return;\n }\n\n const isPublic = publicRoutes.some((r) => pathname.startsWith(r));\n const isAuthRedirect = authRedirectRoutes.includes(pathname);\n\n // Unauthenticated on protected route -> save a visitor's path, go to login\n if (!isAuthenticated && !isPublic) {\n if (sessionEndedOn.current === null) saveRedirectPath(redirectKey, pathname);\n replace(loginRoute);\n return;\n }\n\n // Authenticated user on auth-only route (login, register, etc.)\n if (isAuthenticated && isAuthRedirect) {\n if (authRouteRedirected.current) return;\n authRouteRedirected.current = true;\n let target = defaultRoute;\n const saved = readRedirectPath(redirectKey);\n if (saved && !authRedirectRoutes.includes(saved)) {\n target = saved;\n }\n clearRedirectPath(redirectKey);\n replace(target);\n }\n }, [\n loading,\n isAuthenticated,\n pathname,\n publicRoutes,\n authRedirectRoutes,\n loginRoute,\n defaultRoute,\n rootRedirect,\n redirectKey,\n replace,\n ]);\n}\n"]}
@@ -1,4 +1,4 @@
1
- import type { Auth, User } from "firebase/auth";
1
+ import type { Auth, User } from 'firebase/auth';
2
2
  export type AuthState = {
3
3
  user: User | null;
4
4
  loading: boolean;
@@ -1,5 +1,5 @@
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
4
  // Initialize state based on current auth status to avoid initial flash
5
5
  const [user, setUser] = useState(() => auth.currentUser);
@@ -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,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 '../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 // 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}\n"]}
@@ -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":"AAYA;;;;;GAKG;AACH,wBAAgB,yBAAyB,CAAC,GAAG,EAAE,OAAO,GAAG,OAAO,CAM/D"}
package/dist/refusal.js CHANGED
@@ -5,8 +5,8 @@
5
5
  // The HttpsError code, as the server (`failed-precondition`) and the Firebase client SDK
6
6
  // (`functions/failed-precondition`) spell it.
7
7
  const FAILED_PRECONDITION_CODES = new Set([
8
- "failed-precondition",
9
- "functions/failed-precondition",
8
+ 'failed-precondition',
9
+ 'functions/failed-precondition',
10
10
  ]);
11
11
  /**
12
12
  * The `details` of `err` when it is a `failed-precondition` error in either spelling —
@@ -15,9 +15,11 @@ const FAILED_PRECONDITION_CODES = new Set([
15
15
  * `details.reason`, never by message text.
16
16
  */
17
17
  export function failedPreconditionDetails(err) {
18
- if (!err || typeof err !== "object")
18
+ if (!err || typeof err !== 'object')
19
19
  return undefined;
20
20
  const e = err;
21
- return typeof e.code === "string" && FAILED_PRECONDITION_CODES.has(e.code) ? e.details : undefined;
21
+ return typeof e.code === 'string' && FAILED_PRECONDITION_CODES.has(e.code)
22
+ ? e.details
23
+ : undefined;
22
24
  }
23
25
  //# 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,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;QACxE,CAAC,CAAC,CAAC,CAAC,OAAO;QACX,CAAC,CAAC,SAAS,CAAC;AAChB,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)\n ? e.details\n : undefined;\n}\n"]}
@@ -1,3 +1,3 @@
1
- import type { AssertAuthConfig, AssertAuthFn } from "./types.js";
1
+ import type { AssertAuthConfig, AssertAuthFn } from './types.js';
2
2
  export declare function createAssertAuth<TUser, TAdmin = void>(config: AssertAuthConfig<TUser, TAdmin>): AssertAuthFn<TUser, TAdmin>;
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":"AAgBA,OAAO,KAAK,EAAE,gBAAgB,EAAE,YAAY,EAAiC,MAAM,YAAY,CAAC;AAEhG,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,EACnD,MAAM,EAAE,gBAAgB,CAAC,KAAK,EAAE,MAAM,CAAC,GACtC,YAAY,CAAC,KAAK,EAAE,MAAM,CAAC,CAkF7B"}
@@ -9,45 +9,42 @@
9
9
  // 4. Status check (suspended / banned)
10
10
  // 5. Acceptance-level gate (only when config.acceptance is set)
11
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";
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';
15
15
  export function createAssertAuth(config) {
16
16
  return async function assertAuth(request, requirements = {}) {
17
17
  // 1. Authentication check (always first)
18
18
  if (!request.auth) {
19
- throw new AuthAssertionError("unauthenticated", "You must be authenticated");
19
+ throw new AuthAssertionError('unauthenticated', 'You must be authenticated');
20
20
  }
21
21
  const uid = request.auth.uid;
22
22
  const token = request.auth.token;
23
23
  // 2. Email verification check. The refusal carries its reason in `details`, so the client
24
24
  // recognizes it structurally (`isEmailVerificationRequiredError`), never by the message.
25
- if (requirements.emailVerified === true &&
26
- requirements.allowUnverified !== true) {
25
+ if (requirements.emailVerified === true && requirements.allowUnverified !== true) {
27
26
  if (token.email_verified !== true) {
28
- throw new AuthAssertionError("failed-precondition", "Email must be verified", {
27
+ throw new AuthAssertionError('failed-precondition', 'Email must be verified', {
29
28
  reason: EMAIL_VERIFICATION_REQUIRED_REASON,
30
29
  });
31
30
  }
32
31
  }
33
32
  // 3. User doc Firestore read (skipped when allowAnyStatus is set)
34
33
  const db = config.firestore();
35
- const userSnap = requirements.allowAnyStatus !== true
36
- ? await db.doc(config.userProfilePath(uid)).get()
37
- : null;
34
+ const userSnap = requirements.allowAnyStatus !== true ? await db.doc(config.userProfilePath(uid)).get() : null;
38
35
  let userDoc;
39
36
  // 4. Status check (default: block suspended + banned; skipped if allowAnyStatus)
40
37
  if (requirements.allowAnyStatus !== true) {
41
38
  if (!userSnap || !userSnap.exists) {
42
- throw new AuthAssertionError("not-found", "User profile not found");
39
+ throw new AuthAssertionError('not-found', 'User profile not found');
43
40
  }
44
41
  const userData = userSnap.data();
45
42
  const status = config.getUserStatus(userData);
46
- if (status === "banned") {
47
- throw new AuthAssertionError("permission-denied", "Account is not in good standing");
43
+ if (status === 'banned') {
44
+ throw new AuthAssertionError('permission-denied', 'Account is not in good standing');
48
45
  }
49
- if (status === "suspended" && requirements.allowSuspended !== true) {
50
- throw new AuthAssertionError("permission-denied", "Account is not in good standing");
46
+ if (status === 'suspended' && requirements.allowSuspended !== true) {
47
+ throw new AuthAssertionError('permission-denied', 'Account is not in good standing');
51
48
  }
52
49
  userDoc = userData;
53
50
  }
@@ -60,7 +57,7 @@ export function createAssertAuth(config) {
60
57
  if (requiredLevel > 0) {
61
58
  const acceptedLevel = normalizeAcceptanceLevel(token[config.acceptance.claimKey]);
62
59
  if (acceptedLevel < requiredLevel) {
63
- throw new AuthAssertionError("failed-precondition", "The current documents must be accepted before continuing", { reason: ACCEPTANCE_REQUIRED_REASON, requiredLevel, acceptedLevel });
60
+ throw new AuthAssertionError('failed-precondition', 'The current documents must be accepted before continuing', { reason: ACCEPTANCE_REQUIRED_REASON, requiredLevel, acceptedLevel });
64
61
  }
65
62
  }
66
63
  }
@@ -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":["// 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 { AssertAuthConfig, AssertAuthFn, AuthContext, AuthRequirements } 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 (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 // 3. User doc Firestore read (skipped when allowAnyStatus is set)\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 // 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('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 // 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,5 +1,5 @@
1
- import type { AcceptanceRequiredDetails } from "../acceptance.js";
2
- import type { EmailVerificationRequiredDetails } from "../email-verification.js";
1
+ import type { AcceptanceRequiredDetails } from '../acceptance.js';
2
+ import type { EmailVerificationRequiredDetails } from '../email-verification.js';
3
3
  /**
4
4
  * Typed failure channel for `createAssertAuth`.
5
5
  *
@@ -36,7 +36,7 @@ import type { EmailVerificationRequiredDetails } from "../email-verification.js"
36
36
  * packages stay framework-agnostic; the consuming app's adapter binds the framework) and
37
37
  * mirrors `report-core`'s `ReportCoreTaskError`, which already works this way.
38
38
  */
39
- export type AuthAssertionErrorCode = "unauthenticated" | "failed-precondition" | "not-found" | "permission-denied";
39
+ export type AuthAssertionErrorCode = 'unauthenticated' | 'failed-precondition' | 'not-found' | 'permission-denied';
40
40
  /**
41
41
  * Structured details an `AuthAssertionError` may carry — one per refusal the client answers
42
42
  * with a surface of its own, each told apart by `reason`: the email-verification refusal
@@ -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;AAEjF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AACH,MAAM,MAAM,sBAAsB,GAChC,iBAAiB,GAAG,qBAAqB,GAAG,WAAW,GAAG,mBAAmB,CAAC;AAEhF;;;;;;;;;;;GAWG;AACH,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;