pyric-admin 0.1.0-alpha.10 → 0.1.0-alpha.12
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.orig +2 -0
- package/dist/firestore/index.d.ts +30 -4
- package/dist/firestore/index.d.ts.map +1 -1
- package/dist/firestore/index.js +42 -14
- package/dist/firestore/index.js.map +1 -1
- package/package.json +7 -2
- package/src/app/index.ts +235 -0
- package/src/app/lifecycle.ts +17 -0
- package/src/auth/auth.test.ts +322 -0
- package/src/auth/index.ts +905 -0
- package/src/database/database.test.ts +196 -0
- package/src/database/index.ts +1187 -0
- package/src/firestore/index.ts +100 -0
- package/src/messaging/index.ts +319 -0
- package/src/storage/index.ts +615 -0
- package/src/storage/storage.test.ts +236 -0
package/README.md.orig
CHANGED
|
@@ -21,6 +21,7 @@ loads `firebase-admin` directly.
|
|
|
21
21
|
| `pyric-admin/auth` | Admin Auth shape |
|
|
22
22
|
| `pyric-admin/database` | Admin Realtime Database shape |
|
|
23
23
|
| `pyric-admin/storage` | Admin Storage shape |
|
|
24
|
+
| `pyric-admin/messaging` | Admin Cloud Messaging send plane |
|
|
24
25
|
|
|
25
26
|
## Explicit sandbox setup
|
|
26
27
|
|
|
@@ -46,3 +47,4 @@ without sandbox activation and therefore resolves Firebase Admin directly.
|
|
|
46
47
|
- [Auth API](https://pyric.dev/docs/pyric-admin-auth-reference-api/)
|
|
47
48
|
- [Realtime Database API](https://pyric.dev/docs/pyric-admin-database-reference-api/)
|
|
48
49
|
- [Storage API](https://pyric.dev/docs/pyric-admin-storage-reference-api/)
|
|
50
|
+
- [Messaging API](https://pyric.dev/docs/pyric-admin-messaging-reference-api/)
|
|
@@ -14,6 +14,24 @@
|
|
|
14
14
|
* default-app resolution and throw the captured `app/no-app` error when
|
|
15
15
|
* nothing is initialized. The original `getFirestore(ctx)` context form
|
|
16
16
|
* (the load-bearing shape the existing suite uses) is preserved verbatim.
|
|
17
|
+
*
|
|
18
|
+
* RULES-BYPASS PARITY (#394): the two APP-resolution forms — `getFirestore(app)`
|
|
19
|
+
* and no-arg `getFirestore()` — mirror `firebase-admin/firestore`'s
|
|
20
|
+
* `getFirestore(app?)`, and REAL firebase-admin bypasses security rules. So both
|
|
21
|
+
* app forms resolve to the rules-BYPASS admin lens (`getAdminFirestore` →
|
|
22
|
+
* `{ mode: 'admin' }`), exactly as `pyric-admin/database`'s `getDatabase(app)`
|
|
23
|
+
* and `pyric-admin/storage`'s `getStorage(app)` already do. Previously the app
|
|
24
|
+
* forms routed through the anon-lens `getFirestore(sandbox.withAuth(null))`,
|
|
25
|
+
* which evaluates `request.auth == null` and is DENIED by any real ruleset — a
|
|
26
|
+
* deny-direction divergence from production that blocked the RTDB-trigger →
|
|
27
|
+
* Firestore-stamp pattern (a Cloud Function's admin write).
|
|
28
|
+
*
|
|
29
|
+
* The `getFirestore(ctx)` CONTEXT form is UNCHANGED and stays rules-ENFORCED
|
|
30
|
+
* (its captured identity is load-bearing for the rules-simulation suite). No
|
|
31
|
+
* page ever reaches this module: a page's `firebase/firestore` resolves to
|
|
32
|
+
* `pyric/firestore` (the rules-enforced client), not `pyric-admin`; only the
|
|
33
|
+
* `firebase-admin/*` → `pyric-admin/*` swap in the trusted functions child
|
|
34
|
+
* imports this. See {@link getFirestore}.
|
|
17
35
|
*/
|
|
18
36
|
export * from 'pyric/sandbox/admin-firestore';
|
|
19
37
|
import { type SandboxFirestore } from 'pyric/sandbox/admin-firestore';
|
|
@@ -22,11 +40,19 @@ import { type PyricAdminApp } from '../app/index.js';
|
|
|
22
40
|
/**
|
|
23
41
|
* Return the admin Firestore handle.
|
|
24
42
|
*
|
|
25
|
-
* - `getFirestore(ctx)` — the original context form (rules-
|
|
43
|
+
* - `getFirestore(ctx)` — the original context form (rules-APPLIED for the
|
|
26
44
|
* ctx's captured identity). Unchanged; idempotent per `SandboxContext`.
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
45
|
+
* This is the pyric-internal rules-simulation shape, not a firebase-admin
|
|
46
|
+
* shape, so it keeps rule evaluation.
|
|
47
|
+
* - `getFirestore(app)` — resolves a {@link PyricAdminApp}'s sandbox to the
|
|
48
|
+
* rules-BYPASS admin lens (firebase-admin parity, #394).
|
|
49
|
+
* - `getFirestore()` — resolves the default app to the rules-BYPASS admin
|
|
50
|
+
* lens; throws `app/no-app` when nothing is initialized.
|
|
51
|
+
*
|
|
52
|
+
* The app forms mirror `firebase-admin/firestore`'s `getFirestore(app?)`,
|
|
53
|
+
* which bypasses security rules — so a Cloud Function's admin write lands the
|
|
54
|
+
* same way it does in production, instead of being denied as `request.auth ==
|
|
55
|
+
* null` by the sandbox's anon lens (the #394 deny-direction divergence).
|
|
30
56
|
*/
|
|
31
57
|
export declare function getFirestore(target?: SandboxContext | PyricAdminApp): SandboxFirestore;
|
|
32
58
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/firestore/index.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/firestore/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AACH,cAAc,+BAA+B,CAAC;AAE9C,OAAO,EAGL,KAAK,gBAAgB,EACtB,MAAM,+BAA+B,CAAC;AACvC,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AACpD,OAAO,EAIL,KAAK,aAAa,EACnB,MAAM,iBAAiB,CAAC;AAsBzB;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,YAAY,CAC1B,MAAM,CAAC,EAAE,cAAc,GAAG,aAAa,GACtC,gBAAgB,CAUlB"}
|
package/dist/firestore/index.js
CHANGED
|
@@ -14,18 +14,38 @@
|
|
|
14
14
|
* default-app resolution and throw the captured `app/no-app` error when
|
|
15
15
|
* nothing is initialized. The original `getFirestore(ctx)` context form
|
|
16
16
|
* (the load-bearing shape the existing suite uses) is preserved verbatim.
|
|
17
|
+
*
|
|
18
|
+
* RULES-BYPASS PARITY (#394): the two APP-resolution forms — `getFirestore(app)`
|
|
19
|
+
* and no-arg `getFirestore()` — mirror `firebase-admin/firestore`'s
|
|
20
|
+
* `getFirestore(app?)`, and REAL firebase-admin bypasses security rules. So both
|
|
21
|
+
* app forms resolve to the rules-BYPASS admin lens (`getAdminFirestore` →
|
|
22
|
+
* `{ mode: 'admin' }`), exactly as `pyric-admin/database`'s `getDatabase(app)`
|
|
23
|
+
* and `pyric-admin/storage`'s `getStorage(app)` already do. Previously the app
|
|
24
|
+
* forms routed through the anon-lens `getFirestore(sandbox.withAuth(null))`,
|
|
25
|
+
* which evaluates `request.auth == null` and is DENIED by any real ruleset — a
|
|
26
|
+
* deny-direction divergence from production that blocked the RTDB-trigger →
|
|
27
|
+
* Firestore-stamp pattern (a Cloud Function's admin write).
|
|
28
|
+
*
|
|
29
|
+
* The `getFirestore(ctx)` CONTEXT form is UNCHANGED and stays rules-ENFORCED
|
|
30
|
+
* (its captured identity is load-bearing for the rules-simulation suite). No
|
|
31
|
+
* page ever reaches this module: a page's `firebase/firestore` resolves to
|
|
32
|
+
* `pyric/firestore` (the rules-enforced client), not `pyric-admin`; only the
|
|
33
|
+
* `firebase-admin/*` → `pyric-admin/*` swap in the trusted functions child
|
|
34
|
+
* imports this. See {@link getFirestore}.
|
|
17
35
|
*/
|
|
18
36
|
export * from 'pyric/sandbox/admin-firestore';
|
|
19
|
-
import { getFirestore as baseGetFirestore, } from 'pyric/sandbox/admin-firestore';
|
|
37
|
+
import { getFirestore as baseGetFirestore, getAdminFirestore as baseGetAdminFirestore, } from 'pyric/sandbox/admin-firestore';
|
|
20
38
|
import { ADMIN_APP_TARGET, getApp, isSandboxAdminApp, } from '../app/index.js';
|
|
21
39
|
import { assertAdminAppActive } from '../app/lifecycle.js';
|
|
22
|
-
/** Narrow a `PyricAdminApp` to the
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
40
|
+
/** Narrow a `PyricAdminApp` to the {@link SandboxContext} the admin
|
|
41
|
+
* firestore backend runs against. Sandbox apps expose their `Sandbox` —
|
|
42
|
+
* LOCAL and REMOTE alike: the base resolvers are remote-aware (they
|
|
43
|
+
* dispatch a remote-branded sandbox to the channel-backed arm), so no
|
|
44
|
+
* guard is needed here. The context's captured auth is IRRELEVANT on the
|
|
45
|
+
* admin path (rules are bypassed, so no rule reads `request.auth`); it is
|
|
46
|
+
* normalised to `withAuth(null)` only to obtain a context. Prod apps
|
|
47
|
+
* require firebase-admin's real Firestore, which the in-process backend
|
|
48
|
+
* does not model. */
|
|
29
49
|
function adminAppToContext(app) {
|
|
30
50
|
if (isSandboxAdminApp(app)) {
|
|
31
51
|
return app.sandbox.withAuth(null);
|
|
@@ -36,20 +56,28 @@ function adminAppToContext(app) {
|
|
|
36
56
|
/**
|
|
37
57
|
* Return the admin Firestore handle.
|
|
38
58
|
*
|
|
39
|
-
* - `getFirestore(ctx)` — the original context form (rules-
|
|
59
|
+
* - `getFirestore(ctx)` — the original context form (rules-APPLIED for the
|
|
40
60
|
* ctx's captured identity). Unchanged; idempotent per `SandboxContext`.
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
61
|
+
* This is the pyric-internal rules-simulation shape, not a firebase-admin
|
|
62
|
+
* shape, so it keeps rule evaluation.
|
|
63
|
+
* - `getFirestore(app)` — resolves a {@link PyricAdminApp}'s sandbox to the
|
|
64
|
+
* rules-BYPASS admin lens (firebase-admin parity, #394).
|
|
65
|
+
* - `getFirestore()` — resolves the default app to the rules-BYPASS admin
|
|
66
|
+
* lens; throws `app/no-app` when nothing is initialized.
|
|
67
|
+
*
|
|
68
|
+
* The app forms mirror `firebase-admin/firestore`'s `getFirestore(app?)`,
|
|
69
|
+
* which bypasses security rules — so a Cloud Function's admin write lands the
|
|
70
|
+
* same way it does in production, instead of being denied as `request.auth ==
|
|
71
|
+
* null` by the sandbox's anon lens (the #394 deny-direction divergence).
|
|
44
72
|
*/
|
|
45
73
|
export function getFirestore(target) {
|
|
46
74
|
if (target === undefined) {
|
|
47
|
-
return
|
|
75
|
+
return baseGetAdminFirestore(adminAppToContext(getApp()));
|
|
48
76
|
}
|
|
49
77
|
if (typeof target === 'object' && target !== null && ADMIN_APP_TARGET in target) {
|
|
50
78
|
const app = target;
|
|
51
79
|
assertAdminAppActive(app);
|
|
52
|
-
return
|
|
80
|
+
return baseGetAdminFirestore(adminAppToContext(app));
|
|
53
81
|
}
|
|
54
82
|
return baseGetFirestore(target);
|
|
55
83
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/firestore/index.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/firestore/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AACH,cAAc,+BAA+B,CAAC;AAE9C,OAAO,EACL,YAAY,IAAI,gBAAgB,EAChC,iBAAiB,IAAI,qBAAqB,GAE3C,MAAM,+BAA+B,CAAC;AAEvC,OAAO,EACL,gBAAgB,EAChB,MAAM,EACN,iBAAiB,GAElB,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAAE,oBAAoB,EAAE,MAAM,qBAAqB,CAAC;AAE3D;;;;;;;;sBAQsB;AACtB,SAAS,iBAAiB,CAAC,GAAkB;IAC3C,IAAI,iBAAiB,CAAC,GAAG,CAAC,EAAE,CAAC;QAC3B,OAAO,GAAG,CAAC,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;IACpC,CAAC;IACD,MAAM,IAAI,KAAK,CACb,wEAAwE;QACtE,sEAAsE,CACzE,CAAC;AACJ,CAAC;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,UAAU,YAAY,CAC1B,MAAuC;IAEvC,IAAI,MAAM,KAAK,SAAS,EAAE,CAAC;QACzB,OAAO,qBAAqB,CAAC,iBAAiB,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC;IAC5D,CAAC;IACD,IAAI,OAAO,MAAM,KAAK,QAAQ,IAAI,MAAM,KAAK,IAAI,IAAI,gBAAgB,IAAI,MAAM,EAAE,CAAC;QAChF,MAAM,GAAG,GAAG,MAAuB,CAAC;QACpC,oBAAoB,CAAC,GAAG,CAAC,CAAC;QAC1B,OAAO,qBAAqB,CAAC,iBAAiB,CAAC,GAAG,CAAC,CAAC,CAAC;IACvD,CAAC;IACD,OAAO,gBAAgB,CAAC,MAAwB,CAAC,CAAC;AACpD,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pyric-admin",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
3
|
+
"version": "0.1.0-alpha.12",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"homepage": "https://pyric.dev",
|
|
6
6
|
"repository": {
|
|
@@ -33,10 +33,15 @@
|
|
|
33
33
|
"./storage": {
|
|
34
34
|
"types": "./dist/storage/index.d.ts",
|
|
35
35
|
"import": "./dist/storage/index.js"
|
|
36
|
+
},
|
|
37
|
+
"./messaging": {
|
|
38
|
+
"types": "./dist/messaging/index.d.ts",
|
|
39
|
+
"import": "./dist/messaging/index.js"
|
|
36
40
|
}
|
|
37
41
|
},
|
|
38
42
|
"files": [
|
|
39
43
|
"dist",
|
|
44
|
+
"src",
|
|
40
45
|
"README.md",
|
|
41
46
|
"LICENSE"
|
|
42
47
|
],
|
|
@@ -46,7 +51,7 @@
|
|
|
46
51
|
"typecheck": "bun x tsc -p tsconfig.json --noEmit"
|
|
47
52
|
},
|
|
48
53
|
"dependencies": {
|
|
49
|
-
"pyric": "^0.1.0-alpha.
|
|
54
|
+
"pyric": "^0.1.0-alpha.12"
|
|
50
55
|
},
|
|
51
56
|
"devDependencies": {
|
|
52
57
|
"@types/bun": "latest",
|
package/src/app/index.ts
ADDED
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `pyric-admin/app` — sandbox-only admin app registry.
|
|
3
|
+
*
|
|
4
|
+
* Production selection happens before this module loads: activated Node
|
|
5
|
+
* processes resolve `firebase-admin/app` here, while inactive applications
|
|
6
|
+
* continue resolving their own `firebase-admin/app` package unchanged.
|
|
7
|
+
*/
|
|
8
|
+
import {
|
|
9
|
+
REMOTE_SANDBOX_FACTORY,
|
|
10
|
+
type RemoteSandboxFactory,
|
|
11
|
+
type RemoteSandboxFactoryOptions,
|
|
12
|
+
type Sandbox,
|
|
13
|
+
} from 'pyric/sandbox';
|
|
14
|
+
import { assertAdminAppActive, markAdminAppDeleted } from './lifecycle.js';
|
|
15
|
+
|
|
16
|
+
/** Brand carried by every sandbox admin app. */
|
|
17
|
+
export const ADMIN_APP_TARGET = Symbol.for('pyric.admin.app.target');
|
|
18
|
+
export type PyricAdminAppTarget = 'sandbox';
|
|
19
|
+
|
|
20
|
+
/** firebase-admin's default app name. */
|
|
21
|
+
export const DEFAULT_APP_NAME = '[DEFAULT]';
|
|
22
|
+
|
|
23
|
+
export interface SandboxAdminApp {
|
|
24
|
+
readonly [ADMIN_APP_TARGET]: 'sandbox';
|
|
25
|
+
readonly sandbox: Sandbox;
|
|
26
|
+
readonly name: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export type PyricAdminApp = SandboxAdminApp;
|
|
30
|
+
export type InitializeAdminAppConfig = { sandbox: Sandbox };
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Firebase Functions' ESM runtime statically imports this credential factory
|
|
34
|
+
* while linking its database provider. Pyric initializes the sandbox app
|
|
35
|
+
* before that provider executes, so the factory is not used by supported
|
|
36
|
+
* Functions flows. Keep the named export link-compatible, but fail clearly if
|
|
37
|
+
* application code asks the development sandbox for production credentials.
|
|
38
|
+
*/
|
|
39
|
+
export function applicationDefault(): never {
|
|
40
|
+
throw new Error(
|
|
41
|
+
'pyric-admin/app: applicationDefault() is unavailable in the sandbox. ' +
|
|
42
|
+
'Pyric development does not use production credentials.',
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Local error with the observable firebase-admin app-error shape. */
|
|
47
|
+
class FirebaseAppError extends Error {
|
|
48
|
+
readonly code: string;
|
|
49
|
+
|
|
50
|
+
constructor(code: string, message: string) {
|
|
51
|
+
super(message);
|
|
52
|
+
this.name = 'FirebaseAppError';
|
|
53
|
+
this.code = `app/${code}`;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Node may evaluate this ESM-only mirror through distinct require(esm) and
|
|
58
|
+
// import module records when the register hook rewrites both CJS and ESM
|
|
59
|
+
// Firebase consumers. Firebase Admin's app registry is process-wide, so keep
|
|
60
|
+
// the mirror registry behind Symbol.for as well: both module records must see
|
|
61
|
+
// the same default app and sandbox handle.
|
|
62
|
+
const APP_REGISTRY = Symbol.for('pyric.admin.app.registry');
|
|
63
|
+
const AMBIENT_APPS = Symbol.for('pyric.admin.app.ambientApps');
|
|
64
|
+
interface GlobalAppRegistry {
|
|
65
|
+
[APP_REGISTRY]?: Map<string, PyricAdminApp>;
|
|
66
|
+
[AMBIENT_APPS]?: WeakSet<PyricAdminApp>;
|
|
67
|
+
}
|
|
68
|
+
const globalRegistry = globalThis as GlobalAppRegistry;
|
|
69
|
+
const appRegistry = globalRegistry[APP_REGISTRY] ??= new Map<string, PyricAdminApp>();
|
|
70
|
+
const ambientApps = globalRegistry[AMBIENT_APPS] ??= new WeakSet<PyricAdminApp>();
|
|
71
|
+
|
|
72
|
+
function validateAppName(name: unknown): asserts name is string {
|
|
73
|
+
if (typeof name !== 'string' || name === '') {
|
|
74
|
+
throw new FirebaseAppError(
|
|
75
|
+
'invalid-app-name',
|
|
76
|
+
`Invalid Firebase app name "${String(name)}" provided. App name must be a non-empty string.`,
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function alreadyExists(name: string, code: 'duplicate-app' | 'invalid-app-options'): FirebaseAppError {
|
|
82
|
+
return new FirebaseAppError(
|
|
83
|
+
code,
|
|
84
|
+
`A Firebase app named "${name}" already exists with a different configuration.`,
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Initialize a sandbox admin app.
|
|
90
|
+
*
|
|
91
|
+
* An explicit `{ sandbox }` config binds an in-process or remote sandbox.
|
|
92
|
+
* A bare call resolves the remote sandbox factory installed by
|
|
93
|
+
* `@pyric/cli/register`. Production callers must import `firebase-admin/app`
|
|
94
|
+
* without Pyric activation instead of passing production options here.
|
|
95
|
+
*/
|
|
96
|
+
export function initializeApp(
|
|
97
|
+
config?: InitializeAdminAppConfig,
|
|
98
|
+
name: string = DEFAULT_APP_NAME,
|
|
99
|
+
): PyricAdminApp {
|
|
100
|
+
validateAppName(name);
|
|
101
|
+
const existing = appRegistry.get(name);
|
|
102
|
+
|
|
103
|
+
if (config === undefined) {
|
|
104
|
+
if (existing !== undefined) {
|
|
105
|
+
if (ambientApps.has(existing)) return existing;
|
|
106
|
+
throw alreadyExists(name, 'invalid-app-options');
|
|
107
|
+
}
|
|
108
|
+
const app = initializeAmbientApp(name);
|
|
109
|
+
appRegistry.set(name, app);
|
|
110
|
+
ambientApps.add(app);
|
|
111
|
+
return app;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (!isSandboxConfig(config)) {
|
|
115
|
+
throw new TypeError(
|
|
116
|
+
'pyric-admin/app is a sandbox-only mirror. Production applications must ' +
|
|
117
|
+
'load firebase-admin/app without Pyric activation.',
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (existing !== undefined) {
|
|
122
|
+
if (!ambientApps.has(existing) && existing.sandbox === config.sandbox) return existing;
|
|
123
|
+
throw alreadyExists(
|
|
124
|
+
name,
|
|
125
|
+
ambientApps.has(existing) ? 'invalid-app-options' : 'duplicate-app',
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const app: SandboxAdminApp = {
|
|
130
|
+
[ADMIN_APP_TARGET]: 'sandbox',
|
|
131
|
+
sandbox: config.sandbox,
|
|
132
|
+
name,
|
|
133
|
+
};
|
|
134
|
+
appRegistry.set(name, app);
|
|
135
|
+
return app;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** Return the registered app for `name`. */
|
|
139
|
+
export function getApp(name: string = DEFAULT_APP_NAME): PyricAdminApp {
|
|
140
|
+
validateAppName(name);
|
|
141
|
+
const app = appRegistry.get(name);
|
|
142
|
+
if (app === undefined) {
|
|
143
|
+
const lead = name === DEFAULT_APP_NAME
|
|
144
|
+
? 'The default Firebase app does not exist. '
|
|
145
|
+
: `Firebase app named "${name}" does not exist. `;
|
|
146
|
+
throw new FirebaseAppError(
|
|
147
|
+
'no-app',
|
|
148
|
+
lead + 'Make sure you call initializeApp() before using any of the Firebase services.',
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
return app;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** Return a copy of the app registry. */
|
|
155
|
+
export function getApps(): PyricAdminApp[] {
|
|
156
|
+
return Array.from(appRegistry.values());
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** Remove a sandbox app from the registry. */
|
|
160
|
+
export function deleteApp(app: PyricAdminApp): Promise<void> {
|
|
161
|
+
if (typeof app !== 'object' || app === null || !(ADMIN_APP_TARGET in app)) {
|
|
162
|
+
throw new FirebaseAppError('invalid-argument', 'Invalid app argument.');
|
|
163
|
+
}
|
|
164
|
+
assertAdminAppActive(app);
|
|
165
|
+
const existing = getApp(app.name);
|
|
166
|
+
appRegistry.delete(existing.name);
|
|
167
|
+
markAdminAppDeleted(existing);
|
|
168
|
+
return Promise.resolve();
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function initializeAmbientApp(name: string): PyricAdminApp {
|
|
172
|
+
const env = process.env.PYRIC_SANDBOX;
|
|
173
|
+
if (env === undefined || env.trim() === '') {
|
|
174
|
+
throw new Error(
|
|
175
|
+
'pyric-admin/app is a sandbox-only mirror and no sandbox is active. ' +
|
|
176
|
+
'Run under `pyric dev`, set PYRIC_SANDBOX with @pyric/cli/register, ' +
|
|
177
|
+
'or load firebase-admin/app without Pyric activation for production.',
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const opts = parsePyricSandboxEnv(env);
|
|
182
|
+
if (process.env.NODE_ENV === 'production' && process.env.PYRIC_SANDBOX_FORCE !== '1') {
|
|
183
|
+
throw new Error(
|
|
184
|
+
'pyric-admin: PYRIC_SANDBOX is set but NODE_ENV is "production" — ' +
|
|
185
|
+
'refusing to route firebase-admin to a development sandbox. ' +
|
|
186
|
+
'Unset PYRIC_SANDBOX in production, or set PYRIC_SANDBOX_FORCE=1 ' +
|
|
187
|
+
'if this routing is intentional.',
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const factory = (globalThis as { [REMOTE_SANDBOX_FACTORY]?: RemoteSandboxFactory })[
|
|
192
|
+
REMOTE_SANDBOX_FACTORY
|
|
193
|
+
];
|
|
194
|
+
if (typeof factory !== 'function') {
|
|
195
|
+
throw new Error(
|
|
196
|
+
`pyric-admin: PYRIC_SANDBOX=${env} is set but no remote sandbox ` +
|
|
197
|
+
"factory is installed (globalThis[Symbol.for('pyric.remote.sandboxFactory')] is absent). " +
|
|
198
|
+
'Run your server under `pyric dev`, or add `--import @pyric/cli/register` to NODE_OPTIONS.',
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const sandbox = factory(opts);
|
|
203
|
+
process.stderr.write(
|
|
204
|
+
`pyric: firebase-admin routed to sandbox${opts.url !== undefined ? ` at ${opts.url}` : ''}\n`,
|
|
205
|
+
);
|
|
206
|
+
return { [ADMIN_APP_TARGET]: 'sandbox', sandbox, name };
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function parsePyricSandboxEnv(env: string): RemoteSandboxFactoryOptions {
|
|
210
|
+
const value = env.trim();
|
|
211
|
+
if (value === 'remote') return {};
|
|
212
|
+
if (value.startsWith('remote:')) {
|
|
213
|
+
const url = value.slice('remote:'.length).trim();
|
|
214
|
+
if (url === '') {
|
|
215
|
+
throw new Error(
|
|
216
|
+
'pyric-admin: PYRIC_SANDBOX=remote: has an empty url. Use ' +
|
|
217
|
+
'PYRIC_SANDBOX=remote to auto-discover the running `pyric dev`, ' +
|
|
218
|
+
'or PYRIC_SANDBOX=remote:<url> with the host url.',
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
return { url };
|
|
222
|
+
}
|
|
223
|
+
throw new Error(
|
|
224
|
+
`pyric-admin: unrecognized PYRIC_SANDBOX value "${env}". Supported ` +
|
|
225
|
+
'values: "remote" (auto-discover the running `pyric dev`) or "remote:<url>".',
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function isSandboxConfig(config: InitializeAdminAppConfig): config is { sandbox: Sandbox } {
|
|
230
|
+
return typeof config === 'object' && config !== null && 'sandbox' in config;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
export function isSandboxAdminApp(app: PyricAdminApp): app is SandboxAdminApp {
|
|
234
|
+
return app[ADMIN_APP_TARGET] === 'sandbox';
|
|
235
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
const deletedApps = new WeakSet<object>();
|
|
2
|
+
|
|
3
|
+
/** Internal lifecycle guard shared by every pyric-admin service factory. */
|
|
4
|
+
export function assertAdminAppActive(app: object & { readonly name: string }): void {
|
|
5
|
+
if (!deletedApps.has(app)) return;
|
|
6
|
+
const error = new Error(
|
|
7
|
+
`Firebase app named "${app.name}" has already been deleted.`,
|
|
8
|
+
) as Error & { code: string };
|
|
9
|
+
error.name = 'FirebaseAppError';
|
|
10
|
+
error.code = 'app/app-deleted';
|
|
11
|
+
throw error;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** Tombstone a wrapper while leaving the caller-owned Sandbox alive. */
|
|
15
|
+
export function markAdminAppDeleted(app: object): void {
|
|
16
|
+
deletedApps.add(app);
|
|
17
|
+
}
|