@dereekb/firebase-server 13.37.0 → 13.39.0
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/calcom/package.json +15 -15
- package/discord/package.json +15 -15
- package/index.esm.js +1716 -512
- package/mailgun/package.json +14 -14
- package/mcp/index.esm.js +561 -40
- package/mcp/package.json +16 -16
- package/mcp/src/lib/mcp.config.d.ts +22 -1
- package/mcp/src/lib/service/index.d.ts +1 -0
- package/mcp/src/lib/service/mcp.server.factory.d.ts +3 -2
- package/mcp/src/lib/service/tools/mcp.tool.model-roles.d.ts +95 -0
- package/model/package.json +14 -14
- package/model/src/lib/notification/notification.module.d.ts +1 -1
- package/oidc/package.json +15 -15
- package/package.json +36 -45
- package/src/lib/env/env.service.d.ts +27 -3
- package/src/lib/nest/controller/api.scope.d.ts +63 -0
- package/src/lib/nest/controller/index.d.ts +2 -0
- package/src/lib/nest/controller/model/model.api.get.service.d.ts +146 -1
- package/src/lib/nest/controller/model/model.api.scope.d.ts +3 -2
- package/src/lib/nest/controller/session/index.d.ts +4 -0
- package/src/lib/nest/controller/session/session.api.config.d.ts +104 -0
- package/src/lib/nest/controller/session/session.api.controller.d.ts +26 -0
- package/src/lib/nest/controller/session/session.api.module.d.ts +49 -0
- package/src/lib/nest/controller/session/session.api.service.d.ts +71 -0
- package/test/package.json +16 -16
- package/twilio/package.json +13 -13
- package/zoho/package.json +15 -15
- package/calcom/index.cjs.default.js +0 -1
- package/calcom/index.cjs.js +0 -1451
- package/calcom/index.cjs.mjs +0 -2
- package/discord/index.cjs.default.js +0 -1
- package/discord/index.cjs.js +0 -761
- package/discord/index.cjs.mjs +0 -2
- package/index.cjs.default.js +0 -1
- package/index.cjs.js +0 -13491
- package/index.cjs.mjs +0 -2
- package/mailgun/index.cjs.default.js +0 -1
- package/mailgun/index.cjs.js +0 -436
- package/mailgun/index.cjs.mjs +0 -2
- package/mcp/index.cjs.default.js +0 -1
- package/mcp/index.cjs.js +0 -8254
- package/mcp/index.cjs.mjs +0 -2
- package/model/index.cjs.default.js +0 -1
- package/model/index.cjs.js +0 -19965
- package/model/index.cjs.mjs +0 -2
- package/oidc/index.cjs.default.js +0 -1
- package/oidc/index.cjs.js +0 -8085
- package/oidc/index.cjs.mjs +0 -2
- package/test/index.cjs.default.js +0 -1
- package/test/index.cjs.js +0 -5728
- package/test/index.cjs.mjs +0 -2
- package/twilio/index.cjs.default.js +0 -1
- package/twilio/index.cjs.js +0 -404
- package/twilio/index.cjs.mjs +0 -2
- package/zoho/index.cjs.default.js +0 -1
- package/zoho/index.cjs.js +0 -1810
- package/zoho/index.cjs.mjs +0 -2
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type Maybe } from '@dereekb/util';
|
|
2
|
-
import { type
|
|
2
|
+
import { type GrantedRoleMap } from '@dereekb/model';
|
|
3
|
+
import { type FirebaseAuthUserId, type FirestoreModelIdentity, type FirestoreModelKey, type FirestoreModelType } from '@dereekb/firebase';
|
|
3
4
|
import { type INestApplicationContext } from '@nestjs/common';
|
|
4
5
|
import { ModelApiDispatchConfig } from './model.api.dispatch';
|
|
5
6
|
import { type FirebaseServerAuthData } from '../auth.context.server';
|
|
@@ -7,6 +8,13 @@ import { type FirebaseServerAuthData } from '../auth.context.server';
|
|
|
7
8
|
* Maximum number of keys allowed in a multi-read request.
|
|
8
9
|
*/
|
|
9
10
|
export declare const MAX_MODEL_ACCESS_MULTI_READ_KEYS = 50;
|
|
11
|
+
/**
|
|
12
|
+
* Error code returned when a read targets a model the app declared SERVER-ONLY.
|
|
13
|
+
*
|
|
14
|
+
* Deliberately distinct from a permission error: nothing the caller can be granted will make this
|
|
15
|
+
* read succeed, so the message needs to say WHY rather than implying a missing role.
|
|
16
|
+
*/
|
|
17
|
+
export declare const MODEL_IS_SERVER_ONLY_ERROR_CODE = "MODEL_IS_SERVER_ONLY";
|
|
10
18
|
/**
|
|
11
19
|
* Result of a single document access read.
|
|
12
20
|
*/
|
|
@@ -29,6 +37,90 @@ export interface ModelAccessReadError {
|
|
|
29
37
|
readonly message: string;
|
|
30
38
|
readonly code?: string;
|
|
31
39
|
}
|
|
40
|
+
/**
|
|
41
|
+
* The resolved permission state for a single model key, as computed by that model's
|
|
42
|
+
* `roleMapForModel()` delegate for a specific user.
|
|
43
|
+
*/
|
|
44
|
+
export interface ModelAccessRoleMapResult {
|
|
45
|
+
readonly key: FirestoreModelKey;
|
|
46
|
+
/**
|
|
47
|
+
* Whether the underlying document exists.
|
|
48
|
+
*
|
|
49
|
+
* Roles are only computed for documents that exist — `FirebaseModelPermissionServiceInstance`
|
|
50
|
+
* gates on `isUsableOutputForRoles(output) => output.exists`, so a missing document always
|
|
51
|
+
* resolves to an empty role set. Surfacing existence separately is what lets a caller tell
|
|
52
|
+
* "the document is not there" apart from "the document is there and you may not touch it";
|
|
53
|
+
* both otherwise present as `roles: []`.
|
|
54
|
+
*/
|
|
55
|
+
readonly exists: boolean;
|
|
56
|
+
/**
|
|
57
|
+
* True when the model granted the full-access marker ({@link FULL_ACCESS_ROLE_KEY}) rather than
|
|
58
|
+
* an enumerated role set — the shape admin short-circuits like `fullAccessRoleMap()` produce.
|
|
59
|
+
* When true, {@link roles} is empty and every role is implicitly granted.
|
|
60
|
+
*/
|
|
61
|
+
readonly fullAccess: boolean;
|
|
62
|
+
/**
|
|
63
|
+
* The granted role names, sorted. Empty when {@link fullAccess} is true or nothing was granted.
|
|
64
|
+
*/
|
|
65
|
+
readonly roles: string[];
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Result of a multi-key role-map resolution.
|
|
69
|
+
*/
|
|
70
|
+
export interface ModelAccessMultiRoleMapResult {
|
|
71
|
+
/**
|
|
72
|
+
* The uid the roles were resolved for — the caller's own uid unless the request targeted
|
|
73
|
+
* another user. `undefined` for an unauthenticated resolution.
|
|
74
|
+
*/
|
|
75
|
+
readonly uid?: string;
|
|
76
|
+
/**
|
|
77
|
+
* True when {@link uid} is someone other than the calling user.
|
|
78
|
+
*/
|
|
79
|
+
readonly targeted: boolean;
|
|
80
|
+
readonly results: ModelAccessRoleMapResult[];
|
|
81
|
+
readonly errors: ModelAccessReadError[];
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Input for {@link ModelApiGetService.readRoleMaps}.
|
|
85
|
+
*/
|
|
86
|
+
export interface ModelAccessRoleMapParams {
|
|
87
|
+
readonly modelType: FirestoreModelType;
|
|
88
|
+
readonly keys: FirestoreModelKey[];
|
|
89
|
+
/**
|
|
90
|
+
* The calling request's auth data.
|
|
91
|
+
*/
|
|
92
|
+
readonly auth: Maybe<FirebaseServerAuthData>;
|
|
93
|
+
/**
|
|
94
|
+
* Resolve roles as this user instead of the caller. Callers are responsible for authorizing
|
|
95
|
+
* this before passing it — the service performs no permission check of its own on the target.
|
|
96
|
+
*/
|
|
97
|
+
readonly targetUid?: Maybe<FirebaseAuthUserId>;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Input for {@link modelAccessRoleMapResultFromGrantedRoles}.
|
|
101
|
+
*/
|
|
102
|
+
export interface ModelAccessRoleMapResultParams {
|
|
103
|
+
readonly key: FirestoreModelKey;
|
|
104
|
+
/**
|
|
105
|
+
* The resolved `ContextGrantedModelRoles` for the key. Typed structurally (rather than importing
|
|
106
|
+
* the generic) so the mapper stays testable with a plain object.
|
|
107
|
+
*/
|
|
108
|
+
readonly granted: {
|
|
109
|
+
readonly data?: Maybe<{
|
|
110
|
+
readonly exists?: boolean;
|
|
111
|
+
}>;
|
|
112
|
+
readonly roleMap: GrantedRoleMap<string>;
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Maps a resolved `ContextGrantedModelRoles` into the flat {@link ModelAccessRoleMapResult} wire
|
|
117
|
+
* shape — collapsing the full-access marker into a boolean, dropping the structural marker keys, and
|
|
118
|
+
* sorting the enumerated role keys so output is stable across calls.
|
|
119
|
+
*
|
|
120
|
+
* @param input - The key and its resolved granted-roles result.
|
|
121
|
+
* @returns The flattened per-key permission state.
|
|
122
|
+
*/
|
|
123
|
+
export declare function modelAccessRoleMapResultFromGrantedRoles(input: ModelAccessRoleMapResultParams): ModelAccessRoleMapResult;
|
|
32
124
|
/**
|
|
33
125
|
* Shape of a single failed-key entry from `useMultipleModels({ throwOnFirstError: false })`.
|
|
34
126
|
* Exposed so the mapper below stays testable without a live nest context.
|
|
@@ -77,6 +169,29 @@ export declare class ModelApiGetService {
|
|
|
77
169
|
* @param auth - The request's auth data (OIDC scopes are read from it).
|
|
78
170
|
*/
|
|
79
171
|
private _assertReadScope;
|
|
172
|
+
/**
|
|
173
|
+
* Refuses a read of a model the app declared SERVER-ONLY, BEFORE `useModel` runs.
|
|
174
|
+
*
|
|
175
|
+
* The model API authorizes through `roleMapForModel` under the Admin SDK, which bypasses
|
|
176
|
+
* `firestore.rules` entirely. For a model the rules file deliberately leaves unmatched (or denies
|
|
177
|
+
* outright), those two systems disagree and the model API is the one handing out the document.
|
|
178
|
+
* Gating here — ahead of the role map, and on the ONE service both `/model/<type>/get` and the
|
|
179
|
+
* `model-get` MCP tool share — is what makes the two read paths agree.
|
|
180
|
+
*
|
|
181
|
+
* @param modelType - The Firestore model type being read.
|
|
182
|
+
* @param auth - The request's auth data; used to build the context the service lookup needs.
|
|
183
|
+
* @throws {HttpsError} `MODEL_IS_SERVER_ONLY` when the model opted in via `serverOnly`.
|
|
184
|
+
*/
|
|
185
|
+
private _assertNotServerOnly;
|
|
186
|
+
/**
|
|
187
|
+
* Reads the `serverOnly` flag off the registered model service, tolerating a type whose service
|
|
188
|
+
* cannot be constructed (which `readDocument` then reports as an unknown model).
|
|
189
|
+
*
|
|
190
|
+
* @param modelType - The Firestore model type to inspect.
|
|
191
|
+
* @param auth - The request's auth data.
|
|
192
|
+
* @returns `true` when the model declared itself server-only.
|
|
193
|
+
*/
|
|
194
|
+
private _isServerOnlyModel;
|
|
80
195
|
/**
|
|
81
196
|
* Returns the registered {@link FirestoreModelIdentity} for the given `modelType` string, or
|
|
82
197
|
* `undefined` when no model of that type is registered.
|
|
@@ -114,6 +229,36 @@ export declare class ModelApiGetService {
|
|
|
114
229
|
* @returns Results and errors for each requested key.
|
|
115
230
|
*/
|
|
116
231
|
readDocuments(modelType: FirestoreModelType, keys: FirestoreModelKey[], auth: Maybe<FirebaseServerAuthData>): Promise<ModelAccessMultiReadResult>;
|
|
232
|
+
/**
|
|
233
|
+
* Resolves the granted role map for one or more keys of the same model type — i.e. "what is this
|
|
234
|
+
* user actually allowed to do with this document?".
|
|
235
|
+
*
|
|
236
|
+
* This is the same computation the permission-checked read path runs (`roleMapForModel()` on the
|
|
237
|
+
* model's registered service factory), but the resolved roles are returned instead of being
|
|
238
|
+
* consumed to allow/deny an operation. Because it runs the model's real delegate, derived and
|
|
239
|
+
* cascading roles are included exactly as the API would grant them.
|
|
240
|
+
*
|
|
241
|
+
* Per-key failures are captured in `errors` rather than thrown, matching {@link readDocuments}.
|
|
242
|
+
* A key that resolves to a missing document is NOT an error — it comes back with
|
|
243
|
+
* `exists: false, roles: []`, which is what distinguishes "not there" from "no access".
|
|
244
|
+
*
|
|
245
|
+
* @param params - Model type, keys, calling auth, and an optional target uid.
|
|
246
|
+
* @returns The per-key permission state plus the uid the roles were resolved for.
|
|
247
|
+
*/
|
|
248
|
+
readRoleMaps(params: ModelAccessRoleMapParams): Promise<ModelAccessMultiRoleMapResult>;
|
|
249
|
+
/**
|
|
250
|
+
* Builds a synthetic {@link AuthData} for an arbitrary uid so role resolution can run *as* that
|
|
251
|
+
* user rather than as the caller.
|
|
252
|
+
*
|
|
253
|
+
* The target's custom claims are read from their Firebase Auth record and spread into the
|
|
254
|
+
* synthetic token, mirroring how Firebase merges custom claims into a real decoded ID token —
|
|
255
|
+
* so claim-reading permission delegates behave identically to a live request from that user.
|
|
256
|
+
*
|
|
257
|
+
* @param uid - The uid to resolve as.
|
|
258
|
+
* @returns An auth ref carrying the target user's claims.
|
|
259
|
+
* @throws {Error} If no Firebase Auth user exists for the uid.
|
|
260
|
+
*/
|
|
261
|
+
private _makeAuthRefForUid;
|
|
117
262
|
/**
|
|
118
263
|
* Builds an {@link AuthDataRef} compatible with `useModel()` from the HTTP request auth.
|
|
119
264
|
*
|
|
@@ -40,8 +40,9 @@ export interface ModelApiOidcScopeConfig {
|
|
|
40
40
|
* `auth.oidcValidatedToken` (with the space-delimited `scope` string); a non-OIDC caller has neither
|
|
41
41
|
* that field nor a `scope` on `auth.token`. Reading is defensive (the auth shape is only typed as
|
|
42
42
|
* {@link FirebaseServerAuthData} here — the OIDC-specific `oidcValidatedToken` lives in the
|
|
43
|
-
* `@dereekb/firebase-server/oidc` sub-package this core layer cannot import), delegating the
|
|
44
|
-
*
|
|
43
|
+
* `@dereekb/firebase-server/oidc` sub-package this core layer cannot import), delegating entirely to the
|
|
44
|
+
* shared {@link oidcScopesFromRequestAuth} so there is no drift with `getOidcScopesFromRequest` or with
|
|
45
|
+
* the non-model endpoints that read scopes the same way.
|
|
45
46
|
*
|
|
46
47
|
* @param auth - The request auth data, or undefined for unauthenticated requests.
|
|
47
48
|
* @returns The granted scope set, or `undefined` when the request carries no OIDC `scope` claim.
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { type Maybe, type Milliseconds, type PromiseOrValue } from '@dereekb/util';
|
|
2
|
+
import { type OidcScopeTerm } from '@dereekb/firebase';
|
|
3
|
+
import { type FirebaseServerAuthData } from '../auth.context.server';
|
|
4
|
+
/**
|
|
5
|
+
* Route prefix the session API controller is mounted at. Under the `/api` global route prefix the
|
|
6
|
+
* routes become `/api/session/*`.
|
|
7
|
+
*/
|
|
8
|
+
export declare const SESSION_API_ROUTE_PREFIX = "session";
|
|
9
|
+
/**
|
|
10
|
+
* Path (relative to the API base URL) of the direct-Firestore session endpoint.
|
|
11
|
+
*
|
|
12
|
+
* The `@dereekb/dbx-cli` client posts to `<apiBaseUrl>${FIRESTORE_SESSION_API_PATH}`.
|
|
13
|
+
*/
|
|
14
|
+
export declare const FIRESTORE_SESSION_API_PATH = "/session/firestore";
|
|
15
|
+
/**
|
|
16
|
+
* Path prefix apps must add to their OIDC `protectedPaths` so the bearer-token middleware
|
|
17
|
+
* authenticates the session endpoint (today typically `['/api/model', '/mcp']`).
|
|
18
|
+
*
|
|
19
|
+
* Without this the endpoint is reachable unauthenticated — `req.auth` would be `undefined` and the
|
|
20
|
+
* request rejected as unauthenticated, but the gate belongs at the middleware, not the handler.
|
|
21
|
+
*/
|
|
22
|
+
export declare const FIREBASE_SERVER_SESSION_API_PROTECTED_PATH = "/api/session";
|
|
23
|
+
/**
|
|
24
|
+
* Default lifetime requested for the minted App Check token. Matches the Admin SDK's own default.
|
|
25
|
+
*/
|
|
26
|
+
export declare const DEFAULT_FIRESTORE_SESSION_APP_CHECK_TTL_MILLIS: Milliseconds;
|
|
27
|
+
/**
|
|
28
|
+
* Minimum lifetime the Admin SDK accepts for an App Check token (30 minutes).
|
|
29
|
+
*/
|
|
30
|
+
export declare const MIN_FIRESTORE_SESSION_APP_CHECK_TTL_MILLIS: Milliseconds;
|
|
31
|
+
/**
|
|
32
|
+
* Maximum lifetime the Admin SDK accepts for an App Check token (7 days).
|
|
33
|
+
*
|
|
34
|
+
* Deliberately NOT the default: this endpoint mints a valid web-app App Check attestation for
|
|
35
|
+
* whoever calls it, so the shortest workable lifetime is the right one.
|
|
36
|
+
*/
|
|
37
|
+
export declare const MAX_FIRESTORE_SESSION_APP_CHECK_TTL_MILLIS: Milliseconds;
|
|
38
|
+
/**
|
|
39
|
+
* Window a Firebase Auth custom token may be exchanged for an ID token within (1 hour, fixed by
|
|
40
|
+
* Firebase). The exchanged ID token then lives its own hour from sign-in.
|
|
41
|
+
*/
|
|
42
|
+
export declare const FIREBASE_CUSTOM_TOKEN_EXCHANGE_WINDOW_MILLIS: Milliseconds;
|
|
43
|
+
/**
|
|
44
|
+
* Signature for the predicate that authorizes a caller to open a direct-Firestore session.
|
|
45
|
+
*
|
|
46
|
+
* Receives the calling request's auth data (`undefined` for an unauthenticated request) and returns
|
|
47
|
+
* true when that caller may be handed a custom token + App Check attestation. Typically an admin
|
|
48
|
+
* check, e.g. `(auth) => authRoleClaimsService.toRoles(auth?.token ?? {}).has('admin')`.
|
|
49
|
+
*
|
|
50
|
+
* This is the LOAD-BEARING gate. `assertIsAdminInRequest` cannot be used here — it needs a
|
|
51
|
+
* `NestContextCallableRequestWithOptionalAuth` with `.nest` attached, and a plain Nest controller
|
|
52
|
+
* only has an Express request carrying `req.auth` — so the check is delegated to the app, keeping
|
|
53
|
+
* `@dereekb/firebase-server` app-agnostic about what "admin" means.
|
|
54
|
+
*
|
|
55
|
+
* When no predicate is provided the endpoint fails closed for EVERY caller.
|
|
56
|
+
*/
|
|
57
|
+
export type FirestoreSessionAdminPredicate = (auth: Maybe<FirebaseServerAuthData>) => PromiseOrValue<boolean>;
|
|
58
|
+
/**
|
|
59
|
+
* NestJS injection token for the {@link FirestoreSessionAdminPredicate} provider.
|
|
60
|
+
*/
|
|
61
|
+
export declare const FIRESTORE_SESSION_ADMIN_PREDICATE = "FIRESTORE_SESSION_ADMIN_PREDICATE";
|
|
62
|
+
/**
|
|
63
|
+
* Optional configuration for the session API module, supplied by the app via its dependency module.
|
|
64
|
+
*
|
|
65
|
+
* Everything here is optional — with no config provided the endpoint still mints custom tokens, just
|
|
66
|
+
* without an App Check attestation (correct for a project that does not enforce App Check, and for
|
|
67
|
+
* emulator-backed local development).
|
|
68
|
+
*/
|
|
69
|
+
export declare abstract class SessionApiModuleConfig {
|
|
70
|
+
/**
|
|
71
|
+
* The Firebase `appId` of the registered **web** app to mint App Check tokens for. This is the same
|
|
72
|
+
* `appId` the browser client initializes with — the attestation must match an app the project's
|
|
73
|
+
* App Check enforcement recognizes.
|
|
74
|
+
*
|
|
75
|
+
* When unset, no App Check token is minted and the response's `appCheckToken` is omitted.
|
|
76
|
+
*/
|
|
77
|
+
readonly appCheckAppId?: string;
|
|
78
|
+
/**
|
|
79
|
+
* Lifetime to request for the minted App Check token. Clamped to
|
|
80
|
+
* [{@link MIN_FIRESTORE_SESSION_APP_CHECK_TTL_MILLIS}, {@link MAX_FIRESTORE_SESSION_APP_CHECK_TTL_MILLIS}].
|
|
81
|
+
*
|
|
82
|
+
* Defaults to {@link DEFAULT_FIRESTORE_SESSION_APP_CHECK_TTL_MILLIS}.
|
|
83
|
+
*/
|
|
84
|
+
readonly appCheckTokenTtlMillis?: Milliseconds;
|
|
85
|
+
/**
|
|
86
|
+
* OIDC scope term an OIDC caller must hold to open a session. Defaults to
|
|
87
|
+
* {@link FIRESTORE_SESSION_OIDC_SCOPE}. Pass `null` to disable scope enforcement entirely (the admin
|
|
88
|
+
* predicate remains the real gate either way).
|
|
89
|
+
*/
|
|
90
|
+
readonly requiredScope?: Maybe<OidcScopeTerm>;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Resolves the effective App Check TTL from the module config, clamped to the Admin SDK's accepted range.
|
|
94
|
+
*
|
|
95
|
+
* @param ttlMillis - The configured TTL, or `undefined` to use the default.
|
|
96
|
+
* @returns The TTL to request, in milliseconds.
|
|
97
|
+
* @__NO_SIDE_EFFECTS__
|
|
98
|
+
*/
|
|
99
|
+
export declare function firestoreSessionAppCheckTtlMillis(ttlMillis: Maybe<Milliseconds>): Milliseconds;
|
|
100
|
+
/**
|
|
101
|
+
* The default {@link SessionApiModuleConfig.requiredScope}, re-exported for apps that want to widen it
|
|
102
|
+
* into an OR-group rather than replace it.
|
|
103
|
+
*/
|
|
104
|
+
export declare const DEFAULT_FIRESTORE_SESSION_REQUIRED_OIDC_SCOPE: OidcScopeTerm;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { type Request } from 'express';
|
|
2
|
+
import { FirestoreSessionApiService, type FirestoreSessionResult } from './session.api.service';
|
|
3
|
+
/**
|
|
4
|
+
* REST controller that hands an authenticated admin caller the credentials needed to talk to
|
|
5
|
+
* Firestore directly, rather than only through the model HTTP API.
|
|
6
|
+
*
|
|
7
|
+
* Mounted at `session` — under the `/api` global prefix the route becomes `GET /api/session/firestore`.
|
|
8
|
+
*
|
|
9
|
+
* Auth comes from the global OIDC bearer middleware, so `'/api/session'` MUST be listed in the OIDC
|
|
10
|
+
* module's `protectedPaths` (see `FIREBASE_SERVER_SESSION_API_PROTECTED_PATH`). Follows `McpController`:
|
|
11
|
+
* there are no NestJS guards in this codebase — protection is path-prefix middleware plus the
|
|
12
|
+
* per-endpoint checks in {@link FirestoreSessionApiService}.
|
|
13
|
+
*/
|
|
14
|
+
export declare class SessionApiController {
|
|
15
|
+
private readonly sessionService;
|
|
16
|
+
constructor(sessionService: FirestoreSessionApiService);
|
|
17
|
+
/**
|
|
18
|
+
* Mints a short-lived Firebase Auth custom token (+ App Check attestation, when configured) for the
|
|
19
|
+
* calling user.
|
|
20
|
+
*
|
|
21
|
+
* @param req - The Express request carrying auth credentials on `req.auth`.
|
|
22
|
+
* @returns The {@link FirestoreSessionResult} credential bundle.
|
|
23
|
+
*/
|
|
24
|
+
getFirestoreSession(req: Request): Promise<FirestoreSessionResult>;
|
|
25
|
+
private _toHttpException;
|
|
26
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { type ModuleMetadata } from '@nestjs/common';
|
|
2
|
+
import { type ClassType } from '@dereekb/util';
|
|
3
|
+
/**
|
|
4
|
+
* Configuration for {@link sessionApiModuleMetadata}.
|
|
5
|
+
*/
|
|
6
|
+
export interface SessionApiModuleMetadataConfig extends Pick<ModuleMetadata, 'imports' | 'exports' | 'providers'> {
|
|
7
|
+
/**
|
|
8
|
+
* Module that exports the session endpoint's dependencies.
|
|
9
|
+
*
|
|
10
|
+
* Should provide:
|
|
11
|
+
* - `FIRESTORE_SESSION_ADMIN_PREDICATE` — the app's admin check. Without it the endpoint rejects
|
|
12
|
+
* EVERY caller (fail-closed), and logs a warning at boot.
|
|
13
|
+
* - {@link SessionApiModuleConfig} — the registered web app's `appId` to mint App Check tokens for,
|
|
14
|
+
* plus optional TTL / scope overrides. Without it sessions carry no App Check attestation.
|
|
15
|
+
*
|
|
16
|
+
* `FIREBASE_APP_TOKEN` is not listed because `nestServerInstance` provides it globally.
|
|
17
|
+
*/
|
|
18
|
+
readonly dependencyModule: ClassType;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Generates NestJS module metadata for the direct-Firestore session API.
|
|
22
|
+
*
|
|
23
|
+
* Mirrors the convention used by `modelApiModuleMetadata` and `mcpModuleMetadata`: the consumer
|
|
24
|
+
* provides a dependency module exposing the required tokens, and this factory wires the controller +
|
|
25
|
+
* service.
|
|
26
|
+
*
|
|
27
|
+
* Remember to add `FIREBASE_SERVER_SESSION_API_PROTECTED_PATH` (`'/api/session'`) to the OIDC module's
|
|
28
|
+
* `protectedPaths` — the controller relies on the bearer middleware having populated `req.auth`.
|
|
29
|
+
*
|
|
30
|
+
* @param metadataConfig - Configuration including the dependency module.
|
|
31
|
+
* @returns NestJS module metadata exposing the session controller + service.
|
|
32
|
+
*
|
|
33
|
+
* @example
|
|
34
|
+
* ```typescript
|
|
35
|
+
* @Module({
|
|
36
|
+
* imports: [DemoApiAuthModule],
|
|
37
|
+
* providers: [
|
|
38
|
+
* { provide: SessionApiModuleConfig, useValue: { appCheckAppId: environment.firebase.appId } },
|
|
39
|
+
* { provide: FIRESTORE_SESSION_ADMIN_PREDICATE, useValue: (auth) => DEMO_AUTH_CLAIMS_SERVICE.toRoles(auth?.token ?? {}).has('admin') }
|
|
40
|
+
* ],
|
|
41
|
+
* exports: [SessionApiModuleConfig, FIRESTORE_SESSION_ADMIN_PREDICATE]
|
|
42
|
+
* })
|
|
43
|
+
* export class DemoSessionApiDependencyModule {}
|
|
44
|
+
*
|
|
45
|
+
* @Module(sessionApiModuleMetadata({ dependencyModule: DemoSessionApiDependencyModule }))
|
|
46
|
+
* export class DemoSessionApiModule {}
|
|
47
|
+
* ```
|
|
48
|
+
*/
|
|
49
|
+
export declare function sessionApiModuleMetadata(metadataConfig: SessionApiModuleMetadataConfig): ModuleMetadata;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import type * as admin from 'firebase-admin';
|
|
2
|
+
import { type ISO8601DateString, type Maybe } from '@dereekb/util';
|
|
3
|
+
import { type FirebaseAuthUserId } from '@dereekb/firebase';
|
|
4
|
+
import { type FirebaseServerAuthData } from '../auth.context.server';
|
|
5
|
+
import { type FirestoreSessionAdminPredicate, SessionApiModuleConfig } from './session.api.config';
|
|
6
|
+
/**
|
|
7
|
+
* Error code thrown when the caller is not authorized to open a direct-Firestore session.
|
|
8
|
+
*/
|
|
9
|
+
export declare const FIRESTORE_SESSION_FORBIDDEN_ERROR_CODE = "FIRESTORE_SESSION_FORBIDDEN_ERROR";
|
|
10
|
+
/**
|
|
11
|
+
* A short-lived credential bundle that lets a headless client connect directly to Firestore as the
|
|
12
|
+
* authenticated user, through the app's security rules.
|
|
13
|
+
*/
|
|
14
|
+
export interface FirestoreSessionResult {
|
|
15
|
+
/**
|
|
16
|
+
* The uid the session was minted for — the calling user's real Firebase Auth uid.
|
|
17
|
+
*/
|
|
18
|
+
readonly uid: FirebaseAuthUserId;
|
|
19
|
+
/**
|
|
20
|
+
* A Firebase Auth custom token to exchange via `signInWithCustomToken`. The user's stored
|
|
21
|
+
* `setCustomUserClaims` claims are spread at the TOP LEVEL of the exchanged ID token, so security
|
|
22
|
+
* rules reading `request.auth.token.<claim>` behave exactly as they do for the browser app.
|
|
23
|
+
*/
|
|
24
|
+
readonly customToken: string;
|
|
25
|
+
/**
|
|
26
|
+
* An App Check attestation minted for the project's registered web app, for clients that cannot run
|
|
27
|
+
* a browser-only attestation provider (reCAPTCHA v3). Feed it to `initializeAppCheck` through a
|
|
28
|
+
* `CustomProvider`.
|
|
29
|
+
*
|
|
30
|
+
* Omitted when the app did not configure {@link SessionApiModuleConfig.appCheckAppId}.
|
|
31
|
+
*/
|
|
32
|
+
readonly appCheckToken?: string;
|
|
33
|
+
/**
|
|
34
|
+
* When the session as a whole stops being usable — the earliest expiry among its credentials.
|
|
35
|
+
* A long-running client should re-fetch rather than assume one session covers the whole job.
|
|
36
|
+
*/
|
|
37
|
+
readonly expiresAt: ISO8601DateString;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Mints the direct-Firestore session credential bundle returned by `SessionApiController`.
|
|
41
|
+
*
|
|
42
|
+
* ## Security
|
|
43
|
+
*
|
|
44
|
+
* This service hands out a valid web-app App Check token and a custom token for the caller's own uid.
|
|
45
|
+
* Two gates apply, in order:
|
|
46
|
+
*
|
|
47
|
+
* 1. The app-supplied {@link FirestoreSessionAdminPredicate} — the load-bearing check. **Fails closed**
|
|
48
|
+
* when the app provides no predicate.
|
|
49
|
+
* 2. The OIDC scope requirement (default {@link FIRESTORE_SESSION_OIDC_SCOPE}) — defence in depth only.
|
|
50
|
+
* It cannot stand alone: a non-OIDC caller carries no `scope` claim and every enforcement site in
|
|
51
|
+
* this codebase treats that as "skip".
|
|
52
|
+
*
|
|
53
|
+
* The custom token is ALWAYS minted for `auth.uid`; there is no way to ask for someone else's session,
|
|
54
|
+
* so a granted session is exactly as privileged as the caller already is under Firestore rules.
|
|
55
|
+
*/
|
|
56
|
+
export declare class FirestoreSessionApiService {
|
|
57
|
+
private readonly _logger;
|
|
58
|
+
private readonly _app;
|
|
59
|
+
private readonly _config;
|
|
60
|
+
private readonly _adminPredicate;
|
|
61
|
+
constructor(app: admin.app.App, config?: SessionApiModuleConfig, adminPredicate?: FirestoreSessionAdminPredicate);
|
|
62
|
+
/**
|
|
63
|
+
* Mints a direct-Firestore session for the calling user after enforcing the admin predicate and the
|
|
64
|
+
* OIDC scope requirement.
|
|
65
|
+
*
|
|
66
|
+
* @param auth - The authenticated request's auth data (`req.auth`).
|
|
67
|
+
* @returns The credential bundle the client needs to connect to Firestore as this user.
|
|
68
|
+
* @throws {HttpsError} A `401` when the request carries no uid, or a `403` when either gate rejects the caller.
|
|
69
|
+
*/
|
|
70
|
+
createFirestoreSession(auth: Maybe<FirebaseServerAuthData>): Promise<FirestoreSessionResult>;
|
|
71
|
+
}
|
package/test/package.json
CHANGED
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dereekb/firebase-server/test",
|
|
3
|
-
"version": "13.
|
|
3
|
+
"version": "13.39.0",
|
|
4
|
+
"type": "module",
|
|
4
5
|
"peerDependencies": {
|
|
5
|
-
"@dereekb/analytics": "13.
|
|
6
|
-
"@dereekb/date": "13.
|
|
7
|
-
"@dereekb/firebase": "13.
|
|
8
|
-
"@dereekb/firebase-server": "13.
|
|
9
|
-
"@dereekb/firebase-server/oidc": "13.
|
|
10
|
-
"@dereekb/model": "13.
|
|
11
|
-
"@dereekb/nestjs": "13.
|
|
12
|
-
"@dereekb/rxjs": "13.
|
|
13
|
-
"@dereekb/util": "13.
|
|
6
|
+
"@dereekb/analytics": "13.39.0",
|
|
7
|
+
"@dereekb/date": "13.39.0",
|
|
8
|
+
"@dereekb/firebase": "13.39.0",
|
|
9
|
+
"@dereekb/firebase-server": "13.39.0",
|
|
10
|
+
"@dereekb/firebase-server/oidc": "13.39.0",
|
|
11
|
+
"@dereekb/model": "13.39.0",
|
|
12
|
+
"@dereekb/nestjs": "13.39.0",
|
|
13
|
+
"@dereekb/rxjs": "13.39.0",
|
|
14
|
+
"@dereekb/util": "13.39.0",
|
|
14
15
|
"@google-cloud/firestore": "^7.11.6",
|
|
15
16
|
"@google-cloud/storage": "^7.19.0",
|
|
16
17
|
"@nestjs/common": "^11.1.19",
|
|
@@ -23,18 +24,17 @@
|
|
|
23
24
|
"supertest": "^7.2.2"
|
|
24
25
|
},
|
|
25
26
|
"devDependencies": {
|
|
26
|
-
"@dereekb/nestjs": "13.
|
|
27
|
+
"@dereekb/nestjs": "13.39.0"
|
|
27
28
|
},
|
|
28
29
|
"exports": {
|
|
29
30
|
"./package.json": "./package.json",
|
|
30
31
|
".": {
|
|
31
|
-
"module": "./index.esm.js",
|
|
32
32
|
"types": "./index.d.ts",
|
|
33
|
-
"import": "./index.
|
|
34
|
-
"default": "./index.
|
|
33
|
+
"import": "./index.esm.js",
|
|
34
|
+
"default": "./index.esm.js"
|
|
35
35
|
}
|
|
36
36
|
},
|
|
37
37
|
"module": "./index.esm.js",
|
|
38
|
-
"main": "./index.
|
|
38
|
+
"main": "./index.esm.js",
|
|
39
39
|
"types": "./index.d.ts"
|
|
40
|
-
}
|
|
40
|
+
}
|
package/twilio/package.json
CHANGED
|
@@ -1,25 +1,25 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dereekb/firebase-server/twilio",
|
|
3
|
-
"version": "13.
|
|
3
|
+
"version": "13.39.0",
|
|
4
|
+
"type": "module",
|
|
4
5
|
"peerDependencies": {
|
|
5
|
-
"@dereekb/date": "13.
|
|
6
|
-
"@dereekb/firebase": "13.
|
|
7
|
-
"@dereekb/firebase-server": "13.
|
|
8
|
-
"@dereekb/model": "13.
|
|
9
|
-
"@dereekb/nestjs": "13.
|
|
10
|
-
"@dereekb/rxjs": "13.
|
|
11
|
-
"@dereekb/util": "13.
|
|
6
|
+
"@dereekb/date": "13.39.0",
|
|
7
|
+
"@dereekb/firebase": "13.39.0",
|
|
8
|
+
"@dereekb/firebase-server": "13.39.0",
|
|
9
|
+
"@dereekb/model": "13.39.0",
|
|
10
|
+
"@dereekb/nestjs": "13.39.0",
|
|
11
|
+
"@dereekb/rxjs": "13.39.0",
|
|
12
|
+
"@dereekb/util": "13.39.0"
|
|
12
13
|
},
|
|
13
14
|
"exports": {
|
|
14
15
|
"./package.json": "./package.json",
|
|
15
16
|
".": {
|
|
16
|
-
"module": "./index.esm.js",
|
|
17
17
|
"types": "./index.d.ts",
|
|
18
|
-
"import": "./index.
|
|
19
|
-
"default": "./index.
|
|
18
|
+
"import": "./index.esm.js",
|
|
19
|
+
"default": "./index.esm.js"
|
|
20
20
|
}
|
|
21
21
|
},
|
|
22
22
|
"module": "./index.esm.js",
|
|
23
|
-
"main": "./index.
|
|
23
|
+
"main": "./index.esm.js",
|
|
24
24
|
"types": "./index.d.ts"
|
|
25
|
-
}
|
|
25
|
+
}
|
package/zoho/package.json
CHANGED
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dereekb/firebase-server/zoho",
|
|
3
|
-
"version": "13.
|
|
3
|
+
"version": "13.39.0",
|
|
4
|
+
"type": "module",
|
|
4
5
|
"peerDependencies": {
|
|
5
|
-
"@dereekb/analytics": "13.
|
|
6
|
-
"@dereekb/date": "13.
|
|
7
|
-
"@dereekb/model": "13.
|
|
8
|
-
"@dereekb/nestjs": "13.
|
|
9
|
-
"@dereekb/rxjs": "13.
|
|
10
|
-
"@dereekb/firebase": "13.
|
|
11
|
-
"@dereekb/firebase-server": "13.
|
|
12
|
-
"@dereekb/util": "13.
|
|
13
|
-
"@dereekb/zoho": "13.
|
|
6
|
+
"@dereekb/analytics": "13.39.0",
|
|
7
|
+
"@dereekb/date": "13.39.0",
|
|
8
|
+
"@dereekb/model": "13.39.0",
|
|
9
|
+
"@dereekb/nestjs": "13.39.0",
|
|
10
|
+
"@dereekb/rxjs": "13.39.0",
|
|
11
|
+
"@dereekb/firebase": "13.39.0",
|
|
12
|
+
"@dereekb/firebase-server": "13.39.0",
|
|
13
|
+
"@dereekb/util": "13.39.0",
|
|
14
|
+
"@dereekb/zoho": "13.39.0",
|
|
14
15
|
"@nestjs/common": "^11.1.19",
|
|
15
16
|
"@nestjs/config": "^4.0.4",
|
|
16
17
|
"express": "^5.2.1"
|
|
@@ -18,13 +19,12 @@
|
|
|
18
19
|
"exports": {
|
|
19
20
|
"./package.json": "./package.json",
|
|
20
21
|
".": {
|
|
21
|
-
"module": "./index.esm.js",
|
|
22
22
|
"types": "./index.d.ts",
|
|
23
|
-
"import": "./index.
|
|
24
|
-
"default": "./index.
|
|
23
|
+
"import": "./index.esm.js",
|
|
24
|
+
"default": "./index.esm.js"
|
|
25
25
|
}
|
|
26
26
|
},
|
|
27
27
|
"module": "./index.esm.js",
|
|
28
|
-
"main": "./index.
|
|
28
|
+
"main": "./index.esm.js",
|
|
29
29
|
"types": "./index.d.ts"
|
|
30
|
-
}
|
|
30
|
+
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
exports._default = require('./index.cjs.js').default;
|