@dereekb/dbx-cli 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.
Files changed (106) hide show
  1. package/eslint/package.json +8 -8
  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.esm.js +12615 -6863
  14. package/lint-cache/package.json +2 -2
  15. package/manifest-extract/index.esm.js +4 -1
  16. package/manifest-extract/package.json +7 -7
  17. package/manifest-extract/src/lib/types.d.ts +6 -0
  18. package/model-test/LICENSE +21 -0
  19. package/model-test/index.d.ts +1 -0
  20. package/model-test/index.esm.js +5984 -0
  21. package/model-test/package.json +21 -0
  22. package/model-test/src/index.d.ts +41 -0
  23. package/model-test/src/lib/fixture/archetype.d.ts +33 -0
  24. package/model-test/src/lib/fixture/extract.d.ts +37 -0
  25. package/model-test/src/lib/fixture/format.json.d.ts +24 -0
  26. package/model-test/src/lib/fixture/format.markdown.d.ts +23 -0
  27. package/model-test/src/lib/fixture/forward.d.ts +48 -0
  28. package/model-test/src/lib/fixture/framework-fixtures.d.ts +70 -0
  29. package/model-test/src/lib/fixture/inspect.d.ts +22 -0
  30. package/model-test/src/lib/fixture/scaffold.d.ts +74 -0
  31. package/model-test/src/lib/fixture/types.d.ts +175 -0
  32. package/model-test/src/lib/test/discover.d.ts +81 -0
  33. package/model-test/src/lib/test/extract.d.ts +49 -0
  34. package/model-test/src/lib/test/format.hotspots.d.ts +23 -0
  35. package/model-test/src/lib/test/format.json.d.ts +25 -0
  36. package/model-test/src/lib/test/format.list-app.d.ts +30 -0
  37. package/model-test/src/lib/test/format.markdown.d.ts +25 -0
  38. package/model-test/src/lib/test/hotspots.d.ts +98 -0
  39. package/model-test/src/lib/test/inspect.d.ts +32 -0
  40. package/model-test/src/lib/test/search.d.ts +34 -0
  41. package/model-test/src/lib/test/types.d.ts +179 -0
  42. package/package.json +23 -22
  43. package/route/package.json +12 -12
  44. package/src/lib/api/firestore-session.client.d.ts +54 -0
  45. package/src/lib/api/get-args.helper.d.ts +4 -1
  46. package/src/lib/api/get-many.command.d.ts +8 -3
  47. package/src/lib/api/get.command.d.ts +13 -5
  48. package/src/lib/api/index.d.ts +1 -0
  49. package/src/lib/config/env.d.ts +106 -0
  50. package/src/lib/config/firestore-session.cache.d.ts +86 -0
  51. package/src/lib/config/index.d.ts +1 -0
  52. package/src/lib/config/paths.d.ts +3 -1
  53. package/src/lib/context/cli.context.d.ts +87 -3
  54. package/src/lib/doctor/firestore-session.check.d.ts +113 -0
  55. package/src/lib/doctor/index.d.ts +1 -0
  56. package/src/lib/firestore/firestore-get.command.d.ts +28 -0
  57. package/src/lib/firestore/firestore-queries.command.d.ts +26 -0
  58. package/src/lib/firestore/firestore-query.command.d.ts +25 -0
  59. package/src/lib/firestore/firestore.accessor.d.ts +85 -0
  60. package/src/lib/firestore/firestore.collection.d.ts +44 -0
  61. package/src/lib/firestore/firestore.error.d.ts +14 -0
  62. package/src/lib/firestore/firestore.models.d.ts +223 -0
  63. package/src/lib/firestore/firestore.query-params.d.ts +34 -0
  64. package/src/lib/firestore/firestore.query.d.ts +79 -0
  65. package/src/lib/firestore/firestore.read.d.ts +180 -0
  66. package/src/lib/firestore/firestore.session.d.ts +97 -0
  67. package/src/lib/firestore/index.d.ts +13 -0
  68. package/src/lib/firestore/query-info-utils.d.ts +48 -0
  69. package/src/lib/firestore/query-registry.d.ts +32 -0
  70. package/src/lib/index.d.ts +3 -0
  71. package/src/lib/manifest/types.d.ts +117 -0
  72. package/src/lib/mcp-scan/manifest/dbx-docs-ui-examples-schema.d.ts +2 -2
  73. package/src/lib/mcp-scan/manifest/model-snapshot-fields-schema.d.ts +2 -2
  74. package/src/lib/mcp-scan/manifest/pipes-schema.d.ts +2 -2
  75. package/src/lib/mcp-scan/manifest/ui-components-schema.d.ts +2 -2
  76. package/src/lib/mcp-scan/manifest/utils-schema.d.ts +2 -2
  77. package/src/lib/mcp-scan/scan/dbx-docs-ui-examples-extract.d.ts +1 -1
  78. package/src/lib/mcp-scan/scan/extract-models/types.d.ts +5 -0
  79. package/src/lib/mcp-scan/scan/ui-components-extract.d.ts +1 -1
  80. package/src/lib/middleware/auth.middleware.d.ts +6 -0
  81. package/src/lib/runner/run.d.ts +28 -1
  82. package/src/lib/scan-helpers/emit-generated-ts.d.ts +76 -0
  83. package/src/lib/scan-helpers/exported-from-package.d.ts +29 -0
  84. package/src/lib/util/index.d.ts +1 -0
  85. package/src/lib/util/output.d.ts +10 -0
  86. package/src/lib/util/stdin.d.ts +39 -2
  87. package/src/lib/util/table.d.ts +40 -0
  88. package/test/index.esm.js +4 -2
  89. package/test/package.json +14 -14
  90. package/test/src/lib/cli-test.d.ts +12 -2
  91. package/validate/LICENSE +21 -0
  92. package/validate/index.js +16641 -0
  93. package/validate/package.json +11 -0
  94. package/eslint/index.cjs.default.js +0 -1
  95. package/eslint/index.cjs.js +0 -1112
  96. package/eslint/index.cjs.mjs +0 -2
  97. package/index.cjs.js +0 -59813
  98. package/manifest-extract/index.cjs.default.js +0 -1
  99. package/manifest-extract/index.cjs.js +0 -1592
  100. package/manifest-extract/index.cjs.mjs +0 -2
  101. package/route/index.cjs.default.js +0 -1
  102. package/route/index.cjs.js +0 -18
  103. package/route/index.cjs.mjs +0 -2
  104. package/test/index.cjs.default.js +0 -1
  105. package/test/index.cjs.js +0 -381
  106. package/test/index.cjs.mjs +0 -2
