@dereekb/dbx-cli 13.37.0 → 13.38.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.
Files changed (69) hide show
  1. package/eslint/package.json +3 -3
  2. package/firebase-api-manifest/main.js +191 -166
  3. package/firebase-api-manifest/package.json +3 -3
  4. package/firestore-query-manifest/main.js +2063 -0
  5. package/firestore-query-manifest/package.json +13 -0
  6. package/firestore-rules/src/firestore-rules-scan.d.ts +89 -0
  7. package/firestore-rules/src/index.d.ts +1 -0
  8. package/generate-firestore-indexes/main.js +2 -2
  9. package/generate-firestore-indexes/package.json +2 -2
  10. package/generate-mcp-manifest/main.js +1 -0
  11. package/generate-mcp-manifest/package.json +3 -3
  12. package/generate-route-manifest/package.json +2 -2
  13. package/index.cjs.js +12672 -6861
  14. package/index.esm.js +12615 -6863
  15. package/lint-cache/package.json +2 -2
  16. package/manifest-extract/index.cjs.js +4 -1
  17. package/manifest-extract/index.esm.js +4 -1
  18. package/manifest-extract/package.json +2 -2
  19. package/manifest-extract/src/lib/types.d.ts +6 -0
  20. package/package.json +11 -6
  21. package/route/package.json +7 -7
  22. package/src/lib/api/firestore-session.client.d.ts +54 -0
  23. package/src/lib/api/get-args.helper.d.ts +4 -1
  24. package/src/lib/api/get-many.command.d.ts +8 -3
  25. package/src/lib/api/get.command.d.ts +13 -5
  26. package/src/lib/api/index.d.ts +1 -0
  27. package/src/lib/config/env.d.ts +106 -0
  28. package/src/lib/config/firestore-session.cache.d.ts +86 -0
  29. package/src/lib/config/index.d.ts +1 -0
  30. package/src/lib/config/paths.d.ts +3 -1
  31. package/src/lib/context/cli.context.d.ts +87 -3
  32. package/src/lib/doctor/firestore-session.check.d.ts +113 -0
  33. package/src/lib/doctor/index.d.ts +1 -0
  34. package/src/lib/firestore/firestore-get.command.d.ts +28 -0
  35. package/src/lib/firestore/firestore-queries.command.d.ts +26 -0
  36. package/src/lib/firestore/firestore-query.command.d.ts +25 -0
  37. package/src/lib/firestore/firestore.accessor.d.ts +85 -0
  38. package/src/lib/firestore/firestore.collection.d.ts +44 -0
  39. package/src/lib/firestore/firestore.error.d.ts +14 -0
  40. package/src/lib/firestore/firestore.models.d.ts +223 -0
  41. package/src/lib/firestore/firestore.query-params.d.ts +34 -0
  42. package/src/lib/firestore/firestore.query.d.ts +79 -0
  43. package/src/lib/firestore/firestore.read.d.ts +180 -0
  44. package/src/lib/firestore/firestore.session.d.ts +97 -0
  45. package/src/lib/firestore/index.d.ts +13 -0
  46. package/src/lib/firestore/query-info-utils.d.ts +48 -0
  47. package/src/lib/firestore/query-registry.d.ts +32 -0
  48. package/src/lib/index.d.ts +3 -0
  49. package/src/lib/manifest/types.d.ts +117 -0
  50. package/src/lib/mcp-scan/manifest/dbx-docs-ui-examples-schema.d.ts +2 -2
  51. package/src/lib/mcp-scan/manifest/model-snapshot-fields-schema.d.ts +2 -2
  52. package/src/lib/mcp-scan/manifest/pipes-schema.d.ts +2 -2
  53. package/src/lib/mcp-scan/manifest/ui-components-schema.d.ts +2 -2
  54. package/src/lib/mcp-scan/manifest/utils-schema.d.ts +2 -2
  55. package/src/lib/mcp-scan/scan/dbx-docs-ui-examples-extract.d.ts +1 -1
  56. package/src/lib/mcp-scan/scan/extract-models/types.d.ts +5 -0
  57. package/src/lib/mcp-scan/scan/ui-components-extract.d.ts +1 -1
  58. package/src/lib/middleware/auth.middleware.d.ts +6 -0
  59. package/src/lib/runner/run.d.ts +28 -1
  60. package/src/lib/scan-helpers/emit-generated-ts.d.ts +76 -0
  61. package/src/lib/scan-helpers/exported-from-package.d.ts +29 -0
  62. package/src/lib/util/index.d.ts +1 -0
  63. package/src/lib/util/output.d.ts +10 -0
  64. package/src/lib/util/stdin.d.ts +39 -2
  65. package/src/lib/util/table.d.ts +40 -0
  66. package/test/index.cjs.js +4 -2
  67. package/test/index.esm.js +4 -2
  68. package/test/package.json +9 -9
  69. package/test/src/lib/cli-test.d.ts +12 -2
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dereekb/dbx-cli-lint-cache",
3
- "version": "13.37.0",
3
+ "version": "13.38.0",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "devDependencies": {
@@ -8,7 +8,7 @@
8
8
  "eslint": "10.4.0"
9
9
  },
