@dereekb/dbx-cli 14.3.0 → 14.5.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.
@@ -1,12 +1,9 @@
1
1
  import { type FirestoreModelKey, type FirestoreModelType } from '@dereekb/firebase';
2
2
  import { type Maybe } from '@dereekb/util';
3
+ import { type FirebaseClientFirestoreIdentityContext, type FirebaseClientFirestoreIdentityProblem, type FirebaseClientFirestoreIdentityReport, type FirebaseClientFirestoreSdkModuleIdentity } from '@dereekb/oauth-resource/firebase';
3
4
  /**
4
- * The `drivers.firestoreDriverIdentifier` a client-SDK `FirestoreContext` reports.
5
- *
6
- * `clientFirebaseFirestoreContextFactory` stamps this; the server's
7
- * `googleCloudFirestoreContextFactory` stamps `@google-cloud/firestore` instead. Comparing against
8
- * it is how {@link inspectCliFirestoreSdkIdentity} tells an admin-SDK context handed to the CLI apart
9
- * from a genuinely broken client one.
5
+ * The `drivers.firestoreDriverIdentifier` a client-SDK `FirestoreContext` reports. See
6
+ * {@link CLIENT_FIRESTORE_DRIVER_IDENTIFIER}.
10
7
  */
11
8
  export declare const CLI_CLIENT_FIRESTORE_DRIVER_IDENTIFIER = "@firebase/firestore";
12
9
  /**
@@ -22,94 +19,31 @@ export declare const FIRESTORE_SDK_IDENTITY_STAGE = "firestore-sdk-identity";
22
19
  */
23
20
  export declare const FIRESTORE_SDK_INSTANCE_MISMATCH_CODE = "FIRESTORE_SDK_INSTANCE_MISMATCH";
24
21
  /**
25
- * The distinct ways a session's Firestore handle can be unusable, in the order
26
- * {@link inspectCliFirestoreSdkIdentity} tests them — most specific diagnosis first.
27
- */
28
- export type CliFirestoreSdkIdentityProblem =
29
- /**
30
- * `firestoreContext` is absent, or its `firestore` is null/undefined. Every `collection()` call
31
- * built off it throws `Expected first argument to collection() to be a CollectionReference, a
32
- * DocumentReference or FirebaseFirestore` — the SDK's message for ANY non-Firestore first
33
- * argument, which is why it never named this.
34
- */
35
- 'no-firestore-handle'
36
- /**
37
- * The context reports a driver other than {@link CLI_CLIENT_FIRESTORE_DRIVER_IDENTIFIER} — an
38
- * admin/`@google-cloud/firestore` context reached the client-SDK read path.
39
- */
40
- | 'unexpected-driver'
41
- /**
42
- * Two copies of `@firebase/firestore` are loaded, so the handle minted by one fails the other's
43
- * brand check.
22
+ * `CliError` code for a collection/document reference the SDK refused to build for a reason the
23
+ * wiring identity check cannot explain — a bad `--parent`, or an app collection whose own
24
+ * construction is broken.
44
25
  */
45
- | 'duplicated-firestore-sdk'
26
+ export declare const FIRESTORE_COLLECTION_UNRESOLVABLE_CODE = "FIRESTORE_COLLECTION_UNRESOLVABLE";
46
27
  /**
47
- * The handle is present and the driver looks right, but it is not an instance of THIS copy of the
48
- * SDK's `Firestore` class and no duplicate install explains it.
28
+ * The distinct ways a session's Firestore handle can be unusable. See
29
+ * {@link FirebaseClientFirestoreIdentityProblem}.
49
30
  */
50
- | 'foreign-firestore-instance';
31
+ export type CliFirestoreSdkIdentityProblem = FirebaseClientFirestoreIdentityProblem;
51
32
  /**
52
- * Where one consumer resolved `@firebase/firestore` to, and at which version.
53
- *
54
- * Reported for both `@dereekb/dbx-cli` and `@dereekb/firebase` because the whole point of the
55
- * duplicated-SDK hypothesis is that those two answers can differ.
33
+ * Where one consumer resolved `@firebase/firestore` to, and at which version. See
34
+ * {@link FirebaseClientFirestoreSdkModuleIdentity}.
56
35
  */