@@ -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
  */
@@ -0,0 +1,113 @@
1
+ import { type CliFirestoreBinding } from '../firestore/firestore.models';
2
+ import { type CliReadSourceReason } from '../firestore/firestore.read';
3
+ import { type CliFirestoreSessionContext } from '../firestore/firestore.session';
4
+ import { type CliFirestoreQueryManifest, type CliModelManifest } from '../manifest/types';
5
+ import { type DoctorCheck } from './doctor.command.factory';
6
+ /**
7
+ * Name reported by the check {@link createFirestoreSessionDoctorCheck} builds.
8
+ */
9
+ export declare const FIRESTORE_SESSION_DOCTOR_CHECK_NAME = "firestore-session";
10
+ /**
11
+ * An app-supplied read that proves the direct Firestore connection works end to end.
12
+ *
13
+ * The framework cannot know which collection is rules-protected in a given app, so the last hop —
14
+ * "App Check accepted and a rules-protected read succeeded" — is delegated. Point it at a collection
15
+ * an admin may list and a non-admin may not (e.g. `getDocs(query(collection(firestore, 'wk'), limit(1)))`).
16
+ *
17
+ * Return anything JSON-serializable; it is surfaced on the check's `detail.probe`.
18
+ */
19
+ export type FirestoreSessionDoctorProbe = (context: CliFirestoreSessionContext) => Promise<unknown>;
20
+ export interface CreateFirestoreSessionDoctorCheckInput {
21
+ /**
22
+ * The rules-protected read that proves the session is genuinely usable. Strongly recommended —
23
+ * without it the check stops after sign-in and cannot tell whether App Check was accepted.
24
+ */
25
+ readonly probe?: FirestoreSessionDoctorProbe;
26
+ /**
27
+ * Human-readable label for what the probe reads, surfaced in the check detail (e.g. `list /wk`).
28
+ */
29
+ readonly probeName?: string;
30
+ /**
31
+ * The same `firestore` binding passed to `runCli`. Doctor checks run PRE-AUTH with no `CliContext`,
32
+ * so the binding cannot be discovered — pass it here and the check reports whether
33
+ * `CliContext.getFirestoreModels` would exist, and therefore whether `--via auto` can ever go direct.
34
+ */
35
+ readonly firestore?: CliFirestoreBinding;
36
+ /**
37
+ * The same `modelManifest` passed to `runCli`, for the server-only model count.
38
+ */
39
+ readonly modelManifest?: CliModelManifest;
40
+ /**
41
+ * The same `firestoreQueryManifest` passed to `runCli`, for the invocable query-entry count.
42
+ */
43
+ readonly firestoreQueryManifest?: CliFirestoreQueryManifest;
44
+ }
45
+ /**
46
+ * The read-routing summary the check reports alongside the session handshake — the `--via auto`
47
+ * decision an operator would otherwise have to infer from three separate facts.
48
+ */
49
+ export interface FirestoreSessionDoctorReadRouting {
50
+ /**
51
+ * Whether `CliContext.getFirestoreModels` exists — i.e. `runCli` was given a `firestore` binding.
52
+ */
53
+ readonly getFirestoreModels: boolean;
54
+ /**
55
+ * What `--via auto` would choose given the state this check just observed.
56
+ */
57
+ readonly readPreference: 'firestore' | 'api';
58
+ readonly reason: CliReadSourceReason;
59
+ /**
60
+ * Query-catalog entries whose factory bound to a real runtime export, and are therefore runnable
61
+ * by `firestore-query`. A non-invocable entry is listed by `firestore-queries` with `INVOCABLE = no`.
62
+ */
63
+ readonly invocableQueryEntries: number;
64
+ readonly totalQueryEntries: number;
65
+ /**
66
+ * Models the manifest marks `@dbxModelServerOnly` — refused on every `--via` value.
67
+ */
68
+ readonly serverOnlyModels: number;
69
+ }
70
+ /**
71
+ * Summarizes the `--via auto` routing decision from what doctor can observe.
72
+ *
73
+ * @param input - The wired bindings/manifests plus whether the session actually opened.
74
+ * @param input.firestore - The `firestore` binding, when supplied.
75
+ * @param input.modelManifest - The model manifest, when supplied.
76
+ * @param input.firestoreQueryManifest - The query manifest, when supplied.
77
+ * @param input.firebaseConfigComplete - Whether the env carries a complete Firebase client config.
78
+ * @param input.sessionOpened - Whether the session handshake succeeded in this run.
79
+ * @returns The routing summary.
80
+ *
81
+ * @__NO_SIDE_EFFECTS__
82
+ */
83
+ export declare function buildFirestoreSessionDoctorReadRouting(input: {
84
+ readonly firestore?: CliFirestoreBinding;
85
+ readonly modelManifest?: CliModelManifest;
86
+ readonly firestoreQueryManifest?: CliFirestoreQueryManifest;
87
+ readonly firebaseConfigComplete: boolean;
88
+ readonly sessionOpened: boolean;
89
+ }): FirestoreSessionDoctorReadRouting;
90
+ /**
91
+ * Builds the doctor check for the direct-Firestore session path.
92
+ *
93
+ * This is the **fail-loudly surface** for the feature: `CliContext.getFirestoreContext()` throws
94
+ * rather than falling back to the HTTP model API, so `doctor` is where an operator finds out which
95
+ * hop broke. It walks the whole chain in order:
96
+ *
97
+ * 1. the env carries a complete Firebase client config;
98
+ * 2. a usable access token is available (cached, env-supplied, or refreshed);
99
+ * 3. `GET /session/firestore` is reachable and mints a custom token (+ an App Check token when the
100
+ * API is configured with a web `appId`);
101
+ * 4. `signInWithCustomToken` succeeds against the configured project;
102
+ * 5. the app-supplied {@link FirestoreSessionDoctorProbe} performs one rules-protected read.
103
+ *
104
+ * Doctor checks run PRE-AUTH — `DoctorCheckInput` is only `{ cliName, envName, env, config }`, with no
105
+ * token and no `CliContext` — so this loads credentials itself via `buildCliPaths` +
106
+ * `createCliTokenCacheStore`, matching the built-in `token-cache-fresh` / `token-refresh-round-trip`
107
+ * checks.
108
+ *
109
+ * @param input - Optional probe configuration.
110
+ * @returns A {@link DoctorCheck} to append to a CLI's `doctor` check list.
111
+ * @__NO_SIDE_EFFECTS__
112
+ */
113
+ export declare function createFirestoreSessionDoctorCheck(input?: CreateFirestoreSessionDoctorCheckInput): DoctorCheck;
@@ -1 +1,2 @@
1
1
  export * from './doctor.command.factory';
