@aglyn/tenant-feature-instance 1.0.0-beta.143

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 (154) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +3 -0
  3. package/jest.config.d.ts +36 -0
  4. package/package.json +45 -0
  5. package/src/index.d.ts +17 -0
  6. package/src/index.js +18 -0
  7. package/src/index.js.map +1 -0
  8. package/src/lib/components/duplicate-resource-dialog.d.ts +77 -0
  9. package/src/lib/components/duplicate-resource-dialog.js +217 -0
  10. package/src/lib/components/duplicate-resource-dialog.js.map +1 -0
  11. package/src/lib/constants/firebase-config.d.ts +68 -0
  12. package/src/lib/constants/firebase-config.js +155 -0
  13. package/src/lib/constants/firebase-config.js.map +1 -0
  14. package/src/lib/constants/firebase-emulator-hosts.d.ts +60 -0
  15. package/src/lib/constants/firebase-emulator-hosts.js +81 -0
  16. package/src/lib/constants/firebase-emulator-hosts.js.map +1 -0
  17. package/src/lib/constants/mock-data.d.ts +24 -0
  18. package/src/lib/constants/mock-data.js +49 -0
  19. package/src/lib/constants/mock-data.js.map +1 -0
  20. package/src/lib/constants/site-paths.d.ts +47 -0
  21. package/src/lib/constants/site-paths.js +66 -0
  22. package/src/lib/constants/site-paths.js.map +1 -0
  23. package/src/lib/hooks/firebase/auth-persistence.d.ts +74 -0
  24. package/src/lib/hooks/firebase/auth-persistence.js +117 -0
  25. package/src/lib/hooks/firebase/auth-persistence.js.map +1 -0
  26. package/src/lib/hooks/firebase/firebase-services.d.ts +172 -0
  27. package/src/lib/hooks/firebase/firebase-services.js +522 -0
  28. package/src/lib/hooks/firebase/firebase-services.js.map +1 -0
  29. package/src/lib/hooks/firebase/firestore-cache.d.ts +125 -0
  30. package/src/lib/hooks/firebase/firestore-cache.js +152 -0
  31. package/src/lib/hooks/firebase/firestore-cache.js.map +1 -0
  32. package/src/lib/hooks/firebase/firestore-shared-client-state.d.ts +169 -0
  33. package/src/lib/hooks/firebase/firestore-shared-client-state.js +316 -0
  34. package/src/lib/hooks/firebase/firestore-shared-client-state.js.map +1 -0
  35. package/src/lib/hooks/firestore-denial-reporter.d.ts +192 -0
  36. package/src/lib/hooks/firestore-denial-reporter.js +293 -0
  37. package/src/lib/hooks/firestore-denial-reporter.js.map +1 -0
  38. package/src/lib/hooks/helpers/besigner-nodes-converter.d.ts +62 -0
  39. package/src/lib/hooks/helpers/besigner-nodes-converter.js +91 -0
  40. package/src/lib/hooks/helpers/besigner-nodes-converter.js.map +1 -0
  41. package/src/lib/hooks/helpers/guarded-nodes-save.d.ts +80 -0
  42. package/src/lib/hooks/helpers/guarded-nodes-save.js +89 -0
  43. package/src/lib/hooks/helpers/guarded-nodes-save.js.map +1 -0
  44. package/src/lib/hooks/helpers/guarded-seed-write.d.ts +125 -0
  45. package/src/lib/hooks/helpers/guarded-seed-write.js +193 -0
  46. package/src/lib/hooks/helpers/guarded-seed-write.js.map +1 -0
  47. package/src/lib/hooks/helpers/listen-options.d.ts +91 -0
  48. package/src/lib/hooks/helpers/listen-options.js +92 -0
  49. package/src/lib/hooks/helpers/listen-options.js.map +1 -0
  50. package/src/lib/hooks/helpers/use-doc.d.ts +38 -0
  51. package/src/lib/hooks/helpers/use-doc.js +213 -0
  52. package/src/lib/hooks/helpers/use-doc.js.map +1 -0
  53. package/src/lib/hooks/helpers/use-modify-doc-callback.d.ts +28 -0
  54. package/src/lib/hooks/helpers/use-modify-doc-callback.js +110 -0
  55. package/src/lib/hooks/helpers/use-modify-doc-callback.js.map +1 -0
  56. package/src/lib/hooks/host-collection-queries.d.ts +125 -0
  57. package/src/lib/hooks/host-collection-queries.js +126 -0
  58. package/src/lib/hooks/host-collection-queries.js.map +1 -0
  59. package/src/lib/hooks/list-filter-constraints.d.ts +64 -0
  60. package/src/lib/hooks/list-filter-constraints.js +288 -0
  61. package/src/lib/hooks/list-filter-constraints.js.map +1 -0
  62. package/src/lib/hooks/sorted-collection-window.d.ts +207 -0
  63. package/src/lib/hooks/sorted-collection-window.js +150 -0
  64. package/src/lib/hooks/sorted-collection-window.js.map +1 -0
  65. package/src/lib/hooks/use-component-version.d.ts +43 -0
  66. package/src/lib/hooks/use-component-version.js +73 -0
  67. package/src/lib/hooks/use-component-version.js.map +1 -0
  68. package/src/lib/hooks/use-component.d.ts +39 -0
  69. package/src/lib/hooks/use-component.js +55 -0
  70. package/src/lib/hooks/use-component.js.map +1 -0
  71. package/src/lib/hooks/use-console-host-route.d.ts +24 -0
  72. package/src/lib/hooks/use-console-host-route.js +77 -0
  73. package/src/lib/hooks/use-console-host-route.js.map +1 -0
  74. package/src/lib/hooks/use-duplicate-resource-api.d.ts +43 -0
  75. package/src/lib/hooks/use-duplicate-resource-api.js +61 -0
  76. package/src/lib/hooks/use-duplicate-resource-api.js.map +1 -0
  77. package/src/lib/hooks/use-firestore-collection.d.ts +62 -0
  78. package/src/lib/hooks/use-firestore-collection.js +307 -0
  79. package/src/lib/hooks/use-firestore-collection.js.map +1 -0
  80. package/src/lib/hooks/use-firestore-doc.d.ts +42 -0
  81. package/src/lib/hooks/use-firestore-doc.js +192 -0
  82. package/src/lib/hooks/use-firestore-doc.js.map +1 -0
  83. package/src/lib/hooks/use-form-version.d.ts +44 -0
  84. package/src/lib/hooks/use-form-version.js +73 -0
  85. package/src/lib/hooks/use-form-version.js.map +1 -0
  86. package/src/lib/hooks/use-host-activity-logger.d.ts +35 -0
  87. package/src/lib/hooks/use-host-activity-logger.js +96 -0
  88. package/src/lib/hooks/use-host-activity-logger.js.map +1 -0
  89. package/src/lib/hooks/use-host-campaigns.d.ts +53 -0
  90. package/src/lib/hooks/use-host-campaigns.js +93 -0
  91. package/src/lib/hooks/use-host-campaigns.js.map +1 -0
  92. package/src/lib/hooks/use-host-org-id.d.ts +96 -0
  93. package/src/lib/hooks/use-host-org-id.js +154 -0
  94. package/src/lib/hooks/use-host-org-id.js.map +1 -0
  95. package/src/lib/hooks/use-host-resource-api.d.ts +43 -0
  96. package/src/lib/hooks/use-host-resource-api.js +60 -0
  97. package/src/lib/hooks/use-host-resource-api.js.map +1 -0
  98. package/src/lib/hooks/use-host-template.d.ts +44 -0
  99. package/src/lib/hooks/use-host-template.js +82 -0
  100. package/src/lib/hooks/use-host-template.js.map +1 -0
  101. package/src/lib/hooks/use-host-version-api.d.ts +59 -0
  102. package/src/lib/hooks/use-host-version-api.js +69 -0
  103. package/src/lib/hooks/use-host-version-api.js.map +1 -0
  104. package/src/lib/hooks/use-host.d.ts +101 -0
  105. package/src/lib/hooks/use-host.js +45 -0
  106. package/src/lib/hooks/use-host.js.map +1 -0
  107. package/src/lib/hooks/use-layout-version.d.ts +33 -0
  108. package/src/lib/hooks/use-layout-version.js +64 -0
  109. package/src/lib/hooks/use-layout-version.js.map +1 -0
  110. package/src/lib/hooks/use-layout.d.ts +41 -0
  111. package/src/lib/hooks/use-layout.js +49 -0
  112. package/src/lib/hooks/use-layout.js.map +1 -0
  113. package/src/lib/hooks/use-live-artifact-count.d.ts +50 -0
  114. package/src/lib/hooks/use-live-artifact-count.js +102 -0
  115. package/src/lib/hooks/use-live-artifact-count.js.map +1 -0
  116. package/src/lib/hooks/use-org-member-options.d.ts +57 -0
  117. package/src/lib/hooks/use-org-member-options.js +112 -0
  118. package/src/lib/hooks/use-org-member-options.js.map +1 -0
  119. package/src/lib/hooks/use-org-plan.d.ts +46 -0
  120. package/src/lib/hooks/use-org-plan.js +86 -0
  121. package/src/lib/hooks/use-org-plan.js.map +1 -0
  122. package/src/lib/hooks/use-paged-collection.d.ts +95 -0
  123. package/src/lib/hooks/use-paged-collection.js +130 -0
  124. package/src/lib/hooks/use-paged-collection.js.map +1 -0
  125. package/src/lib/hooks/use-plugin-config.d.ts +38 -0
  126. package/src/lib/hooks/use-plugin-config.js +120 -0
  127. package/src/lib/hooks/use-plugin-config.js.map +1 -0
  128. package/src/lib/hooks/use-scope-tokens.d.ts +28 -0
  129. package/src/lib/hooks/use-scope-tokens.js +100 -0
  130. package/src/lib/hooks/use-scope-tokens.js.map +1 -0
  131. package/src/lib/hooks/use-screen-version.d.ts +33 -0
  132. package/src/lib/hooks/use-screen-version.js +65 -0
  133. package/src/lib/hooks/use-screen-version.js.map +1 -0
  134. package/src/lib/hooks/use-screen.d.ts +68 -0
  135. package/src/lib/hooks/use-screen.js +50 -0
  136. package/src/lib/hooks/use-screen.js.map +1 -0
  137. package/src/lib/hooks/use-sending-identity-api.d.ts +197 -0
  138. package/src/lib/hooks/use-sending-identity-api.js +95 -0
  139. package/src/lib/hooks/use-sending-identity-api.js.map +1 -0
  140. package/src/lib/hooks/use-sorted-paged-collection.d.ts +48 -0
  141. package/src/lib/hooks/use-sorted-paged-collection.js +213 -0
  142. package/src/lib/hooks/use-sorted-paged-collection.js.map +1 -0
  143. package/src/lib/hooks/use-switcher-collection.d.ts +105 -0
  144. package/src/lib/hooks/use-switcher-collection.js +275 -0
  145. package/src/lib/hooks/use-switcher-collection.js.map +1 -0
  146. package/src/lib/hooks/use-user-name.d.ts +51 -0
  147. package/src/lib/hooks/use-user-name.js +90 -0
  148. package/src/lib/hooks/use-user-name.js.map +1 -0
  149. package/src/lib/hooks/use-user-photo.d.ts +50 -0
  150. package/src/lib/hooks/use-user-photo.js +87 -0
  151. package/src/lib/hooks/use-user-photo.js.map +1 -0
  152. package/src/lib/tenant.d.ts +61 -0
  153. package/src/lib/tenant.js +62 -0
  154. package/src/lib/tenant.js.map +1 -0