57
- export interface CliFirestoreSdkModuleIdentity {
58
- /**
59
- * The resolved package directory, or `undefined` when resolution failed.
60
- */
61
- readonly packageDir?: Maybe<string>;
62
- readonly version?: Maybe<string>;
63
- /**
64
- * Why resolution failed, when it did.
65
- */
66
- readonly error?: Maybe<string>;
67
- }
36
+ export type CliFirestoreSdkModuleIdentity = FirebaseClientFirestoreSdkModuleIdentity;
68
37
  /**
69
- * The provenance + brand-check report {@link inspectCliFirestoreSdkIdentity} produces.
70
- *
71
- * Everything here is reported whether or not the check passed: a `sdkDuplicated: true` alongside
72
- * `ok: true` is a latent hazard worth seeing before it becomes an outage.
38
+ * The provenance + brand-check report {@link inspectCliFirestoreSdkIdentity} produces. See
39
+ * {@link FirebaseClientFirestoreIdentityReport}.
73
40
  */
74
- export interface CliFirestoreSdkIdentityReport {
75
- readonly ok: boolean;
76
- readonly problem?: CliFirestoreSdkIdentityProblem;
77
- readonly firestorePresent: boolean;
78
- /**
79
- * Whether the handle passes `instanceof Firestore` against the copy of `@firebase/firestore` THIS
80
- * package loaded.
81
- */
82
- readonly firestoreIsSdkInstance: boolean;
83
- /**
84
- * The handle's constructor name, which distinguishes a duplicate `Firestore` (same name, different
85
- * class) from a genuinely foreign object.
86
- */
87
- readonly firestoreConstructor?: Maybe<string>;
88
- readonly firestoreDriverIdentifier?: Maybe<string>;
89
- /**
90
- * The version of the `firebase` umbrella package resolved at runtime.
91
- */
92
- readonly firebaseVersion?: Maybe<string>;
93
- readonly sdkFromDbxCli: CliFirestoreSdkModuleIdentity;
94
- readonly sdkFromDbxFirebase: CliFirestoreSdkModuleIdentity;
95
- /**
96
- * True when the two resolutions above name DIFFERENT package directories.
97
- */
98
- readonly sdkDuplicated: boolean;
99
- }
41
+ export type CliFirestoreSdkIdentityReport = FirebaseClientFirestoreIdentityReport;
100
42
  /**
101
- * The slice of a session context {@link inspectCliFirestoreSdkIdentity} reads.
102
- *
103
- * Deliberately looser than `FirestoreContext`: this check exists precisely for the case where the
104
- * object is not the shape its declared type claims, so narrowing it here would assume away the
105
- * failure. A real `CliFirestoreSessionContext['firestoreContext']` is structurally assignable.
43
+ * The slice of a session context {@link inspectCliFirestoreSdkIdentity} reads. See
44
+ * {@link FirebaseClientFirestoreIdentityContext}.
106
45
  */
107
- export interface CliFirestoreSdkIdentityContext {
108
- readonly firestore?: unknown;
109
- readonly drivers?: {
110
- readonly firestoreDriverIdentifier?: unknown;
111
- };
112
- }
46
+ export type CliFirestoreSdkIdentityContext = FirebaseClientFirestoreIdentityContext;
113
47
  /**
114
48
  * Input for {@link inspectCliFirestoreSdkIdentity}.
115
49
  */
@@ -117,20 +51,12 @@ export interface InspectCliFirestoreSdkIdentityInput {
117
51
  readonly firestoreContext?: Maybe<CliFirestoreSdkIdentityContext>;
118
52
  }