10
10
  "peerDependencies": {
11
- "@dereekb/util": "13.37.0",
11
+ "@dereekb/util": "13.38.0",
12
12
  "yargs": "^18.0.0"
13
13
  }
14
14
  }
@@ -828,6 +828,7 @@ function buildInterface(decl) {
828
828
  var jsDocs = decl.getJsDocs();
829
829
  var hasDbxModelTag = jsDocsHaveTag(jsDocs, 'dbxModel');
830
830
  var dbxModelRead = readDbxModelReadTag(jsDocs);
831
+ var dbxModelServerOnly = jsDocsHaveTag(jsDocs, 'dbxModelServerOnly');
831
832
  var mcpToolNameSegment = readMcpToolNameSegmentTag(jsDocs);
832
833
  var extendsNames = decl.getExtends().map(dbxCli.resolveExtendsName);
833
834
  var props = [];
@@ -873,7 +874,9 @@ function buildInterface(decl) {
873
874
  props: props
874
875
  }, dbxModelRead === undefined ? {} : {
875
876
  dbxModelRead: dbxModelRead
876
- }, mcpToolNameSegment === undefined ? {} : {
877
+ }, dbxModelServerOnly ? {
878
+ dbxModelServerOnly: true
879
+ } : {}, mcpToolNameSegment === undefined ? {} : {
877
880
  mcpToolNameSegment: mcpToolNameSegment
878
881
  });
879
882
  }
@@ -826,6 +826,7 @@ function buildInterface(decl) {
826
826
  var jsDocs = decl.getJsDocs();
827
827
  var hasDbxModelTag = jsDocsHaveTag(jsDocs, 'dbxModel');
828
828
  var dbxModelRead = readDbxModelReadTag(jsDocs);
829
+ var dbxModelServerOnly = jsDocsHaveTag(jsDocs, 'dbxModelServerOnly');
829
830
  var mcpToolNameSegment = readMcpToolNameSegmentTag(jsDocs);
830
831
  var extendsNames = decl.getExtends().map(resolveExtendsName);
831
832
  var props = [];
@@ -871,7 +872,9 @@ function buildInterface(decl) {
871
872
  props: props
872
873
  }, dbxModelRead === undefined ? {} : {
873
874
  dbxModelRead: dbxModelRead
874
- }, mcpToolNameSegment === undefined ? {} : {
875
+ }, dbxModelServerOnly ? {
876
+ dbxModelServerOnly: true
877
+ } : {}, mcpToolNameSegment === undefined ? {} : {
875
878
  mcpToolNameSegment: mcpToolNameSegment
876
879
  });
877
880
  }
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "@dereekb/dbx-cli/manifest-extract",
3
- "version": "13.37.0",
3
+ "version": "13.38.0",
4
4
  "sideEffects": false,
