@dereekb/dbx-cli 14.5.1 → 14.7.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,6 +1,6 @@
1
1
  {
2
2
  "name": "@dereekb/dbx-cli-lint-cache",
3
- "version": "14.5.1",
3
+ "version": "14.7.0",
4
4
  "sideEffects": false,
5
5
  "private": true,
6
6
  "type": "module",
@@ -9,7 +9,7 @@
9
9
  "eslint": "10.9.1"
10
10
  },
11
11
  "peerDependencies": {
12
- "@dereekb/util": "14.5.1",
12
+ "@dereekb/util": "14.7.0",
13
13
  "yargs": "^18.0.0"
14
14
  }
15
15
  }
@@ -695,6 +695,7 @@ var SERVICE_FACTORY_TAG = 'dbxModelServiceFactory';
695
695
  var MCP_TOOL_NAME_SEGMENT_TAG = 'dbxModelMcpToolNameSegment';
696
696
  var MODEL_TYPE_VALUE_PATTERN = /^[a-z][A-Za-z0-9_$]*$/;
697
697
  var TOOL_NAME_SEGMENT_PATTERN = /^[A-Za-z][A-Za-z0-9_$]*$/;
698
+ var COMPOSITE_KEY_MODEL_NAME_PATTERN = /^[A-Za-z][A-Za-z0-9_$]*$/;
698
699
  var IDENTITY_FN = 'firestoreModelIdentity';
699
700
  // the declared-type counterparts of IDENTITY_FN, which is all a .d.ts retains of the call
700
701
  var IDENTITY_TYPE = 'FirestoreModelIdentity';
@@ -940,6 +941,7 @@ function buildInterface(decl) {
940
941
  var dbxModelRead = readDbxModelReadTag(jsDocs);
941
942
  var dbxModelServerOnly = jsDocsHaveTag(jsDocs, 'dbxModelServerOnly');
942
943
  var mcpToolNameSegment = readMcpToolNameSegmentTag(jsDocs);
944
+ var compositeKey = readDbxModelCompositeKeyTag(jsDocs);
943
945
  var extendsNames = decl.getExtends().map(resolveExtendsName);
944
946
  var props = [];
945
947
  var _iteratorNormalCompletion = true, _didIteratorError = false, _iteratorError = undefined;
@@ -988,6 +990,8 @@ function buildInterface(decl) {
988
990
  dbxModelServerOnly: true
989
991
  } : {}, mcpToolNameSegment === undefined ? {} : {
990
992
  mcpToolNameSegment: mcpToolNameSegment
993
+ }, compositeKey === undefined ? {} : {
994
+ compositeKey: compositeKey
991
995
  });
992
996
  }
993
997
  function readMcpToolNameSegmentTag(jsDocs) {
@@ -1092,6 +1096,115 @@ function readDbxModelReadTag(jsDocs) {
1092
1096
  }
1093
1097
  return result;
1094
1098
  }