@@ -0,0 +1,74 @@
1
+ import { type FirebaseApp } from 'firebase/app';
2
+ import { type Auth } from 'firebase/auth';
3
+ /**
4
+ * How much of a session an origin is allowed to keep (AGL-1379).
5
+ *
6
+ * - `durable` — the SDK default hierarchy (IndexedDB → localStorage →
7
+ * sessionStorage). A **refresh token lands on disk in plaintext**, which is
8
+ * correct on `*.aglyn.com`: we own the DNS forever, and a 14-day session is
9
+ * the product.
10
+ * - `ephemeral` — `initializeAuth(app, { persistence: inMemoryPersistence })`
11
+ * with **no `popupRedirectResolver`**, per AGL-1099a D6. For a **custom
12
+ * console domain**, whose DNS the customer can re-point at their own server
13
+ * after a detach: a refresh token left in that origin's IndexedDB is a
14
+ * durable account-takeover primitive that no cookie TTL and no server-side
15
+ * revocation short of `revokeRefreshTokens` can reach.
16
+ *
17
+ * The class is a property of the **instance**, not of any call site, which is
18
+ * why it is declared here and nowhere else — see `createAuthInstance`.
19
+ */
20
+ export type AuthPersistenceClass = 'durable' | 'ephemeral';
21
+ /** `code` on {@link SealedAuthPersistenceError}, shaped like a Firebase error code. */
22
+ export declare const AUTH_PERSISTENCE_SEALED_CODE = "aglyn-auth/persistence-sealed";
23
+ /**
24
+ * Thrown when something tries to re-persist an `ephemeral` auth instance.
25
+ *
26
+ * Thrown **synchronously**, deliberately, even though `setPersistence` is
27
+ * typed `Promise<void>`. That matches how the SDK already refuses the
28
+ * federated family on a resolver-less instance (`signInWithPopup` and friends
29
+ * throw `auth/argument-error` before any network request, measured in
30
+ * `docs/design/agl-1099a-poc-findings.md` §3), and it means a fire-and-forget
31
+ * `void setPersistence(...)` fails loudly instead of becoming an unhandled
32
+ * rejection nobody reads.
33
+ */
34
+ export declare class SealedAuthPersistenceError extends Error {
35
+ readonly code = "aglyn-auth/persistence-sealed";
36
+ constructor();
37
+ }
38
+ /**
39
+ * Is `setPersistence` sealed on this instance? A diagnostic, not a boundary —
40
+ * the boundary is the property descriptor itself.
41
+ */
42
+ export declare function isAuthPersistenceSealed(auth: Auth): boolean;
43
+ /**
44
+ * The only way an Aglyn `Auth` instance is born, and the persistence class is
45
+ * a **required positional argument** (AGL-1379).
46
+ *
47
+ * ## Why this shape
48
+ *
49
+ * The thing that has to be guarded is a `setPersistence` call passing
50
+ * `browserLocalPersistence`, which the PoC measured flipping a refresh token
51
+ * from absent to present in IndexedDB on an in-memory instance — accepted at
52
+ * runtime, no error, no warning.
53
+ *
54
+ * - **A lint rule cannot classify it.** Whether a given `setPersistence` call
55
+ * is safe depends on which host the browser is on, and the same source file
56
+ * serves both. `apps/console/utils/passkeys.ts:133` is *correct* on
57
+ * `app.aglyn.com` and *fatal* on `console.acme-agency.com`. A rule banning
58
+ * the call would be false half the time, and AGL-1357's lesson — derive the
59
+ * classification from what the code declares — has nothing to derive from
60
+ * at these call sites, because nothing there declares the instance's class.
61
+ * - **A wrapper the six sites go through is bypassable** by the next person
62
+ * who imports `setPersistence` from `firebase/auth` directly, which is what
63
+ * all six already do.
64
+ * - **A required argument works only where the class is knowable**, and that
65
+ * is here, at creation — the AGL-1366 shape (`setNode`'s required `parent`)
66
+ * applied at the one place it is honest. Every creation site must now say
67
+ * which kind of origin it is on, and the guard rides the instance from
68
+ * there.
69
+ *
70
+ * `durable` is the SDK default, reached by the exact `getAuth(app)` the
71
+ * console has always called — this factory changes nothing about how
72
+ * `*.aglyn.com` persists.
73
+ */
74
+ export declare function createAuthInstance(app: FirebaseApp, persistenceClass: AuthPersistenceClass): Auth;
@@ -0,0 +1,117 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */ 'use client';
17
+ import { getAuth, inMemoryPersistence, initializeAuth } from "firebase/auth";
18
+ /** `code` on {@link SealedAuthPersistenceError}, shaped like a Firebase error code. */ export const AUTH_PERSISTENCE_SEALED_CODE = 'aglyn-auth/persistence-sealed';
19
+ /**
20
+ * Thrown when something tries to re-persist an `ephemeral` auth instance.
21
+ *
22
+ * Thrown **synchronously**, deliberately, even though `setPersistence` is
23
+ * typed `Promise<void>`. That matches how the SDK already refuses the
24
+ * federated family on a resolver-less instance (`signInWithPopup` and friends
25
+ * throw `auth/argument-error` before any network request, measured in
26
+ * `docs/design/agl-1099a-poc-findings.md` §3), and it means a fire-and-forget
27
+ * `void setPersistence(...)` fails loudly instead of becoming an unhandled
28
+ * rejection nobody reads.
29
+ */ export class SealedAuthPersistenceError extends Error {
30
+ constructor(){
31
+ super('setPersistence is sealed on this Auth instance: it was created with ' + "persistence class 'ephemeral' (AGL-1099a D6), so persisting a " + 'refresh token to this origin is not permitted. If this origin may ' + "keep a session, create its Auth instance as 'durable' — do not " + 'change persistence after the fact (AGL-1379).'), this.code = AUTH_PERSISTENCE_SEALED_CODE;
32
+ this.name = 'SealedAuthPersistenceError';
33
+ }
34
+ }
35
+ /**
36
+ * Is `setPersistence` sealed on this instance? A diagnostic, not a boundary —
37
+ * the boundary is the property descriptor itself.
38
+ */ export function isAuthPersistenceSealed(auth) {
39
+ const descriptor = Object.getOwnPropertyDescriptor(auth, 'setPersistence');
40
+ return !!descriptor && descriptor.writable === false && descriptor.configurable === false;
41
+ }
42
+ /**
43
+ * Make `setPersistence` unrepresentable on this instance.
44
+ *
45
+ * The modular `setPersistence(auth, persistence)` is a one-line delegation —
46
+ * `getModularInstance(auth).setPersistence(persistence)` (verified in
47
+ * `@firebase/auth` 1.13.3) — so an own, non-writable, non-configurable
48
+ * property shadowing the prototype method closes **every** route that goes
49
+ * through the object: the free function, the method, a wrapper, an aliased
50
+ * import, and any call site nobody has written yet. That is the point. The
51
+ * six existing `setPersistence` call sites did not have to be touched, and
52
+ * the seventh nobody has typed yet is covered too.
53
+ *
54
+ * Deliberate limit, stated rather than overclaimed: a caller who reaches
55
+ * around the instance — pulling the method off `Object.getPrototypeOf(auth)`
56
+ * and `call`-ing it — is not stopped. Overriding the prototype would seal
57
+ * every `Auth` in the process, including the `durable` ones this file exists
58
+ * to leave alone.
59
+ */ function sealPersistence(auth) {
60
+ // Idempotent: `initializeAuth` returns the existing instance when called
61
+ // twice with deep-equal options, and `usePresence` re-enters its effect on
62
+ // every room change. Re-defining a non-configurable property is a
63
+ // TypeError, so the second pass must be a no-op rather than a crash.
64
+ if (isAuthPersistenceSealed(auth)) return auth;
65
+ Object.defineProperty(auth, 'setPersistence', {
66
+ value: ()=>{
67
+ throw new SealedAuthPersistenceError();
68
+ },
69
+ writable: false,
70
+ configurable: false,
71
+ enumerable: false
72
+ });
73
+ return auth;
74
+ }
75
+ /**
76
+ * The only way an Aglyn `Auth` instance is born, and the persistence class is
77
+ * a **required positional argument** (AGL-1379).
78
+ *
79
+ * ## Why this shape
80
+ *
81
+ * The thing that has to be guarded is a `setPersistence` call passing
82
+ * `browserLocalPersistence`, which the PoC measured flipping a refresh token
83
+ * from absent to present in IndexedDB on an in-memory instance — accepted at
84
+ * runtime, no error, no warning.
85
+ *
86
+ * - **A lint rule cannot classify it.** Whether a given `setPersistence` call
87
+ * is safe depends on which host the browser is on, and the same source file
88
+ * serves both. `apps/console/utils/passkeys.ts:133` is *correct* on
89
+ * `app.aglyn.com` and *fatal* on `console.acme-agency.com`. A rule banning
90
+ * the call would be false half the time, and AGL-1357's lesson — derive the
91
+ * classification from what the code declares — has nothing to derive from
92
+ * at these call sites, because nothing there declares the instance's class.
93
+ * - **A wrapper the six sites go through is bypassable** by the next person
94
+ * who imports `setPersistence` from `firebase/auth` directly, which is what
95
+ * all six already do.
96
+ * - **A required argument works only where the class is knowable**, and that
97
+ * is here, at creation — the AGL-1366 shape (`setNode`'s required `parent`)
98
+ * applied at the one place it is honest. Every creation site must now say
99
+ * which kind of origin it is on, and the guard rides the instance from
100
+ * there.
101
+ *
102
+ * `durable` is the SDK default, reached by the exact `getAuth(app)` the
103
+ * console has always called — this factory changes nothing about how
104
+ * `*.aglyn.com` persists.
105
+ */ export function createAuthInstance(app, persistenceClass) {
106
+ if (persistenceClass === 'ephemeral') {
107
+ // No `popupRedirectResolver`: D6's second half. The auth helper iframe is
108
+ // never loaded, so the custom domain needs no `frame-ancestors` entry and
109
+ // can never need a Firebase authorized-domain entry for OAuth.
110
+ return sealPersistence(initializeAuth(app, {
111
+ persistence: inMemoryPersistence
112
+ }));
113
+ }
114
+ return getAuth(app);
115
+ }
116
+
117
+ //# sourceMappingURL=auth-persistence.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../../../libs/tenant/feature/instance/src/lib/hooks/firebase/auth-persistence.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport { type FirebaseApp } from 'firebase/app'\nimport {\n type Auth,\n getAuth,\n inMemoryPersistence,\n initializeAuth,\n} from 'firebase/auth'\n\n/**\n * How much of a session an origin is allowed to keep (AGL-1379).\n *\n * - `durable` — the SDK default hierarchy (IndexedDB → localStorage →\n * sessionStorage). A **refresh token lands on disk in plaintext**, which is\n * correct on `*.aglyn.com`: we own the DNS forever, and a 14-day session is\n * the product.\n * - `ephemeral` — `initializeAuth(app, { persistence: inMemoryPersistence })`\n * with **no `popupRedirectResolver`**, per AGL-1099a D6. For a **custom\n * console domain**, whose DNS the customer can re-point at their own server\n * after a detach: a refresh token left in that origin's IndexedDB is a\n * durable account-takeover primitive that no cookie TTL and no server-side\n * revocation short of `revokeRefreshTokens` can reach.\n *\n * The class is a property of the **instance**, not of any call site, which is\n * why it is declared here and nowhere else — see `createAuthInstance`.\n */\nexport type AuthPersistenceClass = 'durable' | 'ephemeral'\n\n/** `code` on {@link SealedAuthPersistenceError}, shaped like a Firebase error code. */\nexport const AUTH_PERSISTENCE_SEALED_CODE = 'aglyn-auth/persistence-sealed'\n\n/**\n * Thrown when something tries to re-persist an `ephemeral` auth instance.\n *\n * Thrown **synchronously**, deliberately, even though `setPersistence` is\n * typed `Promise<void>`. That matches how the SDK already refuses the\n * federated family on a resolver-less instance (`signInWithPopup` and friends\n * throw `auth/argument-error` before any network request, measured in\n * `docs/design/agl-1099a-poc-findings.md` §3), and it means a fire-and-forget\n * `void setPersistence(...)` fails loudly instead of becoming an unhandled\n * rejection nobody reads.\n */\nexport class SealedAuthPersistenceError extends Error {\n readonly code = AUTH_PERSISTENCE_SEALED_CODE\n constructor() {\n super(\n 'setPersistence is sealed on this Auth instance: it was created with ' +\n \"persistence class 'ephemeral' (AGL-1099a D6), so persisting a \" +\n 'refresh token to this origin is not permitted. If this origin may ' +\n \"keep a session, create its Auth instance as 'durable' — do not \" +\n 'change persistence after the fact (AGL-1379).',\n )\n this.name = 'SealedAuthPersistenceError'\n }\n}\n\n/**\n * Is `setPersistence` sealed on this instance? A diagnostic, not a boundary —\n * the boundary is the property descriptor itself.\n */\nexport function isAuthPersistenceSealed(auth: Auth): boolean {\n const descriptor = Object.getOwnPropertyDescriptor(auth, 'setPersistence')\n return (\n !!descriptor && descriptor.writable === false && descriptor.configurable === false\n )\n}\n\n/**\n * Make `setPersistence` unrepresentable on this instance.\n *\n * The modular `setPersistence(auth, persistence)` is a one-line delegation —\n * `getModularInstance(auth).setPersistence(persistence)` (verified in\n * `@firebase/auth` 1.13.3) — so an own, non-writable, non-configurable\n * property shadowing the prototype method closes **every** route that goes\n * through the object: the free function, the method, a wrapper, an aliased\n * import, and any call site nobody has written yet. That is the point. The\n * six existing `setPersistence` call sites did not have to be touched, and\n * the seventh nobody has typed yet is covered too.\n *\n * Deliberate limit, stated rather than overclaimed: a caller who reaches\n * around the instance — pulling the method off `Object.getPrototypeOf(auth)`\n * and `call`-ing it — is not stopped. Overriding the prototype would seal\n * every `Auth` in the process, including the `durable` ones this file exists\n * to leave alone.\n */\nfunction sealPersistence(auth: Auth): Auth {\n // Idempotent: `initializeAuth` returns the existing instance when called\n // twice with deep-equal options, and `usePresence` re-enters its effect on\n // every room change. Re-defining a non-configurable property is a\n // TypeError, so the second pass must be a no-op rather than a crash.\n if (isAuthPersistenceSealed(auth)) return auth\n Object.defineProperty(auth, 'setPersistence', {\n value: (): Promise<void> => {\n throw new SealedAuthPersistenceError()\n },\n writable: false,\n configurable: false,\n enumerable: false,\n })\n return auth\n}\n\n/**\n * The only way an Aglyn `Auth` instance is born, and the persistence class is\n * a **required positional argument** (AGL-1379).\n *\n * ## Why this shape\n *\n * The thing that has to be guarded is a `setPersistence` call passing\n * `browserLocalPersistence`, which the PoC measured flipping a refresh token\n * from absent to present in IndexedDB on an in-memory instance — accepted at\n * runtime, no error, no warning.\n *\n * - **A lint rule cannot classify it.** Whether a given `setPersistence` call\n * is safe depends on which host the browser is on, and the same source file\n * serves both. `apps/console/utils/passkeys.ts:133` is *correct* on\n * `app.aglyn.com` and *fatal* on `console.acme-agency.com`. A rule banning\n * the call would be false half the time, and AGL-1357's lesson — derive the\n * classification from what the code declares — has nothing to derive from\n * at these call sites, because nothing there declares the instance's class.\n * - **A wrapper the six sites go through is bypassable** by the next person\n * who imports `setPersistence` from `firebase/auth` directly, which is what\n * all six already do.\n * - **A required argument works only where the class is knowable**, and that\n * is here, at creation — the AGL-1366 shape (`setNode`'s required `parent`)\n * applied at the one place it is honest. Every creation site must now say\n * which kind of origin it is on, and the guard rides the instance from\n * there.\n *\n * `durable` is the SDK default, reached by the exact `getAuth(app)` the\n * console has always called — this factory changes nothing about how\n * `*.aglyn.com` persists.\n */\nexport function createAuthInstance(\n app: FirebaseApp,\n persistenceClass: AuthPersistenceClass,\n): Auth {\n if (persistenceClass === 'ephemeral') {\n // No `popupRedirectResolver`: D6's second half. The auth helper iframe is\n // never loaded, so the custom domain needs no `frame-ancestors` entry and\n // can never need a Firebase authorized-domain entry for OAuth.\n return sealPersistence(initializeAuth(app, { persistence: inMemoryPersistence }))\n }\n return getAuth(app)\n}\n"],"names":["getAuth","inMemoryPersistence","initializeAuth","AUTH_PERSISTENCE_SEALED_CODE","SealedAuthPersistenceError","Error","code","name","isAuthPersistenceSealed","auth","descriptor","Object","getOwnPropertyDescriptor","writable","configurable","sealPersistence","defineProperty","value","enumerable","createAuthInstance","app","persistenceClass","persistence"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GACD;AAGA,SAEEA,OAAO,EACPC,mBAAmB,EACnBC,cAAc,QACT,gBAAe;AAqBtB,qFAAqF,GACrF,OAAO,MAAMC,+BAA+B,gCAA+B;AAE3E;;;;;;;;;;CAUC,GACD,OAAO,MAAMC,mCAAmCC;IAE9C,aAAc;QACZ,KAAK,CACH,yEACE,mEACA,uEACA,oEACA,uDAPGC,OAAOH;QASd,IAAI,CAACI,IAAI,GAAG;IACd;AACF;AAEA;;;CAGC,GACD,OAAO,SAASC,wBAAwBC,IAAU;IAChD,MAAMC,aAAaC,OAAOC,wBAAwB,CAACH,MAAM;IACzD,OACE,CAAC,CAACC,cAAcA,WAAWG,QAAQ,KAAK,SAASH,WAAWI,YAAY,KAAK;AAEjF;AAEA;;;;;;;;;;;;;;;;;CAiBC,GACD,SAASC,gBAAgBN,IAAU;IACjC,yEAAyE;IACzE,2EAA2E;IAC3E,kEAAkE;IAClE,qEAAqE;IACrE,IAAID,wBAAwBC,OAAO,OAAOA;IAC1CE,OAAOK,cAAc,CAACP,MAAM,kBAAkB;QAC5CQ,OAAO;YACL,MAAM,IAAIb;QACZ;QACAS,UAAU;QACVC,cAAc;QACdI,YAAY;IACd;IACA,OAAOT;AACT;AAEA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA8BC,GACD,OAAO,SAASU,mBACdC,GAAgB,EAChBC,gBAAsC;IAEtC,IAAIA,qBAAqB,aAAa;QACpC,0EAA0E;QAC1E,0EAA0E;QAC1E,+DAA+D;QAC/D,OAAON,gBAAgBb,eAAekB,KAAK;YAAEE,aAAarB;QAAoB;IAChF;IACA,OAAOD,QAAQoB;AACjB"}
@@ -0,0 +1,172 @@
1
+ import { type FirebaseApp, type FirebaseOptions } from 'firebase/app';
2
+ import { type Analytics } from 'firebase/analytics';
3
+ import { type Auth, type User } from 'firebase/auth';
4
+ import { type Firestore } from 'firebase/firestore';
5
+ import { type RemoteConfig } from 'firebase/remote-config';
6
+ import { type ReactNode } from 'react';
7
+ import { type AuthPersistenceClass } from './auth-persistence';
8
+ /**
9
+ * Drop-in replacement for reactfire's `ObservableStatus<T>` — reactfire is
10
+ * unmaintained against firebase majors past v9, but its shape is preserved
11
+ * here so `helpers/use-doc.ts`'s already-battle-tested onSnapshot+retry
12
+ * implementation (AGL-216/223) didn't need to change.
13
+ */
14
+ export interface ObservableStatus<T> {
15
+ status: 'loading' | 'error' | 'success';
16
+ hasEmitted: boolean;
17
+ isComplete: boolean;
18
+ data: T;
19
+ error: Error | undefined;
20
+ firstValuePromise: Promise<void>;
21
+ /**
22
+ * The emitted `data` includes local writes the server has NOT
23
+ * acknowledged (`snapshot.metadata.hasPendingWrites`).
24
+ *
25
+ * Carried rather than dropped because a caller cannot otherwise tell "the
26
+ * store holds this" from "my browser holds this". `persistentLocalCache`
27
+ * replays a queued write into the very first snapshot after a reload, so a
28
+ * caller that treats every snapshot as authoritative will adopt its own
29
+ * unacknowledged edit as the saved state — which is how the besigner ended
30
+ * up unable to save real changes (AGL-1262).
31
+ */
32
+ hasPendingWrites: boolean;
33
+ /**
34
+ * The emitted `data` came from the local cache and the server has NOT
35
+ * confirmed it (`snapshot.metadata.fromCache`).
36
+ *
37
+ * Carried because it is the only per-listener ground truth about
38
+ * freshness the SDK offers, and the console needs it (AGL-1066). Under
39
+ * `persistentLocalCache` a listener whose server listen is being REFUSED
40
+ * still emits cached snapshots and still reports `status: 'success'` —
41
+ * the retry budget is reset by each cached emission and never spends, so
42
+ * `status` can never reveal the fault (see
43
+ * `use-firestore-collection-cached-retry.spec.ts`).
44
+ *
45
+ * Prefer this over `session-health`'s `staleSession` for anything that
46
+ * asks "is what I am looking at live?". `staleSession` is a heuristic
47
+ * needing two denied collections inside a window, and it is unreachable
48
+ * on listener-only pages; this is a fact about THIS listener, costs
49
+ * nothing, and clears itself the instant a server snapshot lands.
50
+ */
51
+ fromCache: boolean;
52
+ /**
53
+ * The server has REFUSED this listen for longer than the retry budget, and
54
+ * no server snapshot has arrived since (AGL-1066).
55
+ *
56
+ * `fromCache` says "unconfirmed", which is also true offline and true for
57
+ * the first snapshot of a perfectly healthy load. This says "refused" —
58
+ * only `permission-denied` counts it, and it takes a whole budget's worth
59
+ * in a row, so it cannot fire offline and cannot fire on the AGL-216/217
60
+ * post-sign-in token race.
61
+ *
62
+ * It is what `status` would say if the retry budget were spendable. It is
63
+ * carried separately because correcting `status` in place would blank
64
+ * every surface that renders cached data through an `error` branch — see
65
+ * the note on `attempt = 0` in `use-firestore-collection`.
66
+ */
67
+ serverDenied: boolean;
68
+ }
69
+ /** Replaces reactfire's `ReactFireOptions<T>` — drops the `suspense` field, which was never read. */
70
+ export interface FirestoreDocOptions<T> {
71
+ idField?: string;
72
+ initialData?: T;
73
+ }
74
+ /**
75
+ * A surface's answer to "may an analytics tag exist for this visitor at all?".
76
+ *
77
+ * Registered from the app, at module scope, in the same shape as
78
+ * `setFirestoreSessionReporters` and `setStaleSessionCheck` — and for a
79
+ * sharper version of the same reason. This is consulted during the provider's
80
+ * FIRST render, before any effect anywhere has run, so a gate installed from
81
+ * an effect would arrive after the decision it exists to make.
82
+ *
83
+ * Answering `false` means the SDK is never asked to initialize: `gtag.js` is
84
+ * never fetched, `window.gtag` never comes into existence, and the shared
85
+ * `deliver()` in `analytics-events.ts` — whose fallback IS `window.gtag` —
86
+ * therefore has nowhere to put a hit. That is the difference between a gate
87
+ * and a suppression, and it is the difference AGL-1608 paid for: a resident
88
+ * tag reports on its own, through enhanced measurement, whatever any flag
89
+ * says afterwards.
90
+ */
91
+ export type AnalyticsConsentGate = () => boolean;
92
+ /**
93
+ * Install the consent gate for this document, or clear it with `null`.
94
+ *
95
+ * UNREGISTERED MEANS ALLOWED, which is deliberate and is the only default that
96
+ * is safe here. The tenant runtime does not use this provider's analytics
97
+ * branch at all (it builds its own tag in `site-analytics.tsx`, behind the
98
+ * AGL-1498 gate), and a self-hosted console points at the operator's own GA
99
+ * property — silencing an operator's analytics because Aglyn's own surface
100
+ * grew a gate is a failure they cannot diagnose. The surface that needs the
101
+ * gate is the one that registers it.
102
+ */
103
+ export declare function setAnalyticsConsentGate(gate: AnalyticsConsentGate | null): void;
104
+ export interface FirebaseServicesProviderProps {
105
+ firebaseConfig: FirebaseOptions;
106
+ appName: string;
107
+ /**
108
+ * How much of *anything* this origin may keep on disk. Defaults to
109
+ * `durable`, which is what every host serving this provider today is:
110
+ * `*.aglyn.com` and `*.aglyn.app`, whose DNS we own and whose 14-day
111
+ * session is the product.
112
+ *
113
+ * A **custom console domain** (AGL-1099c) must pass `ephemeral`. That is
114
+ * the whole reason this is a prop rather than a constant: the class differs
115
+ * per origin, not per build, and the decision belongs to whoever knows
116
+ * which origin is being served.
117
+ *
118
+ * **It governs two things, and the name only names one.** It selects the
119
+ * `Auth` persistence class (AGL-1379) *and* the Firestore `localCache`
120
+ * (AGL-1456) — because both answer the same question about the same origin,
121
+ * and splitting them into two props is precisely how the cache came to be
122
+ * unconditional while the credential was hardened. See `firestore-cache.ts`.
123
+ */
124
+ authPersistence?: AuthPersistenceClass;
125
+ children?: ReactNode;
126
+ }
127
+ export declare function FirebaseServicesProvider(props: FirebaseServicesProviderProps): import("react").JSX.Element;
128
+ export declare namespace FirebaseServicesProvider {
129
+ var displayName: string;
130
+ }
131
+ export declare function useFirebaseApp(): FirebaseApp;
132
+ export declare function useFirestore(): Firestore;
133
+ export declare function useAuth(): Auth;
134
+ /**
135
+ * The persistence class the provider's `Auth` was created with (AGL-1379).
136
+ *
137
+ * Read it — never re-derive it — when building a **second** Firebase app on
138
+ * the same origin, so the second instance cannot end up more persistent than
139
+ * the first. `apps/console/hooks/use-presence.ts` is the existing case.
140
+ */
141
+ export declare function useAuthPersistence(): AuthPersistenceClass;
142
+ export declare function useAnalytics(): Analytics;
143
+ export declare function useRemoteConfig(): RemoteConfig;
144
+ /**
145
+ * Replaces reactfire's `useUser()`. Reactfire subscribes via
146
+ * `onIdTokenChanged` rather than `onAuthStateChanged` so fields like
147
+ * `emailVerified` stay live across token refresh (e.g. after
148
+ * `user.reload()` post-email-verification), not just sign-in/out — matched
149
+ * here for behavioral parity.
150
+ *
151
+ * THREE states, not two (AGL-1261): `undefined` = auth has not resolved yet,
152
+ * `null` = resolved, signed OUT, a `User` = resolved, signed in. This used to
153
+ * collapse the emitted `null` into `undefined`, which made "signed out" and
154
+ * "still loading" the same value — a state that, for a signed-out visitor,
155
+ * never left "loading". `useSessionCookie` gates its whole body on
156
+ * `user === undefined`, so its silent restore from the shared `__session`
157
+ * cookie could never run: the cookie exchange returned a valid custom token
158
+ * and nothing ever asked for it.
159
+ */
160
+ export declare function useUser(): {
161
+ data: User | null | undefined;
162
+ };
163
+ export interface SigninCheckResult {
164
+ signedIn: boolean;
165
+ user: User | null;
166
+ }
167
+ /** Replaces reactfire's `useSigninCheck()` — same `onIdTokenChanged` basis as `useUser()`. */
168
+ export declare function useSigninCheck(): {
169
+ status: 'loading' | 'success' | 'error';
170
+ data: SigninCheckResult | undefined;
171
+ error: Error | undefined;
172
+ };