119
53
  /**
120
- * Checks that a session's Firestore handle is one THIS copy of the client SDK will accept, and
121
- * reports where every consumer resolved the SDK from.
122
- *
123
- * Exists because `collection()` refuses any non-`Firestore` first argument with one message —
124
- * `Expected first argument to collection() to be a CollectionReference, a DocumentReference or
125
- * FirebaseFirestore` — that names neither the model, nor the collection, nor which of the three
126
- * distinct causes ({@link CliFirestoreSdkIdentityProblem}) produced it. Every downstream CLI built on
127
- * `@dereekb/dbx-cli` shares that hazard, so the diagnosis belongs here rather than in an app.
54
+ * Checks that a session's Firestore handle is one the loaded client SDK will accept, and reports
55
+ * where every consumer resolved the SDK from.
128
56
  *
129
- * The `instanceof` test is the load-bearing one and it is deliberately NOT structural: an identity
130
- * check across the package boundary is the only thing that can detect a duplicated
131
- * `@firebase/firestore`, which is exactly what a structural check would hide. (Contrast
132
- * `firestore.error.ts`, which narrows a thrown `FirebaseError` structurally — there the goal is to
133
- * classify an error even when copies differ, so the tradeoff runs the other way.)
57
+ * The CLI wrapper over {@link inspectFirebaseClientFirestoreIdentity}: it supplies this package's own
58
+ * `require` as the resolution root and names the CLI in the report, so `doctor`'s output is unchanged
59
+ * by the move of the inspection core into `@dereekb/oauth-resource/firebase`.
134
60
  *
135
61
  * @param input - The session's `firestoreContext`, when one was built.
136
62
  * @returns The provenance + brand-check report.
@@ -149,12 +75,6 @@ export declare function inspectCliFirestoreSdkIdentity(input: InspectCliFirestor
149
75
  * @__NO_SIDE_EFFECTS__
150
76
  */
151
77
  export declare function cliFirestoreSdkIdentitySuggestion(report: CliFirestoreSdkIdentityReport): Maybe<string>;
152
- /**
153
- * `CliError` code for a collection/document reference the SDK refused to build for a reason the
154
- * wiring identity check cannot explain — a bad `--parent`, or an app collection whose own
155
- * construction is broken.
156
- */
157
- export declare const FIRESTORE_COLLECTION_UNRESOLVABLE_CODE = "FIRESTORE_COLLECTION_UNRESOLVABLE";
158
78
  /**
159
79
  * Input for {@link cliFirestoreWiringError}.
160
80
  */
@@ -1,7 +1,4 @@
1
- import { type FirebaseApp } from 'firebase/app';
2
- import { type Auth } from 'firebase/auth';
3
- import { type Firestore } from 'firebase/firestore';
4
- import { type FirestoreContext } from '@dereekb/firebase';
1
+ import { type FirebaseUserSession } from '@dereekb/oauth-resource/firebase';
5
2
  import { type CliFirestoreSession } from '../api/firestore-session.client';
6
3
  import { type CliEnvConfig } from '../config/env';
7
4
  import { type CliFirestoreSessionCacheStore } from '../config/firestore-session.cache';
@@ -9,25 +6,23 @@ import { type CliFirestoreSessionCacheStore } from '../config/firestore-session.
9
6
  * A live direct-Firestore session: the Firebase client objects the CLI signed in with, plus the
10
7
  * `FirestoreContext` an app's collections factory consumes.
11
8
  *
9
+ * The CLI's view of {@link FirebaseUserSession}, whose implementation lives in
10
+ * `@dereekb/oauth-resource/firebase` so any resource server that verified a bearer token carrying the
11
+ * `session.firestore` scope gets the same bridge.
12
+ *
12
13
  * The `firestoreContext` is built by `clientFirebaseFirestoreContextFactory`, the exact analogue of
13
14
  * the server's `googleCloudFirestoreContextFactory` — both satisfy `FirestoreContextFactory` — so an
14
15
  * app's `make<App>FirestoreCollections(context)` accepts it unchanged, and the CLI runs the SAME
15
16
  * queries the Angular app runs, through the SAME security rules.
16
17
  */