5
5
  "peerDependencies": {
6
6
  "@dereekb/date": "13.15.0",
7
- "@dereekb/dbx-cli": "13.37.0",
7
+ "@dereekb/dbx-cli": "13.38.0",
8
8
  "@dereekb/firebase": "13.15.0",
9
9
  "@dereekb/model": "13.15.0",
10
10
  "@dereekb/nestjs": "13.15.0",
@@ -156,6 +156,12 @@ export interface ModelExtractionInterface {
156
156
  * / `permissions`). Absent when the interface omits the tag or declares an invalid value.
157
157
  */
158
158
  readonly dbxModelRead?: 'system' | 'owner' | 'admin-only' | 'permissions';
159
+ /**
160
+ * True when the interface carries `@dbxModelServerOnly` — the model has no client read grant in
161
+ * `firestore.rules` and must be refused on the model API too. See
162
+ * `FirebaseModelServiceConfig.serverOnly`, the runtime half of the same declaration.
163
+ */
164
+ readonly dbxModelServerOnly?: boolean;
159
165
  /**
160
166
  * Per-model MCP tool-name segment from `@dbxModelMcpToolNameSegment <segment>`. Replaces the model
161
167
  * type in generated tool names (e.g. the collection prefix). Absent when the tag is omitted or invalid.
package/package.json CHANGED
@@ -1,11 +1,12 @@
1
1
  {
2
2
  "name": "@dereekb/dbx-cli",
3
- "version": "13.37.0",
3
+ "version": "13.38.0",
4
4
  "type": "module",
5
5
  "sideEffects": false,
6
6
  "bin": {
7
7
  "dbx-cli-generate-firebase-api-manifest": "firebase-api-manifest/main.js",
8
8
  "dbx-cli-generate-firestore-indexes": "generate-firestore-indexes/main.js",
9
+ "dbx-cli-generate-firestore-query-manifest": "firestore-query-manifest/main.js",
9
10
  "dbx-cli-generate-mcp-manifest": "generate-mcp-manifest/main.js",
10
11
  "dbx-cli-generate-route-manifest": "generate-route-manifest/main.js",
11
12
  "dbx-cli-lint-cache": "lint-cache/main.js"
@@ -20,6 +21,9 @@
20
21
  "./firebase-api-manifest": {
21
22
  "default": "./firebase-api-manifest/main.js"
22
23
  },
24
+ "./firestore-query-manifest": {
25
+ "default": "./firestore-query-manifest/main.js"
26
+ },
23
27
  "./generate-firestore-indexes": {
24
28
  "default": "./generate-firestore-indexes/main.js"
25
29
  },
@@ -66,13 +70,14 @@
66
70
  }
67
71
  },
68
72
  "peerDependencies": {
69
- "@dereekb/date": "13.37.0",
70
- "@dereekb/firebase": "13.37.0",
71
- "@dereekb/model": "13.37.0",
72
- "@dereekb/nestjs": "13.37.0",
73
- "@dereekb/util": "13.37.0",
73
+ "@dereekb/date": "13.38.0",
74
+ "@dereekb/firebase": "13.38.0",
75
+ "@dereekb/model": "13.38.0",
76
+ "@dereekb/nestjs": "13.38.0",
77
+ "@dereekb/util": "13.38.0",
74
78
  "@nestjs/common": "^11.1.19",
75
79
  "arktype": "^2.2.0",
80
+ "firebase": "^12.12.1",
76
81
  "jiti": "2.6.1",
77
82
  "prettier": "3.8.3",
78
83
  "ts-morph": "^21.0.0",
@@ -1,14 +1,14 @@
1
1
  {
2
2
  "name": "@dereekb/dbx-cli/route",
3
- "version": "13.37.0",
3
+ "version": "13.38.0",
4
4
  "sideEffects": false,
5
5
  "peerDependencies": {
6
- "@dereekb/date": "13.37.0",
7
- "@dereekb/dbx-cli": "13.37.0",
8
- "@dereekb/firebase": "13.37.0",
9
- "@dereekb/model": "13.37.0",
10
- "@dereekb/nestjs": "13.37.0",
11
- "@dereekb/util": "13.37.0"
6
+ "@dereekb/date": "13.38.0",
7
+ "@dereekb/dbx-cli": "13.38.0",
8
+ "@dereekb/firebase": "13.38.0",
9
+ "@dereekb/model": "13.38.0",
10
+ "@dereekb/nestjs": "13.38.0",
11
+ "@dereekb/util": "13.38.0"
12
12
  },
13
13
  "exports": {
14
14
  "./package.json": "./package.json",
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Path (relative to the API base URL) of the direct-Firestore session endpoint served by
3
+ * `@dereekb/firebase-server`'s `SessionApiController`.
4
+ *
5
+ * Duplicated here rather than imported so `dbx-cli` keeps no dependency on the server package —
6
+ * the same arrangement `CALL_MODEL_API_PATH` uses.
7
+ */
8
+ export declare const FIRESTORE_SESSION_API_PATH = "/session/firestore";
9
+ /**
10
+ * The credential bundle `GET <apiBaseUrl>/session/firestore` returns.
11
+ */
12
+ export interface CliFirestoreSession {
13
+ /**
14
+ * The uid the session was minted for.
15
+ */
16
+ readonly uid: string;
17
+ /**
18
+ * A Firebase Auth custom token to exchange via `signInWithCustomToken`.
19
+ */
20
+ readonly customToken: string;
21
+ /**
22
+ * An App Check attestation minted server-side for the project's registered web app. Absent when
23
+ * the API has no `appCheckAppId` configured (a project that does not enforce App Check).
24
+ */
25
+ readonly appCheckToken?: string;
26
+ /**
27
+ * ISO timestamp at which the session's shortest-lived credential expires.
28
+ */
29
+ readonly expiresAt: string;
30
+ }
31
+ export interface FetchFirestoreSessionInput {
32
+ /**
33
+ * The API base URL — typically `<host>/<project>/us-central1/api` or `https://<domain>/api`.
34
+ *
35
+ * The `/session/firestore` path is appended automatically.
36
+ */
37
+ readonly apiBaseUrl: string;
38
+ readonly accessToken: string;
39
+ /**
40
+ * Custom fetch implementation for tests.
41
+ */
42
+ readonly fetcher?: typeof fetch;
43
+ }
44
+ /**
45
+ * Fetches a direct-Firestore session from the API with the cached Bearer access token.
46
+ *
47
+ * The endpoint is admin-only and additionally gated on the `session.firestore` OIDC scope, so a 403
48
+ * here usually means the logged-in user is not an admin or logged in without that scope.
49
+ *
50
+ * @param input - The API target, access token, and optional fetch override.
51
+ * @returns The parsed {@link CliFirestoreSession}.
52
+ * @throws {CliError} When the endpoint answers non-2xx or returns an unusable body.
53
+ */
54
+ export declare function fetchFirestoreSession(input: FetchFirestoreSessionInput): Promise<CliFirestoreSession>;
@@ -49,7 +49,10 @@ export declare function parseGetArgs(input: {
49
49
  * `modelType` or a {@link CliError} is thrown (the backend route is single-modelType per call).
50
50
  * 2. Otherwise `firstArg` is treated as the explicit `modelType` and `rest` are the keys.
51
51
  *
52
- * Always rejects empty key lists and lists exceeding 50 keys.
52
+ * Always rejects an empty key list. There is deliberately NO length cap: `get-many -` reads its keys
53
+ * from stdin and is unbounded, and both read paths batch for themselves —
54
+ * `getMultipleModelsOverHttpChunked` chunks at 50 per request, and `getMultipleModelsOverFirestore`
55
+ * holds a sliding window of 50 in-flight reads.
53
56
  *
54
57
  * @param input - Positionals captured by yargs plus the optional model manifest.
55
58
  * @param input.firstArg - The first positional from yargs.
@@ -4,12 +4,17 @@ import type { CommandModule } from 'yargs';
4
4
  *
5
5
  * Batch-reads Firestore documents by key. The first positional can either be an explicit
6
6
  * modelType (followed by ≥1 keys) or a full key (followed by additional keys whose prefixes
7
- * must resolve to the same modelType). Beyond 50 keys the request is automatically chunked
8
- * via `context.getMultipleModels`.
7
+ * must resolve to the same modelType). Beyond 50 keys the API request is automatically chunked
8
+ * via `context.getMultipleModels`; the direct path has no per-request cap and issues the reads
9
+ * concurrently.
9
10
  *
10
11
  * Stdin: pass `-` as the only positional to read whitespace-separated keys from stdin
11
12
  * (e.g. `cat keys.txt | <cli> get-many -`).
12
13
  *
13
- * Backend: `POST <apiBaseUrl>/model/<modelType>/get` with body `{ keys }` (ModelApiController.getMany).
14
+ * Transport is chosen by `--via` (see {@link CLI_READ_VIA_EPILOGUE}). Both paths emit the identical
15
+ * `{ results, errors }` envelope, so `--via` is observable only through `meta.source`.
16
+ *
17
+ * Backends: `POST <apiBaseUrl>/model/<modelType>/get` with body `{ keys }`
18
+ * (ModelApiController.getMany), or direct Firestore reads through the app's security rules.
14
19
  */
15
20
  export declare const GET_MANY_COMMAND: CommandModule;
@@ -1,12 +1,20 @@
1
1
  import type { CommandModule } from 'yargs';
2
+ /**
3
+ * Epilogue shared by the routed read commands, documenting what `--via` does and does not do.
4
+ */
5
+ export declare const CLI_READ_VIA_EPILOGUE: string;
2
6
  /**
3
7
  * Top-level `get <modelOrKey> [key]` command.
4
8
  *
5
- * Reads a single Firestore document via the typed model-access endpoint. The `model` arg is optional:
6
- * when only one positional is supplied, the CLI resolves the modelType from the key's leading
7
- * collection-name prefix via {@link decodeFirestoreModelKey}. The two-positional form passes the
8
- * explicit `modelType` straight through.
9
+ * Reads a single Firestore document. The `model` arg is optional: when only one positional is
10
+ * supplied, the CLI resolves the modelType from the key's leading collection-name prefix via
11
+ * {@link decodeFirestoreModelKey}. The two-positional form passes the explicit `modelType` straight
12
+ * through.
13
+ *
14
+ * Transport is chosen by `--via` (see {@link CLI_READ_VIA_EPILOGUE}). Both paths emit the identical
15
+ * `{ key, data }` envelope, so `--via` is observable only through `meta.source`.
9
16
  *
10
- * Backend: `GET <apiBaseUrl>/model/<modelType>/get?key=<key>` (ModelApiController.getOne).
17
+ * Backends: `GET <apiBaseUrl>/model/<modelType>/get?key=<key>` (ModelApiController.getOne), or a
18
+ * direct Firestore read through the app's security rules as the authenticated user.
11
19
  */
12
20
  export declare const GET_COMMAND: CommandModule;
@@ -2,6 +2,7 @@ export * from './call-model.client';
2
2
  export * from './call-model.command.factory';
3
3
  export * from './call.passthrough.command';
4
4
  export * from './expand-keys';
5
+ export * from './firestore-session.client';
5
6
  export * from './get-args.helper';
6
7
  export * from './get.command';
7
8
  export * from './get-many.command';
@@ -102,6 +102,97 @@ export interface MergeCliEnvWithDefaultInput {
102
102
  * @returns The merged {@link CliEnvConfig}, or `undefined` when both inputs are empty.
103
103
  */
104
104
  export declare function mergeCliEnvWithDefault(input: MergeCliEnvWithDefaultInput): Maybe<CliEnvConfig>;
105
+ /**
106
+ * Merges a stored Firebase client config on top of a default one, field by field, so an env can
107
+ * override just the `projectId` of a registered default without restating the whole block.
108
+ *
109
+ * @param env - The user's persisted Firebase config, if any.
110
+ * @param defaultEnv - The registered default's Firebase config, if any.
111
+ * @returns The merged config, or `undefined` when neither side supplies one.
112
+ */
113
+ export declare function mergeCliFirebaseConfig(env: Maybe<CliFirebaseConfig>, defaultEnv: Maybe<CliFirebaseConfig>): CliFirebaseConfig | undefined;
114
+ /**
115
+ * Local Firebase emulator targets for a CLI env.
116
+ *
117
+ * Mirrors the semantics of `DbxFirebaseEmulatorsConfig` in `@dereekb/dbx-firebase` (whose parse
118
+ * helper is Angular-bound and not reusable here): the presence of this object means "use emulators"
119
+ * unless {@link useEmulators} is explicitly `false`.
120
+ *
121
+ * App Check is auto-disabled whenever emulators are in use — the emulators do not verify
122
+ * attestations, and `initializeAppCheck` against a fake project only gets in the way.
123
+ */
124
+ export interface CliFirebaseEmulatorsConfig {
125
+ /**
126
+ * Set `false` to keep the emulator targets configured but inactive. Defaults to `true`.
127
+ */
128
+ readonly useEmulators?: boolean;
129
+ /**
130
+ * Host the emulators are reachable at. Defaults to {@link DEFAULT_CLI_FIREBASE_EMULATOR_HOST}.
131
+ */
132
+ readonly host?: string;
133
+ /**
134
+ * Port of the Auth emulator. When unset, Auth is not redirected to an emulator.
135
+ */
136
+ readonly authPort?: number;
137
+ /**
138
+ * Port of the Firestore emulator. When unset, Firestore is not redirected to an emulator.
139
+ */
140
+ readonly firestorePort?: number;
141
+ }
142
+ /**
143
+ * Firebase client-SDK configuration for a CLI env, used only by the direct-Firestore session
144
+ * (`CliContext.getFirestoreContext`). Everything else the CLI does goes over the model HTTP API and
145
+ * needs none of this.
146
+ *
147
+ * These are the same public values the app's browser client initializes with — copy them from the
148
+ * target app's environment file. `appId` in particular must be the registered **web** app, since the
149
+ * server mints its App Check attestation for that app.
150
+ */
151
+ export interface CliFirebaseConfig {
152
+ /**
153
+ * The Firebase web API key.
154
+ */
155
+ readonly apiKey?: string;
156
+ /**
157
+ * The project's auth domain (e.g. `my-project.firebaseapp.com`).
158
+ */
159
+ readonly authDomain?: string;
160
+ /**
161
+ * The Firebase project id.
162
+ */
163
+ readonly projectId?: string;
164
+ /**
165
+ * The registered **web** app id (e.g. `1:1234567890:web:abcdef`).
166
+ */
167
+ readonly appId?: string;
168
+ /**
169
+ * Optional emulator targets for local development.
170
+ */
171
+ readonly emulators?: CliFirebaseEmulatorsConfig;
172
+ }
173
+ /**
174
+ * Default host used for Firebase emulator connections when a {@link CliFirebaseEmulatorsConfig}
175
+ * omits one.
176
+ */
177
+ export declare const DEFAULT_CLI_FIREBASE_EMULATOR_HOST = "localhost";
178
+ /**
179
+ * Returns true when the env carries the minimum Firebase client config needed to open a direct
180
+ * Firestore session.
181
+ *
182
+ * Deliberately separate from {@link isCliEnvConfigComplete}: the Firebase config is optional, and
183
+ * folding it into the general completeness check would break every CLI that only uses the model API.
184
+ *
185
+ * @param firebase - The env's Firebase client config, if any.
186
+ * @returns `true` when `apiKey`, `projectId`, and `appId` are all present and non-empty.
187
+ */
188
+ export declare function isCliFirebaseConfigComplete(firebase: Maybe<CliFirebaseConfig>): firebase is Required<Pick<CliFirebaseConfig, 'apiKey' | 'projectId' | 'appId'>> & CliFirebaseConfig;
189
+ /**
190
+ * Returns true when the env's emulator config is present and active.
191
+ *
192
+ * @param firebase - The env's Firebase client config, if any.
193
+ * @returns `true` when emulators are configured and not explicitly disabled.
194
+ */
195
+ export declare function cliFirebaseEmulatorsInUse(firebase: Maybe<CliFirebaseConfig>): boolean;
105
196
  /**
106
197
  * Environment-targeting config for a CLI invocation.
107
198
  *
@@ -154,6 +245,12 @@ export interface CliEnvConfig {
154
245
  * Space-separated OAuth scopes to request. Defaults to {@link DEFAULT_CLI_OIDC_SCOPES}.
155
246
  */
156
247
  readonly scopes?: string;
248
+ /**
249
+ * Optional Firebase client config enabling the direct-Firestore session
250
+ * (`CliContext.getFirestoreContext()`). Optional by design — a CLI that only calls the model API
251
+ * never needs it, and requiring it would break every existing consumer.
252
+ */
253
+ readonly firebase?: CliFirebaseConfig;
157
254
  }
158
255
  /**
159
256
  * Resolves the active env name from a flag, env-var, or the persisted config default.
@@ -189,6 +286,15 @@ export declare function resolveActiveEnvName(input: ResolveActiveEnvInput): Mayb
189
286
  * - `DEMO_CLI_CLIENT_SECRET`
190
287
  * - `DEMO_CLI_REDIRECT_URI`
191
288
  * - `DEMO_CLI_SCOPES`
289
+ *
290
+ * Plus the optional direct-Firestore session config:
291
+ * - `DEMO_CLI_FIREBASE_API_KEY`
292
+ * - `DEMO_CLI_FIREBASE_AUTH_DOMAIN`
293
+ * - `DEMO_CLI_FIREBASE_PROJECT_ID`
294
+ * - `DEMO_CLI_FIREBASE_APP_ID`
295
+ * - `DEMO_CLI_FIREBASE_EMULATOR_HOST`
296
+ * - `DEMO_CLI_FIREBASE_AUTH_EMULATOR_PORT`
297
+ * - `DEMO_CLI_FIREBASE_FIRESTORE_EMULATOR_PORT`
192
298
  */
193
299
  export interface EnvVarOverrideInput {
194
300
  readonly cliName: string;
@@ -0,0 +1,86 @@
1
+ import { type AsyncKeyedValueCache, type Maybe } from '@dereekb/util';
2
+ import { type CliFirestoreSession } from '../api/firestore-session.client';
3
+ /**
4
+ * Hard ceiling on how long a minted direct-Firestore session may be reused, regardless of what the
5
+ * API reported in `expiresAt`.
6
+ *
7
+ * One hour, because that is the Firebase ceiling the credentials themselves sit under: a custom
8
+ * token is exchangeable for one hour, and the ID token it mints lives one hour. Holding a session
9
+ * past that buys nothing — the sign-in would fail — and re-minting is one HTTP round-trip.
10
+ */
11
+ export declare const CLI_FIRESTORE_SESSION_MAX_CACHE_MS: number;
12
+ /**
13
+ * Default skew/latency buffer applied when deciding whether a cached session is still usable.
14
+ */
15
+ export declare const CLI_FIRESTORE_SESSION_EXPIRY_BUFFER_MS = 60000;
16
+ /**
17
+ * A cached direct-Firestore session for a single env.
18
+ *
19
+ * Stores the credential envelope the API minted, not the live Firebase objects — those are
20
+ * per-process and cannot be serialized. A cache hit still signs in; it just skips the
21
+ * `GET /session/firestore` round-trip.
22
+ */
23
+ export interface CliFirestoreSessionEntry {
24
+ /**
25
+ * The credential bundle returned by `GET /session/firestore`.
26
+ */
27
+ readonly session: CliFirestoreSession;
28
+ /**
29
+ * Unix epoch milliseconds at which the entry was written.
30
+ */
31
+ readonly cachedAt: number;
32
+ /**
33
+ * The uid the entry was minted for, denormalized so a stale entry belonging to a different user
34
+ * can be detected without parsing the custom token.
35
+ */
36
+ readonly uid: string;
37
+ }
38
+ /**
39
+ * Firestore session cache shape on disk — keyed by env name.
40
+ */
41
+ export type CliFirestoreSessionCache = Record<string, CliFirestoreSessionEntry>;
42
+ /**
43
+ * Session cache store keyed by env name.
44
+ *
45
+ * Backed by a single JSON file with per-process in-memory memoization, exactly like the token
46
+ * cache — see {@link createMemoizedJsonFileAsyncKeyedValueCache}.
47
+ */
48
+ export type CliFirestoreSessionCacheStore = AsyncKeyedValueCache<CliFirestoreSessionEntry>;
49
+ export interface CreateCliFirestoreSessionCacheStoreInput {
50
+ readonly firestoreSessionCachePath: string;
51
+ }
52
+ /**
53
+ * Creates a per-env direct-Firestore session cache store backed by a single JSON file.
54
+ *
55
+ * Entries are written with mode 0o600 — they hold a Firebase custom token, which is a bearer
56
+ * credential for the user it was minted for.
57
+ *
58
+ * @param input - The cache store inputs.
59
+ * @param input.firestoreSessionCachePath - Absolute path to the JSON file backing the cache.
60
+ * @returns A {@link CliFirestoreSessionCacheStore} keyed by env name.
61
+ * @__NO_SIDE_EFFECTS__
62
+ */
63
+ export declare function createCliFirestoreSessionCacheStore(input: CreateCliFirestoreSessionCacheStoreInput): CliFirestoreSessionCacheStore;
64
+ /**
65
+ * Resolves the epoch-millis instant at which a cached session stops being usable.
66
+ *
67
+ * The effective expiry is the EARLIER of the API-reported `expiresAt` and
68
+ * {@link CLI_FIRESTORE_SESSION_MAX_CACHE_MS} past the write. Taking the earlier of the two means a
69
+ * server that reports an over-long (or unparsable) window still cannot push a session past the
70
+ * Firebase credential ceiling.
71
+ *
72
+ * @param entry - The cached entry.
73
+ * @returns The effective expiry in unix epoch milliseconds.
74
+ *
75
+ * @__NO_SIDE_EFFECTS__
76
+ */
77
+ export declare function cliFirestoreSessionEntryExpiresAt(entry: CliFirestoreSessionEntry): number;
78
+ /**
79
+ * Returns true when the cached session is at or near its effective expiry.
80
+ *
81
+ * @param entry - The cached entry (`null`/`undefined` is treated as expired).
82
+ * @param nowMs - The current time in unix epoch milliseconds. Defaults to `Date.now()`.
83
+ * @param bufferMs - Skew/latency buffer; the entry is treated as expired this far ahead of its effective expiry.
84
+ * @returns `true` when the entry is unusable, otherwise `false`.
85
+ */
86
+ export declare function isCliFirestoreSessionExpired(entry: Maybe<CliFirestoreSessionEntry>, nowMs?: number, bufferMs?: number): boolean;
@@ -1,5 +1,6 @@
1
1
  export * from './cli.config';
2
2
  export * from './env';
3
+ export * from './firestore-session.cache';
3
4
  export * from './env.resolve';
4
5
  export * from './paths';
5
6
  export * from './token.cache';
@@ -5,6 +5,7 @@ export interface CliPaths {
5
5
  readonly configDir: string;
6
6
  readonly configFilePath: string;
7
7
  readonly tokenCachePath: string;
8
+ readonly firestoreSessionCachePath: string;
8
9
  }
9
10
  export interface CliPathsConfig {
10
11
  /**
@@ -24,11 +25,12 @@ export interface CliPathsConfig {
24
25
  * Layout:
25
26
  * - `<configDir>/config.json` — the persistent CLI config (envs, output settings)
26
27
  * - `<configDir>/.tokens.json` — per-env access/refresh token cache (mode 0600)
28
+ * - `<configDir>/.firestore-sessions.json` — per-env direct-Firestore session cache (mode 0600)
27
29
  *
28
30
  * @param config - The path-building inputs.
29
31
  * @param config.cliName - The CLI's binary name; the default config dir is `~/.<cliName>`.
30
32
  * @param config.configDirOverride - Optional override that replaces the default config directory verbatim (used by tests).
31
- * @returns The {@link CliPaths} pointing at `configDir`, the config file, and the token cache file.
33
+ * @returns The {@link CliPaths} pointing at `configDir`, the config file, the token cache file, and the Firestore session cache file.
32
34
  * @__NO_SIDE_EFFECTS__
33
35
  */
34
36
  export declare function buildCliPaths(config: CliPathsConfig): CliPaths;
@@ -1,6 +1,10 @@
1
- import type { OnCallTypedModelParams } from '@dereekb/firebase';
1
+ import type { FirestoreContext, OnCallTypedModelParams } from '@dereekb/firebase';
2
+ import type { Maybe } from '@dereekb/util';
2
3
  import { type CliEnvConfig } from '../config/env';
4
+ import { type CliFirestoreSessionCacheStore } from '../config/firestore-session.cache';
3
5
  import { type GetModelOverHttpResult, type GetMultipleModelsOverHttpResult } from '../api/call-model.client';
6
+ import { type CliFirestoreBinding, type CliFirestoreModels } from '../firestore/firestore.models';
7
+ import { type CliFirestoreSessionContext } from '../firestore/firestore.session';
4
8
  import { type CliModelManifest } from '../manifest/types';
5
9
  /**
6
10
  * The CLI context attached to argv by the auth middleware.
@@ -23,9 +27,76 @@ export interface CliContext {
23
27
  readonly getModel: <TResult = unknown>(modelType: string, key: string) => Promise<GetModelOverHttpResult<TResult>>;
24
28
  readonly getMultipleModels: <TResult = unknown>(modelType: string, keys: ReadonlyArray<string>) => Promise<GetMultipleModelsOverHttpResult<TResult>>;
25
29
  readonly modelManifest?: CliModelManifest;
30
+ /**
31
+ * Opens (once per invocation) a direct Firestore connection as the authenticated user and returns
32
+ * the `FirestoreContext` an app's `make<App>FirestoreCollections(context)` factory consumes — the
33
+ * same object the Angular app builds, so the same queries run through the same security rules.
34
+ *
35
+ * Lazy on purpose: the handshake costs an HTTP round-trip plus a sign-in, and the auth middleware
36
+ * builds a context on EVERY invocation, including ones that never touch Firestore. Memoized within
37
+ * the invocation, and — when the runner supplies a session cache — the minted credential envelope
38
+ * is reused ACROSS invocations for up to an hour, so only the sign-in is repaid.
39
+ *
40
+ * OPTIONAL so that a hand-built test context (see `createPassthroughAuthMiddleware`) stays valid.
41
+ * Calling it throws when the env carries no Firebase client config — there is no fallback to the
42
+ * HTTP model API by design.
43
+ */
44
+ readonly getFirestoreContext?: () => Promise<FirestoreContext>;
45
+ /**
46
+ * The full session behind {@link CliContext.getFirestoreContext} — the signed-in `Auth`, the raw
47
+ * `Firestore`, and the minted credential envelope. Memoized alongside it, so calling both opens one
48
+ * session.
49
+ */
50
+ readonly getFirestoreSession?: () => Promise<CliFirestoreSessionContext>;
51
+ /**
52
+ * The app's models bound to the direct-Firestore session — what `firestore-get` /
53
+ * `firestore-query` dispatch through. Layered on the {@link CliContext.getFirestoreSession} memo,
54
+ * so all three thunks share ONE session, and separately memoized itself, so the app's collections
55
+ * object is built ONCE per invocation rather than once per call.
56
+ *
57
+ * Present only when the CLI was configured with a `firestore` binding (`runCli({ firestore })`).
58
+ */
59
+ readonly getFirestoreModels?: () => Promise<CliFirestoreModels>;
60
+ /**
61
+ * Releases the direct-Firestore session opened during this invocation, if any.
62
+ *
63
+ * {@link runCli} calls this once the command has finished. Without it the CLI PRINTS ITS RESULT AND
64
+ * THEN HANGS: the signed-in `Auth` and live `Firestore` keep the Node event loop alive, so the
65
+ * process never exits and a caller piping the output waits forever.
66
+ *
67
+ * Never opens a session in order to close one — a command that never touched Firestore resolves
68
+ * immediately. Safe to call more than once.
69
+ */
70
+ readonly closeFirestoreSession?: () => Promise<void>;
26
71
  }
27
- export declare const setCliContext: (value: import("@dereekb/util").Maybe<CliContext>) => void;
28
- export declare const getCliContext: () => import("@dereekb/util").Maybe<CliContext>;
72
+ /**
73
+ * Returns the context's {@link CliFirestoreSessionContext} thunk result, or throws a {@link CliError}
74
+ * when the context has none.
75
+ *
76
+ * {@link CliContext.getFirestoreSession} is optional so a hand-built test context stays valid, which
77
+ * would otherwise leave every consumer action writing the same guard. Every context built by
78
+ * {@link createCliContext} does provide it, so reaching this error in practice means the action ran
79
+ * against a context that was assembled by hand.
80
+ *
81
+ * @param context - The live CLI context.
82
+ * @returns The opened (and memoized) direct-Firestore session.
83
+ * @throws {CliError} When the context does not support direct-Firestore sessions.
84
+ */
85
+ export declare function requireCliFirestoreSession(context: CliContext): Promise<CliFirestoreSessionContext>;
86
+ /**
87
+ * Returns the client `FirestoreContext` for the current invocation, opening the direct-Firestore
88
+ * session on first use.
89
+ *
90
+ * The returned context is the exact analogue of the server's, so an app's
91
+ * `make<App>FirestoreCollections(context)` accepts it unchanged.
92
+ *
93
+ * @param context - The live CLI context.
94
+ * @returns The client `FirestoreContext` an app's collections factory consumes.
95
+ * @throws {CliError} When the context does not support direct-Firestore sessions.
96
+ */
97
+ export declare function requireCliFirestoreContext(context: CliContext): Promise<FirestoreContext>;
98
+ export declare const setCliContext: (value: Maybe<CliContext>) => void;
99
+ export declare const getCliContext: () => Maybe<CliContext>;
29
100
  /**
30
101
  * Returns the current {@link CliContext} or throws — for use in command handlers that require auth.
31
102
  */
@@ -40,6 +111,17 @@ export interface CreateCliContextInput {
40
111
  * (e.g. `get <key>`) can resolve `prefix/id` keys to a `modelType` via `decodeFirestoreModelKey`.
41
112
  */
42
113
  readonly modelManifest?: CliModelManifest;
114
+ /**
115
+ * Optional on-disk direct-Firestore session cache. When supplied, the session opened by
116
+ * {@link CliContext.getFirestoreSession} is reused across invocations for up to an hour instead of
117
+ * costing a `GET /session/firestore` round-trip every time.
118
+ */
119
+ readonly firestoreSessionCache?: CliFirestoreSessionCacheStore;
120
+ /**
121
+ * The app-supplied Firestore binding (`cliFirestoreBinding({ collections, models })`). When
122
+ * present, the context exposes {@link CliContext.getFirestoreModels}.
123
+ */
124
+ readonly firestore?: CliFirestoreBinding;
43
125
  }
44
126
  /**
45
127
  * Builds a {@link CliContext} for the current invocation.
@@ -53,6 +135,8 @@ export interface CreateCliContextInput {
53
135
  * @param input.env - The resolved {@link CliEnvConfig} for the active env.
54
136
  * @param input.accessToken - The Bearer access token to include on outgoing API calls.
55
137
  * @param input.modelManifest - Optional generated {@link CliModelManifest} for key→modelType resolution.
138
+ * @param input.firestoreSessionCache - Optional on-disk direct-Firestore session cache shared across invocations.
139
+ * @param input.firestore - Optional app-supplied Firestore binding enabling the generic direct-read commands.
56
140
  * @returns The constructed {@link CliContext}.
57
141
  * @__NO_SIDE_EFFECTS__
58
142
  */