1099
+ /**
1100
+ * Reads the first well-formed `@dbxModelCompositeKey from=<...> encoding=<...>` tag. Both parts are
1101
+ * required — a tag missing `from=` or carrying an unknown encoding is dropped so the manifest never
1102
+ * publishes a composite-key declaration a consumer could not act on.
1103
+ *
1104
+ * @param jsDocs - JSDoc blocks attached to the interface declaration.
1105
+ * @returns The parsed tag, or `undefined` when absent or malformed.
1106
+ */ function readDbxModelCompositeKeyTag(jsDocs) {
1107
+ var result;
1108
+ var _iteratorNormalCompletion = true, _didIteratorError = false, _iteratorError = undefined;
1109
+ try {
1110
+ for(var _iterator = jsDocs[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true){
1111
+ var doc = _step.value;
1112
+ var _iteratorNormalCompletion1 = true, _didIteratorError1 = false, _iteratorError1 = undefined;
1113
+ try {
1114
+ for(var _iterator1 = doc.getTags()[Symbol.iterator](), _step1; !(_iteratorNormalCompletion1 = (_step1 = _iterator1.next()).done); _iteratorNormalCompletion1 = true){
1115
+ var tag = _step1.value;
1116
+ var _tag_getCommentText;
1117
+ if (tag.getTagName() !== 'dbxModelCompositeKey') continue;
1118
+ if (result !== undefined) continue;
1119
+ result = parseCompositeKeyTagValue((_tag_getCommentText = tag.getCommentText()) !== null && _tag_getCommentText !== void 0 ? _tag_getCommentText : '');
1120
+ }
1121
+ } catch (err) {
1122
+ _didIteratorError1 = true;
1123
+ _iteratorError1 = err;
1124
+ } finally{
1125
+ try {
1126
+ if (!_iteratorNormalCompletion1 && _iterator1.return != null) {
1127
+ _iterator1.return();
1128
+ }
1129
+ } finally{
1130
+ if (_didIteratorError1) {
1131
+ throw _iteratorError1;
1132
+ }
1133
+ }
1134
+ }
1135
+ }
1136
+ } catch (err) {
1137
+ _didIteratorError = true;
1138
+ _iteratorError = err;
1139
+ } finally{
1140
+ try {
1141
+ if (!_iteratorNormalCompletion && _iterator.return != null) {
1142
+ _iterator.return();
1143
+ }
1144
+ } finally{
1145
+ if (_didIteratorError) {
1146
+ throw _iteratorError;
1147
+ }
1148
+ }
1149
+ }
1150
+ return result;
1151
+ }
1152
+ /**
1153
+ * Parses the `from=<...> encoding=<...>` tokens of a `@dbxModelCompositeKey` tag.
1154
+ *
1155
+ * @param raw - The tag text after the tag name.
1156
+ * @returns The declaration, or `undefined` when `from=` is missing/empty or the encoding is not
1157
+ * `one-way` / `two-way`.
1158
+ */ function parseCompositeKeyTagValue(raw) {
1159
+ var from;
1160
+ var encoding;
1161
+ var _iteratorNormalCompletion = true, _didIteratorError = false, _iteratorError = undefined;
1162
+ try {
1163
+ for(var _iterator = raw.trim().split(/\s+/)[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true){
1164
+ var token = _step.value;
1165
+ var eq = token.indexOf('=');
1166
+ if (eq <= 0) continue;
1167
+ var key = token.slice(0, eq);
1168
+ var value = token.slice(eq + 1);
1169
+ if (key === 'from') {
1170
+ from = parseCompositeKeyFromValue(value);
1171
+ } else if (key === 'encoding' && (value === 'one-way' || value === 'two-way')) {
1172
+ encoding = value;
1173
+ }
1174
+ }
1175
+ } catch (err) {
1176
+ _didIteratorError = true;
1177
+ _iteratorError = err;
1178
+ } finally{
1179
+ try {
1180
+ if (!_iteratorNormalCompletion && _iterator.return != null) {
1181
+ _iterator.return();
1182
+ }
1183
+ } finally{
1184
+ if (_didIteratorError) {
1185
+ throw _iteratorError;
1186
+ }
1187
+ }
1188
+ }
1189
+ return from !== undefined && encoding !== undefined ? {
1190
+ from: from,
1191
+ encoding: encoding
1192
+ } : undefined;
1193
+ }
1194
+ function parseCompositeKeyFromValue(value) {
1195
+ var result;
1196
+ if (value === '*') {
1197
+ result = '*';
1198
+ } else {
1199
+ var names = value.split(',').map(function(x) {
1200
+ return x.trim();
1201
+ }).filter(function(x) {
1202
+ return COMPOSITE_KEY_MODEL_NAME_PATTERN.test(x);
1203
+ });
1204
+ result = names.length > 0 ? names : undefined;
1205
+ }
1206
+ return result;
1207
+ }
1095
1208
  function readServiceFactories(sourceFile) {
1096
1209
  var out = [];
1097
1210
  var _iteratorNormalCompletion = true, _didIteratorError = false, _iteratorError = undefined;
@@ -1,21 +1,21 @@
1
1
  {
2
2
  "name": "@dereekb/dbx-cli/manifest-extract",
3
- "version": "14.5.1",
3
+ "version": "14.7.0",
4
4
  "type": "module",
5
5
  "sideEffects": false,
6
6
  "peerDependencies": {
7
- "@dereekb/date": "14.5.1",
8
- "@dereekb/dbx-cli": "14.5.1",
9
- "@dereekb/firebase": "14.5.1",
10
- "@dereekb/model": "14.5.1",
11
- "@dereekb/nestjs": "14.5.1",
12
- "@dereekb/oauth-resource": "14.5.1",
13
- "@dereekb/rxjs": "14.5.1",
14
- "@dereekb/util": "14.5.1",
7
+ "@dereekb/date": "14.7.0",
8
+ "@dereekb/dbx-cli": "14.7.0",
9
+ "@dereekb/firebase": "14.7.0",
10
+ "@dereekb/model": "14.7.0",
11
+ "@dereekb/nestjs": "14.7.0",
12
+ "@dereekb/oauth-resource": "14.7.0",
13
+ "@dereekb/rxjs": "14.7.0",
14
+ "@dereekb/util": "14.7.0",
15
15
  "ts-morph": "^28.0.0"
16
16
  },
17
17
  "devDependencies": {
18
- "@dereekb/firebase": "14.5.1"
18
+ "@dereekb/firebase": "14.7.0"
19
19
  },
20
20
  "exports": {
21
21
  "./package.json": "./package.json",
@@ -177,6 +177,27 @@ export interface ModelExtractionInterface {
177
177
  * type in generated tool names (e.g. the collection prefix). Absent when the tag is omitted or invalid.
178
178
  */
179
179
  readonly mcpToolNameSegment?: string;
180
+ /**
181
+ * Parsed `@dbxModelCompositeKey from=<ModelA>[,<ModelB>...] encoding=<two-way|one-way>` tag. Absent
182
+ * when the tag is omitted or malformed (no `from=`, or an encoding other than `two-way` / `one-way`).
183
+ */
184
+ readonly compositeKey?: ModelExtractionCompositeKey;
185
+ }
186
+ /**
187
+ * Parsed `@dbxModelCompositeKey` tag on a `@dbxModel`-tagged interface whose document id is a
188
+ * flattened encoding of another model's key.
189
+ */
190
+ export interface ModelExtractionCompositeKey {
191
+ /**
192
+ * `'*'` for the wildcard form (any model may be the source), or the ordered list of source
193
+ * model names exactly as written (interface name, identity const, or modelType).
194
+ */
195
+ readonly from: readonly string[] | '*';
196
+ /**
197
+ * `'one-way'` for `flatFirestoreModelKey` (slashes removed), `'two-way'` for
198
+ * `twoWayFlatFirestoreModelKey` (slashes replaced with underscores, recoverable).
199
+ */
200
+ readonly encoding: 'one-way' | 'two-way';
180
201
  }
181
202
  /**
182
203
  * One field inside a converter's `fields` map.
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "@dereekb/dbx-cli/model-test",
3
- "version": "14.5.1",
3
+ "version": "14.7.0",
4
4
  "type": "module",
5
5
  "sideEffects": false,
6
6
  "peerDependencies": {
7
- "@dereekb/util": "14.5.1",
7
+ "@dereekb/util": "14.7.0",
8
8
  "ts-morph": "^28.0.0"
9
9
  },
10
10
  "exports": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dereekb/dbx-cli",
3
- "version": "14.5.1",
3
+ "version": "14.7.0",
4
4
  "type": "module",
5
5
  "sideEffects": false,
6
6
  "bin": {
@@ -66,12 +66,12 @@
66
66
  }
67
67
  },
68
68
  "peerDependencies": {
69
- "@dereekb/date": "14.5.1",
70
- "@dereekb/firebase": "14.5.1",
71
- "@dereekb/model": "14.5.1",
72
- "@dereekb/nestjs": "14.5.1",
73
- "@dereekb/oauth-resource": "14.5.1",
74
- "@dereekb/util": "14.5.1",
69
+ "@dereekb/date": "14.7.0",
70
+ "@dereekb/firebase": "14.7.0",
71
+ "@dereekb/model": "14.7.0",
72
+ "@dereekb/nestjs": "14.7.0",
73
+ "@dereekb/oauth-resource": "14.7.0",
74
+ "@dereekb/util": "14.7.0",
75
75
  "@nestjs/common": "^12.0.1",
76
76
  "arktype": "^2.2.0",
77
77
  "firebase": "^12.18.0",
@@ -1,16 +1,16 @@
1
1
  {
2
2
  "name": "@dereekb/dbx-cli/route",
3
- "version": "14.5.1",
3
+ "version": "14.7.0",
4
4
  "type": "module",
5
5
  "sideEffects": false,
6
6
  "peerDependencies": {
7
- "@dereekb/date": "14.5.1",
8
- "@dereekb/dbx-cli": "14.5.1",
9
- "@dereekb/firebase": "14.5.1",
10
- "@dereekb/model": "14.5.1",
11
- "@dereekb/nestjs": "14.5.1",
12
- "@dereekb/oauth-resource": "14.5.1",
13
- "@dereekb/util": "14.5.1"
7
+ "@dereekb/date": "14.7.0",
8
+ "@dereekb/dbx-cli": "14.7.0",
9
+ "@dereekb/firebase": "14.7.0",
10
+ "@dereekb/model": "14.7.0",
11
+ "@dereekb/nestjs": "14.7.0",
12
+ "@dereekb/oauth-resource": "14.7.0",
13
+ "@dereekb/util": "14.7.0"
14
14
  },
15
15
  "exports": {
16
16
  "./package.json": "./package.json",
@@ -1,4 +1,4 @@
1
- import { type AsyncKeyedValueCache, type Maybe } from '@dereekb/util';
1
+ import { type AsyncKeyedValueCache, type Maybe, type OidcTokenResponse } from '@dereekb/util';
2
2
  /**
3
3
  * A cached token entry for a single env.
4
4
  */
@@ -67,3 +67,34 @@ export declare function createCliTokenCacheStore(input: CreateCliTokenCacheStore
67
67
  * @returns `true` when the token is at or within `bufferMs` of expiry, otherwise `false`.
68
68
  */
69
69
  export declare function isTokenExpired(entry: Maybe<CliTokenEntry>, nowMs?: number, bufferMs?: number): boolean;
70
+ /**
71
+ * Input for {@link mergeRefreshedTokenEntry}.
72
+ */
73
+ export interface MergeRefreshedTokenEntryInput {
74
+ readonly entry: CliTokenEntry;
75
+ readonly refreshed: OidcTokenResponse;
76
+ /**
77
+ * The current time in unix epoch milliseconds, used to derive `expiresAt`. Defaults to `Date.now()`.
78
+ */
79
+ readonly nowMs?: number;
80
+ }
81
+ /**
82
+ * Folds an OIDC refresh response into the cached entry it was obtained with.
83
+ *
84
+ * EVERY refresh must persist the result of this merge. A refresh CONSUMES the supplied refresh
85
+ * token whenever the grant rotates, so keeping the old entry leaves the cache holding a spent
86
+ * credential and strands the session on the next refresh — with replay detection on the provider
87
+ * side, reusing the spent token can revoke the whole grant. Centralized so the refresh call sites
88
+ * (auth middleware, `auth check`, `doctor`) cannot drift apart on this.
89
+ *
90
+ * @param input - The entry that was refreshed and the token response it produced.
91
+ * @returns The updated entry to persist. Fields the provider omits fall back to the previous entry.
92
+ */
93
+ export declare function mergeRefreshedTokenEntry(input: MergeRefreshedTokenEntryInput): CliTokenEntry;
94
+ /**
95
+ * Whether a refresh response rotated the refresh token it was obtained with.
96
+ *
97
+ * @param input - The entry that was refreshed and the token response it produced.
98
+ * @returns `true` when the provider issued a DIFFERENT refresh token, meaning the old one is spent.
99
+ */
100
+ export declare function didRotateRefreshToken(input: Pick<MergeRefreshedTokenEntryInput, 'entry' | 'refreshed'>): boolean;
@@ -1,3 +1,4 @@
1
1
  export * from './build-drift.check';
2
2
  export * from './doctor.command.factory';
3
3
  export * from './firestore-session.check';
4
+ export * from './token-refresh.check';
@@ -0,0 +1,32 @@
1
+ import { discoverOidcMetadata, refreshAccessToken } from '../auth/oidc.client';
2
+ import { type CliTokenCacheStore } from '../config/token.cache';
3
+ import { type DoctorCheck } from './doctor.command.factory';
4
+ /**
5
+ * Name reported by the check {@link createTokenRefreshDoctorCheck} builds.
6
+ */
7
+ export declare const TOKEN_REFRESH_DOCTOR_CHECK_NAME = "token-refresh-round-trip";
8
+ /**
9
+ * Seams for {@link createTokenRefreshDoctorCheck}. Production omits all of them and gets the real
10
+ * OIDC client plus the on-disk token cache; tests substitute fakes so the check's PERSIST step can
11
+ * be asserted without a network or a home directory.
12
+ */
13
+ export interface TokenRefreshDoctorCheckOverrides {
14
+ readonly tokenStoreForCli?: (cliName: string) => CliTokenCacheStore;
15
+ readonly discoverOidcMetadata?: typeof discoverOidcMetadata;
16
+ readonly refreshAccessToken?: typeof refreshAccessToken;
17
+ }
18
+ /**
19
+ * Builds the doctor check that proves the cached refresh token still works.
20
+ *
21
+ * The round-trip is a REAL refresh, so on a rotating grant it consumes the cached refresh token and
22
+ * the response must be written back — see `mergeRefreshedTokenEntry`. Discarding it (as this check
23
+ * once did) leaves the cache holding a spent credential: the very next refresh fails
24
+ * `invalid_grant`, and a provider doing replay detection revokes the whole grant. The symptom is
25
+ * maximally confusing because the session dies only AFTER the cached access token lapses, so the
26
+ * damage surfaces well away from the `doctor` run that caused it.
27
+ *
28
+ * @param overrides - Optional test seams; production passes none.
29
+ * @returns The {@link DoctorCheck}.
30
+ * @__NO_SIDE_EFFECTS__
31
+ */
32
+ export declare function createTokenRefreshDoctorCheck(overrides?: TokenRefreshDoctorCheckOverrides): DoctorCheck;
@@ -1,5 +1,5 @@
1
1
  import type { CommandModule } from 'yargs';
2
- import type { CliModelManifest } from './types';
2
+ import type { CliModelCompositeKeyEncoding, CliModelManifest, CliModelManifestEntry } from './types';
3
3
  /**
4
4
  * Default command name for the model-decode command. Top-level so it stays
5
5
  * out of the API-call namespace owned by `model <model> <action>`.
@@ -31,14 +31,45 @@ export interface DecodedKeySegment {
31
31
  readonly sourceFile?: string;
32
32
  }
33
33
  /**
34
- * Result of decoding a Firestore model key into its model + id components.
34
+ * Segments of a decoded Firestore key — the leaf, its ancestor chain, and any prefixes the manifest
35
+ * could not resolve.
35
36
  */
36
- export interface DecodedKey {
37
+ export interface DecodedKeySegments {
37
38
  readonly key: string;
38
39
  readonly leaf: DecodedKeySegment;
39
40
  readonly ancestors: readonly DecodedKeySegment[];
40
41
  readonly unresolvedPrefixes: readonly string[];
41
42
  }
43
+ /**
44
+ * A composite-key model whose document id is derived from the decoded key, with that document's
45
+ * ready-to-use key (e.g. decoding a `District` key also yields the `jobDistrict` key
46
+ * `jd/<flattened district key>`).
47
+ */
48
+ export interface DerivedCompositeKey {
49
+ /**
50
+ * The derived document's full key (`<collectionPrefix>/<flattened source key>`).
51
+ */
52
+ readonly key: string;
53
+ readonly modelType: string;
54
+ readonly modelName: string;
55
+ readonly collectionPrefix: string;
56
+ readonly encoding: CliModelCompositeKeyEncoding;
57
+ }
58
+ /**
59
+ * Result of decoding a Firestore model key into its model + id components.
60
+ */
61
+ export interface DecodedKey extends DecodedKeySegments {
62
+ /**
63
+ * Composite-key models derived from this key. Empty when the leaf prefix is unresolved or no
64
+ * model declares this leaf as a composite-key source.
65
+ */
66
+ readonly derivedKeys: readonly DerivedCompositeKey[];
67
+ /**
68
+ * When the leaf is itself a `two-way` composite-key model, the source key recovered from its id,
69
+ * decoded. Absent for `one-way` models and when the id does not parse as a flattened key.
70
+ */
71
+ readonly compositeSource?: DecodedKeySegments;
72
+ }
42
73
  /**
43
74
  * Builds the top-level `model-decode <key>` command.
44
75
  *
@@ -58,17 +89,49 @@ export interface DecodedKey {
58
89
  export declare function buildModelDecodeCommand(manifest: CliModelManifest, options?: BuildModelDecodeCommandOptions): CommandModule;
59
90
  /**
60
91
  * Splits `rawKey` on `/`, resolves each `[prefix, id]` pair against the
61
- * manifest, and returns the leaf segment + ancestor chain. Throws
92
+ * manifest, and returns the leaf segment + ancestor chain, plus the
93
+ * composite-key relationships declared on the manifest: the keys of models
94
+ * derived from this key (`derivedKeys`) and, for a two-way composite-key
95
+ * leaf, the recovered source key (`compositeSource`). Throws
62
96
  * {@link CliError} for malformed inputs.
63
97
  *
98
+ * Mirrors the `@dereekb/firebase-server/mcp` `model-decode` tool; both
99
+ * implementations must stay in lockstep on segment count, resolution order,
100
+ * and composite-key output.
101
+ *
64
102
  * @param rawKey - The Firestore key string.
65
103
  * @param manifest - The generated model manifest.
66
- * @returns The decoded key with leaf, ancestors, and any unresolved prefixes.
104
+ * @returns The decoded key with leaf, ancestors, unresolved prefixes, and composite-key relationships.
67
105
  * @throws {CliError} When `rawKey` is empty or does not parse into an even number of `prefix/id` segments.
68
106
  *
69
107
  * @__NO_SIDE_EFFECTS__
70
108
  */
71
109
  export declare function decodeFirestoreModelKey(rawKey: string, manifest: CliModelManifest): DecodedKey;
110
+ /**
111
+ * Resolves the manifest entries whose `compositeKey.from` names `source` — the models whose document
112
+ * id is derived from a `source` document's key. A wildcard `from=*` matches every model except
113
+ * `source` itself.
114
+ *
115
+ * @param source - The manifest entry of the decoded key's leaf model.
116
+ * @param manifest - The generated model manifest.
117
+ * @returns The composite-key entries derived from `source`, in manifest order.
118
+ *
119
+ * @__NO_SIDE_EFFECTS__
120
+ */
121
+ export declare function findCompositeKeyModelsDerivedFrom(source: CliModelManifestEntry, manifest: CliModelManifest): CliModelManifestEntry[];
122
+ /**
123
+ * Splits `rawKey` on `/` and resolves each `[prefix, id]` pair against the
124
+ * manifest — the segment walk shared by {@link decodeFirestoreModelKey} and
125
+ * the two-way composite-source decode.
126
+ *
127
+ * @param rawKey - The Firestore key string.
128
+ * @param manifest - The generated model manifest.
129
+ * @returns The decoded key with leaf, ancestors, and any unresolved prefixes.
130
+ * @throws {CliError} When `rawKey` is empty or does not parse into an even number of `prefix/id` segments.
131
+ *
132
+ * @__NO_SIDE_EFFECTS__
133
+ */
134
+ export declare function decodeFirestoreModelKeySegments(rawKey: string, manifest: CliModelManifest): DecodedKeySegments;
72
135
  /**
73
136
  * Renders a {@link DecodedKey} as a human-readable text block. Mirrors the
74
137
  * MCP `dbx_model_decode` key-mode output for consistency between agent and
@@ -185,6 +185,37 @@ export interface CliModelManifestEntry {
185
185
  readonly exportName: string;
186
186
  readonly sourceFile: string;
187
187
  };
188
+ /**
189
+ * Declared `@dbxModelCompositeKey from=<ModelA>[,<ModelB>...] encoding=<two-way|one-way>` on the
190
+ * model interface — this model's document id is a flattened encoding of a source model's key.
191
+ * Absent when the model omits the tag.
192
+ *
193
+ * Consumed by `model-decode` (to publish the derived key when a source key is decoded, and to
194
+ * recover the source key from a two-way id) and by `model-get` (to accept a source key in place
195
+ * of the flattened id).
196
+ */
197
+ readonly compositeKey?: CliModelCompositeKey;
198
+ }
199
+ /**
200
+ * Flat-key encoding declared by a `@dbxModelCompositeKey` tag.
201
+ *
202
+ * - `one-way` — `flatFirestoreModelKey()`: slashes removed; the source key cannot be recovered
203
+ * from the id.
204
+ * - `two-way` — `twoWayFlatFirestoreModelKey()`: slashes replaced with underscores; recover the
205
+ * source key with `inferKeyFromTwoWayFlatFirestoreModelKey()`.
206
+ */
207
+ export type CliModelCompositeKeyEncoding = 'one-way' | 'two-way';
208
+ /**
209
+ * Composite-key declaration carried on a {@link CliModelManifestEntry}.
210
+ */
211
+ export interface CliModelCompositeKey {
212
+ /**
213
+ * `'*'` when any model's key may be the source (framework models such as `NotificationBox`), or
214
+ * the source model names as written in the tag — each an interface name, identity const, or
215
+ * modelType, resolved against the manifest at decode time.
216
+ */
217
+ readonly from: readonly string[] | '*';
218
+ readonly encoding: CliModelCompositeKeyEncoding;
188
219
  }
189
220
  /**
190
221
  * Generated array of {@link CliModelManifestEntry} consumed by `model-info`
@@ -394,6 +425,11 @@ export interface McpManifestModelEntry {
394
425
  * client read grant in `firestore.rules` either) rather than a missing role it could be granted.
395
426
  */
396
427
  readonly serverOnly?: boolean;
428
+ /**
429
+ * Composite-key declaration (mirror of {@link CliModelManifestEntry.compositeKey}). Lets the
430
+ * runtime `model-decode` publish derived keys and `model-get` accept a source key.
431
+ */
432
+ readonly compositeKey?: CliModelCompositeKey;
397
433
  }
398
434
  /**
399
435
  * One auth claim entry in the pre-rendered MCP manifest JSON. Powers the
@@ -44,6 +44,15 @@ export interface LoadUtilRegistryResult {
44
44
  readonly loaderWarnings: readonly UtilLoaderWarning[];
45
45
  readonly externalSourceCount: number;
46
46
  }
47
+ /**
48
+ * The bundled `@dereekb/*` utils manifests to load at startup. Must stay in step with the `utils.scan` entries in
49
+ * `dbx-mcp.config.json` — a generated manifest that is missing from this list is silently invisible to
50
+ * `dbx_util_lookup`/`dbx_util_search` no matter how well its exports are tagged.
51
+ *
52
+ * Loading is non-strict, so naming a file that a given workspace has not generated warns and skips rather than
53
+ * failing server startup.
54
+ */
55
+ export declare const DEFAULT_BUNDLED_FILENAMES: readonly ["dereekb-util.utils.mcp.generated.json", "dereekb-util-fetch.utils.mcp.generated.json", "dereekb-date.utils.mcp.generated.json", "dereekb-rxjs.utils.mcp.generated.json", "dereekb-model.utils.mcp.generated.json", "dereekb-firebase.utils.mcp.generated.json", "dereekb-firebase-server.utils.mcp.generated.json"];
47
56
  /**
48
57
  * Loads the merged utils registry for the current MCP server.
49
58
  *
@@ -185,6 +185,32 @@ export declare function buildDumpFilePath(extension: 'json' | 'ndjson', suffix?:
185
185
  * @returns The filtered value (typed as the input).
186
186
  */
187
187
  export declare function pickFields<T>(data: T, pick: string): T;
188
+ /**
189
+ * Applies the configured `--pick` filter to the document payload(s) carried by a command result.
190
+ *
191
+ * Returns `unknown` rather than `T` because picking REDUCES each payload — the reduced value is a
192
+ * partial view of the document and does not satisfy the result's own type.
193
+ *
194
+ * @param data - The full command result.
195
+ * @param pick - Reduces a document to the configured fields. Apply it to each payload, not the wrapper.
196
+ * @returns The result with its payload(s) reduced, wrapper fields intact.
197
+ */
198
+ export type CliOutputPickApplier<T> = (data: T, pick: (value: unknown) => unknown) => unknown;
199
+ /**
200
+ * Per-command output overrides for {@link outputResult}.
201
+ */
202
+ export interface CliOutputResultOptions<T> {
203
+ /**
204
+ * Redirects the `--pick` filter into the result's document payload(s).
205
+ *
206
+ * Read commands wrap documents in an envelope (`{ key, data }`, `{ results, errors }`), so the
207
+ * default top-level pick can only ever match the wrapper's own keys — a pick naming real document
208
+ * fields silently yields `{}`, which is indistinguishable from an empty document. Supplied by the
209
+ * command because only it knows its envelope shape. Commands whose result *is* the payload
210
+ * (the common case) omit this and keep the top-level behavior.
211
+ */
212
+ readonly applyPick?: CliOutputPickApplier<T>;
213
+ }
188
214
  /**
189
215
  * Prints a successful command result as a `{ ok: true, data, meta? }` JSON envelope on stdout.
190
216
  *
@@ -194,8 +220,9 @@ export declare function pickFields<T>(data: T, pick: string): T;
194
220
  *
195
221
  * @param data - The command result to emit.
196
222
  * @param meta - Optional additional metadata to attach to the envelope.
223
+ * @param options - Optional per-command overrides — see {@link CliOutputResultOptions}.
197
224
  */
198
- export declare function outputResult<T>(data: T, meta?: Record<string, unknown>): void;
225
+ export declare function outputResult<T>(data: T, meta?: Record<string, unknown>, options?: CliOutputResultOptions<T>): void;
199
226
  /**
200
227
  * Prints a failed command result as a `{ ok: false, error, code, suggestion? }` JSON envelope on stdout.
201
228
  *
package/test/package.json CHANGED
@@ -1,18 +1,18 @@
1
1
  {
2
2
  "name": "@dereekb/dbx-cli/test",
3
- "version": "14.5.1",
3
+ "version": "14.7.0",
4
4
  "sideEffects": false,
5
5
  "type": "module",
6
6
  "peerDependencies": {
7
- "@dereekb/date": "14.5.1",
8
- "@dereekb/dbx-cli": "14.5.1",
9
- "@dereekb/firebase": "14.5.1",
10
- "@dereekb/firebase-server/test": "14.5.0",
11
- "@dereekb/model": "14.5.1",
12
- "@dereekb/nestjs": "14.5.1",
13
- "@dereekb/oauth-resource": "14.5.1",
14
- "@dereekb/rxjs": "14.5.1",
15
- "@dereekb/util": "14.5.1",
7
+ "@dereekb/date": "14.7.0",
8
+ "@dereekb/dbx-cli": "14.7.0",
9
+ "@dereekb/firebase": "14.7.0",
10
+ "@dereekb/firebase-server/test": "14.6.0",
11
+ "@dereekb/model": "14.7.0",
12
+ "@dereekb/nestjs": "14.7.0",
13
+ "@dereekb/oauth-resource": "14.7.0",
14
+ "@dereekb/rxjs": "14.7.0",
15
+ "@dereekb/util": "14.7.0",
16
16
  "@nestjs/common": "^12.0.1",
17
17
  "arktype": "^2.2.0",
18
18
  "vitest": "4.1.11",
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "@dereekb/dbx-cli/validate",
3
- "version": "14.5.1",
3
+ "version": "14.7.0",
4
4
  "type": "module",
5
5
  "sideEffects": false,
6
6
  "peerDependencies": {
7
- "@dereekb/dbx-cli": "14.5.1",
8
- "@dereekb/util": "14.5.1",
7
+ "@dereekb/dbx-cli": "14.7.0",
8
+ "@dereekb/util": "14.7.0",
9
9
  "ts-morph": "^28.0.0"
10
10
  }
11
11
  }