pyric-admin 0.1.0-alpha.7
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/README.md +49 -0
- package/dist/app/index.d.ts +158 -0
- package/dist/app/index.d.ts.map +1 -0
- package/dist/app/index.js +297 -0
- package/dist/app/index.js.map +1 -0
- package/dist/auth/index.d.ts +146 -0
- package/dist/auth/index.d.ts.map +1 -0
- package/dist/auth/index.js +785 -0
- package/dist/auth/index.js.map +1 -0
- package/dist/database/index.d.ts +119 -0
- package/dist/database/index.d.ts.map +1 -0
- package/dist/database/index.js +965 -0
- package/dist/database/index.js.map +1 -0
- package/dist/firestore/index.d.ts +32 -0
- package/dist/firestore/index.d.ts.map +1 -0
- package/dist/firestore/index.js +53 -0
- package/dist/firestore/index.js.map +1 -0
- package/dist/storage/index.d.ts +178 -0
- package/dist/storage/index.d.ts.map +1 -0
- package/dist/storage/index.js +442 -0
- package/dist/storage/index.js.map +1 -0
- package/package.json +52 -0
|
@@ -0,0 +1,785 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `pyric-admin/auth` — Phase 3 (branded-app dispatch) + Phase 4b
|
|
3
|
+
* (minimal in-memory sandbox backend).
|
|
4
|
+
*
|
|
5
|
+
* Mirrors `firebase-admin/auth` for a useful subset of methods. The
|
|
6
|
+
* `app` argument is the branded handle from `pyric-admin/app`
|
|
7
|
+
* ({@link PyricAdminApp}); the brand symbol picks the backend:
|
|
8
|
+
*
|
|
9
|
+
* - **Prod path** (`app[ADMIN_APP_TARGET] === 'prod'`) — delegates to
|
|
10
|
+
* `firebase-admin/auth`'s `getAuth(app.adminApp)` and returns the
|
|
11
|
+
* production `Auth` instance unchanged. Drop-in replacement for
|
|
12
|
+
* `firebase-admin/auth` so agents already calling it keep their
|
|
13
|
+
* exact behavior (tenants, providers, MFA, session cookies, action
|
|
14
|
+
* codes, all present on the returned handle).
|
|
15
|
+
*
|
|
16
|
+
* - **Sandbox path** (`app[ADMIN_APP_TARGET] === 'sandbox'`) — backed
|
|
17
|
+
* by an in-memory store keyed off `app.sandbox`. Implements the
|
|
18
|
+
* core user-management subset listed below. Tokens are NOT real
|
|
19
|
+
* JWTs — they're deterministic strings the same sandbox backend
|
|
20
|
+
* parses on `verifyIdToken`. The sandbox backend is intentionally
|
|
21
|
+
* minimal: enough to exercise agent code paths that use
|
|
22
|
+
* `createUser` / `getUser` / `setCustomUserClaims` /
|
|
23
|
+
* `createCustomToken` / `verifyIdToken`, not enough to model a
|
|
24
|
+
* real identity platform.
|
|
25
|
+
*
|
|
26
|
+
* - **Remote sandbox arm** — when the sandbox carries `pyric/sandbox`'s
|
|
27
|
+
* remote brand (a Node-side handle onto the browser-hosted
|
|
28
|
+
* SharedWorker sandbox from `pyric-tools`' `connectRemoteSandbox()`),
|
|
29
|
+
* user CRUD relays over the handle's worker channel instead of the
|
|
30
|
+
* in-memory store, so server-created users land in the ONE user pool
|
|
31
|
+
* the browser app + Studio share. See the "Remote sandbox arm"
|
|
32
|
+
* section below for the details (including the extra methods it
|
|
33
|
+
* supports: `updateUser`, `listUsers`).
|
|
34
|
+
*
|
|
35
|
+
* Surface scope on the sandbox backend (what works):
|
|
36
|
+
*
|
|
37
|
+
* - {@link getAuth}
|
|
38
|
+
* - `Auth.createCustomToken(uid, claims?)` — returns a deterministic
|
|
39
|
+
* `pyric-sandbox-custom:${uid}:${json}` string; no signing.
|
|
40
|
+
* - `Auth.verifyIdToken(token)` — parses tokens minted by
|
|
41
|
+
* `createCustomToken`; returns a {@link DecodedIdToken}-shaped
|
|
42
|
+
* object.
|
|
43
|
+
* - `Auth.createUser(properties)` — stores a {@link UserRecord}
|
|
44
|
+
* in an in-memory `Map<uid, UserRecord>`. Auto-generates a `uid`
|
|
45
|
+
* when one is not supplied.
|
|
46
|
+
* - `Auth.getUser(uid)` — Map lookup.
|
|
47
|
+
* - `Auth.getUserByEmail(email)` — linear scan.
|
|
48
|
+
* - `Auth.deleteUser(uid)` — Map delete.
|
|
49
|
+
* - `Auth.setCustomUserClaims(uid, claims)` — updates the stored
|
|
50
|
+
* `UserRecord.customClaims`.
|
|
51
|
+
*
|
|
52
|
+
* Sandbox backend — explicitly NOT implemented (throws
|
|
53
|
+
* `'not implemented in pyric-admin/auth sandbox backend'` so callers
|
|
54
|
+
* get a clear remediation message):
|
|
55
|
+
*
|
|
56
|
+
* - Tenancy: `tenantManager` and any per-tenant call.
|
|
57
|
+
* - Identity providers: `createProviderConfig`, `getProviderConfig`,
|
|
58
|
+
* `listProviderConfigs`, `updateProviderConfig`,
|
|
59
|
+
* `deleteProviderConfig`.
|
|
60
|
+
* - Multi-factor: `MultiFactorSettings` on UserRecord is always
|
|
61
|
+
* `undefined`; MFA enrollment is unsupported.
|
|
62
|
+
* - Session cookies: `createSessionCookie`, `verifySessionCookie`.
|
|
63
|
+
* - Action codes / password reset / email link sign-in:
|
|
64
|
+
* `generatePasswordResetLink`, `generateEmailVerificationLink`,
|
|
65
|
+
* `generateSignInWithEmailLink`, `generateVerifyAndChangeEmailLink`.
|
|
66
|
+
* - Bulk operations: `listUsers`, `getUsers`, `deleteUsers`,
|
|
67
|
+
* `importUsers`.
|
|
68
|
+
* - Revocation: `revokeRefreshTokens`.
|
|
69
|
+
* - `getUserByPhoneNumber`, `getUserByProviderUid`.
|
|
70
|
+
* - `updateUser` — not required by the brief.
|
|
71
|
+
*
|
|
72
|
+
* Real `firebase-admin` types are imported with `import type` so
|
|
73
|
+
* signatures stay identical to upstream — there's no shape drift to
|
|
74
|
+
* keep in sync.
|
|
75
|
+
*/
|
|
76
|
+
import { isRemoteSandbox, } from 'pyric/sandbox';
|
|
77
|
+
import { ADMIN_APP_TARGET, getApp, } from '../app/index.js';
|
|
78
|
+
// ─── Sandbox backend ────────────────────────────────────────────────────
|
|
79
|
+
/**
|
|
80
|
+
* Per-sandbox in-memory store. One instance per `Sandbox` (tracked in
|
|
81
|
+
* the {@link sandboxStores} WeakMap below). Holds the user table; the
|
|
82
|
+
* token format is stateless (`createCustomToken` mints, `verifyIdToken`
|
|
83
|
+
* parses) so it doesn't need to live here.
|
|
84
|
+
*
|
|
85
|
+
* `usersByUid` is the canonical index. `getUserByEmail` does a linear
|
|
86
|
+
* scan over its values — the sandbox is for development and agent test
|
|
87
|
+
* runs, not production traffic, so an extra index isn't worth the
|
|
88
|
+
* write-path complexity.
|
|
89
|
+
*/
|
|
90
|
+
class AuthStore {
|
|
91
|
+
usersByUid = new Map();
|
|
92
|
+
/** Monotonic counter for auto-generated uids. Reset along with the
|
|
93
|
+
* user map when the sandbox calls `reset()`. */
|
|
94
|
+
nextAutoUid = 1;
|
|
95
|
+
/**
|
|
96
|
+
* Mint an auto-uid in the same shape Firebase Auth uses (28 chars,
|
|
97
|
+
* URL-safe alphabet). The sandbox doesn't need cryptographic
|
|
98
|
+
* collision resistance — it needs a stable, debuggable identifier
|
|
99
|
+
* that doesn't collide *within one sandbox session*. A counter plus
|
|
100
|
+
* a constant prefix is enough; padding keeps the visual width
|
|
101
|
+
* roughly consistent with prod uids.
|
|
102
|
+
*/
|
|
103
|
+
mintUid() {
|
|
104
|
+
const n = String(this.nextAutoUid++).padStart(20, '0');
|
|
105
|
+
return `pyric-sandbox-${n}`;
|
|
106
|
+
}
|
|
107
|
+
/** Wipe state. Called on `sandbox.reset()`. */
|
|
108
|
+
clear() {
|
|
109
|
+
this.usersByUid.clear();
|
|
110
|
+
this.nextAutoUid = 1;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* One {@link AuthStore} per `Sandbox`. WeakMap so a sandbox that gets
|
|
115
|
+
* GC'd by its host takes its auth state with it — no manual disposal
|
|
116
|
+
* needed.
|
|
117
|
+
*/
|
|
118
|
+
const sandboxStores = new WeakMap();
|
|
119
|
+
/**
|
|
120
|
+
* Tracks which sandboxes already have a `session_boundary` listener
|
|
121
|
+
* attached. Without this guard, calling `getAuth(app)` twice for the
|
|
122
|
+
* same sandbox would register two listeners that each clear the store
|
|
123
|
+
* on reset — harmless functionally, but a noisy leak.
|
|
124
|
+
*/
|
|
125
|
+
const sandboxesWithReset = new WeakSet();
|
|
126
|
+
/**
|
|
127
|
+
* Get-or-create the auth store for a sandbox, and on first creation
|
|
128
|
+
* subscribe to `session_boundary` events so the store wipes itself when
|
|
129
|
+
* the sandbox is reset. The subscription is attached once per sandbox.
|
|
130
|
+
*
|
|
131
|
+
* `dispose` is also a session boundary; we clear on either phase so a
|
|
132
|
+
* disposed-and-replaced sandbox doesn't hand its successor stale state.
|
|
133
|
+
* (In practice WeakMap GC handles that, but clearing eagerly costs
|
|
134
|
+
* nothing.)
|
|
135
|
+
*/
|
|
136
|
+
function storeFor(sandbox) {
|
|
137
|
+
let store = sandboxStores.get(sandbox);
|
|
138
|
+
if (store)
|
|
139
|
+
return store;
|
|
140
|
+
store = new AuthStore();
|
|
141
|
+
sandboxStores.set(sandbox, store);
|
|
142
|
+
if (!sandboxesWithReset.has(sandbox)) {
|
|
143
|
+
sandboxesWithReset.add(sandbox);
|
|
144
|
+
sandbox.onEvent((event) => {
|
|
145
|
+
if (event.kind === 'session_boundary') {
|
|
146
|
+
store.clear();
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
return store;
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Token format minted by `createCustomToken` and parsed by
|
|
154
|
+
* `verifyIdToken`. Exported as a constant so tests can lock the shape.
|
|
155
|
+
*
|
|
156
|
+
* Layout: `pyric-sandbox-custom:${uid}:${jsonClaims}`
|
|
157
|
+
*
|
|
158
|
+
* - The prefix lets `verifyIdToken` reject foreign tokens with a clear
|
|
159
|
+
* "not a sandbox token" error rather than NaN'ing out.
|
|
160
|
+
* - `uid` is colon-free per the auto-uid format above.
|
|
161
|
+
* - `jsonClaims` is the JSON-stringified developer claims (or `{}` when
|
|
162
|
+
* none were provided). Round-trips losslessly through `JSON.parse`.
|
|
163
|
+
*
|
|
164
|
+
* NOT a JWT. NOT signed. Do not use this token format to talk to any
|
|
165
|
+
* real Firebase service — it only round-trips through this same
|
|
166
|
+
* sandbox backend.
|
|
167
|
+
*/
|
|
168
|
+
export const SANDBOX_TOKEN_PREFIX = 'pyric-sandbox-custom';
|
|
169
|
+
/**
|
|
170
|
+
* Mint a deterministic sandbox token (the {@link SANDBOX_TOKEN_PREFIX}
|
|
171
|
+
* format). Stateless — shared verbatim by the local and remote sandbox
|
|
172
|
+
* arms, so a token minted against either round-trips through
|
|
173
|
+
* {@link verifySandboxIdToken} on the other.
|
|
174
|
+
*/
|
|
175
|
+
function mintSandboxCustomToken(uid, developerClaims) {
|
|
176
|
+
const claims = developerClaims ?? {};
|
|
177
|
+
const token = `${SANDBOX_TOKEN_PREFIX}:${uid}:${JSON.stringify(claims)}`;
|
|
178
|
+
return Promise.resolve(token);
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Parse a token minted by {@link mintSandboxCustomToken}. Returns a
|
|
182
|
+
* `DecodedIdToken`-shaped object — every required field is filled with a
|
|
183
|
+
* sandbox-appropriate placeholder (`iss`/`aud` = `pyric-sandbox`, time
|
|
184
|
+
* fields = now), and the developer claims are spread onto the result so
|
|
185
|
+
* `decoded.role` etc. work the same way they do in prod.
|
|
186
|
+
*
|
|
187
|
+
* Throws on any token that doesn't match the
|
|
188
|
+
* `${SANDBOX_TOKEN_PREFIX}:${uid}:${json}` shape — including real JWTs
|
|
189
|
+
* that another verifier would parse. The sandbox backends are
|
|
190
|
+
* intentionally not drop-ins for production token verification.
|
|
191
|
+
*/
|
|
192
|
+
function verifySandboxIdToken(idToken) {
|
|
193
|
+
if (typeof idToken !== 'string' || !idToken.startsWith(`${SANDBOX_TOKEN_PREFIX}:`)) {
|
|
194
|
+
return Promise.reject(new Error('pyric-admin/auth: verifyIdToken on the sandbox backend only ' +
|
|
195
|
+
'accepts tokens minted by this sandbox\'s createCustomToken. ' +
|
|
196
|
+
`Token prefix must be "${SANDBOX_TOKEN_PREFIX}:".`));
|
|
197
|
+
}
|
|
198
|
+
// The token is `prefix:uid:json`. Split on the first two colons
|
|
199
|
+
// only — the JSON payload may itself contain colons (e.g. inside
|
|
200
|
+
// a string value) so `split(':')` with no limit would corrupt it.
|
|
201
|
+
const firstColon = idToken.indexOf(':');
|
|
202
|
+
const secondColon = idToken.indexOf(':', firstColon + 1);
|
|
203
|
+
if (secondColon < 0) {
|
|
204
|
+
return Promise.reject(new Error(`pyric-admin/auth: verifyIdToken received a malformed sandbox token (missing claims segment): ${idToken}`));
|
|
205
|
+
}
|
|
206
|
+
const uid = idToken.slice(firstColon + 1, secondColon);
|
|
207
|
+
const jsonClaims = idToken.slice(secondColon + 1);
|
|
208
|
+
let claims;
|
|
209
|
+
try {
|
|
210
|
+
claims = JSON.parse(jsonClaims);
|
|
211
|
+
}
|
|
212
|
+
catch (e) {
|
|
213
|
+
return Promise.reject(new Error(`pyric-admin/auth: verifyIdToken failed to parse sandbox token claims as JSON: ${e.message}`));
|
|
214
|
+
}
|
|
215
|
+
const nowSec = Math.floor(Date.now() / 1000);
|
|
216
|
+
// `DecodedIdToken` requires `aud`/`iss`/`sub`/`uid`/`auth_time`/
|
|
217
|
+
// `exp`/`iat`/`firebase` to be present; the sandbox fills them
|
|
218
|
+
// with placeholders so consumers that read them get sensible
|
|
219
|
+
// values rather than `undefined`. Developer claims are spread on
|
|
220
|
+
// top so they shadow nothing critical.
|
|
221
|
+
const decoded = {
|
|
222
|
+
aud: 'pyric-sandbox',
|
|
223
|
+
auth_time: nowSec,
|
|
224
|
+
exp: nowSec + 3600,
|
|
225
|
+
firebase: {
|
|
226
|
+
identities: {},
|
|
227
|
+
sign_in_provider: 'custom',
|
|
228
|
+
},
|
|
229
|
+
iat: nowSec,
|
|
230
|
+
iss: 'pyric-sandbox',
|
|
231
|
+
sub: uid,
|
|
232
|
+
uid,
|
|
233
|
+
...claims,
|
|
234
|
+
};
|
|
235
|
+
return Promise.resolve(decoded);
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* Build the in-memory `Auth` handle for a sandbox app. Returns an
|
|
239
|
+
* object structurally compatible with `firebase-admin/auth`'s `Auth`
|
|
240
|
+
* (cast at the boundary) where the documented method subset is wired
|
|
241
|
+
* to the in-memory store and everything else throws the canonical
|
|
242
|
+
* `not implemented in pyric-admin/auth sandbox backend` error.
|
|
243
|
+
*
|
|
244
|
+
* The cast-to-`Auth` at the return is deliberate — `firebase-admin`'s
|
|
245
|
+
* `Auth` is a large class surface (tenants, providers, MFA, session
|
|
246
|
+
* cookies) that this backend doesn't model. Implementing the entire
|
|
247
|
+
* surface as throwing stubs would be ~30 unused methods of noise; the
|
|
248
|
+
* cast acknowledges the divergence in one place and lets the rest of
|
|
249
|
+
* the file focus on the methods that actually work.
|
|
250
|
+
*/
|
|
251
|
+
function makeSandboxAuth(sandbox) {
|
|
252
|
+
const store = storeFor(sandbox);
|
|
253
|
+
/** Canonical "not implemented" error for surface that the sandbox
|
|
254
|
+
* backend doesn't model. Threaded through every stub so the message
|
|
255
|
+
* is identical wherever it's hit. */
|
|
256
|
+
const notImplemented = (method) => new Error(`pyric-admin/auth: ${method} is not implemented in pyric-admin/auth sandbox backend`);
|
|
257
|
+
/**
|
|
258
|
+
* Convert a `CreateRequest` to a `UserRecord`. `firebase-admin`'s
|
|
259
|
+
* `UserRecord` is a class with `readonly` fields; we build a plain
|
|
260
|
+
* object with the same shape and cast it. The sandbox doesn't need
|
|
261
|
+
* the class's `toJSON()` or its provider-merging logic — it needs the
|
|
262
|
+
* field set that the documented method subset reads back.
|
|
263
|
+
*
|
|
264
|
+
* Defaults match upstream defaults: `emailVerified: false`,
|
|
265
|
+
* `disabled: false`, empty `providerData`, present `metadata` with
|
|
266
|
+
* sandbox-current timestamps.
|
|
267
|
+
*/
|
|
268
|
+
const toUserRecord = (uid, props, customClaims) => {
|
|
269
|
+
const now = new Date().toUTCString();
|
|
270
|
+
const record = {
|
|
271
|
+
uid,
|
|
272
|
+
email: props.email,
|
|
273
|
+
emailVerified: props.emailVerified ?? false,
|
|
274
|
+
displayName: props.displayName ?? undefined,
|
|
275
|
+
photoURL: props.photoURL ?? undefined,
|
|
276
|
+
phoneNumber: props.phoneNumber ?? undefined,
|
|
277
|
+
disabled: props.disabled ?? false,
|
|
278
|
+
metadata: {
|
|
279
|
+
creationTime: now,
|
|
280
|
+
lastSignInTime: '',
|
|
281
|
+
toJSON: () => ({ creationTime: now, lastSignInTime: '' }),
|
|
282
|
+
},
|
|
283
|
+
providerData: [],
|
|
284
|
+
customClaims,
|
|
285
|
+
tenantId: null,
|
|
286
|
+
toJSON: () => ({ uid, email: props.email }),
|
|
287
|
+
};
|
|
288
|
+
return record;
|
|
289
|
+
};
|
|
290
|
+
// The handle. Methods that round-trip to the store are real; the
|
|
291
|
+
// rest throw via `notImplemented`. Cast to `Auth` at return.
|
|
292
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
293
|
+
const handle = {
|
|
294
|
+
/** Stamp the app on the handle so debugging shows where it came
|
|
295
|
+
* from. The prod handle has an `app` getter too — matching the
|
|
296
|
+
* shape keeps consumers that inspect `auth.app` from breaking. */
|
|
297
|
+
get app() {
|
|
298
|
+
throw notImplemented('auth.app');
|
|
299
|
+
},
|
|
300
|
+
/**
|
|
301
|
+
* Mint a deterministic sandbox token. Format is fixed at
|
|
302
|
+
* `${SANDBOX_TOKEN_PREFIX}:${uid}:${JSON.stringify(claims ?? {})}`.
|
|
303
|
+
* Round-trips through {@link verifyIdToken} on the same sandbox
|
|
304
|
+
* backend; rejected by every other token verifier. Shared
|
|
305
|
+
* implementation: {@link mintSandboxCustomToken}.
|
|
306
|
+
*/
|
|
307
|
+
createCustomToken(uid, developerClaims) {
|
|
308
|
+
return mintSandboxCustomToken(uid, developerClaims);
|
|
309
|
+
},
|
|
310
|
+
/**
|
|
311
|
+
* Parse a token minted by {@link createCustomToken}. Shared
|
|
312
|
+
* implementation: {@link verifySandboxIdToken}.
|
|
313
|
+
*/
|
|
314
|
+
verifyIdToken(idToken, _checkRevoked) {
|
|
315
|
+
return verifySandboxIdToken(idToken);
|
|
316
|
+
},
|
|
317
|
+
/**
|
|
318
|
+
* Store a {@link UserRecord} in the in-memory map. If the caller
|
|
319
|
+
* supplied a `uid`, it's used as-is (and a conflict throws
|
|
320
|
+
* `auth/uid-already-exists`-style); otherwise a sandbox-shaped
|
|
321
|
+
* uid is minted.
|
|
322
|
+
*/
|
|
323
|
+
createUser(properties) {
|
|
324
|
+
const uid = properties.uid ?? store.mintUid();
|
|
325
|
+
if (store.usersByUid.has(uid)) {
|
|
326
|
+
return Promise.reject(new Error(`pyric-admin/auth: createUser failed — uid "${uid}" already exists in the sandbox auth store`));
|
|
327
|
+
}
|
|
328
|
+
const record = toUserRecord(uid, properties);
|
|
329
|
+
store.usersByUid.set(uid, record);
|
|
330
|
+
return Promise.resolve(record);
|
|
331
|
+
},
|
|
332
|
+
/** Map lookup; rejects with a `user-not-found` message on miss. */
|
|
333
|
+
getUser(uid) {
|
|
334
|
+
const record = store.usersByUid.get(uid);
|
|
335
|
+
if (!record) {
|
|
336
|
+
return Promise.reject(new Error(`pyric-admin/auth: getUser failed — no user with uid "${uid}"`));
|
|
337
|
+
}
|
|
338
|
+
return Promise.resolve(record);
|
|
339
|
+
},
|
|
340
|
+
/** Linear scan. Sandbox-scale data only — see class JSDoc. */
|
|
341
|
+
getUserByEmail(email) {
|
|
342
|
+
for (const record of store.usersByUid.values()) {
|
|
343
|
+
if (record.email === email)
|
|
344
|
+
return Promise.resolve(record);
|
|
345
|
+
}
|
|
346
|
+
return Promise.reject(new Error(`pyric-admin/auth: getUserByEmail failed — no user with email "${email}"`));
|
|
347
|
+
},
|
|
348
|
+
/** Idempotent: removing a nonexistent uid is a no-op (matches
|
|
349
|
+
* upstream's "successful response" behavior on missing users for
|
|
350
|
+
* the admin SDK's delete semantics — the SDK does throw, but the
|
|
351
|
+
* test fixtures consume the throw as a non-fatal). We throw on
|
|
352
|
+
* miss to match upstream's stricter contract on `deleteUser`. */
|
|
353
|
+
deleteUser(uid) {
|
|
354
|
+
if (!store.usersByUid.has(uid)) {
|
|
355
|
+
return Promise.reject(new Error(`pyric-admin/auth: deleteUser failed — no user with uid "${uid}"`));
|
|
356
|
+
}
|
|
357
|
+
store.usersByUid.delete(uid);
|
|
358
|
+
return Promise.resolve();
|
|
359
|
+
},
|
|
360
|
+
/**
|
|
361
|
+
* Update the stored UserRecord's `customClaims`. Passing `null`
|
|
362
|
+
* clears them (matches the upstream contract:
|
|
363
|
+
* `customUserClaims: object | null`).
|
|
364
|
+
*
|
|
365
|
+
* The UserRecord is rewritten — `customClaims` is `readonly` on
|
|
366
|
+
* the upstream type, so an in-place mutation would type-error.
|
|
367
|
+
* We rebuild the record from the prior props and the new claims,
|
|
368
|
+
* preserving every other field.
|
|
369
|
+
*/
|
|
370
|
+
setCustomUserClaims(uid, customUserClaims) {
|
|
371
|
+
const prior = store.usersByUid.get(uid);
|
|
372
|
+
if (!prior) {
|
|
373
|
+
return Promise.reject(new Error(`pyric-admin/auth: setCustomUserClaims failed — no user with uid "${uid}"`));
|
|
374
|
+
}
|
|
375
|
+
const updated = {
|
|
376
|
+
...prior,
|
|
377
|
+
customClaims: customUserClaims === null
|
|
378
|
+
? undefined
|
|
379
|
+
: customUserClaims,
|
|
380
|
+
toJSON: prior.toJSON.bind(prior),
|
|
381
|
+
};
|
|
382
|
+
store.usersByUid.set(uid, updated);
|
|
383
|
+
return Promise.resolve();
|
|
384
|
+
},
|
|
385
|
+
// ─── Explicitly-not-implemented surface ─────────────────────────
|
|
386
|
+
//
|
|
387
|
+
// The rest of `BaseAuth` (and `Auth`) — tenants, providers, MFA,
|
|
388
|
+
// session cookies, action codes, bulk ops, refresh-token
|
|
389
|
+
// revocation, phone/provider lookups, `updateUser`. Each throws
|
|
390
|
+
// the canonical `not implemented in pyric-admin/auth sandbox
|
|
391
|
+
// backend` error so the caller knows the surface exists upstream
|
|
392
|
+
// but isn't modelled here.
|
|
393
|
+
updateUser() {
|
|
394
|
+
return Promise.reject(notImplemented('updateUser'));
|
|
395
|
+
},
|
|
396
|
+
getUserByPhoneNumber() {
|
|
397
|
+
return Promise.reject(notImplemented('getUserByPhoneNumber'));
|
|
398
|
+
},
|
|
399
|
+
getUserByProviderUid() {
|
|
400
|
+
return Promise.reject(notImplemented('getUserByProviderUid'));
|
|
401
|
+
},
|
|
402
|
+
getUsers() {
|
|
403
|
+
return Promise.reject(notImplemented('getUsers'));
|
|
404
|
+
},
|
|
405
|
+
deleteUsers() {
|
|
406
|
+
return Promise.reject(notImplemented('deleteUsers'));
|
|
407
|
+
},
|
|
408
|
+
listUsers() {
|
|
409
|
+
return Promise.reject(notImplemented('listUsers'));
|
|
410
|
+
},
|
|
411
|
+
importUsers() {
|
|
412
|
+
return Promise.reject(notImplemented('importUsers'));
|
|
413
|
+
},
|
|
414
|
+
revokeRefreshTokens() {
|
|
415
|
+
return Promise.reject(notImplemented('revokeRefreshTokens'));
|
|
416
|
+
},
|
|
417
|
+
createSessionCookie() {
|
|
418
|
+
return Promise.reject(notImplemented('createSessionCookie'));
|
|
419
|
+
},
|
|
420
|
+
verifySessionCookie() {
|
|
421
|
+
return Promise.reject(notImplemented('verifySessionCookie'));
|
|
422
|
+
},
|
|
423
|
+
generatePasswordResetLink() {
|
|
424
|
+
return Promise.reject(notImplemented('generatePasswordResetLink'));
|
|
425
|
+
},
|
|
426
|
+
generateEmailVerificationLink() {
|
|
427
|
+
return Promise.reject(notImplemented('generateEmailVerificationLink'));
|
|
428
|
+
},
|
|
429
|
+
generateSignInWithEmailLink() {
|
|
430
|
+
return Promise.reject(notImplemented('generateSignInWithEmailLink'));
|
|
431
|
+
},
|
|
432
|
+
generateVerifyAndChangeEmailLink() {
|
|
433
|
+
return Promise.reject(notImplemented('generateVerifyAndChangeEmailLink'));
|
|
434
|
+
},
|
|
435
|
+
createProviderConfig() {
|
|
436
|
+
return Promise.reject(notImplemented('createProviderConfig'));
|
|
437
|
+
},
|
|
438
|
+
getProviderConfig() {
|
|
439
|
+
return Promise.reject(notImplemented('getProviderConfig'));
|
|
440
|
+
},
|
|
441
|
+
listProviderConfigs() {
|
|
442
|
+
return Promise.reject(notImplemented('listProviderConfigs'));
|
|
443
|
+
},
|
|
444
|
+
updateProviderConfig() {
|
|
445
|
+
return Promise.reject(notImplemented('updateProviderConfig'));
|
|
446
|
+
},
|
|
447
|
+
deleteProviderConfig() {
|
|
448
|
+
return Promise.reject(notImplemented('deleteProviderConfig'));
|
|
449
|
+
},
|
|
450
|
+
get tenantManager() {
|
|
451
|
+
throw notImplemented('tenantManager');
|
|
452
|
+
},
|
|
453
|
+
get projectConfigManager() {
|
|
454
|
+
throw notImplemented('projectConfigManager');
|
|
455
|
+
},
|
|
456
|
+
};
|
|
457
|
+
return handle;
|
|
458
|
+
}
|
|
459
|
+
// ─── Remote sandbox arm (remote sandbox, slice 1) ───────────────────────
|
|
460
|
+
//
|
|
461
|
+
// The app's `Sandbox` is a Node-side handle onto the browser-hosted
|
|
462
|
+
// SharedWorker sandbox (`pyric/sandbox`'s remote brand). User CRUD relays
|
|
463
|
+
// over the handle's worker channel as the existing admin auth ops
|
|
464
|
+
// (`auth.adminCreateUser` / `auth.adminUpdateUser` / `auth.adminDeleteUser`
|
|
465
|
+
// / `auth.listUsers`) so server-created users land in the ONE user pool
|
|
466
|
+
// the browser app + Studio + agents share — an in-memory `AuthStore` keyed
|
|
467
|
+
// off a remote handle would be a private user table the browser never
|
|
468
|
+
// sees. Auth ops are never lensed (they operate the worker's user pool
|
|
469
|
+
// directly), so no `actAs` is pinned here, unlike the RTDB arm.
|
|
470
|
+
//
|
|
471
|
+
// Single-user lookups (`getUser` / `getUserByEmail`) go through
|
|
472
|
+
// `auth.listUsers` + a client-side filter: the worker protocol has no
|
|
473
|
+
// dedicated single-lookup op, and O(n) over the wire is fine at sandbox
|
|
474
|
+
// scale (per the design spike — add an op if it ever matters).
|
|
475
|
+
//
|
|
476
|
+
// Tokens stay stateless and local: `createCustomToken` / `verifyIdToken`
|
|
477
|
+
// are the same string transforms as the local arm
|
|
478
|
+
// ({@link mintSandboxCustomToken} / {@link verifySandboxIdToken}), so a
|
|
479
|
+
// token minted server-side verifies against any pyric-admin backend.
|
|
480
|
+
/** Map a firebase-admin `CreateRequest` onto the worker's sandbox
|
|
481
|
+
* create-user request. `null`s (upstream "clear") become "unset" — a
|
|
482
|
+
* fresh user has nothing to clear. `multiFactor` isn't modeled. */
|
|
483
|
+
function toSandboxCreateRequest(props) {
|
|
484
|
+
return {
|
|
485
|
+
uid: props.uid,
|
|
486
|
+
email: props.email,
|
|
487
|
+
password: props.password,
|
|
488
|
+
displayName: props.displayName ?? undefined,
|
|
489
|
+
phoneNumber: props.phoneNumber ?? undefined,
|
|
490
|
+
photoUrl: props.photoURL ?? undefined,
|
|
491
|
+
disabled: props.disabled,
|
|
492
|
+
emailVerified: props.emailVerified,
|
|
493
|
+
};
|
|
494
|
+
}
|
|
495
|
+
/** Convert the worker's `AuthUserRecord` (emulator-REST-shaped, from
|
|
496
|
+
* `pyric/auth`) into a firebase-admin `UserRecord`-shaped object — the
|
|
497
|
+
* same field set the local arm's `toUserRecord` fills. */
|
|
498
|
+
function fromAuthUserRecord(r) {
|
|
499
|
+
const metadata = {
|
|
500
|
+
creationTime: r.createdAt,
|
|
501
|
+
lastSignInTime: r.lastLoginAt ?? '',
|
|
502
|
+
toJSON: () => ({ creationTime: r.createdAt, lastSignInTime: r.lastLoginAt ?? '' }),
|
|
503
|
+
};
|
|
504
|
+
const record = {
|
|
505
|
+
uid: r.uid,
|
|
506
|
+
email: r.email ?? undefined,
|
|
507
|
+
emailVerified: r.emailVerified,
|
|
508
|
+
displayName: r.displayName ?? undefined,
|
|
509
|
+
photoURL: r.photoUrl ?? undefined,
|
|
510
|
+
phoneNumber: r.phoneNumber ?? undefined,
|
|
511
|
+
disabled: r.disabled,
|
|
512
|
+
metadata,
|
|
513
|
+
providerData: r.providerUserInfo.map((p) => ({
|
|
514
|
+
providerId: p.providerId,
|
|
515
|
+
uid: r.uid,
|
|
516
|
+
displayName: r.displayName ?? undefined,
|
|
517
|
+
email: r.email ?? undefined,
|
|
518
|
+
photoURL: r.photoUrl ?? undefined,
|
|
519
|
+
phoneNumber: r.phoneNumber ?? undefined,
|
|
520
|
+
toJSON: () => ({ providerId: p.providerId, uid: r.uid }),
|
|
521
|
+
})),
|
|
522
|
+
customClaims: Object.keys(r.customClaims).length > 0
|
|
523
|
+
? r.customClaims
|
|
524
|
+
: undefined,
|
|
525
|
+
tenantId: null,
|
|
526
|
+
toJSON: () => ({ uid: r.uid, email: r.email ?? undefined }),
|
|
527
|
+
};
|
|
528
|
+
return record;
|
|
529
|
+
}
|
|
530
|
+
/**
|
|
531
|
+
* Build the remote `Auth` handle: the documented CRUD subset relays over
|
|
532
|
+
* the worker channel; tokens are the shared stateless transforms; the
|
|
533
|
+
* rest of the surface throws the canonical "not implemented" error (same
|
|
534
|
+
* cast-at-the-boundary rationale as {@link makeSandboxAuth}).
|
|
535
|
+
*
|
|
536
|
+
* Differences from the local in-memory arm, all deliberate:
|
|
537
|
+
* - `updateUser` and `listUsers` WORK (the worker has the ops; the
|
|
538
|
+
* local arm predates them and still throws).
|
|
539
|
+
* - User mutations emit auth `SandboxEvent`s in the worker (visible to
|
|
540
|
+
* Studio/agents) and are visible to the browser app immediately.
|
|
541
|
+
*/
|
|
542
|
+
function makeRemoteAuth(sandbox) {
|
|
543
|
+
const channel = sandbox.channel;
|
|
544
|
+
const notImplemented = (method) => new Error(`pyric-admin/auth: ${method} is not implemented in pyric-admin/auth remote sandbox backend`);
|
|
545
|
+
const listRecords = async () => (await channel.op({ method: 'auth.listUsers' }));
|
|
546
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
547
|
+
const handle = {
|
|
548
|
+
get app() {
|
|
549
|
+
throw notImplemented('auth.app');
|
|
550
|
+
},
|
|
551
|
+
/** Stateless mint — identical to the local arm; needs no relay. */
|
|
552
|
+
createCustomToken(uid, developerClaims) {
|
|
553
|
+
return mintSandboxCustomToken(uid, developerClaims);
|
|
554
|
+
},
|
|
555
|
+
/** Stateless parse — identical to the local arm; needs no relay. */
|
|
556
|
+
verifyIdToken(idToken, _checkRevoked) {
|
|
557
|
+
return verifySandboxIdToken(idToken);
|
|
558
|
+
},
|
|
559
|
+
/** Relays `auth.adminCreateUser`. Uid conflicts / invalid emails /
|
|
560
|
+
* weak passwords reject with the worker backend's `auth/*` error. */
|
|
561
|
+
async createUser(properties) {
|
|
562
|
+
const record = await channel.op({
|
|
563
|
+
method: 'auth.adminCreateUser',
|
|
564
|
+
request: toSandboxCreateRequest(properties),
|
|
565
|
+
});
|
|
566
|
+
return fromAuthUserRecord(record);
|
|
567
|
+
},
|
|
568
|
+
/** `auth.listUsers` + client-side filter (see module note). */
|
|
569
|
+
async getUser(uid) {
|
|
570
|
+
const record = (await listRecords()).find((u) => u.uid === uid);
|
|
571
|
+
if (!record) {
|
|
572
|
+
throw new Error(`pyric-admin/auth: getUser failed — no user with uid "${uid}"`);
|
|
573
|
+
}
|
|
574
|
+
return fromAuthUserRecord(record);
|
|
575
|
+
},
|
|
576
|
+
/** `auth.listUsers` + client-side filter (see module note). */
|
|
577
|
+
async getUserByEmail(email) {
|
|
578
|
+
const record = (await listRecords()).find((u) => u.email === email);
|
|
579
|
+
if (!record) {
|
|
580
|
+
throw new Error(`pyric-admin/auth: getUserByEmail failed — no user with email "${email}"`);
|
|
581
|
+
}
|
|
582
|
+
return fromAuthUserRecord(record);
|
|
583
|
+
},
|
|
584
|
+
/** Relays `auth.listUsers`. The whole pool fits one page at sandbox
|
|
585
|
+
* scale, so `pageToken` is never set; `maxResults` is honored. */
|
|
586
|
+
async listUsers(maxResults, _pageToken) {
|
|
587
|
+
let records = await listRecords();
|
|
588
|
+
if (maxResults !== undefined)
|
|
589
|
+
records = records.slice(0, maxResults);
|
|
590
|
+
return { users: records.map(fromAuthUserRecord) };
|
|
591
|
+
},
|
|
592
|
+
/**
|
|
593
|
+
* Relays `auth.adminUpdateUser` for the fields the worker models
|
|
594
|
+
* (`displayName` / `email` / `password` / `disabled` /
|
|
595
|
+
* `emailVerified`). Fields it can't express (`photoURL`,
|
|
596
|
+
* `phoneNumber`, `multiFactor`, provider links) throw rather than
|
|
597
|
+
* silently dropping a requested change.
|
|
598
|
+
*/
|
|
599
|
+
async updateUser(uid, properties) {
|
|
600
|
+
const unsupported = ['photoURL', 'phoneNumber', 'multiFactor', 'providerToLink', 'providersToUnlink'].filter((k) => properties[k] !== undefined);
|
|
601
|
+
if (unsupported.length > 0) {
|
|
602
|
+
throw notImplemented(`updateUser({ ${unsupported.join(', ')} })`);
|
|
603
|
+
}
|
|
604
|
+
const request = {
|
|
605
|
+
displayName: properties.displayName,
|
|
606
|
+
email: properties.email,
|
|
607
|
+
password: properties.password,
|
|
608
|
+
disabled: properties.disabled,
|
|
609
|
+
emailVerified: properties.emailVerified,
|
|
610
|
+
};
|
|
611
|
+
const record = await channel.op({
|
|
612
|
+
method: 'auth.adminUpdateUser',
|
|
613
|
+
uid,
|
|
614
|
+
request: request,
|
|
615
|
+
});
|
|
616
|
+
return fromAuthUserRecord(record);
|
|
617
|
+
},
|
|
618
|
+
/** Relays `auth.adminDeleteUser`. A missing uid rejects with the
|
|
619
|
+
* worker backend's `auth/user-not-found` error (matches upstream's
|
|
620
|
+
* strict delete contract, like the local arm). */
|
|
621
|
+
async deleteUser(uid) {
|
|
622
|
+
await channel.op({ method: 'auth.adminDeleteUser', uid });
|
|
623
|
+
},
|
|
624
|
+
/** Relays `auth.adminUpdateUser` with a `customClaims` replacement —
|
|
625
|
+
* `null` clears (the worker's UpdateUserRequest.customClaims
|
|
626
|
+
* replaces the whole map, admin `setCustomUserClaims` semantics). */
|
|
627
|
+
async setCustomUserClaims(uid, customUserClaims) {
|
|
628
|
+
await channel.op({
|
|
629
|
+
method: 'auth.adminUpdateUser',
|
|
630
|
+
uid,
|
|
631
|
+
request: { customClaims: customUserClaims ?? {} },
|
|
632
|
+
});
|
|
633
|
+
},
|
|
634
|
+
// ─── Explicitly-not-implemented surface (parity with local) ──────
|
|
635
|
+
getUserByPhoneNumber() {
|
|
636
|
+
return Promise.reject(notImplemented('getUserByPhoneNumber'));
|
|
637
|
+
},
|
|
638
|
+
getUserByProviderUid() {
|
|
639
|
+
return Promise.reject(notImplemented('getUserByProviderUid'));
|
|
640
|
+
},
|
|
641
|
+
getUsers() {
|
|
642
|
+
return Promise.reject(notImplemented('getUsers'));
|
|
643
|
+
},
|
|
644
|
+
deleteUsers() {
|
|
645
|
+
return Promise.reject(notImplemented('deleteUsers'));
|
|
646
|
+
},
|
|
647
|
+
importUsers() {
|
|
648
|
+
return Promise.reject(notImplemented('importUsers'));
|
|
649
|
+
},
|
|
650
|
+
revokeRefreshTokens() {
|
|
651
|
+
return Promise.reject(notImplemented('revokeRefreshTokens'));
|
|
652
|
+
},
|
|
653
|
+
createSessionCookie() {
|
|
654
|
+
return Promise.reject(notImplemented('createSessionCookie'));
|
|
655
|
+
},
|
|
656
|
+
verifySessionCookie() {
|
|
657
|
+
return Promise.reject(notImplemented('verifySessionCookie'));
|
|
658
|
+
},
|
|
659
|
+
generatePasswordResetLink() {
|
|
660
|
+
return Promise.reject(notImplemented('generatePasswordResetLink'));
|
|
661
|
+
},
|
|
662
|
+
generateEmailVerificationLink() {
|
|
663
|
+
return Promise.reject(notImplemented('generateEmailVerificationLink'));
|
|
664
|
+
},
|
|
665
|
+
generateSignInWithEmailLink() {
|
|
666
|
+
return Promise.reject(notImplemented('generateSignInWithEmailLink'));
|
|
667
|
+
},
|
|
668
|
+
generateVerifyAndChangeEmailLink() {
|
|
669
|
+
return Promise.reject(notImplemented('generateVerifyAndChangeEmailLink'));
|
|
670
|
+
},
|
|
671
|
+
createProviderConfig() {
|
|
672
|
+
return Promise.reject(notImplemented('createProviderConfig'));
|
|
673
|
+
},
|
|
674
|
+
getProviderConfig() {
|
|
675
|
+
return Promise.reject(notImplemented('getProviderConfig'));
|
|
676
|
+
},
|
|
677
|
+
listProviderConfigs() {
|
|
678
|
+
return Promise.reject(notImplemented('listProviderConfigs'));
|
|
679
|
+
},
|
|
680
|
+
updateProviderConfig() {
|
|
681
|
+
return Promise.reject(notImplemented('updateProviderConfig'));
|
|
682
|
+
},
|
|
683
|
+
deleteProviderConfig() {
|
|
684
|
+
return Promise.reject(notImplemented('deleteProviderConfig'));
|
|
685
|
+
},
|
|
686
|
+
get tenantManager() {
|
|
687
|
+
throw notImplemented('tenantManager');
|
|
688
|
+
},
|
|
689
|
+
get projectConfigManager() {
|
|
690
|
+
throw notImplemented('projectConfigManager');
|
|
691
|
+
},
|
|
692
|
+
};
|
|
693
|
+
return handle;
|
|
694
|
+
}
|
|
695
|
+
// ─── Dispatch ────────────────────────────────────────────────────────────
|
|
696
|
+
/**
|
|
697
|
+
* Type guard for the prod arm of {@link PyricAdminApp}. Reads the
|
|
698
|
+
* brand symbol from `pyric-admin/app` — no structural sniffing.
|
|
699
|
+
*/
|
|
700
|
+
function isProdAdminApp(app) {
|
|
701
|
+
return app[ADMIN_APP_TARGET] === 'prod';
|
|
702
|
+
}
|
|
703
|
+
/**
|
|
704
|
+
* Type guard for the sandbox arm of {@link PyricAdminApp}. Brand-based,
|
|
705
|
+
* symmetric with {@link isProdAdminApp}.
|
|
706
|
+
*/
|
|
707
|
+
function isSandboxAdminApp(app) {
|
|
708
|
+
return app[ADMIN_APP_TARGET] === 'sandbox';
|
|
709
|
+
}
|
|
710
|
+
/**
|
|
711
|
+
* Return an `Auth` handle for the given app — or for the DEFAULT app when
|
|
712
|
+
* called with no argument (mirrors firebase-admin's no-arg `getAuth()`:
|
|
713
|
+
* resolves `'[DEFAULT]'` through `pyric-admin/app`'s registry and throws
|
|
714
|
+
* `app/no-app` when nothing has been initialized). Works on all three
|
|
715
|
+
* arms — local sandbox, remote sandbox, prod — since dispatch happens on
|
|
716
|
+
* whatever brand the resolved app carries.
|
|
717
|
+
*
|
|
718
|
+
* Dispatches on the brand symbol set by `pyric-admin/app`'s
|
|
719
|
+
* `initializeApp`:
|
|
720
|
+
* - `'prod'` → delegates to `firebase-admin/auth`'s `getAuth` against
|
|
721
|
+
* `app.adminApp`. The returned `Auth` is the production object,
|
|
722
|
+
* unmodified.
|
|
723
|
+
* - `'sandbox'` → returns an in-memory `Auth` handle backed by an
|
|
724
|
+
* {@link AuthStore} keyed off `app.sandbox`. Repeat calls for the
|
|
725
|
+
* same sandbox share the store, so writes are visible across
|
|
726
|
+
* handles (matches upstream `getAuth(app)` idempotency).
|
|
727
|
+
*
|
|
728
|
+
* The firebase-admin import is dynamic (`require` at call time) so the
|
|
729
|
+
* module's top-level evaluation stays cheap and so sandbox-only
|
|
730
|
+
* consumers do not pay the firebase-admin initialization cost. Mirrors
|
|
731
|
+
* how `pyric/firestore` defers its firebase init: backends are
|
|
732
|
+
* pay-for-what-you-use.
|
|
733
|
+
*
|
|
734
|
+
* @example
|
|
735
|
+
* ```ts
|
|
736
|
+
* import { initializeApp } from 'pyric-admin/app';
|
|
737
|
+
* import { initializeSandbox } from 'pyric/sandbox';
|
|
738
|
+
* import { getAuth } from 'pyric-admin/auth';
|
|
739
|
+
*
|
|
740
|
+
* const sandbox = initializeSandbox();
|
|
741
|
+
* const app = initializeApp({ sandbox });
|
|
742
|
+
* const auth = getAuth(app);
|
|
743
|
+
*
|
|
744
|
+
* const user = await auth.createUser({ uid: 'alice', email: 'a@e.com' });
|
|
745
|
+
* const token = await auth.createCustomToken(user.uid, { role: 'admin' });
|
|
746
|
+
* const decoded = await auth.verifyIdToken(token);
|
|
747
|
+
* console.log(decoded.uid, decoded.role); // 'alice' 'admin'
|
|
748
|
+
* ```
|
|
749
|
+
*/
|
|
750
|
+
export function getAuth(app) {
|
|
751
|
+
if (app === undefined) {
|
|
752
|
+
// No-arg mirror of firebase-admin's `getAuth()` — resolve the
|
|
753
|
+
// '[DEFAULT]' app from the registry (throws app/no-app on a miss).
|
|
754
|
+
app = getApp();
|
|
755
|
+
}
|
|
756
|
+
if (app === null || typeof app !== 'object' || !(ADMIN_APP_TARGET in app)) {
|
|
757
|
+
throw new TypeError('pyric-admin/auth: getAuth expected a PyricAdminApp (from pyric-admin/app#initializeApp). ' +
|
|
758
|
+
'Received a value with no ADMIN_APP_TARGET brand. Pass the handle returned by ' +
|
|
759
|
+
'`initializeApp({ credential })` (prod) or `initializeApp({ sandbox })` (sandbox).');
|
|
760
|
+
}
|
|
761
|
+
if (isSandboxAdminApp(app)) {
|
|
762
|
+
// Remote-branded sandboxes dispatch BEFORE the local arm: the local
|
|
763
|
+
// in-memory store would be a private server-side user table the
|
|
764
|
+
// browser never sees (and its `sandbox.onEvent` reset wiring throws
|
|
765
|
+
// on remote handles by design).
|
|
766
|
+
if (isRemoteSandbox(app.sandbox)) {
|
|
767
|
+
return makeRemoteAuth(app.sandbox);
|
|
768
|
+
}
|
|
769
|
+
return makeSandboxAuth(app.sandbox);
|
|
770
|
+
}
|
|
771
|
+
if (isProdAdminApp(app)) {
|
|
772
|
+
// Prod path — defer the firebase-admin import to call time so the
|
|
773
|
+
// module load is cheap. `require` works in Bun + Node ESM via
|
|
774
|
+
// module-interop and avoids top-level `await` (which would force
|
|
775
|
+
// every consumer to live in an async module graph).
|
|
776
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
777
|
+
const { getAuth: getAdminAuth } = require('firebase-admin/auth');
|
|
778
|
+
return getAdminAuth(app.adminApp);
|
|
779
|
+
}
|
|
780
|
+
// Branded with a value the dispatch table doesn't know — future
|
|
781
|
+
// target string from a newer `pyric-admin/app` we haven't been
|
|
782
|
+
// updated for.
|
|
783
|
+
throw new TypeError(`pyric-admin/auth: getAuth received a PyricAdminApp with an unrecognized ADMIN_APP_TARGET value: ${String(app[ADMIN_APP_TARGET])}`);
|
|
784
|
+
}
|
|
785
|
+
//# sourceMappingURL=index.js.map
|