@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.
- package/LICENSE +201 -0
- package/README.md +3 -0
- package/jest.config.d.ts +36 -0
- package/package.json +45 -0
- package/src/index.d.ts +17 -0
- package/src/index.js +18 -0
- package/src/index.js.map +1 -0
- package/src/lib/components/duplicate-resource-dialog.d.ts +77 -0
- package/src/lib/components/duplicate-resource-dialog.js +217 -0
- package/src/lib/components/duplicate-resource-dialog.js.map +1 -0
- package/src/lib/constants/firebase-config.d.ts +68 -0
- package/src/lib/constants/firebase-config.js +155 -0
- package/src/lib/constants/firebase-config.js.map +1 -0
- package/src/lib/constants/firebase-emulator-hosts.d.ts +60 -0
- package/src/lib/constants/firebase-emulator-hosts.js +81 -0
- package/src/lib/constants/firebase-emulator-hosts.js.map +1 -0
- package/src/lib/constants/mock-data.d.ts +24 -0
- package/src/lib/constants/mock-data.js +49 -0
- package/src/lib/constants/mock-data.js.map +1 -0
- package/src/lib/constants/site-paths.d.ts +47 -0
- package/src/lib/constants/site-paths.js +66 -0
- package/src/lib/constants/site-paths.js.map +1 -0
- package/src/lib/hooks/firebase/auth-persistence.d.ts +74 -0
- package/src/lib/hooks/firebase/auth-persistence.js +117 -0
- package/src/lib/hooks/firebase/auth-persistence.js.map +1 -0
- package/src/lib/hooks/firebase/firebase-services.d.ts +172 -0
- package/src/lib/hooks/firebase/firebase-services.js +522 -0
- package/src/lib/hooks/firebase/firebase-services.js.map +1 -0
- package/src/lib/hooks/firebase/firestore-cache.d.ts +125 -0
- package/src/lib/hooks/firebase/firestore-cache.js +152 -0
- package/src/lib/hooks/firebase/firestore-cache.js.map +1 -0
- package/src/lib/hooks/firebase/firestore-shared-client-state.d.ts +169 -0
- package/src/lib/hooks/firebase/firestore-shared-client-state.js +316 -0
- package/src/lib/hooks/firebase/firestore-shared-client-state.js.map +1 -0
- package/src/lib/hooks/firestore-denial-reporter.d.ts +192 -0
- package/src/lib/hooks/firestore-denial-reporter.js +293 -0
- package/src/lib/hooks/firestore-denial-reporter.js.map +1 -0
- package/src/lib/hooks/helpers/besigner-nodes-converter.d.ts +62 -0
- package/src/lib/hooks/helpers/besigner-nodes-converter.js +91 -0
- package/src/lib/hooks/helpers/besigner-nodes-converter.js.map +1 -0
- package/src/lib/hooks/helpers/guarded-nodes-save.d.ts +80 -0
- package/src/lib/hooks/helpers/guarded-nodes-save.js +89 -0
- package/src/lib/hooks/helpers/guarded-nodes-save.js.map +1 -0
- package/src/lib/hooks/helpers/guarded-seed-write.d.ts +125 -0
- package/src/lib/hooks/helpers/guarded-seed-write.js +193 -0
- package/src/lib/hooks/helpers/guarded-seed-write.js.map +1 -0
- package/src/lib/hooks/helpers/listen-options.d.ts +91 -0
- package/src/lib/hooks/helpers/listen-options.js +92 -0
- package/src/lib/hooks/helpers/listen-options.js.map +1 -0
- package/src/lib/hooks/helpers/use-doc.d.ts +38 -0
- package/src/lib/hooks/helpers/use-doc.js +213 -0
- package/src/lib/hooks/helpers/use-doc.js.map +1 -0
- package/src/lib/hooks/helpers/use-modify-doc-callback.d.ts +28 -0
- package/src/lib/hooks/helpers/use-modify-doc-callback.js +110 -0
- package/src/lib/hooks/helpers/use-modify-doc-callback.js.map +1 -0
- package/src/lib/hooks/host-collection-queries.d.ts +125 -0
- package/src/lib/hooks/host-collection-queries.js +126 -0
- package/src/lib/hooks/host-collection-queries.js.map +1 -0
- package/src/lib/hooks/list-filter-constraints.d.ts +64 -0
- package/src/lib/hooks/list-filter-constraints.js +288 -0
- package/src/lib/hooks/list-filter-constraints.js.map +1 -0
- package/src/lib/hooks/sorted-collection-window.d.ts +207 -0
- package/src/lib/hooks/sorted-collection-window.js +150 -0
- package/src/lib/hooks/sorted-collection-window.js.map +1 -0
- package/src/lib/hooks/use-component-version.d.ts +43 -0
- package/src/lib/hooks/use-component-version.js +73 -0
- package/src/lib/hooks/use-component-version.js.map +1 -0
- package/src/lib/hooks/use-component.d.ts +39 -0
- package/src/lib/hooks/use-component.js +55 -0
- package/src/lib/hooks/use-component.js.map +1 -0
- package/src/lib/hooks/use-console-host-route.d.ts +24 -0
- package/src/lib/hooks/use-console-host-route.js +77 -0
- package/src/lib/hooks/use-console-host-route.js.map +1 -0
- package/src/lib/hooks/use-duplicate-resource-api.d.ts +43 -0
- package/src/lib/hooks/use-duplicate-resource-api.js +61 -0
- package/src/lib/hooks/use-duplicate-resource-api.js.map +1 -0
- package/src/lib/hooks/use-firestore-collection.d.ts +62 -0
- package/src/lib/hooks/use-firestore-collection.js +307 -0
- package/src/lib/hooks/use-firestore-collection.js.map +1 -0
- package/src/lib/hooks/use-firestore-doc.d.ts +42 -0
- package/src/lib/hooks/use-firestore-doc.js +192 -0
- package/src/lib/hooks/use-firestore-doc.js.map +1 -0
- package/src/lib/hooks/use-form-version.d.ts +44 -0
- package/src/lib/hooks/use-form-version.js +73 -0
- package/src/lib/hooks/use-form-version.js.map +1 -0
- package/src/lib/hooks/use-host-activity-logger.d.ts +35 -0
- package/src/lib/hooks/use-host-activity-logger.js +96 -0
- package/src/lib/hooks/use-host-activity-logger.js.map +1 -0
- package/src/lib/hooks/use-host-campaigns.d.ts +53 -0
- package/src/lib/hooks/use-host-campaigns.js +93 -0
- package/src/lib/hooks/use-host-campaigns.js.map +1 -0
- package/src/lib/hooks/use-host-org-id.d.ts +96 -0
- package/src/lib/hooks/use-host-org-id.js +154 -0
- package/src/lib/hooks/use-host-org-id.js.map +1 -0
- package/src/lib/hooks/use-host-resource-api.d.ts +43 -0
- package/src/lib/hooks/use-host-resource-api.js +60 -0
- package/src/lib/hooks/use-host-resource-api.js.map +1 -0
- package/src/lib/hooks/use-host-template.d.ts +44 -0
- package/src/lib/hooks/use-host-template.js +82 -0
- package/src/lib/hooks/use-host-template.js.map +1 -0
- package/src/lib/hooks/use-host-version-api.d.ts +59 -0
- package/src/lib/hooks/use-host-version-api.js +69 -0
- package/src/lib/hooks/use-host-version-api.js.map +1 -0
- package/src/lib/hooks/use-host.d.ts +101 -0
- package/src/lib/hooks/use-host.js +45 -0
- package/src/lib/hooks/use-host.js.map +1 -0
- package/src/lib/hooks/use-layout-version.d.ts +33 -0
- package/src/lib/hooks/use-layout-version.js +64 -0
- package/src/lib/hooks/use-layout-version.js.map +1 -0
- package/src/lib/hooks/use-layout.d.ts +41 -0
- package/src/lib/hooks/use-layout.js +49 -0
- package/src/lib/hooks/use-layout.js.map +1 -0
- package/src/lib/hooks/use-live-artifact-count.d.ts +50 -0
- package/src/lib/hooks/use-live-artifact-count.js +102 -0
- package/src/lib/hooks/use-live-artifact-count.js.map +1 -0
- package/src/lib/hooks/use-org-member-options.d.ts +57 -0
- package/src/lib/hooks/use-org-member-options.js +112 -0
- package/src/lib/hooks/use-org-member-options.js.map +1 -0
- package/src/lib/hooks/use-org-plan.d.ts +46 -0
- package/src/lib/hooks/use-org-plan.js +86 -0
- package/src/lib/hooks/use-org-plan.js.map +1 -0
- package/src/lib/hooks/use-paged-collection.d.ts +95 -0
- package/src/lib/hooks/use-paged-collection.js +130 -0
- package/src/lib/hooks/use-paged-collection.js.map +1 -0
- package/src/lib/hooks/use-plugin-config.d.ts +38 -0
- package/src/lib/hooks/use-plugin-config.js +120 -0
- package/src/lib/hooks/use-plugin-config.js.map +1 -0
- package/src/lib/hooks/use-scope-tokens.d.ts +28 -0
- package/src/lib/hooks/use-scope-tokens.js +100 -0
- package/src/lib/hooks/use-scope-tokens.js.map +1 -0
- package/src/lib/hooks/use-screen-version.d.ts +33 -0
- package/src/lib/hooks/use-screen-version.js +65 -0
- package/src/lib/hooks/use-screen-version.js.map +1 -0
- package/src/lib/hooks/use-screen.d.ts +68 -0
- package/src/lib/hooks/use-screen.js +50 -0
- package/src/lib/hooks/use-screen.js.map +1 -0
- package/src/lib/hooks/use-sending-identity-api.d.ts +197 -0
- package/src/lib/hooks/use-sending-identity-api.js +95 -0
- package/src/lib/hooks/use-sending-identity-api.js.map +1 -0
- package/src/lib/hooks/use-sorted-paged-collection.d.ts +48 -0
- package/src/lib/hooks/use-sorted-paged-collection.js +213 -0
- package/src/lib/hooks/use-sorted-paged-collection.js.map +1 -0
- package/src/lib/hooks/use-switcher-collection.d.ts +105 -0
- package/src/lib/hooks/use-switcher-collection.js +275 -0
- package/src/lib/hooks/use-switcher-collection.js.map +1 -0
- package/src/lib/hooks/use-user-name.d.ts +51 -0
- package/src/lib/hooks/use-user-name.js +90 -0
- package/src/lib/hooks/use-user-name.js.map +1 -0
- package/src/lib/hooks/use-user-photo.d.ts +50 -0
- package/src/lib/hooks/use-user-photo.js +87 -0
- package/src/lib/hooks/use-user-photo.js.map +1 -0
- package/src/lib/tenant.d.ts +61 -0
- package/src/lib/tenant.js +62 -0
- 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
|
+
};
|