17
- export interface CliFirestoreSessionContext {
18
+ export interface CliFirestoreSessionContext extends FirebaseUserSession {
18
19
  /**
19
20
  * The credential bundle the API minted for this session.
21
+ *
22
+ * The CLI's long-standing name for {@link FirebaseUserSession.credentials}; both name the same
23
+ * object.
20
24
  */
21
25
  readonly session: CliFirestoreSession;
22
- /**
23
- * True when {@link session} came from the on-disk session cache rather than a fresh
24
- * `GET /session/firestore`. Surfaced for `doctor` and `--verbose`, not for control flow.
25
- */
26
- readonly fromCache: boolean;
27
- readonly app: FirebaseApp;
28
- readonly auth: Auth;
29
- readonly firestore: Firestore;
30
- readonly firestoreContext: FirestoreContext;
31
26
  }
32
27
  export interface CreateCliFirestoreSessionContextInput {
33
28
  readonly cliName: string;
@@ -49,39 +44,15 @@ export interface CreateCliFirestoreSessionContextInput {
49
44
  */
50
45
  readonly refreshSession?: boolean;
51
46
  }
52
- /**
53
- * The Firebase app name a direct-Firestore session registers for a CLI + env pair.
54
- *
55
- * Deriving the name rather than tracking apps in a side registry is what makes ONE app per CLI + env
56
- * per process a property of the code instead of a convention: {@link createCliFirestoreSessionContext}
57
- * looks this name up in `getApps()` before initializing, so repeated session opens in one process —
58
- * a doctor probe alongside a command's own session, an action that re-resolves the accessor — all
59
- * share a single app, and {@link closeAllCliFirebaseApps} finds it again from the name alone.
60
- *
61
- * @param input - The CLI name and env name the session targets.
62
- * @returns The Firebase app name for that pair.
63
- * @__NO_SIDE_EFFECTS__
64
- */
65
- export declare function cliFirebaseAppName(input: Pick<CreateCliFirestoreSessionContextInput, 'cliName' | 'envName'>): string;
66
47
  /**
67
48
  * Opens a direct Firestore connection as the authenticated CLI user.
68
49
  *
69
- * Steps, in a strict order:
50
+ * A thin wrapper over `openFirebaseUserSession`: it validates the env's Firebase client config with
51
+ * the CLI's own env-var-naming remediation, then delegates. The strict step ORDER (mint → fresh app →
52
+ * App Check FIRST → emulator wiring → sign-in) and its reasoning live with the implementation.
70
53
  *
71
- * 0. When a `sessionCache` is supplied, reuse the env's cached credential envelope if it is still
72
- * live. Sessions are cached for up to an hour (see `CLI_FIRESTORE_SESSION_MAX_CACHE_MS`), which is
73
- * the ceiling the Firebase credentials themselves sit under. A hit skips step 1 only — the
74
- * Firebase app is per-process, so the sign-in in step 5 always runs.
75
- * 1. `GET <apiBaseUrl>/session/firestore` for a custom token + App Check attestation.
76
- * 2. `initializeApp` with the env's Firebase client config.
77
- * 3. `initializeAppCheck` with a `CustomProvider` handing back the server-minted token. **This must
78
- * happen before any other Firebase call** — `dbx-firebase`'s provider documents the same
79
- * constraint: "App Check must be initialized before any Firebase request goes out, otherwise
80
- * requests are sent without an App Check token and are rejected in production." Skipped when the
81
- * env targets emulators (which do not verify attestations) or when the API minted no token.
82
- * 4. `getAuth` / `getFirestore`, connecting each to its emulator when configured.
83
- * 5. `signInWithCustomToken`. The user's stored custom claims land at the top level of the exchanged
84
- * ID token, so `request.auth.token.<claim>` reads in security rules behave exactly as in the app.
54
+ * The session's Firebase app is named `<cliName>::<envName>::<uid>`, so
55
+ * {@link closeAllCliFirebaseApps} finds every app this CLI opened from the name alone.
85
56
  *
86
57
  * There is deliberately NO fallback to the HTTP model API — a failure here throws so the operator
87
58
  * sees it. `createFirestoreSessionDoctorCheck` is the diagnostic surface for why.
@@ -96,15 +67,7 @@ export declare function createCliFirestoreSessionContext(input: CreateCliFiresto
96
67
  *
97
68
  * Required for the CLI to EXIT. A signed-in `Auth` and a live `Firestore` both hold open handles
98
69
  * that keep the Node event loop alive indefinitely, so without this a command prints its result and
99
- * then hangs forever — the process never returns to the shell. Nothing in the CLI is long-lived
100
- * enough to want that: a session is opened for one invocation and is dead weight afterwards.
101
- *
102
- * `deleteApp` is the single call that covers it — it disposes every registered component, which for
103
- * Firestore runs the same shutdown `terminate()` does, and for Auth stops the token-refresh timer.
104
- *
105
- * Deliberately tolerant: teardown runs in a `finally` after the command has already produced its
106
- * output, so a failure here must not change the exit code or mask the real result. A session that
107
- * was never opened is a no-op.
70
+ * then hangs forever — the process never returns to the shell.
108
71
  *
109
72
  * @param session - The session context to close.
110
73
  */
@@ -112,23 +75,15 @@ export declare function closeCliFirestoreSessionContext(session: CliFirestoreSes
112
75
  /**
113
76
  * Deletes every still-live Firebase app this CLI opened, whether or not a session was handed back.
114
77
  *
115
- * The CLI's last line of defence against a hang. `closeCliFirestoreSessionContext` covers the normal
116
- * path, but it needs a session to be handed to it, and three cases never produce one:
78
+ * The CLI's last line of defence against a hang. {@link closeCliFirestoreSessionContext} covers the
79
+ * normal path, but it needs a session to be handed to it, and three cases never produce one:
117
80
  *
118
- * - a handshake that fails AFTER `initializeApp` — a rejected custom token, a failed App Check
119
- * registration — throws, so the caller that catches it has an initialized app and no session;
81
+ * - a handshake that fails AFTER `initializeApp` — a failed App Check registration, a broken emulator
82
+ * connection — throws, so the caller that catches it has an initialized app and no session;
120
83
  * - a caller that opens its own session outside the context memo (the doctor probe) owns its own
121
84
  * teardown, and forgetting it hangs the process;
122
85
  * - a {@link CliContext} orphaned mid-invocation carries the only reference to its session memo.
123
86
  *
124
- * Each leaves an app whose `Firestore` and signed-in `Auth` hold the Node event loop open forever.
125
- * `getApps()` already tracks every live app and `deleteApp` removes it from that list, so the app
126
- * names {@link cliFirebaseAppName} derives are enough to find them again — no side registry to keep
127
- * in sync, and idempotent by construction.
128
- *
129
- * Tolerant of failures for the same reason {@link closeCliFirestoreSessionContext} is: it runs after
130
- * the result is already on stdout.
131
- *
132
87
  * @param input - The function inputs.
133
88
  * @param input.cliName - The CLI whose apps should be closed. Apps belonging to other Firebase
134
89
  * consumers in the same process are left alone.
@@ -9,6 +9,7 @@
9
9
  export * from './scan-config-schema.js';
10
10
  export * from './extract.js';
11
11
  export * from './build-manifest.js';
12
+ export * from './sanitize-type-text.js';
12
13
  export * from './cli.js';
13
14
  export * from './filters-cli.js';
14
15
  export * from './filters-extract.js';
@@ -26,6 +26,12 @@ export type BuildModelSnapshotFieldsGlobber = ScanGlobber;
26
26
  export interface BuildModelSnapshotFieldsManifestInput {
27
27
  readonly projectRoot: string;
28
28
  readonly generator: string;
29
+ /**
30
+ * Absolute workspace root that absolute paths baked into type text are made
31
+ * relative to. Defaults to `process.cwd()`, which is the workspace root because
32
+ * the generate-* targets all run with `cwd: {workspaceRoot}`.
33
+ */
34
+ readonly workspaceRoot?: string;
29
35
  readonly now?: () => Date;
30
36
  readonly readFile?: BuildModelSnapshotFieldsReadFile;
31
37
  readonly globber?: BuildModelSnapshotFieldsGlobber;
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Makes ts-morph type text reproducible across checkouts.
3
+ *
4
+ * TypeScript emits `import("<absolute path>").Type` for any type it cannot name
5
+ * from the enclosing scope, and ts-morph surfaces that text verbatim. The path is
6
+ * machine-specific: it is the host checkout on a developer's machine and `/code`
7
+ * inside the build container's bind mount.
8
+ *
9
+ * That difference is not cosmetic. The generated manifests are tracked files under
10
+ * `packages/dbx-cli/generated`, which is inside `dbx-cli`'s project root, and the
11
+ * workspace's `default` named input is `{projectRoot}/**\/*`. So an absolute path
12
+ * baked into a manifest invalidates the Nx cache for `dbx-cli` — and everything
13
+ * downstream of it — on every alternation between a host build and a container
14
+ * build, and leaves the git tree permanently dirty.
15
+ *
16
+ * Rewriting the path to be workspace-relative makes the emitted bytes identical
17
+ * everywhere the workspace is checked out.
18
+ */
19
+ /**
20
+ * Input to {@link sanitizeTypeText}.
21
+ */
22
+ export interface SanitizeTypeTextInput {
23
+ /**
24
+ * Type text as reported by the type checker, possibly containing one or more
25
+ * `import("<absolute path>")` segments.
26
+ */
27
+ readonly typeText: string;
28
+ /**
29
+ * Absolute path to the workspace root that emitted paths are made relative to.
30
+ */
31
+ readonly workspaceRoot: string;
32
+ }
33
+ /**
34
+ * Rewrites every absolute `import("…")` path in `typeText` to a workspace-relative,
35
+ * forward-slashed path.
36
+ *
37
+ * Paths that are already relative, or that resolve outside `workspaceRoot`, are left
38
+ * untouched — there is no stable shorter form for them, and rewriting to a `../`
39
+ * chain would reintroduce the machine dependence this exists to remove.
40
+ *
41
+ * @param input - The type text and the workspace root to relativize against.
42
+ * @returns The type text with workspace-internal absolute paths relativized.
43
+ */
44
+ export declare function sanitizeTypeText(input: SanitizeTypeTextInput): string;
@@ -25,6 +25,12 @@ export type BuildUtilsGlobber = ScanGlobber;
25
25
  export interface BuildUtilsManifestInput {
26
26
  readonly projectRoot: string;
27
27
  readonly generator: string;
28
+ /**
29
+ * Absolute workspace root that absolute paths baked into type text are made
30
+ * relative to. Defaults to `process.cwd()`, which is the workspace root because
31
+ * the generate-* targets all run with `cwd: {workspaceRoot}`.
32
+ */
33
+ readonly workspaceRoot?: string;
28
34
  readonly now?: () => Date;
29
35
  readonly readFile?: BuildUtilsReadFile;
30
36
  readonly globber?: BuildUtilsGlobber;
package/test/package.json CHANGED
@@ -1,17 +1,18 @@
1
1
  {
2
2
  "name": "@dereekb/dbx-cli/test",
3
- "version": "14.3.0",
3
+ "version": "14.5.0",
4
4
  "sideEffects": false,
5
5
  "type": "module",
6
6
  "peerDependencies": {
7
- "@dereekb/date": "14.3.0",
8
- "@dereekb/dbx-cli": "14.3.0",
9
- "@dereekb/firebase": "14.3.0",
10
- "@dereekb/firebase-server/test": "14.3.0",
11
- "@dereekb/model": "14.3.0",
12
- "@dereekb/nestjs": "14.3.0",
13
- "@dereekb/rxjs": "14.3.0",
14
- "@dereekb/util": "14.3.0",
7
+ "@dereekb/date": "14.5.0",
8
+ "@dereekb/dbx-cli": "14.5.0",
9
+ "@dereekb/firebase": "14.5.0",
10
+ "@dereekb/firebase-server/test": "14.4.0",
11
+ "@dereekb/model": "14.5.0",
12
+ "@dereekb/nestjs": "14.5.0",
13
+ "@dereekb/oauth-resource": "14.5.0",
14
+ "@dereekb/rxjs": "14.5.0",
15
+ "@dereekb/util": "14.5.0",
15
16
  "@nestjs/common": "^12.0.1",
16
17
  "arktype": "^2.2.0",
17
18
  "vitest": "4.1.11",
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "@dereekb/dbx-cli/validate",
3
- "version": "14.3.0",
3
+ "version": "14.5.0",
4
4
  "type": "module",
5
5
  "sideEffects": false,
6
6
  "peerDependencies": {
7
- "@dereekb/dbx-cli": "14.3.0",
8
- "@dereekb/util": "14.3.0",
7
+ "@dereekb/dbx-cli": "14.5.0",
8
+ "@dereekb/util": "14.5.0",
9
9
  "ts-morph": "^28.0.0"
10
10
  }
11
11
  }