2
+ export * from './firestore-session.check';
@@ -0,0 +1,28 @@
1
+ import type { CommandModule } from 'yargs';
2
+ /**
3
+ * Default command name for the direct-Firestore single-document read.
4
+ */
5
+ export declare const DEFAULT_FIRESTORE_GET_COMMAND_NAME = "firestore-get";
6
+ /**
7
+ * Options accepted by {@link buildFirestoreGetCommand}.
8
+ */
9
+ export interface BuildFirestoreGetCommandOptions {
10
+ readonly commandName?: string;
11
+ }
12
+ /**
13
+ * Builds the top-level `firestore-get <modelOrKey> [key]` command.
14
+ *
15
+ * Reads one document over the direct Firestore connection, through security rules. Positional
16
+ * parsing goes through the same {@link parseGetArgs} the API-backed `get` uses, so inferred-model
17
+ * resolution behaves identically, and the emitted `{ key, data }` is byte-identical to
18
+ * `GetModelOverHttpResult`.
19
+ *
20
+ * There is no `--via` here — the command name IS the transport selection. `get --via firestore` is
21
+ * the routed equivalent for callers that want a fallback.
22
+ *
23
+ * @param options - Optional command-name override.
24
+ * @returns A yargs `CommandModule` for `runCli({ apiCommands })`.
25
+ *
26
+ * @__NO_SIDE_EFFECTS__
27
+ */
28
+ export declare function buildFirestoreGetCommand(options?: BuildFirestoreGetCommandOptions): CommandModule;
@@ -0,0 +1,26 @@
1
+ import type { CommandModule } from 'yargs';
2
+ import { type CliFirestoreQueryManifest } from '../manifest/types';
3
+ /**
4
+ * Default command name for the Firestore query catalog command.
5
+ */
6
+ export declare const DEFAULT_FIRESTORE_QUERIES_COMMAND_NAME = "firestore-queries";
7
+ /**
8
+ * Options accepted by {@link buildFirestoreQueriesCommand}.
9
+ */
10
+ export interface BuildFirestoreQueriesCommandOptions {
11
+ readonly commandName?: string;
12
+ }
13
+ /**
14
+ * Builds the top-level `firestore-queries [query]` catalog command.
15
+ *
16
+ * Registered as a CONFIG command, not an API command — browsing the catalog is a documentation
17
+ * read, and demanding a login to see what queries exist would be the wrong trade. This mirrors the
18
+ * existing `model-info` (config, auth-bypassed) vs `model <m> get` (api, post-auth) split.
19
+ *
20
+ * @param manifest - The generated Firestore query manifest.
21
+ * @param options - Optional command-name override.
22
+ * @returns A yargs `CommandModule` for `runCli({ configCommands })`.
23
+ *
24
+ * @__NO_SIDE_EFFECTS__
25
+ */
26
+ export declare function buildFirestoreQueriesCommand(manifest: CliFirestoreQueryManifest, options?: BuildFirestoreQueriesCommandOptions): CommandModule;