pyric-admin 0.1.0-alpha.30 → 0.1.0-alpha.30.1
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/dist/app/index.d.ts +5 -0
- package/dist/app/index.d.ts.map +1 -1
- package/dist/app/index.js +11 -1
- package/dist/app/index.js.map +1 -1
- package/dist/app/project-id.d.ts +11 -0
- package/dist/app/project-id.d.ts.map +1 -0
- package/dist/app/project-id.js +62 -0
- package/dist/app/project-id.js.map +1 -0
- package/dist/app-check/error.d.ts +7 -0
- package/dist/app-check/error.d.ts.map +1 -0
- package/dist/app-check/error.js +8 -0
- package/dist/app-check/error.js.map +1 -0
- package/dist/app-check/firebase-token.d.ts +7 -0
- package/dist/app-check/firebase-token.d.ts.map +1 -0
- package/dist/app-check/firebase-token.js +53 -0
- package/dist/app-check/firebase-token.js.map +1 -0
- package/dist/app-check/index.d.ts +5 -0
- package/dist/app-check/index.d.ts.map +1 -1
- package/dist/app-check/index.js +32 -22
- package/dist/app-check/index.js.map +1 -1
- package/package.json +2 -2
- package/src/app/index.ts +11 -1
- package/src/app/project-id.ts +67 -0
- package/src/app-check/error.ts +10 -0
- package/src/app-check/firebase-token.ts +60 -0
- package/src/app-check/index.ts +35 -25
package/dist/app/index.d.ts
CHANGED
|
@@ -40,6 +40,11 @@ export declare function applicationDefault(): never;
|
|
|
40
40
|
* Link-compatible mirror of `firebase-admin/app`'s `cert(serviceAccountPathOrObject)`.
|
|
41
41
|
* Returns an inert sandbox credential token so unchanged application setup code
|
|
42
42
|
* can assign `appOptions.credential = cert(...)` under `@pyric/cli/register`.
|
|
43
|
+
*
|
|
44
|
+
* It keeps one field, the service account's project id, because firebase-admin
|
|
45
|
+
* finds an app's project there when the options name none. A path is read as a
|
|
46
|
+
* JSON file; a file that is missing, unreadable, or has no project id gives a
|
|
47
|
+
* credential without one, and never throws, so apps run without a key on disk.
|
|
43
48
|
*/
|
|
44
49
|
export declare function cert(serviceAccountPathOrObject: unknown): object;
|
|
45
50
|
/**
|
package/dist/app/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/app/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,EAIL,KAAK,OAAO,EACb,MAAM,eAAe,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/app/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,EAIL,KAAK,OAAO,EACb,MAAM,eAAe,CAAC;AAOvB,gDAAgD;AAChD,eAAO,MAAM,gBAAgB,eAAuC,CAAC;AACrE,MAAM,MAAM,mBAAmB,GAAG,SAAS,CAAC;AAE5C,yCAAyC;AACzC,eAAO,MAAM,gBAAgB,cAAc,CAAC;AAE5C,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,CAAC,gBAAgB,CAAC,EAAE,SAAS,CAAC;IACvC,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,8FAA8F;IAC9F,QAAQ,CAAC,OAAO,EAAE,eAAe,CAAC;CACnC;AAED,sLAAsL;AACtL,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,CAAC;IAChC,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;CAC7B;AAED,MAAM,MAAM,aAAa,GAAG,eAAe,CAAC;AAC5C,MAAM,MAAM,wBAAwB,GAAG;IAAE,OAAO,EAAE,OAAO,CAAA;CAAE,GAAG,eAAe,CAAC;AAE9E;;;;;;GAMG;AACH,wBAAgB,kBAAkB,IAAI,KAAK,CAK1C;AAED;;;;;;;;;GASG;AACH,wBAAgB,IAAI,CAAC,0BAA0B,EAAE,OAAO,GAAG,MAAM,CAIhE;AAED;;;GAGG;AACH,wBAAgB,YAAY,CAAC,wBAAwB,EAAE,OAAO,GAAG,MAAM,CAEtE;AA4CD;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAC3B,MAAM,CAAC,EAAE,wBAAwB,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC3D,IAAI,GAAE,MAAyB,GAC9B,aAAa,CAqCf;AAED,4CAA4C;AAC5C,wBAAgB,MAAM,CAAC,IAAI,GAAE,MAAyB,GAAG,aAAa,CAarE;AAED,yCAAyC;AACzC,wBAAgB,OAAO,IAAI,aAAa,EAAE,CAEzC;AAED,8CAA8C;AAC9C,wBAAgB,SAAS,CAAC,GAAG,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC,CAS3D;AAwED,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,aAAa,GAAG,GAAG,IAAI,eAAe,CAE5E"}
|
package/dist/app/index.js
CHANGED
|
@@ -9,6 +9,7 @@ import { REMOTE_SANDBOX_FACTORY, } from 'pyric/sandbox';
|
|
|
9
9
|
import { parsePyricSandbox, processActivation } from 'pyric/sandbox/internal';
|
|
10
10
|
import { readFileSync } from 'node:fs';
|
|
11
11
|
import { assertAdminAppActive, markAdminAppDeleted } from './lifecycle.js';
|
|
12
|
+
import { keepAppCredentialProjectId, keepCredentialProjectId } from './project-id.js';
|
|
12
13
|
/** Brand carried by every sandbox admin app. */
|
|
13
14
|
export const ADMIN_APP_TARGET = Symbol.for('pyric.admin.app.target');
|
|
14
15
|
/** firebase-admin's default app name. */
|
|
@@ -28,9 +29,16 @@ export function applicationDefault() {
|
|
|
28
29
|
* Link-compatible mirror of `firebase-admin/app`'s `cert(serviceAccountPathOrObject)`.
|
|
29
30
|
* Returns an inert sandbox credential token so unchanged application setup code
|
|
30
31
|
* can assign `appOptions.credential = cert(...)` under `@pyric/cli/register`.
|
|
32
|
+
*
|
|
33
|
+
* It keeps one field, the service account's project id, because firebase-admin
|
|
34
|
+
* finds an app's project there when the options name none. A path is read as a
|
|
35
|
+
* JSON file; a file that is missing, unreadable, or has no project id gives a
|
|
36
|
+
* credential without one, and never throws, so apps run without a key on disk.
|
|
31
37
|
*/
|
|
32
38
|
export function cert(serviceAccountPathOrObject) {
|
|
33
|
-
|
|
39
|
+
const credential = { [Symbol.for('pyric.admin.credential')]: 'cert' };
|
|
40
|
+
keepCredentialProjectId(credential, serviceAccountPathOrObject);
|
|
41
|
+
return credential;
|
|
34
42
|
}
|
|
35
43
|
/**
|
|
36
44
|
* Link-compatible mirror of `firebase-admin/app`'s `refreshToken(refreshTokenPathOrObject)`.
|
|
@@ -87,6 +95,7 @@ export function initializeApp(config, name = DEFAULT_APP_NAME) {
|
|
|
87
95
|
throw alreadyExists(name, 'invalid-app-options');
|
|
88
96
|
}
|
|
89
97
|
const app = initializeAmbientApp(name, ambientAppOptions(config));
|
|
98
|
+
keepAppCredentialProjectId(app, config);
|
|
90
99
|
appRegistry.set(name, app);
|
|
91
100
|
ambientApps.add(app);
|
|
92
101
|
return app;
|
|
@@ -102,6 +111,7 @@ export function initializeApp(config, name = DEFAULT_APP_NAME) {
|
|
|
102
111
|
name,
|
|
103
112
|
options: appOptionsFrom(config),
|
|
104
113
|
};
|
|
114
|
+
keepAppCredentialProjectId(app, config);
|
|
105
115
|
appRegistry.set(name, app);
|
|
106
116
|
return app;
|
|
107
117
|
}
|
package/dist/app/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/app/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,EACL,sBAAsB,GAIvB,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAC;AAC9E,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAEvC,OAAO,EAAE,oBAAoB,EAAE,mBAAmB,EAAE,MAAM,gBAAgB,CAAC;
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/app/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/** Keeps the service account's project id on an inert credential, as a non-enumerable property. */
|
|
2
|
+
export declare function keepCredentialProjectId(credential: object, serviceAccountPathOrObject: unknown): void;
|
|
3
|
+
/** Copies a cert credential's project id from initialization options onto the app, beside `options`. */
|
|
4
|
+
export declare function keepAppCredentialProjectId(app: object, config: unknown): void;
|
|
5
|
+
/** firebase-admin's `getExplicitProjectId` for a sandbox admin app. */
|
|
6
|
+
export declare function explicitProjectId(app: {
|
|
7
|
+
readonly options: {
|
|
8
|
+
readonly projectId?: string;
|
|
9
|
+
};
|
|
10
|
+
}): string | undefined;
|
|
11
|
+
//# sourceMappingURL=project-id.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"project-id.d.ts","sourceRoot":"","sources":["../../src/app/project-id.ts"],"names":[],"mappings":"AA4CA,mGAAmG;AACnG,wBAAgB,uBAAuB,CAAC,UAAU,EAAE,MAAM,EAAE,0BAA0B,EAAE,OAAO,GAAG,IAAI,CAGrG;AAED,wGAAwG;AACxG,wBAAgB,0BAA0B,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,GAAG,IAAI,CAM7E;AAED,uEAAuE;AACvE,wBAAgB,iBAAiB,CAAC,GAAG,EAAE;IAAE,QAAQ,CAAC,OAAO,EAAE;QAAE,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAA;KAAE,CAAA;CAAE,GAAG,MAAM,GAAG,SAAS,CAMhH"}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The project id an admin app resolves, in firebase-admin's order
|
|
3
|
+
* (`getExplicitProjectId`, firebase-admin 13 `lib/utils/index.js`): the
|
|
4
|
+
* `projectId` app option, then a cert credential's project id, then
|
|
5
|
+
* `GOOGLE_CLOUD_PROJECT`, then `GCLOUD_PROJECT`.
|
|
6
|
+
*
|
|
7
|
+
* Pyric's `cert()` is inert: it keeps the service account's project id and
|
|
8
|
+
* nothing else, so no private key or client email is held in the sandbox.
|
|
9
|
+
* Both keys sit behind `Symbol.for`, as the app registry does, so the require
|
|
10
|
+
* and import copies of this module that the register hook can load agree.
|
|
11
|
+
*/
|
|
12
|
+
import { readFileSync } from 'node:fs';
|
|
13
|
+
const CREDENTIAL_PROJECT_ID = Symbol.for('pyric.admin.credential.projectId');
|
|
14
|
+
const APP_CREDENTIAL_PROJECT_ID = Symbol.for('pyric.admin.app.credentialProjectId');
|
|
15
|
+
function nonEmptyString(value) {
|
|
16
|
+
return typeof value === 'string' && value !== '' ? value : undefined;
|
|
17
|
+
}
|
|
18
|
+
/** firebase-admin's `ServiceAccount.fromPath`, except that a file it cannot read or parse yields no object. */
|
|
19
|
+
function readServiceAccountFile(path) {
|
|
20
|
+
try {
|
|
21
|
+
return JSON.parse(readFileSync(path, 'utf8'));
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return undefined;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
/** The project id of a service account object or file path: `projectId`, then `project_id`, as firebase-admin's `copyAttr` reads it. */
|
|
28
|
+
function serviceAccountProjectId(serviceAccountPathOrObject) {
|
|
29
|
+
const isPath = typeof serviceAccountPathOrObject === 'string';
|
|
30
|
+
const account = isPath ? readServiceAccountFile(serviceAccountPathOrObject) : serviceAccountPathOrObject;
|
|
31
|
+
if (typeof account !== 'object' || account === null)
|
|
32
|
+
return undefined;
|
|
33
|
+
const { projectId, project_id: projectIdSnake } = account;
|
|
34
|
+
return nonEmptyString(projectId) ?? nonEmptyString(projectIdSnake);
|
|
35
|
+
}
|
|
36
|
+
function defineHidden(target, key, value) {
|
|
37
|
+
Object.defineProperty(target, key, { value, enumerable: false, writable: false, configurable: false });
|
|
38
|
+
}
|
|
39
|
+
/** Keeps the service account's project id on an inert credential, as a non-enumerable property. */
|
|
40
|
+
export function keepCredentialProjectId(credential, serviceAccountPathOrObject) {
|
|
41
|
+
const projectId = serviceAccountProjectId(serviceAccountPathOrObject);
|
|
42
|
+
if (projectId !== undefined)
|
|
43
|
+
defineHidden(credential, CREDENTIAL_PROJECT_ID, projectId);
|
|
44
|
+
}
|
|
45
|
+
/** Copies a cert credential's project id from initialization options onto the app, beside `options`. */
|
|
46
|
+
export function keepAppCredentialProjectId(app, config) {
|
|
47
|
+
if (typeof config !== 'object' || config === null)
|
|
48
|
+
return;
|
|
49
|
+
const credential = config.credential;
|
|
50
|
+
if (typeof credential !== 'object' || credential === null)
|
|
51
|
+
return;
|
|
52
|
+
const projectId = nonEmptyString(credential[CREDENTIAL_PROJECT_ID]);
|
|
53
|
+
if (projectId !== undefined)
|
|
54
|
+
defineHidden(app, APP_CREDENTIAL_PROJECT_ID, projectId);
|
|
55
|
+
}
|
|
56
|
+
/** firebase-admin's `getExplicitProjectId` for a sandbox admin app. */
|
|
57
|
+
export function explicitProjectId(app) {
|
|
58
|
+
return (nonEmptyString(app.options.projectId) ??
|
|
59
|
+
nonEmptyString(app[APP_CREDENTIAL_PROJECT_ID]) ??
|
|
60
|
+
nonEmptyString(process.env.GOOGLE_CLOUD_PROJECT || process.env.GCLOUD_PROJECT));
|
|
61
|
+
}
|
|
62
|
+
//# sourceMappingURL=project-id.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"project-id.js","sourceRoot":"","sources":["../../src/app/project-id.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAEvC,MAAM,qBAAqB,GAAG,MAAM,CAAC,GAAG,CAAC,kCAAkC,CAAC,CAAC;AAC7E,MAAM,yBAAyB,GAAG,MAAM,CAAC,GAAG,CAAC,qCAAqC,CAAC,CAAC;AAIpF,SAAS,cAAc,CAAC,KAAc;IACpC,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,SAAS,CAAC;AACvE,CAAC;AAED,+GAA+G;AAC/G,SAAS,sBAAsB,CAAC,IAAY;IAC1C,IAAI,CAAC;QACH,OAAO,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,CAAC;IAChD,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,SAAS,CAAC;IACnB,CAAC;AACH,CAAC;AAED,wIAAwI;AACxI,SAAS,uBAAuB,CAAC,0BAAmC;IAClE,MAAM,MAAM,GAAG,OAAO,0BAA0B,KAAK,QAAQ,CAAC;IAC9D,MAAM,OAAO,GAAG,MAAM,CAAC,CAAC,CAAC,sBAAsB,CAAC,0BAA0B,CAAC,CAAC,CAAC,CAAC,0BAA0B,CAAC;IACzG,IAAI,OAAO,OAAO,KAAK,QAAQ,IAAI,OAAO,KAAK,IAAI;QAAE,OAAO,SAAS,CAAC;IACtE,MAAM,EAAE,SAAS,EAAE,UAAU,EAAE,cAAc,EAAE,GAAG,OAAkC,CAAC;IACrF,OAAO,cAAc,CAAC,SAAS,CAAC,IAAI,cAAc,CAAC,cAAc,CAAC,CAAC;AACrE,CAAC;AAED,SAAS,YAAY,CAAC,MAAc,EAAE,GAAW,EAAE,KAAa;IAC9D,MAAM,CAAC,cAAc,CAAC,MAAM,EAAE,GAAG,EAAE,EAAE,KAAK,EAAE,UAAU,EAAE,KAAK,EAAE,QAAQ,EAAE,KAAK,EAAE,YAAY,EAAE,KAAK,EAAE,CAAC,CAAC;AACzG,CAAC;AAED,mGAAmG;AACnG,MAAM,UAAU,uBAAuB,CAAC,UAAkB,EAAE,0BAAmC;IAC7F,MAAM,SAAS,GAAG,uBAAuB,CAAC,0BAA0B,CAAC,CAAC;IACtE,IAAI,SAAS,KAAK,SAAS;QAAE,YAAY,CAAC,UAAU,EAAE,qBAAqB,EAAE,SAAS,CAAC,CAAC;AAC1F,CAAC;AAED,wGAAwG;AACxG,MAAM,UAAU,0BAA0B,CAAC,GAAW,EAAE,MAAe;IACrE,IAAI,OAAO,MAAM,KAAK,QAAQ,IAAI,MAAM,KAAK,IAAI;QAAE,OAAO;IAC1D,MAAM,UAAU,GAAI,MAAmC,CAAC,UAAU,CAAC;IACnE,IAAI,OAAO,UAAU,KAAK,QAAQ,IAAI,UAAU,KAAK,IAAI;QAAE,OAAO;IAClE,MAAM,SAAS,GAAG,cAAc,CAAE,UAA4B,CAAC,qBAAqB,CAAC,CAAC,CAAC;IACvF,IAAI,SAAS,KAAK,SAAS;QAAE,YAAY,CAAC,GAAG,EAAE,yBAAyB,EAAE,SAAS,CAAC,CAAC;AACvF,CAAC;AAED,uEAAuE;AACvE,MAAM,UAAU,iBAAiB,CAAC,GAA0D;IAC1F,OAAO,CACL,cAAc,CAAC,GAAG,CAAC,OAAO,CAAC,SAAS,CAAC;QACrC,cAAc,CAAE,GAAqB,CAAC,yBAAyB,CAAC,CAAC;QACjE,cAAc,CAAC,OAAO,CAAC,GAAG,CAAC,oBAAoB,IAAI,OAAO,CAAC,GAAG,CAAC,cAAc,CAAC,CAC/E,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { PrefixedFirebaseError } from '../app/firebase-error.js';
|
|
2
|
+
export type AppCheckErrorCode = 'invalid-argument' | 'invalid-credential' | 'app-check-token-expired' | 'unknown-error';
|
|
3
|
+
/** firebase-admin's FirebaseAppCheckError: a PrefixedFirebaseError with prefix `app-check`. */
|
|
4
|
+
export declare class FirebaseAppCheckError extends PrefixedFirebaseError {
|
|
5
|
+
constructor(code: AppCheckErrorCode, message: string);
|
|
6
|
+
}
|
|
7
|
+
//# sourceMappingURL=error.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"error.d.ts","sourceRoot":"","sources":["../../src/app-check/error.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,qBAAqB,EAAE,MAAM,0BAA0B,CAAC;AAEjE,MAAM,MAAM,iBAAiB,GAAG,kBAAkB,GAAG,oBAAoB,GAAG,yBAAyB,GAAG,eAAe,CAAC;AAExH,+FAA+F;AAC/F,qBAAa,qBAAsB,SAAQ,qBAAqB;gBAClD,IAAI,EAAE,iBAAiB,EAAE,OAAO,EAAE,MAAM;CAGrD"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { PrefixedFirebaseError } from '../app/firebase-error.js';
|
|
2
|
+
/** firebase-admin's FirebaseAppCheckError: a PrefixedFirebaseError with prefix `app-check`. */
|
|
3
|
+
export class FirebaseAppCheckError extends PrefixedFirebaseError {
|
|
4
|
+
constructor(code, message) {
|
|
5
|
+
super('app-check', code, message);
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
//# sourceMappingURL=error.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"error.js","sourceRoot":"","sources":["../../src/app-check/error.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,qBAAqB,EAAE,MAAM,0BAA0B,CAAC;AAIjE,+FAA+F;AAC/F,MAAM,OAAO,qBAAsB,SAAQ,qBAAqB;IAC9D,YAAY,IAAuB,EAAE,OAAe;QAClD,KAAK,CAAC,WAAW,EAAE,IAAI,EAAE,OAAO,CAAC,CAAC;IACpC,CAAC;CACF"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { VerifyAppCheckTokenResponse } from './index.js';
|
|
2
|
+
/**
|
|
3
|
+
* Verifies a token from Firebase with firebase-admin, through the verifier the
|
|
4
|
+
* register installs. Its result and its errors come back unchanged.
|
|
5
|
+
*/
|
|
6
|
+
export declare function verifyFirebaseToken(token: string, projectId: string): Promise<VerifyAppCheckTokenResponse>;
|
|
7
|
+
//# sourceMappingURL=firebase-token.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"firebase-token.d.ts","sourceRoot":"","sources":["../../src/app-check/firebase-token.ts"],"names":[],"mappings":"AAgBA,OAAO,KAAK,EAAE,2BAA2B,EAAE,MAAM,YAAY,CAAC;AA8B9D;;;GAGG;AACH,wBAAsB,mBAAmB,CAAC,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,2BAA2B,CAAC,CAShH"}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* App Check tokens from Firebase: a token whose `kid` names a key that is not
|
|
3
|
+
* the sandbox's (#1552, #1301).
|
|
4
|
+
*
|
|
5
|
+
* In production AI mode the page's `firebase/app-check` is the Firebase SDK,
|
|
6
|
+
* so its tokens come from Firebase, and only firebase-admin's own verifier can
|
|
7
|
+
* check them. `@pyric/cli/register` installs that verifier behind
|
|
8
|
+
* {@link FIREBASE_VERIFIER} in production AI mode only, the same kind of seam
|
|
9
|
+
* as its `pyric.remote.sandboxFactory`: this package never learns what an AI
|
|
10
|
+
* mode is, and never loads firebase-admin itself.
|
|
11
|
+
*
|
|
12
|
+
* With no verifier installed the token is refused with a message that says
|
|
13
|
+
* why. The code stays `invalid-argument`, so a server that branches on it
|
|
14
|
+
* still answers 401.
|
|
15
|
+
*/
|
|
16
|
+
import { FirebaseAppCheckError } from './error.js';
|
|
17
|
+
const FIREBASE_VERIFIER = Symbol.for('pyric.admin.appCheck.firebaseVerifier');
|
|
18
|
+
const MESSAGES = {
|
|
19
|
+
notSandbox: "This App Check token was not minted by the Pyric sandbox, so the sandbox's firebase-admin/app-check cannot verify it. " +
|
|
20
|
+
'A page in production AI mode gets its App Check tokens from Firebase. Start this server in production AI mode too ' +
|
|
21
|
+
'(PYRIC_AI_MODE=production) and Pyric verifies them with firebase-admin. In sandbox AI mode, page tokens come from the sandbox and verify here.',
|
|
22
|
+
install: 'Production AI mode verifies App Check tokens from Firebase with firebase-admin, which is not installed beside this server. ' +
|
|
23
|
+
'Install it in the project that runs the server (npm install firebase-admin) and restart.',
|
|
24
|
+
};
|
|
25
|
+
function installedVerifier() {
|
|
26
|
+
const verifier = globalThis[FIREBASE_VERIFIER];
|
|
27
|
+
return typeof verifier === 'function' ? verifier : undefined;
|
|
28
|
+
}
|
|
29
|
+
/** Whether a failure is the `firebase-admin` package itself not resolving, from ESM or CJS. */
|
|
30
|
+
function isFirebaseAdminMissing(error) {
|
|
31
|
+
const { code, message } = (error ?? {});
|
|
32
|
+
const isNotFound = code === 'ERR_MODULE_NOT_FOUND' || code === 'MODULE_NOT_FOUND';
|
|
33
|
+
const namesFirebaseAdmin = typeof message === 'string' && /'firebase-admin[/']/.test(message);
|
|
34
|
+
return isNotFound && namesFirebaseAdmin;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Verifies a token from Firebase with firebase-admin, through the verifier the
|
|
38
|
+
* register installs. Its result and its errors come back unchanged.
|
|
39
|
+
*/
|
|
40
|
+
export async function verifyFirebaseToken(token, projectId) {
|
|
41
|
+
const verifier = installedVerifier();
|
|
42
|
+
if (verifier === undefined)
|
|
43
|
+
throw new FirebaseAppCheckError('invalid-argument', MESSAGES.notSandbox);
|
|
44
|
+
try {
|
|
45
|
+
return await verifier(token, projectId);
|
|
46
|
+
}
|
|
47
|
+
catch (error) {
|
|
48
|
+
if (isFirebaseAdminMissing(error))
|
|
49
|
+
throw new FirebaseAppCheckError('invalid-argument', MESSAGES.install);
|
|
50
|
+
throw error;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
//# sourceMappingURL=firebase-token.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"firebase-token.js","sourceRoot":"","sources":["../../src/app-check/firebase-token.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AACH,OAAO,EAAE,qBAAqB,EAAE,MAAM,YAAY,CAAC;AAGnD,MAAM,iBAAiB,GAAG,MAAM,CAAC,GAAG,CAAC,uCAAuC,CAAC,CAAC;AAK9E,MAAM,QAAQ,GAAG;IACf,UAAU,EACR,wHAAwH;QACxH,oHAAoH;QACpH,gJAAgJ;IAClJ,OAAO,EACL,6HAA6H;QAC7H,0FAA0F;CAC7F,CAAC;AAEF,SAAS,iBAAiB;IACxB,MAAM,QAAQ,GAAI,UAAsC,CAAC,iBAAiB,CAAC,CAAC;IAC5E,OAAO,OAAO,QAAQ,KAAK,UAAU,CAAC,CAAC,CAAE,QAA6B,CAAC,CAAC,CAAC,SAAS,CAAC;AACrF,CAAC;AAED,+FAA+F;AAC/F,SAAS,sBAAsB,CAAC,KAAc;IAC5C,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,GAAG,CAAC,KAAK,IAAI,EAAE,CAA0C,CAAC;IACjF,MAAM,UAAU,GAAG,IAAI,KAAK,sBAAsB,IAAI,IAAI,KAAK,kBAAkB,CAAC;IAClF,MAAM,kBAAkB,GAAG,OAAO,OAAO,KAAK,QAAQ,IAAI,qBAAqB,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;IAC9F,OAAO,UAAU,IAAI,kBAAkB,CAAC;AAC1C,CAAC;AAED;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,mBAAmB,CAAC,KAAa,EAAE,SAAiB;IACxE,MAAM,QAAQ,GAAG,iBAAiB,EAAE,CAAC;IACrC,IAAI,QAAQ,KAAK,SAAS;QAAE,MAAM,IAAI,qBAAqB,CAAC,kBAAkB,EAAE,QAAQ,CAAC,UAAU,CAAC,CAAC;IACrG,IAAI,CAAC;QACH,OAAO,MAAM,QAAQ,CAAC,KAAK,EAAE,SAAS,CAAC,CAAC;IAC1C,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,IAAI,sBAAsB,CAAC,KAAK,CAAC;YAAE,MAAM,IAAI,qBAAqB,CAAC,kBAAkB,EAAE,QAAQ,CAAC,OAAO,CAAC,CAAC;QACzG,MAAM,KAAK,CAAC;IACd,CAAC;AACH,CAAC"}
|
|
@@ -10,6 +10,11 @@
|
|
|
10
10
|
* One deliberate difference: the sandbox has no list of the project's
|
|
11
11
|
* registered apps, so `createToken` accepts any app id (row
|
|
12
12
|
* `app-check-admin#create-token-unknown-app`, diverged by design).
|
|
13
|
+
*
|
|
14
|
+
* A token whose `kid` is not the sandbox's came from Firebase. It goes to
|
|
15
|
+
* firebase-admin's own verifier when `@pyric/cli/register` installed one
|
|
16
|
+
* (production AI mode), and is refused with a message that says why otherwise
|
|
17
|
+
* (./firebase-token.ts).
|
|
13
18
|
*/
|
|
14
19
|
import { type AppCheckTokenClaims } from 'pyric/sandbox/internal';
|
|
15
20
|
import { type PyricAdminApp } from '../app/index.js';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/app-check/index.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/app-check/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AACH,OAAO,EAAyC,KAAK,mBAAmB,EAAE,MAAM,wBAAwB,CAAC;AACzG,OAAO,EAAU,KAAK,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAiC7D,MAAM,WAAW,oBAAoB;IACnC,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,aAAa;IAC5B,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,0BAA0B;IACzC,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED,MAAM,WAAW,oBAAqB,SAAQ,mBAAmB;IAC/D,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,2BAA2B;IAC1C,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,oBAAoB,CAAC;IAC5B,eAAe,CAAC,EAAE,OAAO,CAAC;CAC3B;AAgCD,qBAAa,QAAQ;;IAGnB,gBAAgB;gBACJ,GAAG,EAAE,aAAa;IAI9B,IAAI,GAAG,IAAI,aAAa,CAEvB;IAEK,WAAW,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,oBAAoB,GAAG,OAAO,CAAC,aAAa,CAAC;IAUxF;;;;;OAKG;IACH,WAAW,CAAC,aAAa,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,0BAA0B,GAAG,OAAO,CAAC,2BAA2B,CAAC;CAqD/G;AAoCD,wBAAgB,WAAW,CAAC,GAAG,CAAC,EAAE,aAAa,GAAG,QAAQ,CAQzD"}
|
package/dist/app-check/index.js
CHANGED
|
@@ -10,11 +10,18 @@
|
|
|
10
10
|
* One deliberate difference: the sandbox has no list of the project's
|
|
11
11
|
* registered apps, so `createToken` accepts any app id (row
|
|
12
12
|
* `app-check-admin#create-token-unknown-app`, diverged by design).
|
|
13
|
+
*
|
|
14
|
+
* A token whose `kid` is not the sandbox's came from Firebase. It goes to
|
|
15
|
+
* firebase-admin's own verifier when `@pyric/cli/register` installed one
|
|
16
|
+
* (production AI mode), and is refused with a message that says why otherwise
|
|
17
|
+
* (./firebase-token.ts).
|
|
13
18
|
*/
|
|
14
19
|
import { checkAppCheckToken, mintAppCheckToken } from 'pyric/sandbox/internal';
|
|
15
|
-
import { PrefixedFirebaseError } from '../app/firebase-error.js';
|
|
16
20
|
import { getApp } from '../app/index.js';
|
|
21
|
+
import { explicitProjectId } from '../app/project-id.js';
|
|
17
22
|
import { assertAdminAppActive } from '../app/lifecycle.js';
|
|
23
|
+
import { FirebaseAppCheckError } from './error.js';
|
|
24
|
+
import { verifyFirebaseToken } from './firebase-token.js';
|
|
18
25
|
const MINUTE_MILLIS = 60 * 1000;
|
|
19
26
|
const MIN_TTL_MILLIS = 30 * MINUTE_MILLIS;
|
|
20
27
|
const MAX_TTL_MILLIS = 7 * 24 * 60 * MINUTE_MILLIS;
|
|
@@ -25,7 +32,6 @@ const MESSAGES = {
|
|
|
25
32
|
algorithm: (alg) => `The provided App Check token has incorrect algorithm. Expected "RS256" but got "${String(alg)}".`,
|
|
26
33
|
noSub: 'The provided App Check token has no "sub" (subject) claim.',
|
|
27
34
|
emptySub: 'The provided App Check token has an empty string "sub" (subject) claim.',
|
|
28
|
-
noMatchingKid: 'The provided App Check token has "kid" claim which does not correspond to a known public key. Most likely the provided App Check token is expired, so get a fresh token from your client app and try again.',
|
|
29
35
|
ttl: 'ttlMillis must be a duration in milliseconds between 30 minutes and 7 days (inclusive).',
|
|
30
36
|
appId: '`appId` must be a non-empty string.',
|
|
31
37
|
decode: 'Decoding App Check token failed. Make sure you passed the entire string JWT which represents the Firebase App Check token.',
|
|
@@ -37,12 +43,6 @@ const MESSAGES = {
|
|
|
37
43
|
audience: (scoped, aud) => `The provided App Check token has incorrect "aud" (audience) claim. Expected "${scoped}" but got "${String(aud)}". Make sure the App Check token comes from the same Firebase project as the service account used to authenticate this SDK.`,
|
|
38
44
|
};
|
|
39
45
|
const APP_CHECK_ISSUER = 'https://firebaseappcheck.googleapis.com/';
|
|
40
|
-
/** firebase-admin's FirebaseAppCheckError: a PrefixedFirebaseError with prefix `app-check`. */
|
|
41
|
-
class FirebaseAppCheckError extends PrefixedFirebaseError {
|
|
42
|
-
constructor(code, message) {
|
|
43
|
-
super('app-check', code, message);
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
46
|
/**
|
|
47
47
|
* Consumed token ids, per sandbox handle, for this process. The map sits
|
|
48
48
|
* behind Symbol.for, as the app registry in ../app/index.ts does, so the
|
|
@@ -84,7 +84,7 @@ export class AppCheck {
|
|
|
84
84
|
if (typeof appId !== 'string' || appId.length === 0)
|
|
85
85
|
throw new FirebaseAppCheckError('invalid-argument', MESSAGES.appId);
|
|
86
86
|
const ttlMillis = ttlFrom(options);
|
|
87
|
-
const projectId = this.#
|
|
87
|
+
const projectId = explicitProjectId(this.#app);
|
|
88
88
|
if (!projectId)
|
|
89
89
|
throw new FirebaseAppCheckError('unknown-error', MESSAGES.createNoProject);
|
|
90
90
|
const minted = await mintAppCheckToken({ appId, projectId }, Math.floor(ttlMillis / 1000));
|
|
@@ -110,30 +110,40 @@ export class AppCheck {
|
|
|
110
110
|
const check = await checkAppCheckToken(appCheckToken);
|
|
111
111
|
if (!check.ok && check.reason === 'malformed')
|
|
112
112
|
throw new FirebaseAppCheckError('invalid-argument', MESSAGES.decode);
|
|
113
|
+
// A kid that names a key other than the sandbox's: the token came from
|
|
114
|
+
// Firebase, so firebase-admin runs every check, in production's order.
|
|
115
|
+
const isFirebaseToken = !check.ok && check.reason === 'kid';
|
|
116
|
+
if (isFirebaseToken) {
|
|
117
|
+
const verified = await verifyFirebaseToken(appCheckToken, projectId);
|
|
118
|
+
return this.#recordConsume(verified, appCheckToken, options);
|
|
119
|
+
}
|
|
113
120
|
verifyContent(check.header, check.claims, projectId);
|
|
114
|
-
if (!check.ok && check.reason === 'kid')
|
|
115
|
-
throw new FirebaseAppCheckError('invalid-argument', MESSAGES.noMatchingKid);
|
|
116
121
|
if (!check.ok)
|
|
117
122
|
throw new FirebaseAppCheckError('invalid-argument', MESSAGES.signature);
|
|
118
123
|
verifyNotBefore(check.claims.nbf);
|
|
119
124
|
verifyExpiry(check.claims.exp);
|
|
120
125
|
const claims = check.claims;
|
|
121
126
|
const result = { appId: claims.sub, token: { ...claims, app_id: claims.sub } };
|
|
122
|
-
|
|
123
|
-
const consumed = consumedFor(this.#app);
|
|
124
|
-
const tokenId = typeof claims.jti === 'string' ? claims.jti : appCheckToken;
|
|
125
|
-
result.alreadyConsumed = consumed.has(tokenId);
|
|
126
|
-
consumed.add(tokenId);
|
|
127
|
-
}
|
|
128
|
-
return result;
|
|
127
|
+
return this.#recordConsume(result, appCheckToken, options);
|
|
129
128
|
}
|
|
130
|
-
/**
|
|
131
|
-
|
|
132
|
-
|
|
129
|
+
/**
|
|
130
|
+
* `consume: true` marks the token used, in this process, for sandbox and
|
|
131
|
+
* Firebase tokens alike. It is never forwarded to Firebase, which would need
|
|
132
|
+
* a credential with App Check permissions.
|
|
133
|
+
*/
|
|
134
|
+
#recordConsume(result, appCheckToken, options) {
|
|
135
|
+
if (!options?.consume)
|
|
136
|
+
return result;
|
|
137
|
+
const consumed = consumedFor(this.#app);
|
|
138
|
+
const jti = result.token.jti;
|
|
139
|
+
const tokenId = typeof jti === 'string' ? jti : appCheckToken;
|
|
140
|
+
result.alreadyConsumed = consumed.has(tokenId);
|
|
141
|
+
consumed.add(tokenId);
|
|
142
|
+
return result;
|
|
133
143
|
}
|
|
134
144
|
/** The project a token must be scoped to, resolved as firebase-admin resolves it. */
|
|
135
145
|
#projectId() {
|
|
136
|
-
const projectId = this.#
|
|
146
|
+
const projectId = explicitProjectId(this.#app);
|
|
137
147
|
if (!projectId)
|
|
138
148
|
throw new FirebaseAppCheckError('invalid-credential', MESSAGES.noProject);
|
|
139
149
|
return projectId;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/app-check/index.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/app-check/index.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pyric-admin",
|
|
3
|
-
"version": "0.1.0-alpha.30",
|
|
3
|
+
"version": "0.1.0-alpha.30.1",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"homepage": "https://pyric.dev",
|
|
6
6
|
"repository": {
|
|
@@ -106,7 +106,7 @@
|
|
|
106
106
|
"typecheck": "bun x tsc -p tsconfig.json --noEmit"
|
|
107
107
|
},
|
|
108
108
|
"dependencies": {
|
|
109
|
-
"pyric": "^0.1.0-alpha.30"
|
|
109
|
+
"pyric": "^0.1.0-alpha.30.1"
|
|
110
110
|
},
|
|
111
111
|
"devDependencies": {
|
|
112
112
|
"@types/bun": "1.3.14",
|
package/src/app/index.ts
CHANGED
|
@@ -15,6 +15,7 @@ import { parsePyricSandbox, processActivation } from 'pyric/sandbox/internal';
|
|
|
15
15
|
import { readFileSync } from 'node:fs';
|
|
16
16
|
|
|
17
17
|
import { assertAdminAppActive, markAdminAppDeleted } from './lifecycle.js';
|
|
18
|
+
import { keepAppCredentialProjectId, keepCredentialProjectId } from './project-id.js';
|
|
18
19
|
|
|
19
20
|
/** Brand carried by every sandbox admin app. */
|
|
20
21
|
export const ADMIN_APP_TARGET = Symbol.for('pyric.admin.app.target');
|
|
@@ -59,9 +60,16 @@ export function applicationDefault(): never {
|
|
|
59
60
|
* Link-compatible mirror of `firebase-admin/app`'s `cert(serviceAccountPathOrObject)`.
|
|
60
61
|
* Returns an inert sandbox credential token so unchanged application setup code
|
|
61
62
|
* can assign `appOptions.credential = cert(...)` under `@pyric/cli/register`.
|
|
63
|
+
*
|
|
64
|
+
* It keeps one field, the service account's project id, because firebase-admin
|
|
65
|
+
* finds an app's project there when the options name none. A path is read as a
|
|
66
|
+
* JSON file; a file that is missing, unreadable, or has no project id gives a
|
|
67
|
+
* credential without one, and never throws, so apps run without a key on disk.
|
|
62
68
|
*/
|
|
63
69
|
export function cert(serviceAccountPathOrObject: unknown): object {
|
|
64
|
-
|
|
70
|
+
const credential = { [Symbol.for('pyric.admin.credential')]: 'cert' };
|
|
71
|
+
keepCredentialProjectId(credential, serviceAccountPathOrObject);
|
|
72
|
+
return credential;
|
|
65
73
|
}
|
|
66
74
|
|
|
67
75
|
/**
|
|
@@ -139,6 +147,7 @@ export function initializeApp(
|
|
|
139
147
|
throw alreadyExists(name, 'invalid-app-options');
|
|
140
148
|
}
|
|
141
149
|
const app = initializeAmbientApp(name, ambientAppOptions(config));
|
|
150
|
+
keepAppCredentialProjectId(app, config);
|
|
142
151
|
appRegistry.set(name, app);
|
|
143
152
|
ambientApps.add(app);
|
|
144
153
|
return app;
|
|
@@ -158,6 +167,7 @@ export function initializeApp(
|
|
|
158
167
|
name,
|
|
159
168
|
options: appOptionsFrom(config),
|
|
160
169
|
};
|
|
170
|
+
keepAppCredentialProjectId(app, config);
|
|
161
171
|
appRegistry.set(name, app);
|
|
162
172
|
return app;
|
|
163
173
|
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The project id an admin app resolves, in firebase-admin's order
|
|
3
|
+
* (`getExplicitProjectId`, firebase-admin 13 `lib/utils/index.js`): the
|
|
4
|
+
* `projectId` app option, then a cert credential's project id, then
|
|
5
|
+
* `GOOGLE_CLOUD_PROJECT`, then `GCLOUD_PROJECT`.
|
|
6
|
+
*
|
|
7
|
+
* Pyric's `cert()` is inert: it keeps the service account's project id and
|
|
8
|
+
* nothing else, so no private key or client email is held in the sandbox.
|
|
9
|
+
* Both keys sit behind `Symbol.for`, as the app registry does, so the require
|
|
10
|
+
* and import copies of this module that the register hook can load agree.
|
|
11
|
+
*/
|
|
12
|
+
import { readFileSync } from 'node:fs';
|
|
13
|
+
|
|
14
|
+
const CREDENTIAL_PROJECT_ID = Symbol.for('pyric.admin.credential.projectId');
|
|
15
|
+
const APP_CREDENTIAL_PROJECT_ID = Symbol.for('pyric.admin.app.credentialProjectId');
|
|
16
|
+
|
|
17
|
+
type WithProjectId = { [CREDENTIAL_PROJECT_ID]?: unknown; [APP_CREDENTIAL_PROJECT_ID]?: unknown };
|
|
18
|
+
|
|
19
|
+
function nonEmptyString(value: unknown): string | undefined {
|
|
20
|
+
return typeof value === 'string' && value !== '' ? value : undefined;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** firebase-admin's `ServiceAccount.fromPath`, except that a file it cannot read or parse yields no object. */
|
|
24
|
+
function readServiceAccountFile(path: string): unknown {
|
|
25
|
+
try {
|
|
26
|
+
return JSON.parse(readFileSync(path, 'utf8'));
|
|
27
|
+
} catch {
|
|
28
|
+
return undefined;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** The project id of a service account object or file path: `projectId`, then `project_id`, as firebase-admin's `copyAttr` reads it. */
|
|
33
|
+
function serviceAccountProjectId(serviceAccountPathOrObject: unknown): string | undefined {
|
|
34
|
+
const isPath = typeof serviceAccountPathOrObject === 'string';
|
|
35
|
+
const account = isPath ? readServiceAccountFile(serviceAccountPathOrObject) : serviceAccountPathOrObject;
|
|
36
|
+
if (typeof account !== 'object' || account === null) return undefined;
|
|
37
|
+
const { projectId, project_id: projectIdSnake } = account as Record<string, unknown>;
|
|
38
|
+
return nonEmptyString(projectId) ?? nonEmptyString(projectIdSnake);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function defineHidden(target: object, key: symbol, value: string): void {
|
|
42
|
+
Object.defineProperty(target, key, { value, enumerable: false, writable: false, configurable: false });
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Keeps the service account's project id on an inert credential, as a non-enumerable property. */
|
|
46
|
+
export function keepCredentialProjectId(credential: object, serviceAccountPathOrObject: unknown): void {
|
|
47
|
+
const projectId = serviceAccountProjectId(serviceAccountPathOrObject);
|
|
48
|
+
if (projectId !== undefined) defineHidden(credential, CREDENTIAL_PROJECT_ID, projectId);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Copies a cert credential's project id from initialization options onto the app, beside `options`. */
|
|
52
|
+
export function keepAppCredentialProjectId(app: object, config: unknown): void {
|
|
53
|
+
if (typeof config !== 'object' || config === null) return;
|
|
54
|
+
const credential = (config as { credential?: unknown }).credential;
|
|
55
|
+
if (typeof credential !== 'object' || credential === null) return;
|
|
56
|
+
const projectId = nonEmptyString((credential as WithProjectId)[CREDENTIAL_PROJECT_ID]);
|
|
57
|
+
if (projectId !== undefined) defineHidden(app, APP_CREDENTIAL_PROJECT_ID, projectId);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** firebase-admin's `getExplicitProjectId` for a sandbox admin app. */
|
|
61
|
+
export function explicitProjectId(app: { readonly options: { readonly projectId?: string } }): string | undefined {
|
|
62
|
+
return (
|
|
63
|
+
nonEmptyString(app.options.projectId) ??
|
|
64
|
+
nonEmptyString((app as WithProjectId)[APP_CREDENTIAL_PROJECT_ID]) ??
|
|
65
|
+
nonEmptyString(process.env.GOOGLE_CLOUD_PROJECT || process.env.GCLOUD_PROJECT)
|
|
66
|
+
);
|
|
67
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { PrefixedFirebaseError } from '../app/firebase-error.js';
|
|
2
|
+
|
|
3
|
+
export type AppCheckErrorCode = 'invalid-argument' | 'invalid-credential' | 'app-check-token-expired' | 'unknown-error';
|
|
4
|
+
|
|
5
|
+
/** firebase-admin's FirebaseAppCheckError: a PrefixedFirebaseError with prefix `app-check`. */
|
|
6
|
+
export class FirebaseAppCheckError extends PrefixedFirebaseError {
|
|
7
|
+
constructor(code: AppCheckErrorCode, message: string) {
|
|
8
|
+
super('app-check', code, message);
|
|
9
|
+
}
|
|
10
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* App Check tokens from Firebase: a token whose `kid` names a key that is not
|
|
3
|
+
* the sandbox's (#1552, #1301).
|
|
4
|
+
*
|
|
5
|
+
* In production AI mode the page's `firebase/app-check` is the Firebase SDK,
|
|
6
|
+
* so its tokens come from Firebase, and only firebase-admin's own verifier can
|
|
7
|
+
* check them. `@pyric/cli/register` installs that verifier behind
|
|
8
|
+
* {@link FIREBASE_VERIFIER} in production AI mode only, the same kind of seam
|
|
9
|
+
* as its `pyric.remote.sandboxFactory`: this package never learns what an AI
|
|
10
|
+
* mode is, and never loads firebase-admin itself.
|
|
11
|
+
*
|
|
12
|
+
* With no verifier installed the token is refused with a message that says
|
|
13
|
+
* why. The code stays `invalid-argument`, so a server that branches on it
|
|
14
|
+
* still answers 401.
|
|
15
|
+
*/
|
|
16
|
+
import { FirebaseAppCheckError } from './error.js';
|
|
17
|
+
import type { VerifyAppCheckTokenResponse } from './index.js';
|
|
18
|
+
|
|
19
|
+
const FIREBASE_VERIFIER = Symbol.for('pyric.admin.appCheck.firebaseVerifier');
|
|
20
|
+
|
|
21
|
+
/** firebase-admin's `getAppCheck(app).verifyToken(token)` for an app scoped to `projectId`, with no options. */
|
|
22
|
+
type FirebaseVerifier = (token: string, projectId: string) => Promise<VerifyAppCheckTokenResponse>;
|
|
23
|
+
|
|
24
|
+
const MESSAGES = {
|
|
25
|
+
notSandbox:
|
|
26
|
+
"This App Check token was not minted by the Pyric sandbox, so the sandbox's firebase-admin/app-check cannot verify it. " +
|
|
27
|
+
'A page in production AI mode gets its App Check tokens from Firebase. Start this server in production AI mode too ' +
|
|
28
|
+
'(PYRIC_AI_MODE=production) and Pyric verifies them with firebase-admin. In sandbox AI mode, page tokens come from the sandbox and verify here.',
|
|
29
|
+
install:
|
|
30
|
+
'Production AI mode verifies App Check tokens from Firebase with firebase-admin, which is not installed beside this server. ' +
|
|
31
|
+
'Install it in the project that runs the server (npm install firebase-admin) and restart.',
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
function installedVerifier(): FirebaseVerifier | undefined {
|
|
35
|
+
const verifier = (globalThis as Record<symbol, unknown>)[FIREBASE_VERIFIER];
|
|
36
|
+
return typeof verifier === 'function' ? (verifier as FirebaseVerifier) : undefined;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Whether a failure is the `firebase-admin` package itself not resolving, from ESM or CJS. */
|
|
40
|
+
function isFirebaseAdminMissing(error: unknown): boolean {
|
|
41
|
+
const { code, message } = (error ?? {}) as { code?: unknown; message?: unknown };
|
|
42
|
+
const isNotFound = code === 'ERR_MODULE_NOT_FOUND' || code === 'MODULE_NOT_FOUND';
|
|
43
|
+
const namesFirebaseAdmin = typeof message === 'string' && /'firebase-admin[/']/.test(message);
|
|
44
|
+
return isNotFound && namesFirebaseAdmin;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Verifies a token from Firebase with firebase-admin, through the verifier the
|
|
49
|
+
* register installs. Its result and its errors come back unchanged.
|
|
50
|
+
*/
|
|
51
|
+
export async function verifyFirebaseToken(token: string, projectId: string): Promise<VerifyAppCheckTokenResponse> {
|
|
52
|
+
const verifier = installedVerifier();
|
|
53
|
+
if (verifier === undefined) throw new FirebaseAppCheckError('invalid-argument', MESSAGES.notSandbox);
|
|
54
|
+
try {
|
|
55
|
+
return await verifier(token, projectId);
|
|
56
|
+
} catch (error) {
|
|
57
|
+
if (isFirebaseAdminMissing(error)) throw new FirebaseAppCheckError('invalid-argument', MESSAGES.install);
|
|
58
|
+
throw error;
|
|
59
|
+
}
|
|
60
|
+
}
|
package/src/app-check/index.ts
CHANGED
|
@@ -10,11 +10,18 @@
|
|
|
10
10
|
* One deliberate difference: the sandbox has no list of the project's
|
|
11
11
|
* registered apps, so `createToken` accepts any app id (row
|
|
12
12
|
* `app-check-admin#create-token-unknown-app`, diverged by design).
|
|
13
|
+
*
|
|
14
|
+
* A token whose `kid` is not the sandbox's came from Firebase. It goes to
|
|
15
|
+
* firebase-admin's own verifier when `@pyric/cli/register` installed one
|
|
16
|
+
* (production AI mode), and is refused with a message that says why otherwise
|
|
17
|
+
* (./firebase-token.ts).
|
|
13
18
|
*/
|
|
14
19
|
import { checkAppCheckToken, mintAppCheckToken, type AppCheckTokenClaims } from 'pyric/sandbox/internal';
|
|
15
|
-
import { PrefixedFirebaseError } from '../app/firebase-error.js';
|
|
16
20
|
import { getApp, type PyricAdminApp } from '../app/index.js';
|
|
21
|
+
import { explicitProjectId } from '../app/project-id.js';
|
|
17
22
|
import { assertAdminAppActive } from '../app/lifecycle.js';
|
|
23
|
+
import { FirebaseAppCheckError } from './error.js';
|
|
24
|
+
import { verifyFirebaseToken } from './firebase-token.js';
|
|
18
25
|
|
|
19
26
|
const MINUTE_MILLIS = 60 * 1000;
|
|
20
27
|
const MIN_TTL_MILLIS = 30 * MINUTE_MILLIS;
|
|
@@ -27,8 +34,6 @@ const MESSAGES = {
|
|
|
27
34
|
algorithm: (alg: unknown) => `The provided App Check token has incorrect algorithm. Expected "RS256" but got "${String(alg)}".`,
|
|
28
35
|
noSub: 'The provided App Check token has no "sub" (subject) claim.',
|
|
29
36
|
emptySub: 'The provided App Check token has an empty string "sub" (subject) claim.',
|
|
30
|
-
noMatchingKid:
|
|
31
|
-
'The provided App Check token has "kid" claim which does not correspond to a known public key. Most likely the provided App Check token is expired, so get a fresh token from your client app and try again.',
|
|
32
37
|
ttl: 'ttlMillis must be a duration in milliseconds between 30 minutes and 7 days (inclusive).',
|
|
33
38
|
appId: '`appId` must be a non-empty string.',
|
|
34
39
|
decode: 'Decoding App Check token failed. Make sure you passed the entire string JWT which represents the Firebase App Check token.',
|
|
@@ -45,15 +50,6 @@ const MESSAGES = {
|
|
|
45
50
|
|
|
46
51
|
const APP_CHECK_ISSUER = 'https://firebaseappcheck.googleapis.com/';
|
|
47
52
|
|
|
48
|
-
type AppCheckErrorCode = 'invalid-argument' | 'invalid-credential' | 'app-check-token-expired' | 'unknown-error';
|
|
49
|
-
|
|
50
|
-
/** firebase-admin's FirebaseAppCheckError: a PrefixedFirebaseError with prefix `app-check`. */
|
|
51
|
-
class FirebaseAppCheckError extends PrefixedFirebaseError {
|
|
52
|
-
constructor(code: AppCheckErrorCode, message: string) {
|
|
53
|
-
super('app-check', code, message);
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
53
|
export interface AppCheckTokenOptions {
|
|
58
54
|
ttlMillis?: number;
|
|
59
55
|
}
|
|
@@ -123,7 +119,7 @@ export class AppCheck {
|
|
|
123
119
|
assertAdminAppActive(this.#app);
|
|
124
120
|
if (typeof appId !== 'string' || appId.length === 0) throw new FirebaseAppCheckError('invalid-argument', MESSAGES.appId);
|
|
125
121
|
const ttlMillis = ttlFrom(options);
|
|
126
|
-
const projectId = this.#
|
|
122
|
+
const projectId = explicitProjectId(this.#app);
|
|
127
123
|
if (!projectId) throw new FirebaseAppCheckError('unknown-error', MESSAGES.createNoProject);
|
|
128
124
|
const minted = await mintAppCheckToken({ appId, projectId }, Math.floor(ttlMillis / 1000));
|
|
129
125
|
return { token: minted.token, ttlMillis };
|
|
@@ -147,30 +143,44 @@ export class AppCheck {
|
|
|
147
143
|
const projectId = this.#projectId();
|
|
148
144
|
const check = await checkAppCheckToken(appCheckToken);
|
|
149
145
|
if (!check.ok && check.reason === 'malformed') throw new FirebaseAppCheckError('invalid-argument', MESSAGES.decode);
|
|
146
|
+
// A kid that names a key other than the sandbox's: the token came from
|
|
147
|
+
// Firebase, so firebase-admin runs every check, in production's order.
|
|
148
|
+
const isFirebaseToken = !check.ok && check.reason === 'kid';
|
|
149
|
+
if (isFirebaseToken) {
|
|
150
|
+
const verified = await verifyFirebaseToken(appCheckToken, projectId);
|
|
151
|
+
return this.#recordConsume(verified, appCheckToken, options);
|
|
152
|
+
}
|
|
150
153
|
verifyContent(check.header, check.claims, projectId);
|
|
151
|
-
if (!check.ok && check.reason === 'kid') throw new FirebaseAppCheckError('invalid-argument', MESSAGES.noMatchingKid);
|
|
152
154
|
if (!check.ok) throw new FirebaseAppCheckError('invalid-argument', MESSAGES.signature);
|
|
153
155
|
verifyNotBefore(check.claims.nbf);
|
|
154
156
|
verifyExpiry(check.claims.exp);
|
|
155
157
|
const claims = check.claims as AppCheckTokenClaims;
|
|
156
158
|
const result: VerifyAppCheckTokenResponse = { appId: claims.sub, token: { ...claims, app_id: claims.sub } };
|
|
157
|
-
|
|
158
|
-
const consumed = consumedFor(this.#app);
|
|
159
|
-
const tokenId = typeof claims.jti === 'string' ? claims.jti : appCheckToken;
|
|
160
|
-
result.alreadyConsumed = consumed.has(tokenId);
|
|
161
|
-
consumed.add(tokenId);
|
|
162
|
-
}
|
|
163
|
-
return result;
|
|
159
|
+
return this.#recordConsume(result, appCheckToken, options);
|
|
164
160
|
}
|
|
165
161
|
|
|
166
|
-
/**
|
|
167
|
-
|
|
168
|
-
|
|
162
|
+
/**
|
|
163
|
+
* `consume: true` marks the token used, in this process, for sandbox and
|
|
164
|
+
* Firebase tokens alike. It is never forwarded to Firebase, which would need
|
|
165
|
+
* a credential with App Check permissions.
|
|
166
|
+
*/
|
|
167
|
+
#recordConsume(
|
|
168
|
+
result: VerifyAppCheckTokenResponse,
|
|
169
|
+
appCheckToken: string,
|
|
170
|
+
options: VerifyAppCheckTokenOptions | undefined,
|
|
171
|
+
): VerifyAppCheckTokenResponse {
|
|
172
|
+
if (!options?.consume) return result;
|
|
173
|
+
const consumed = consumedFor(this.#app);
|
|
174
|
+
const jti = result.token.jti;
|
|
175
|
+
const tokenId = typeof jti === 'string' ? jti : appCheckToken;
|
|
176
|
+
result.alreadyConsumed = consumed.has(tokenId);
|
|
177
|
+
consumed.add(tokenId);
|
|
178
|
+
return result;
|
|
169
179
|
}
|
|
170
180
|
|
|
171
181
|
/** The project a token must be scoped to, resolved as firebase-admin resolves it. */
|
|
172
182
|
#projectId(): string {
|
|
173
|
-
const projectId = this.#
|
|
183
|
+
const projectId = explicitProjectId(this.#app);
|
|
174
184
|
if (!projectId) throw new FirebaseAppCheckError('invalid-credential', MESSAGES.noProject);
|
|
175
185
|
return projectId;
|
|
176
186
|
}
|