@vectros-ai/cli 0.7.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -675,6 +675,10 @@ var ReadAccessLogResponse;
675
675
  })(ReadAccessLogResponse || (ReadAccessLogResponse = {}));
676
676
  var RecordRequest;
677
677
  ((RecordRequest2) => {
678
+ RecordRequest2.Status = {
679
+ Active: "ACTIVE",
680
+ Archived: "ARCHIVED"
681
+ };
678
682
  RecordRequest2.IndexMode = {
679
683
  Hybrid: "HYBRID",
680
684
  Semantic: "SEMANTIC",
@@ -684,6 +688,10 @@ var RecordRequest;
684
688
  })(RecordRequest || (RecordRequest = {}));
685
689
  var RecordResponse;
686
690
  ((RecordResponse2) => {
691
+ RecordResponse2.Status = {
692
+ Active: "ACTIVE",
693
+ Archived: "ARCHIVED"
694
+ };
687
695
  RecordResponse2.IndexStatus = {
688
696
  PendingIndex: "PENDING_INDEX",
689
697
  Indexed: "INDEXED",
@@ -2306,7 +2314,7 @@ function withNoOpAuthProvider(options) {
2306
2314
  authProvider: new NoOpAuthProvider()
2307
2315
  };
2308
2316
  }
2309
- function handleNonStatusCodeError(error, rawResponse, method, path4) {
2317
+ function handleNonStatusCodeError(error, rawResponse, method, path5) {
2310
2318
  switch (error.reason) {
2311
2319
  case "non-json":
2312
2320
  throw new VectrosError({
@@ -2320,7 +2328,7 @@ function handleNonStatusCodeError(error, rawResponse, method, path4) {
2320
2328
  rawResponse
2321
2329
  });
2322
2330
  case "timeout":
2323
- throw new VectrosTimeoutError(`Timeout exceeded when calling ${method} ${path4}.`, {
2331
+ throw new VectrosTimeoutError(`Timeout exceeded when calling ${method} ${path5}.`, {
2324
2332
  cause: error.cause
2325
2333
  });
2326
2334
  case "unknown":
@@ -3158,7 +3166,7 @@ var AuthClient = class {
3158
3166
  * roleId: "engineering-member",
3159
3167
  * name: "Engineering Team Member",
3160
3168
  * scopes: [{
3161
- * allowed_actions: ["read", "write"]
3169
+ * allowed_actions: ["records:cru", "search:r"]
3162
3170
  * }]
3163
3171
  * }
3164
3172
  * })
@@ -3706,7 +3714,7 @@ var AuthClient = class {
3706
3714
  * roleId: "engineering-member",
3707
3715
  * name: "Engineering Team Member",
3708
3716
  * scopes: [{
3709
- * allowed_actions: ["read", "write"]
3717
+ * allowed_actions: ["records:cru", "search:r"]
3710
3718
  * }]
3711
3719
  * }
3712
3720
  * })
@@ -4609,7 +4617,7 @@ var DocumentsClient = class {
4609
4617
  this._options = normalizeClientOptionsWithAuth(options);
4610
4618
  }
4611
4619
  /**
4612
- * Returns a paginated list of your documents, optionally filtered by folder (`folderId`) and/or owner (`userId`, `orgId`, or `clientId`). The response is a `{data, nextCursor}` envelope; pass `nextCursor` back as `startFrom` to fetch the next page. Requires the `documents:r` scope.
4620
+ * Returns a paginated list of your documents, optionally filtered by folder (`folderId`) and/or owner (`userId`, `orgId`, `clientId`, or `scope`). The response is a `{data, nextCursor}` envelope; pass `nextCursor` back as `startFrom` to fetch the next page. Requires the `documents:r` scope.
4613
4621
  *
4614
4622
  * @param {Vectros.ListDocumentsRequest} request
4615
4623
  * @param {DocumentsClient.RequestOptions} requestOptions - Request-specific configuration.
@@ -4619,6 +4627,7 @@ var DocumentsClient = class {
4619
4627
  * userId: "550e8400-e29b-41d4-a716-446655440000",
4620
4628
  * orgId: "6ba7b810-9dad-11d1-80b4-00c04fd430c8",
4621
4629
  * clientId: "f47ac10b-58cc-4372-a567-0e02b2c3d479",
4630
+ * scope: "group:eng-team",
4622
4631
  * folderId: "f47ac10b-58cc-4372-a567-0e02b2c3d479",
4623
4632
  * startFrom: "doc_prev123"
4624
4633
  * })
@@ -4627,11 +4636,12 @@ var DocumentsClient = class {
4627
4636
  return HttpResponsePromise.fromPromise(this.__listDocuments(request, requestOptions));
4628
4637
  }
4629
4638
  async __listDocuments(request = {}, requestOptions) {
4630
- const { userId, orgId, clientId, folderId, startFrom, limit } = request;
4639
+ const { userId, orgId, clientId, scope, folderId, startFrom, limit } = request;
4631
4640
  const _queryParams = {
4632
4641
  userId,
4633
4642
  orgId,
4634
4643
  clientId,
4644
+ scope,
4635
4645
  folderId,
4636
4646
  startFrom,
4637
4647
  limit
@@ -4689,9 +4699,10 @@ var DocumentsClient = class {
4689
4699
  return HttpResponsePromise.fromPromise(this.__ingestDocument(request, requestOptions));
4690
4700
  }
4691
4701
  async __ingestDocument(request, requestOptions) {
4692
- const { upsert, body: _body } = request;
4702
+ const { upsert, allowClear, body: _body } = request;
4693
4703
  const _queryParams = {
4694
- upsert
4704
+ upsert,
4705
+ allowClear
4695
4706
  };
4696
4707
  const _authRequest = await this._options.authProvider.getAuthRequest();
4697
4708
  const _headers = mergeHeaders(
@@ -4815,7 +4826,10 @@ var DocumentsClient = class {
4815
4826
  return HttpResponsePromise.fromPromise(this.__updateDocument(request, requestOptions));
4816
4827
  }
4817
4828
  async __updateDocument(request, requestOptions) {
4818
- const { id, body: _body } = request;
4829
+ const { id, allowClear, body: _body } = request;
4830
+ const _queryParams = {
4831
+ allowClear
4832
+ };
4819
4833
  const _authRequest = await this._options.authProvider.getAuthRequest();
4820
4834
  const _headers = mergeHeaders(
4821
4835
  _authRequest.headers,
@@ -4830,7 +4844,7 @@ var DocumentsClient = class {
4830
4844
  method: "PUT",
4831
4845
  headers: _headers,
4832
4846
  contentType: "application/json",
4833
- queryString: url_exports.queryBuilder().mergeAdditional(requestOptions?.queryParams).build(),
4847
+ queryString: url_exports.queryBuilder().addMany(_queryParams).mergeAdditional(requestOptions?.queryParams).build(),
4834
4848
  requestType: "json",
4835
4849
  body: _body,
4836
4850
  timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1e3,
@@ -7566,7 +7580,7 @@ var RecordsClient = class {
7566
7580
  return handleNonStatusCodeError(_response.error, _response.rawResponse, "POST", "/v1/records/batch");
7567
7581
  }
7568
7582
  /**
7569
- * Returns a paginated list of records in your account as a `{data, nextCursor}` page. Supply exactly one of `type`, `folderId`, or `recent=true` to choose the mode: `type` lists all records of a single type; `folderId` lists all records in a folder (any type); and `recent=true` returns the account-wide recently-updated feed across all types, newest first. You may combine `type` with `folderId` to list a single type within a folder. The owner filters (`userId`, `orgId`, `clientId`) further narrow the type and folder modes; the `recent` feed is standalone and ignores all filters. Each token only sees the record types it is scoped to read. Requires the `records:r` scope. By default the response returns the indexed projection of each record; set `includePayload=true` to include full payloads.
7583
+ * Returns a paginated list of records in your account as a `{data, nextCursor}` page. Supply exactly one of `type`, `folderId`, or `recent=true` to choose the mode: `type` lists all records of a single type; `folderId` lists all records in a folder (any type); and `recent=true` returns the account-wide recently-updated feed across all types, newest first. You may combine `type` with `folderId` to list a single type within a folder. The owner filters (`userId`, `orgId`, `clientId`, `scope`) further narrow the type and folder modes; the `recent` feed is standalone and ignores all filters. Each token only sees the record types it is scoped to read. Requires the `records:r` scope. By default the response returns the indexed projection of each record; set `includePayload=true` to include full payloads.
7570
7584
  *
7571
7585
  * @param {Vectros.ListRecordsRequest} request
7572
7586
  * @param {RecordsClient.RequestOptions} requestOptions - Request-specific configuration.
@@ -7578,6 +7592,7 @@ var RecordsClient = class {
7578
7592
  * userId: "550e8400-e29b-41d4-a716-446655440000",
7579
7593
  * orgId: "6ba7b810-9dad-11d1-80b4-00c04fd430c8",
7580
7594
  * clientId: "f47ac10b-58cc-4372-a567-0e02b2c3d479",
7595
+ * scope: "group:eng-team",
7581
7596
  * startFrom: "550e8400-e29b-41d4-a716-446655440000"
7582
7597
  * })
7583
7598
  */
@@ -7585,13 +7600,25 @@ var RecordsClient = class {
7585
7600
  return HttpResponsePromise.fromPromise(this.__listRecords(request, requestOptions));
7586
7601
  }
7587
7602
  async __listRecords(request = {}, requestOptions) {
7588
- const { type: type_, folderId, userId, orgId, clientId, startFrom, limit, includePayload, recent } = request;
7603
+ const {
7604
+ type: type_,
7605
+ folderId,
7606
+ userId,
7607
+ orgId,
7608
+ clientId,
7609
+ scope,
7610
+ startFrom,
7611
+ limit,
7612
+ includePayload,
7613
+ recent
7614
+ } = request;
7589
7615
  const _queryParams = {
7590
7616
  type: type_,
7591
7617
  folderId,
7592
7618
  userId,
7593
7619
  orgId,
7594
7620
  clientId,
7621
+ scope,
7595
7622
  startFrom,
7596
7623
  limit,
7597
7624
  includePayload,
@@ -7656,9 +7683,10 @@ var RecordsClient = class {
7656
7683
  return HttpResponsePromise.fromPromise(this.__createRecord(request, requestOptions));
7657
7684
  }
7658
7685
  async __createRecord(request, requestOptions) {
7659
- const { upsert, body: _body } = request;
7686
+ const { upsert, allowClear, body: _body } = request;
7660
7687
  const _queryParams = {
7661
- upsert
7688
+ upsert,
7689
+ allowClear
7662
7690
  };
7663
7691
  const _authRequest = await this._options.authProvider.getAuthRequest();
7664
7692
  const _headers = mergeHeaders(
@@ -7780,7 +7808,10 @@ var RecordsClient = class {
7780
7808
  return HttpResponsePromise.fromPromise(this.__updateRecord(request, requestOptions));
7781
7809
  }
7782
7810
  async __updateRecord(request, requestOptions) {
7783
- const { id, body: _body } = request;
7811
+ const { id, allowClear, body: _body } = request;
7812
+ const _queryParams = {
7813
+ allowClear
7814
+ };
7784
7815
  const _authRequest = await this._options.authProvider.getAuthRequest();
7785
7816
  const _headers = mergeHeaders(
7786
7817
  _authRequest.headers,
@@ -7795,7 +7826,7 @@ var RecordsClient = class {
7795
7826
  method: "PUT",
7796
7827
  headers: _headers,
7797
7828
  contentType: "application/json",
7798
- queryString: url_exports.queryBuilder().mergeAdditional(requestOptions?.queryParams).build(),
7829
+ queryString: url_exports.queryBuilder().addMany(_queryParams).mergeAdditional(requestOptions?.queryParams).build(),
7799
7830
  requestType: "json",
7800
7831
  body: _body,
7801
7832
  timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1e3,
@@ -8880,7 +8911,14 @@ var BlueprintAccessProfileSchema = import_zod.z.object({
8880
8911
  // null sentinel (+ ScopeClause). e.g. `{ orgId: ["org_x", null] }` =
8881
8912
  // "org_x's records AND tenant-shared records". Omitting null restricts to
8882
8913
  // the listed owners ONLY (the key will NOT see tenant-level/seed records).
8883
- dataScope: import_zod.z.record(import_zod.z.array(import_zod.z.union([import_zod.z.string().min(1), import_zod.z.null()]))).optional()
8914
+ dataScope: import_zod.z.record(import_zod.z.array(import_zod.z.union([import_zod.z.string().min(1), import_zod.z.null()]))).optional(),
8915
+ // Optional identity overrides for the service principal's profile — scope
8916
+ // values its key STAMPS onto everything it writes (a credential can only
8917
+ // stamp ownership its identity carries). Keys: `orgId`/`clientId` (or the
8918
+ // namespaced `scope:<ns>` form); values may be `${{ identities.* }}` tokens
8919
+ // (substituted at apply time). e.g. `{ orgId: '${{ identities.team }}' }`
8920
+ // makes every record the service key writes org/team-owned.
8921
+ identityOverrides: import_zod.z.record(import_zod.z.string().min(1)).optional()
8884
8922
  }).strict();
8885
8923
  var BlueprintRoleClauseSchema = import_zod.z.object({
8886
8924
  allowedActions: import_zod.z.array(import_zod.z.string().min(1)).min(1),
@@ -8906,7 +8944,14 @@ var SeedCommonShape = {
8906
8944
  * seeds resolve a `reference` against (across surfaces: a record seed may
8907
8945
  * reference a document seed by its externalId, and vice versa).
8908
8946
  */
8909
- externalId: import_zod.z.string().min(1)
8947
+ externalId: import_zod.z.string().min(1),
8948
+ /**
8949
+ * Optional scope ownership for the seeded item, as `namespace:value` entries
8950
+ * (≤2, e.g. `['org:${{ identities.team }}']`; tokens substitute at apply
8951
+ * time). `[]` = a private, user-owned item. Omitted = the seeding
8952
+ * credential's full identity (the default).
8953
+ */
8954
+ scopes: import_zod.z.array(import_zod.z.string()).optional()
8910
8955
  };
8911
8956
  var RecordSeedSchema = import_zod.z.object({
8912
8957
  surface: import_zod.z.literal("record"),
@@ -8930,25 +8975,25 @@ var BlueprintSeedRecordSchema = import_zod.z.discriminatedUnion("surface", [
8930
8975
  ]);
8931
8976
  var SELF_TOKEN_RE = /\$\{\{\s*self\.[A-Za-z_]\w*\s*\}\}/;
8932
8977
  function lintSelfTokenPlacement(value, ctx) {
8933
- const walk = (node, path4, inRoleDataScope) => {
8978
+ const walk = (node, path5, inRoleDataScope) => {
8934
8979
  if (typeof node === "string") {
8935
8980
  if (!inRoleDataScope && SELF_TOKEN_RE.test(node)) {
8936
8981
  ctx.addIssue({
8937
8982
  code: import_zod.z.ZodIssueCode.custom,
8938
- path: path4,
8983
+ path: path5,
8939
8984
  message: "'${{ self.* }}' is a runtime per-principal placeholder \u2014 it is only valid inside a roles[].dataScope value"
8940
8985
  });
8941
8986
  }
8942
8987
  return;
8943
8988
  }
8944
8989
  if (Array.isArray(node)) {
8945
- node.forEach((v, i) => walk(v, [...path4, i], inRoleDataScope));
8990
+ node.forEach((v, i) => walk(v, [...path5, i], inRoleDataScope));
8946
8991
  return;
8947
8992
  }
8948
8993
  if (node && typeof node === "object") {
8949
8994
  for (const [k, v] of Object.entries(node)) {
8950
- const entering = inRoleDataScope || path4.length === 3 && path4[0] === "roles" && k === "dataScope";
8951
- walk(v, [...path4, k], entering);
8995
+ const entering = inRoleDataScope || path5.length === 3 && path5[0] === "roles" && k === "dataScope";
8996
+ walk(v, [...path5, k], entering);
8952
8997
  }
8953
8998
  }
8954
8999
  };
@@ -8959,13 +9004,13 @@ function lintIdentityRefsDeclared(value, ctx) {
8959
9004
  const declared = new Set(
8960
9005
  value && typeof value === "object" && "identities" in value && value.identities ? Object.keys(value.identities) : []
8961
9006
  );
8962
- const walk = (node, path4) => {
9007
+ const walk = (node, path5) => {
8963
9008
  if (typeof node === "string") {
8964
9009
  for (const m of node.matchAll(IDENTITY_REF_RE)) {
8965
9010
  if (!declared.has(m[1])) {
8966
9011
  ctx.addIssue({
8967
9012
  code: import_zod.z.ZodIssueCode.custom,
8968
- path: path4,
9013
+ path: path5,
8969
9014
  message: `'\${{ identities.${m[1]} }}' references an undeclared identity \u2014 add '${m[1]}' to the top-level 'identities' block`
8970
9015
  });
8971
9016
  }
@@ -8973,11 +9018,11 @@ function lintIdentityRefsDeclared(value, ctx) {
8973
9018
  return;
8974
9019
  }
8975
9020
  if (Array.isArray(node)) {
8976
- node.forEach((v, i) => walk(v, [...path4, i]));
9021
+ node.forEach((v, i) => walk(v, [...path5, i]));
8977
9022
  } else if (node && typeof node === "object") {
8978
9023
  for (const [k, v] of Object.entries(node)) {
8979
- if (path4.length === 0 && k === "identities") continue;
8980
- walk(v, [...path4, k]);
9024
+ if (path5.length === 0 && k === "identities") continue;
9025
+ walk(v, [...path5, k]);
8981
9026
  }
8982
9027
  }
8983
9028
  };
@@ -9024,9 +9069,9 @@ var BlueprintSchema = import_zod.z.object({
9024
9069
  lintIdentityRefsDeclared(bp, ctx);
9025
9070
  lintSeedSurfaces(bp, ctx);
9026
9071
  });
9027
- function formatIssuePath(path4) {
9072
+ function formatIssuePath(path5) {
9028
9073
  let out = "";
9029
- for (const seg of path4) {
9074
+ for (const seg of path5) {
9030
9075
  if (typeof seg === "number") out += `[${seg}]`;
9031
9076
  else out += out.length ? `.${seg}` : seg;
9032
9077
  }
@@ -9064,6 +9109,14 @@ function contextNameOf(blueprint) {
9064
9109
  return blueprint.contextName ?? `MCP \u2014 ${blueprint.name}`;
9065
9110
  }
9066
9111
  var IDENTITY_TOKEN_RE = /\$\{\{\s*identities\.([A-Za-z_]\w*)\s*\}\}/g;
9112
+ var BlueprintIdentityError = class extends Error {
9113
+ issues;
9114
+ constructor(message, issues = []) {
9115
+ super(message);
9116
+ this.name = "BlueprintIdentityError";
9117
+ this.issues = issues;
9118
+ }
9119
+ };
9067
9120
  function collectIdentityReferences(value) {
9068
9121
  const found = /* @__PURE__ */ new Set();
9069
9122
  const walk = (node) => {
@@ -9078,6 +9131,61 @@ function collectIdentityReferences(value) {
9078
9131
  walk(value);
9079
9132
  return [...found];
9080
9133
  }
9134
+ function substitute(node, idMap) {
9135
+ if (typeof node === "string") {
9136
+ return node.replace(IDENTITY_TOKEN_RE, (_full, name) => idMap[name]);
9137
+ }
9138
+ if (Array.isArray(node)) return node.map((v) => substitute(v, idMap));
9139
+ if (node && typeof node === "object") {
9140
+ const out = {};
9141
+ for (const [k, v] of Object.entries(node)) out[k] = substitute(v, idMap);
9142
+ return out;
9143
+ }
9144
+ return node;
9145
+ }
9146
+ async function resolveBlueprintIdentities(raw, resolve) {
9147
+ if (raw === null || typeof raw !== "object" || Array.isArray(raw)) return raw;
9148
+ const { identities: rawIdentities, ...body } = raw;
9149
+ const refs = collectIdentityReferences(body);
9150
+ if (rawIdentities === void 0 && refs.length === 0) return raw;
9151
+ const parsed = IdentitiesDeclSchema.safeParse(rawIdentities ?? {});
9152
+ if (!parsed.success) {
9153
+ throw new BlueprintIdentityError(
9154
+ "Blueprint identities block is invalid",
9155
+ parsed.error.issues.map((i) => ({
9156
+ path: i.path.length ? `identities.${i.path.join(".")}` : "identities",
9157
+ message: i.message
9158
+ }))
9159
+ );
9160
+ }
9161
+ const declared = parsed.data;
9162
+ const undeclared = refs.filter((name) => !(name in declared));
9163
+ if (undeclared.length) {
9164
+ throw new BlueprintIdentityError(
9165
+ `Blueprint references undeclared identities: ${undeclared.join(", ")}`,
9166
+ undeclared.map((name) => ({
9167
+ path: `identities.${name}`,
9168
+ message: `'\${{ identities.${name} }}' is referenced but not declared in the 'identities' block`
9169
+ }))
9170
+ );
9171
+ }
9172
+ const idMap = {};
9173
+ for (const [name, decl] of Object.entries(declared)) {
9174
+ try {
9175
+ const id = await resolve(name, decl);
9176
+ if (typeof id !== "string" || id.length === 0) {
9177
+ throw new Error(`resolver returned a non-string id (${JSON.stringify(id)})`);
9178
+ }
9179
+ idMap[name] = id;
9180
+ } catch (err) {
9181
+ throw new BlueprintIdentityError(
9182
+ `Failed to resolve identity '${name}' (${decl.kind} externalId=${decl.externalId}): ${err instanceof Error ? err.message : String(err)}`,
9183
+ [{ path: `identities.${name}`, message: err instanceof Error ? err.message : String(err) }]
9184
+ );
9185
+ }
9186
+ }
9187
+ return substitute(body, idMap);
9188
+ }
9081
9189
  var INPUT_NAME_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
9082
9190
  var InputDeclSchema = import_zod2.z.object({
9083
9191
  type: import_zod2.z.enum(["string", "number", "boolean"]),
@@ -9115,18 +9223,18 @@ var WHOLE_TOKEN_RE = /^\$\{\{\s*([A-Za-z_]\w*)\.([A-Za-z_]\w*)\s*\}\}$/;
9115
9223
  var TOKEN_SCAN_RE = /\$\$\{\{|\$\{\{([\s\S]*?)\}\}|\$\{\{/g;
9116
9224
  var INNER_REF_RE = /^([A-Za-z_]\w*)\.([A-Za-z_]\w*)$/;
9117
9225
  var DEFERRED_NAMESPACES = /* @__PURE__ */ new Set(["self", "identities"]);
9118
- function resolveRef(ns, name, ctx, path4) {
9226
+ function resolveRef(ns, name, ctx, path5) {
9119
9227
  if (ns === "inputs") {
9120
9228
  if (!(name in ctx.declared)) {
9121
9229
  ctx.issues.push({
9122
- path: path4,
9230
+ path: path5,
9123
9231
  message: `unknown input 'inputs.${name}' \u2014 declare it in the top-level 'inputs:' block`
9124
9232
  });
9125
9233
  return void 0;
9126
9234
  }
9127
9235
  if (!(name in ctx.values)) {
9128
9236
  ctx.issues.push({
9129
- path: path4,
9237
+ path: path5,
9130
9238
  message: `input 'inputs.${name}' has no value \u2014 supply it (--set ${name}=\u2026 or --values) or give it a default`
9131
9239
  });
9132
9240
  return void 0;
@@ -9136,14 +9244,14 @@ function resolveRef(ns, name, ctx, path4) {
9136
9244
  if (ns === "vectros") {
9137
9245
  if (name !== "context" && name !== "suffix") {
9138
9246
  ctx.issues.push({
9139
- path: path4,
9247
+ path: path5,
9140
9248
  message: `unknown built-in 'vectros.${name}' (available: vectros.context, vectros.suffix)`
9141
9249
  });
9142
9250
  return void 0;
9143
9251
  }
9144
9252
  if (!ctx.builtins) {
9145
9253
  ctx.issues.push({
9146
- path: path4,
9254
+ path: path5,
9147
9255
  message: `'vectros.${name}' is unavailable: contextId must be a literal string to derive built-ins`
9148
9256
  });
9149
9257
  return void 0;
@@ -9151,12 +9259,12 @@ function resolveRef(ns, name, ctx, path4) {
9151
9259
  return ctx.builtins[name];
9152
9260
  }
9153
9261
  ctx.issues.push({
9154
- path: path4,
9262
+ path: path5,
9155
9263
  message: `unknown namespace '${ns}' in '\${{ ${ns}.${name} }}' (available: inputs, vectros)`
9156
9264
  });
9157
9265
  return void 0;
9158
9266
  }
9159
- function interpolate(str, ctx, path4) {
9267
+ function interpolate(str, ctx, path5) {
9160
9268
  let out = "";
9161
9269
  let last = 0;
9162
9270
  TOKEN_SCAN_RE.lastIndex = 0;
@@ -9167,7 +9275,7 @@ function interpolate(str, ctx, path4) {
9167
9275
  out += "${{";
9168
9276
  } else if (m[0] === "${{" && m[1] === void 0) {
9169
9277
  ctx.issues.push({
9170
- path: path4,
9278
+ path: path5,
9171
9279
  message: `unterminated token '\${{' \u2014 expected a closing '}}' (escape a literal with '$\${{')`
9172
9280
  });
9173
9281
  } else {
@@ -9175,13 +9283,13 @@ function interpolate(str, ctx, path4) {
9175
9283
  const ref = inner.match(INNER_REF_RE);
9176
9284
  if (!ref) {
9177
9285
  ctx.issues.push({
9178
- path: path4,
9286
+ path: path5,
9179
9287
  message: `malformed reference '\${{ ${inner} }}' \u2014 expected '\${{ namespace.name }}' (escape a literal with '$\${{')`
9180
9288
  });
9181
9289
  } else if (DEFERRED_NAMESPACES.has(ref[1])) {
9182
9290
  out += m[0];
9183
9291
  } else {
9184
- const v = resolveRef(ref[1], ref[2], ctx, path4);
9292
+ const v = resolveRef(ref[1], ref[2], ctx, path5);
9185
9293
  if (v !== void 0) out += String(v);
9186
9294
  }
9187
9295
  }
@@ -9190,34 +9298,34 @@ function interpolate(str, ctx, path4) {
9190
9298
  out += str.slice(last);
9191
9299
  return out;
9192
9300
  }
9193
- function substituteValue(val, ctx, path4) {
9301
+ function substituteValue(val, ctx, path5) {
9194
9302
  if (typeof val === "string") {
9195
9303
  const whole = val.match(WHOLE_TOKEN_RE);
9196
9304
  if (whole) {
9197
9305
  if (DEFERRED_NAMESPACES.has(whole[1])) return val;
9198
- const v = resolveRef(whole[1], whole[2], ctx, path4);
9306
+ const v = resolveRef(whole[1], whole[2], ctx, path5);
9199
9307
  return v === void 0 ? val : v;
9200
9308
  }
9201
- return interpolate(val, ctx, path4);
9309
+ return interpolate(val, ctx, path5);
9202
9310
  }
9203
9311
  if (Array.isArray(val)) {
9204
- return val.map((v, i) => substituteValue(v, ctx, `${path4}[${i}]`));
9312
+ return val.map((v, i) => substituteValue(v, ctx, `${path5}[${i}]`));
9205
9313
  }
9206
9314
  if (val !== null && typeof val === "object") {
9207
9315
  const out = {};
9208
9316
  for (const [k, v] of Object.entries(val)) {
9209
- out[k] = substituteValue(v, ctx, path4 ? `${path4}.${k}` : k);
9317
+ out[k] = substituteValue(v, ctx, path5 ? `${path5}.${k}` : k);
9210
9318
  }
9211
9319
  return out;
9212
9320
  }
9213
9321
  return val;
9214
9322
  }
9215
9323
  function coerce(raw, type, name, issues) {
9216
- const path4 = `inputs.${name}`;
9324
+ const path5 = `inputs.${name}`;
9217
9325
  if (type === "string") {
9218
9326
  if (typeof raw === "string") return raw;
9219
9327
  if (typeof raw === "number" || typeof raw === "boolean") return String(raw);
9220
- issues.push({ path: path4, message: `expected a string value` });
9328
+ issues.push({ path: path5, message: `expected a string value` });
9221
9329
  return void 0;
9222
9330
  }
9223
9331
  if (type === "number") {
@@ -9225,12 +9333,12 @@ function coerce(raw, type, name, issues) {
9225
9333
  if (typeof raw === "string") {
9226
9334
  const n = Number(raw.trim());
9227
9335
  if (raw.trim() === "" || Number.isNaN(n)) {
9228
- issues.push({ path: path4, message: `expected a number, got '${raw}'` });
9336
+ issues.push({ path: path5, message: `expected a number, got '${raw}'` });
9229
9337
  return void 0;
9230
9338
  }
9231
9339
  return n;
9232
9340
  }
9233
- issues.push({ path: path4, message: `expected a number` });
9341
+ issues.push({ path: path5, message: `expected a number` });
9234
9342
  return void 0;
9235
9343
  }
9236
9344
  if (typeof raw === "boolean") return raw;
@@ -9238,10 +9346,10 @@ function coerce(raw, type, name, issues) {
9238
9346
  const t = raw.trim().toLowerCase();
9239
9347
  if (t === "true") return true;
9240
9348
  if (t === "false") return false;
9241
- issues.push({ path: path4, message: `expected a boolean ('true'|'false'), got '${raw}'` });
9349
+ issues.push({ path: path5, message: `expected a boolean ('true'|'false'), got '${raw}'` });
9242
9350
  return void 0;
9243
9351
  }
9244
- issues.push({ path: path4, message: `expected a boolean` });
9352
+ issues.push({ path: path5, message: `expected a boolean` });
9245
9353
  return void 0;
9246
9354
  }
9247
9355
  function resolveValues(declared, supplied, issues) {
@@ -9693,9 +9801,20 @@ var DATA_PLANE_ACTIONS = [
9693
9801
  "folders:c"
9694
9802
  ];
9695
9803
  var EDITOR_ACTIONS = [...DATA_PLANE_ACTIONS, "records:d", "documents:d", "folders:d"];
9804
+ var MEMBER_SHARED_READ_ACTIONS = [
9805
+ "records:r:control",
9806
+ "records:r:convention",
9807
+ "records:r:gotcha",
9808
+ "records:r:term",
9809
+ "documents:r",
9810
+ "search:r",
9811
+ "inference:r",
9812
+ "schemas:r",
9813
+ "folders:r"
9814
+ ];
9696
9815
  var agenticSdlc = {
9697
9816
  name: "agentic-sdlc",
9698
- version: "1.3.0",
9817
+ version: "1.6.0",
9699
9818
  description: "A whole-SDLC system of record for an AI development team \u2014 decisions, designs, references, runbooks, post-mortems (as documents) plus controls, conventions, gotchas, and a glossary (as records), cross-linked and recalled by meaning.",
9700
9819
  contextId: "agentic-sdlc",
9701
9820
  contextName: "Agentic SDLC Knowledge Base",
@@ -10394,6 +10513,173 @@ var agenticSdlc = {
10394
10513
  { fieldName: "updatedOn", rangeEnabled: true },
10395
10514
  "sourceRef"
10396
10515
  ]
10516
+ },
10517
+ {
10518
+ // ISOLATED AGENT MEMORY (the "other half" of the KB). Distinct from the
10519
+ // shared records above — which are the team's GLOBAL, curated knowledge —
10520
+ // `memory` is per-principal: a fact an agent (or the human it works for)
10521
+ // records for ITSELF, isolated by ownership via a `${{ self.userId }}`
10522
+ // role dataScope (the private tier; a team tier keyed on a shared scope is
10523
+ // a deliberate future addition, not baked in this version). Deliberately a
10524
+ // SINGLE flexible type so the recall hook queries ONE typeName; the flavor
10525
+ // is the `kind` field, mirroring the file-memory taxonomy it replaces, so
10526
+ // migration is 1:1. Isolation is enforced by the ROLE (type-scoped
10527
+ // `records:*:memory` actions + owner dataScope), NOT the schema — the
10528
+ // schema is tier-agnostic.
10529
+ typeName: "memory",
10530
+ displayName: "Memory",
10531
+ indexMode: "HYBRID",
10532
+ // keyword on the title + semantic on the body (the recalled fact)
10533
+ fields: [
10534
+ {
10535
+ fieldId: "title",
10536
+ fieldType: "string",
10537
+ required: true,
10538
+ searchable: true,
10539
+ validation: { minLength: 1, maxLength: 200 },
10540
+ description: 'A short handle for the memory (the file-memory "name" slug analog).',
10541
+ renderHints: { label: "Title", widget: "text", order: 1, displayField: true }
10542
+ },
10543
+ {
10544
+ fieldId: "body",
10545
+ fieldType: "string",
10546
+ searchable: true,
10547
+ description: "The memory itself \u2014 the durable fact, retrieved by meaning. The RAG-able body.",
10548
+ renderHints: { label: "Body", widget: "textarea", order: 2 }
10549
+ },
10550
+ {
10551
+ // The flavor of memory — mirrors the file-memory frontmatter `type:` so
10552
+ // a migration maps 1:1. `observation` is the episodic/scratch flavor
10553
+ // (TTL-able when the thread tier lands); the rest are durable.
10554
+ fieldId: "kind",
10555
+ fieldType: "enum",
10556
+ filterable: true,
10557
+ enumValues: ["user", "feedback", "project", "reference", "observation"],
10558
+ renderHints: { label: "Kind", widget: "select", order: 3 }
10559
+ },
10560
+ {
10561
+ // Structure axis 2 of 4: WHAT the memory is about — the same
10562
+ // subsystem/topic label every curated KB type carries, so one `area`
10563
+ // filter narrows recall across curated + memory content uniformly.
10564
+ fieldId: "area",
10565
+ fieldType: "string",
10566
+ filterable: true,
10567
+ description: `Subsystem/topic the memory applies to (e.g. "auth", "search") \u2014 matches the shared types' area vocabulary.`,
10568
+ renderHints: { label: "Area", widget: "text", order: 4 }
10569
+ },
10570
+ {
10571
+ // Structure axis 3: WHICH FUNCTION recorded it. A small stable ROLE
10572
+ // vocabulary (e.g. "pm", "builder", "reviewer") — not an instance id:
10573
+ // instances are already distinguished by the owning principal (who)
10574
+ // + threadId (which conversation).
10575
+ fieldId: "agent",
10576
+ fieldType: "string",
10577
+ filterable: true,
10578
+ description: 'The agent role that recorded it (e.g. "pm", "builder") \u2014 the function, not the instance.',
10579
+ renderHints: { label: "Agent", widget: "text", order: 5 }
10580
+ },
10581
+ {
10582
+ fieldId: "tags",
10583
+ fieldType: "array",
10584
+ filterable: true,
10585
+ description: 'Freeform labels for filtering recall (e.g. "auth", "testing").',
10586
+ renderHints: { label: "Tags", order: 6 }
10587
+ },
10588
+ {
10589
+ // The reversible supersede pattern shared with the records above — a
10590
+ // memory is retired by a status flip, never hard-deleted, so how the
10591
+ // agent's understanding evolved stays inspectable. Promotion to a
10592
+ // higher tier = supersede here + re-create at the new scope.
10593
+ fieldId: "status",
10594
+ fieldType: "enum",
10595
+ filterable: true,
10596
+ enumValues: ["active", "superseded"],
10597
+ renderHints: { label: "Status", widget: "select", order: 7 }
10598
+ },
10599
+ {
10600
+ // Structure axis 4: WHICH CONVERSATION (episodic slice). The caller's
10601
+ // session/thread identifier — whatever the agent runtime calls it. A
10602
+ // lookup filter WITHIN what the principal owns, NOT an isolation
10603
+ // boundary; sessions of the same principal share trust.
10604
+ fieldId: "threadId",
10605
+ fieldType: "string",
10606
+ filterable: true,
10607
+ description: "Optional conversation/thread id (your runtime's session identifier) \u2014 slices episodic memory within your own store.",
10608
+ renderHints: { label: "Thread", widget: "text", order: 8 }
10609
+ },
10610
+ {
10611
+ fieldId: "updatedOn",
10612
+ fieldType: "date",
10613
+ description: 'ISO-8601 \u2014 when last revised. Range-queryable \u2014 "what did I learn this week".',
10614
+ renderHints: { label: "Updated on", widget: "date", order: 9 }
10615
+ },
10616
+ {
10617
+ // Importance BAND for the always-load pinned set + recall ranking. Coarse bands
10618
+ // (0 normal / 10 pinned / 20 high / 30 critical) — pick a band, not a fiddly
10619
+ // integer, to avoid priority inflation. NULLABLE and set only on the elevated
10620
+ // subset, so the range-index cost lands only on that low-cardinality set (normal
10621
+ // memories write no range row). Orientation enumerates `priority order=desc
10622
+ // limit=X` — the read query IS the cap (overflow falls below the cutoff, never
10623
+ // evicted). MEMORY-ONLY: a pin is a memory that REFERENCES a curated item, it
10624
+ // never pins the curated item in place (pins = always-load; curated KB = recalled
10625
+ // when relevant).
10626
+ fieldId: "priority",
10627
+ fieldType: "number",
10628
+ filterable: true,
10629
+ description: "Importance band (0/10/20/30) for the always-load pinned set + recall ranking. Optional \u2014 set only on elevated memories.",
10630
+ renderHints: { label: "Priority", order: 10 }
10631
+ },
10632
+ {
10633
+ // Opaque provenance — the file / issue / URL this memory is ABOUT (records parity
10634
+ // with the curated types' sourceRef). Not a typed graph edge.
10635
+ fieldId: "sourceRef",
10636
+ fieldType: "string",
10637
+ description: "Provenance: the file path / issue / URL this memory is about.",
10638
+ renderHints: { label: "Source ref", widget: "text", order: 11 }
10639
+ },
10640
+ {
10641
+ // The evolution/promotion trail WITHIN memory (self-ref, SAME owner): the memory
10642
+ // that replaced this one. Pairs with ARCHIVE-retire. Promotion to the curated tier
10643
+ // is copy-up + archive-down with NO shared->private back-reference, so a curated
10644
+ // (shared) artifact never embeds a private memory's id/slug.
10645
+ fieldId: "supersededBy",
10646
+ fieldType: "reference",
10647
+ targetTypeName: "memory",
10648
+ targetSurface: "record",
10649
+ targetField: "externalId",
10650
+ cardinality: "one",
10651
+ description: "The memory that superseded this one (by externalId) \u2014 the evolution trail.",
10652
+ renderHints: { label: "Superseded by", order: 12 }
10653
+ },
10654
+ {
10655
+ // The `[[see-also]]` link between a principal's own memories (self-ref, SAME
10656
+ // owner). One-cardinality in this version (the proven reference shape); a
10657
+ // many-cardinality see-also graph is a later extension.
10658
+ fieldId: "relatedTo",
10659
+ fieldType: "reference",
10660
+ targetTypeName: "memory",
10661
+ targetSurface: "record",
10662
+ targetField: "externalId",
10663
+ cardinality: "one",
10664
+ description: "A related memory (by externalId) \u2014 the see-also link within your own store.",
10665
+ renderHints: { label: "Related to", order: 13 }
10666
+ }
10667
+ ],
10668
+ // externalId is the memory's stable slug (the file-memory `name:`) — the idempotent
10669
+ // upsert key — sent top-level, not declared as a field.
10670
+ // ⚖ LEAN by design: `memory` is the HIGHEST-VOLUME, churniest record here, so it only
10671
+ // carries lookups for fields we ENUMERATE DETERMINISTICLY. `area`/`agent`/`status` are
10672
+ // `filterable` (typed SEARCH metadata — narrow recall without a per-write lookup row),
10673
+ // NOT lookups; `sourceRef` is a plain provenance field. A typical write pays just
10674
+ // `kind` (eq) + `updatedOn` (range); `threadId`/`priority` cost only when set (both
10675
+ // nullable). Contrast the curated types, which carry more lookups because they are
10676
+ // low-volume and repo-synced (`sourceRef` enumeration).
10677
+ lookupFields: [
10678
+ "kind",
10679
+ "threadId",
10680
+ { fieldName: "updatedOn", rangeEnabled: true },
10681
+ { fieldName: "priority", rangeEnabled: true }
10682
+ ]
10397
10683
  }
10398
10684
  ],
10399
10685
  // Least-privilege, data-plane only. The scope of the `ssk_*` key the bootstrap
@@ -10416,7 +10702,41 @@ var agenticSdlc = {
10416
10702
  // owner may permanently remove genuine strays. Still no control-plane action, so
10417
10703
  // the scope gate accepts it as a data-plane-only role.
10418
10704
  roles: {
10419
- editor: [{ allowedActions: EDITOR_ACTIONS }]
10705
+ // The human curator. NOTE: this role is deliberately UNSCOPED (whole-context),
10706
+ // which includes members' private memory — the trusted-administrator tradeoff,
10707
+ // called out in the bundled guide. Scope it down if your deployment wants
10708
+ // curator access without private-memory visibility.
10709
+ editor: [{ allowedActions: EDITOR_ACTIONS }],
10710
+ // A team member (or their agent) — TWO memory tiers as two UNIONed clauses
10711
+ // (a record is accessible when ANY clause grants it; within one clause every
10712
+ // dataScope dimension must match):
10713
+ // 1) the CURATED shared KB (+ semantic recall). UNSCOPED, but safe: search
10714
+ // and grounded answers are enforced per row against the per-type action
10715
+ // qualifier, so recall over this clause admits only the content it grants
10716
+ // a TYPED read for (the curated record types + documents) — never the
10717
+ // `memory` type, so a member's recall never returns another principal's
10718
+ // private memory.
10719
+ // 2) PRIVATE memory: the member's OWN `memory` records, isolated per-request
10720
+ // by `${{ self.userId }}` (the principal dimension — the stable ownership
10721
+ // axis, unaffected by the evolution of shared/team scopes).
10722
+ // Team-shared *working* memory (a group-owned tier every member reads and
10723
+ // writes) is a planned addition, intentionally NOT in this version: it rides
10724
+ // the ownership axis for shared scopes, which is being finalized, and this
10725
+ // blueprint avoids baking a shape that is about to change. Bind with
10726
+ // `vectros join agentic-sdlc --role member`; verify a binding with
10727
+ // `vectros access explain --principal me --context agentic-sdlc`.
10728
+ member: [
10729
+ { allowedActions: MEMBER_SHARED_READ_ACTIONS },
10730
+ {
10731
+ // `inference:r` is IN this self-scoped clause (not only clause 1) so a
10732
+ // member's `rag_ask` can ground on its OWN private memory — the read
10733
+ // grant (`records:r:memory`) and the inference capability travel in the
10734
+ // SAME clause, so grounding works regardless of cross-clause admission
10735
+ // semantics. Still self-fenced by dataScope: never another principal's.
10736
+ allowedActions: ["records:cru:memory", "search:r", "inference:r"],
10737
+ dataScope: { userId: ["${{ self.userId }}"] }
10738
+ }
10739
+ ]
10420
10740
  },
10421
10741
  servicePrincipal: {
10422
10742
  externalId: "agentic-sdlc",
@@ -10844,13 +11164,13 @@ async function mintBridgeToken(opts) {
10844
11164
  fetchImpl = fetch
10845
11165
  } = opts;
10846
11166
  const base = apiBaseUrl.replace(/\/$/, "");
10847
- const path4 = bridgeTokenPath(useScopedToken);
10848
- const url = `${base}${path4}?ttl=${encodeURIComponent(String(ttlSeconds))}&tenant=${encodeURIComponent(tenant)}`;
11167
+ const path5 = bridgeTokenPath(useScopedToken);
11168
+ const url = `${base}${path5}?ttl=${encodeURIComponent(String(ttlSeconds))}&tenant=${encodeURIComponent(tenant)}`;
10849
11169
  return fetchStToken(
10850
11170
  url,
10851
11171
  cognitoToken,
10852
11172
  fetchImpl,
10853
- path4,
11173
+ path5,
10854
11174
  (status) => status === 404 && !useScopedToken ? ` (the dedicated ${CLI_BOOTSTRAP_TOKEN_PATH} endpoint may not be deployed yet \u2014 retry with --use-scoped-token for the fallback path)` : ""
10855
11175
  );
10856
11176
  }
@@ -11356,6 +11676,54 @@ async function readPersistedKey(pack, dir = vectrosHomeDir()) {
11356
11676
  }
11357
11677
  }
11358
11678
 
11679
+ // src/identity-resolver.ts
11680
+ function userPayload(decl) {
11681
+ const meta = decl.metadata;
11682
+ if (decl.displayName === void 0) return meta;
11683
+ return { displayName: decl.displayName, ...meta ?? {} };
11684
+ }
11685
+ function requireId(id, kind, externalId) {
11686
+ if (typeof id !== "string" || id.length === 0) {
11687
+ throw new Error(`identity ${kind} '${externalId}' create returned no id \u2014 cannot substitute its token.`);
11688
+ }
11689
+ return id;
11690
+ }
11691
+ function makeIdentityResolver(client, log) {
11692
+ return async (name, decl) => {
11693
+ switch (decl.kind) {
11694
+ case "user": {
11695
+ const u = await client.identity.createUser({
11696
+ body: { externalId: decl.externalId, type: "HUMAN", payload: userPayload(decl) }
11697
+ });
11698
+ const id = requireId(u.id, "user", decl.externalId);
11699
+ log.debug({ component: "identity-resolver", name, kind: "user", externalId: decl.externalId, id }, "identity ensured");
11700
+ return id;
11701
+ }
11702
+ case "org": {
11703
+ const o = await client.identity.createOrg({
11704
+ body: { externalId: decl.externalId, name: decl.displayName, payload: decl.metadata }
11705
+ });
11706
+ const id = requireId(o.id, "org", decl.externalId);
11707
+ log.debug({ component: "identity-resolver", name, kind: "org", externalId: decl.externalId, id }, "identity ensured");
11708
+ return id;
11709
+ }
11710
+ case "client": {
11711
+ const c = await client.identity.createClient({
11712
+ body: { externalId: decl.externalId, name: decl.displayName, payload: decl.metadata }
11713
+ });
11714
+ const id = requireId(c.id, "client", decl.externalId);
11715
+ log.debug({ component: "identity-resolver", name, kind: "client", externalId: decl.externalId, id }, "identity ensured");
11716
+ return id;
11717
+ }
11718
+ }
11719
+ };
11720
+ }
11721
+ async function resolveApplyTimeIdentities(blueprint, client, log) {
11722
+ const resolver = makeIdentityResolver(client, log);
11723
+ const resolved = await resolveBlueprintIdentities(blueprint, resolver);
11724
+ return parseBlueprint(resolved);
11725
+ }
11726
+
11359
11727
  // src/loader.ts
11360
11728
  var LostKeyError = class extends Error {
11361
11729
  constructor(keyName) {
@@ -11447,7 +11815,7 @@ function assertNoUnresolvedIdentities(blueprint) {
11447
11815
  if (hasBlock || refs.length > 0) {
11448
11816
  const detail = hasBlock ? `declares an 'identities' block` : `references ${refs.map((r) => `\${{ identities.${r} }}`).join(", ")}`;
11449
11817
  throw new UnresolvedIdentitiesError(
11450
- `Blueprint ${detail}, but the CLI loader does not yet resolve identities. Applying it would write literal '\${{ identities.* }}' tokens into ownership/dataScope fields. Remove the identities usage, or wait for identity-resolution support.`
11818
+ `Blueprint ${detail} that reached the loader UNRESOLVED \u2014 apply-time identity resolution did not run before provisioning (an internal wiring error). Applying it would write literal '\${{ identities.* }}' tokens into ownership/dataScope fields. This is a defense-in-depth backstop; resolution happens in the CLI apply orchestrator (resolveApplyTimeIdentities) before the loader.`
11451
11819
  );
11452
11820
  }
11453
11821
  }
@@ -11519,7 +11887,12 @@ async function runLoader(client, input) {
11519
11887
  // to the SDK's loosely-typed Record<string, Record<string,unknown>>.
11520
11888
  data_scope: blueprint.accessProfile.dataScope
11521
11889
  }
11522
- ]
11890
+ ],
11891
+ // Optional identity overrides — scope values the service key STAMPS onto
11892
+ // everything it writes (e.g. the blueprint's team org, so the curated
11893
+ // knowledge it seeds/writes is org-owned). `${{ identities.* }}` tokens
11894
+ // were substituted before the loader ran.
11895
+ identityOverrides: blueprint.accessProfile.identityOverrides
11523
11896
  // NB: `status` is intentionally omitted — the SDK's body type does not model it
11524
11897
  // and the platform defaults a new profile to 'active' (the only status the loader
11525
11898
  // ever wants). Sending it was redundant.
@@ -11595,7 +11968,9 @@ async function runLoader(client, input) {
11595
11968
  schemaId,
11596
11969
  externalId: rec.externalId,
11597
11970
  // Optional structured payload (the document's `fields`); omitted when absent.
11598
- payload: rec.fields
11971
+ payload: rec.fields,
11972
+ // Optional scope ownership (`namespace:value` entries; `[]` = private).
11973
+ scopes: rec.scopes
11599
11974
  }
11600
11975
  });
11601
11976
  if (created.id) seededDocumentIds.push(created.id);
@@ -11605,7 +11980,8 @@ async function runLoader(client, input) {
11605
11980
  typeName: rec.typeName,
11606
11981
  schemaId,
11607
11982
  externalId: rec.externalId,
11608
- payload: rec.fields
11983
+ payload: rec.fields,
11984
+ scopes: rec.scopes
11609
11985
  }
11610
11986
  });
11611
11987
  if (created.id) seededRecordIds.push(created.id);
@@ -11766,7 +12142,6 @@ function validateBlueprint(input) {
11766
12142
  for (const clauses of Object.values(blueprint.roles ?? {})) {
11767
12143
  for (const clause of clauses) assertScopeGate(clause.allowedActions);
11768
12144
  }
11769
- assertNoUnresolvedIdentities(blueprint);
11770
12145
  return blueprint;
11771
12146
  }
11772
12147
  var BLANK_SLATE = {
@@ -12022,13 +12397,6 @@ ${BOOTSTRAP_HELP}
12022
12397
  process.stderr.write(" Minting nothing.\n");
12023
12398
  return 2;
12024
12399
  }
12025
- if (err instanceof UnresolvedIdentitiesError) {
12026
- process.stderr.write(`
12027
- \u2716 ${err.message}
12028
- Minting nothing.
12029
- `);
12030
- return 2;
12031
- }
12032
12400
  throw err;
12033
12401
  }
12034
12402
  const gate = evaluateScopeGate(blueprint.accessProfile.allowedActions);
@@ -12053,6 +12421,14 @@ ${BOOTSTRAP_HELP}
12053
12421
  process.stdout.write(`
12054
12422
  ${renderPlan(blueprint, planSteps)}
12055
12423
  `);
12424
+ const declaredIdentities = Object.entries(blueprint.identities ?? {});
12425
+ if (declaredIdentities.length > 0) {
12426
+ process.stdout.write("\n Identities to ensure (tenant-wide, idempotent by external id):\n");
12427
+ for (const [name, decl] of declaredIdentities) {
12428
+ process.stdout.write(` ~ ${name} (${decl.kind}) \u2014 external id '${decl.externalId}'
12429
+ `);
12430
+ }
12431
+ }
12056
12432
  if (!args.yes && !args.print && isTty) {
12057
12433
  const ans = (await promptCtx.prompt("\n Apply this plan? [y/N]: ")).trim().toLowerCase();
12058
12434
  if (ans !== "y" && ans !== "yes") {
@@ -12077,17 +12453,25 @@ ${renderPlan(blueprint, planSteps)}
12077
12453
  });
12078
12454
  process.stdout.write(` \u2714 Bootstrap token minted (5-min TTL, ${args.tenant} tenant)
12079
12455
  `);
12456
+ const pingIdentity = deps.resolveIdentity ?? resolveIdentity;
12457
+ const resolveIdentities = deps.resolveApplyTimeIdentities ?? resolveApplyTimeIdentities;
12458
+ const loader = deps.runLoader ?? runLoader;
12080
12459
  const client = new VectrosClient({ token: bridge.token, environment: baseUrl });
12081
- const identity = await resolveIdentity({ log, apiKey: bridge.token, environment: baseUrl });
12460
+ const identity = await pingIdentity({ log, apiKey: bridge.token, environment: baseUrl });
12082
12461
  const tenantId = identity.tenantId;
12083
12462
  if (!tenantId || typeof tenantId !== "string") {
12084
12463
  throw new Error(
12085
12464
  "Could not resolve tenantId from /v1/ping \u2014 the extended ping (tenant_id) must be deployed for bootstrap."
12086
12465
  );
12087
12466
  }
12088
- const machineId = await resolveMachineId();
12089
- const result = await runLoader(client, {
12467
+ const resolvedBlueprint = await resolveIdentities(
12090
12468
  blueprint,
12469
+ client,
12470
+ log
12471
+ );
12472
+ const machineId = await resolveMachineId();
12473
+ const result = await loader(client, {
12474
+ blueprint: resolvedBlueprint,
12091
12475
  tenantId,
12092
12476
  machineId,
12093
12477
  rotate: args.rotate,
@@ -12165,6 +12549,13 @@ ${codeAddHint}`);
12165
12549
  `);
12166
12550
  return 3;
12167
12551
  }
12552
+ if (err instanceof BlueprintIdentityError || err instanceof UnresolvedIdentitiesError) {
12553
+ process.stderr.write(`
12554
+ \u2716 ${err.message}
12555
+ No context or key was provisioned.
12556
+ `);
12557
+ return 2;
12558
+ }
12168
12559
  process.stderr.write(`
12169
12560
  \u2716 Bootstrap failed: ${err instanceof Error ? err.message : String(err)}
12170
12561
  `);
@@ -12444,13 +12835,6 @@ async function resolveAndValidate(arg, verb, supplied) {
12444
12835
  }
12445
12836
  throw err;
12446
12837
  }
12447
- try {
12448
- assertNoUnresolvedIdentities(blueprint);
12449
- } catch (err) {
12450
- process.stderr.write(` \u2716 ${err instanceof Error ? err.message : String(err)}
12451
- `);
12452
- return null;
12453
- }
12454
12838
  const findings = lintBlueprint(blueprint);
12455
12839
  for (const f of findings) {
12456
12840
  const mark = f.severity === "error" ? "\u2716" : "\u26A0";
@@ -12498,6 +12882,14 @@ async function runPlan(args) {
12498
12882
  process.stdout.write(`
12499
12883
  ${renderPlan(blueprint, steps)}
12500
12884
  `);
12885
+ const declaredIdentities = Object.entries(blueprint.identities ?? {});
12886
+ if (declaredIdentities.length > 0) {
12887
+ process.stdout.write("\n Identities to ensure (tenant-wide, idempotent by external id):\n");
12888
+ for (const [name, decl] of declaredIdentities) {
12889
+ process.stdout.write(` ~ ${name} (${decl.kind}) \u2014 external id '${decl.externalId}'
12890
+ `);
12891
+ }
12892
+ }
12501
12893
  const setEcho = [...args.set].join(" --set ");
12502
12894
  process.stdout.write(
12503
12895
  `
@@ -12696,6 +13088,18 @@ function normalizePrincipalUserId(principal) {
12696
13088
  function splitCsv(csv) {
12697
13089
  return csv.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
12698
13090
  }
13091
+ function parseJsonObjectFlag(flag, raw) {
13092
+ let parsed;
13093
+ try {
13094
+ parsed = JSON.parse(raw);
13095
+ } catch {
13096
+ throw new Error(`${flag} is not valid JSON: ${raw}`);
13097
+ }
13098
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
13099
+ throw new Error(`${flag} must be a JSON object (e.g. '{"userId":["<id>"]}').`);
13100
+ }
13101
+ return parsed;
13102
+ }
12699
13103
 
12700
13104
  // src/paginate.ts
12701
13105
  var PAGE_LIMIT = 100;
@@ -13191,6 +13595,23 @@ async function runAccessGrant(opts, deps = {}) {
13191
13595
  process.stderr.write(" \u2716 Provide exactly one of --role <roleId> or --actions <csv>.\n");
13192
13596
  return 2;
13193
13597
  }
13598
+ if (hasActions && splitCsv(opts.actions).length === 0) {
13599
+ process.stderr.write(" \u2716 --actions needs at least one action (an empty/blank list grants nothing).\n");
13600
+ return 2;
13601
+ }
13602
+ let identityOverrides;
13603
+ if (opts.org || opts.identityOverrides) {
13604
+ try {
13605
+ identityOverrides = {
13606
+ ...opts.org ? { orgId: opts.org } : {},
13607
+ ...opts.identityOverrides ? parseJsonObjectFlag("--identity-overrides", opts.identityOverrides) : {}
13608
+ };
13609
+ } catch (err) {
13610
+ process.stderr.write(` \u2716 ${formatError(err)}
13611
+ `);
13612
+ return 2;
13613
+ }
13614
+ }
13194
13615
  if (opts.principal !== SELF_PRINCIPAL_ALIAS) {
13195
13616
  try {
13196
13617
  assertPrincipalShape(opts.principal);
@@ -13207,10 +13628,12 @@ async function runAccessGrant(opts, deps = {}) {
13207
13628
  const scopes = hasActions ? [{ allowed_actions: splitCsv(opts.actions) }] : void 0;
13208
13629
  const profile = await client.auth.createAccessProfile({
13209
13630
  contextId: opts.context,
13631
+ upsert: true,
13210
13632
  body: {
13211
13633
  principalId: principal,
13212
13634
  roleId: opts.role,
13213
- scopes
13635
+ scopes,
13636
+ identityOverrides
13214
13637
  }
13215
13638
  });
13216
13639
  if (opts.json) {
@@ -13333,6 +13756,166 @@ ${renderProfile(p)}
13333
13756
  });
13334
13757
  }
13335
13758
 
13759
+ // src/commands/access-explain.ts
13760
+ var SELF = /* @__PURE__ */ Symbol("self");
13761
+ function canonicalDim(key) {
13762
+ if (key === "userId" || key === "partnerUserId") return "user";
13763
+ if (key === "orgId") return "scope:org";
13764
+ if (key === "clientId") return "scope:client";
13765
+ return key;
13766
+ }
13767
+ var SELF_TOKEN_RE2 = /^\$\{\{\s*self\.[a-zA-Z.]+\s*\}\}$/;
13768
+ function normalizeClauses(raw) {
13769
+ const out = [];
13770
+ for (const c of raw) {
13771
+ if (!c || typeof c !== "object") continue;
13772
+ const obj = c;
13773
+ const actions = obj.allowed_actions ?? obj.allowedActions ?? [];
13774
+ const ds = obj.data_scope ?? obj.dataScope ?? {};
13775
+ const dataScope = {};
13776
+ for (const [k, v] of Object.entries(ds)) {
13777
+ const values = Array.isArray(v) ? v : [v];
13778
+ dataScope[canonicalDim(k)] = values.map((val) => {
13779
+ if (val === null) return null;
13780
+ const s = String(val);
13781
+ return SELF_TOKEN_RE2.test(s) ? SELF : s;
13782
+ });
13783
+ }
13784
+ out.push({ actions: Array.isArray(actions) ? actions.map(String) : [], dataScope });
13785
+ }
13786
+ return out;
13787
+ }
13788
+ function buildArchetypes(clauses) {
13789
+ const rows = [
13790
+ { label: "a record you own alone (private)", owners: { user: SELF } },
13791
+ { label: "another user's private record", owners: { user: "someone-else" } }
13792
+ ];
13793
+ const scopeDims = /* @__PURE__ */ new Map();
13794
+ for (const c of clauses) {
13795
+ for (const [dim, values] of Object.entries(c.dataScope)) {
13796
+ if (dim === "user") continue;
13797
+ const set = scopeDims.get(dim) ?? /* @__PURE__ */ new Set();
13798
+ for (const v of values) if (v !== null) set.add(v);
13799
+ scopeDims.set(dim, set);
13800
+ }
13801
+ }
13802
+ for (const [dim, values] of scopeDims) {
13803
+ for (const v of values) {
13804
+ const shown = v === SELF ? `your ${dim.replace("scope:", "")}` : `${dim}=${String(v)}`;
13805
+ rows.push({ label: `yours, shared to ${shown}`, owners: { user: SELF, [dim]: v } });
13806
+ rows.push({ label: `another user's, shared to ${shown}`, owners: { user: "someone-else", [dim]: v } });
13807
+ rows.push({ label: `${shown} only (no user owner)`, owners: { [dim]: v } });
13808
+ }
13809
+ rows.push({
13810
+ label: `another user's, in a DIFFERENT ${dim.replace("scope:", "")}`,
13811
+ owners: { user: "someone-else", [dim]: `other-${dim.replace("scope:", "")}` }
13812
+ });
13813
+ }
13814
+ rows.push({ label: "no owner values at all (tenant-shared)", owners: {} });
13815
+ return rows;
13816
+ }
13817
+ function clauseMatches(clause, arch) {
13818
+ for (const [dim, allowed] of Object.entries(clause.dataScope)) {
13819
+ const recordValue = arch.owners[dim];
13820
+ const ok = allowed.some((a) => {
13821
+ if (a === null) return recordValue === void 0;
13822
+ if (a === SELF) return recordValue === SELF;
13823
+ return recordValue === a;
13824
+ });
13825
+ if (!ok) return false;
13826
+ }
13827
+ return true;
13828
+ }
13829
+ function explainClauses(clauses) {
13830
+ const archetypes = buildArchetypes(clauses);
13831
+ return archetypes.map((arch) => {
13832
+ const matched = [];
13833
+ const actions = /* @__PURE__ */ new Set();
13834
+ clauses.forEach((c, i) => {
13835
+ if (clauseMatches(c, arch)) {
13836
+ matched.push(i + 1);
13837
+ for (const a of c.actions) actions.add(a);
13838
+ }
13839
+ });
13840
+ return { archetype: arch.label, clauses: matched, actions: [...actions] };
13841
+ });
13842
+ }
13843
+ function renderDataScope(ds) {
13844
+ const entries = Object.entries(ds);
13845
+ if (entries.length === 0) return "(unscoped \u2014 all data in the context)";
13846
+ return entries.map(
13847
+ ([dim, values]) => `${dim}:[${values.map((v) => v === SELF ? "self" : v === null ? "null" : v).join(", ")}]`
13848
+ ).join(" AND ");
13849
+ }
13850
+ var SELF_PRINCIPAL_ALIAS2 = "me";
13851
+ async function runAccessExplain(opts, deps = {}) {
13852
+ return withAuth(opts, deps, async (auth) => {
13853
+ const client = auth.client;
13854
+ try {
13855
+ let principal = opts.principal;
13856
+ if (principal === SELF_PRINCIPAL_ALIAS2) {
13857
+ const self2 = await resolveSelfPrincipal(auth.client, auth.cognitoToken);
13858
+ if (!self2) {
13859
+ process.stderr.write(
13860
+ " \u2716 Could not resolve your own user id from your login. Pass --principal usr_<id> explicitly.\n"
13861
+ );
13862
+ return 1;
13863
+ }
13864
+ principal = self2.principalId;
13865
+ }
13866
+ const profile = await client.auth.getAccessProfile({ contextId: opts.context, principalId: principal });
13867
+ let rawClauses = profile.scopes ?? [];
13868
+ let via = "inline scopes";
13869
+ if (profile.roleId) {
13870
+ const role = await client.auth.getRole({ contextId: opts.context, roleId: profile.roleId });
13871
+ rawClauses = role.scopes ?? [];
13872
+ via = `role '${profile.roleId}'`;
13873
+ }
13874
+ const clauses = normalizeClauses(rawClauses);
13875
+ const rows = explainClauses(clauses);
13876
+ if (opts.json) {
13877
+ process.stdout.write(
13878
+ `${formatJson({
13879
+ principal,
13880
+ context: opts.context,
13881
+ via,
13882
+ identityOverrides: profile.identityOverrides,
13883
+ clauses: clauses.map((c, i) => ({ n: i + 1, actions: c.actions, dataScope: renderDataScope(c.dataScope) })),
13884
+ matrix: rows,
13885
+ note: "Ownership dimensions only: per-type action qualifiers narrow further, and search/RAG visibility is bounded per row by the same clause's typed read grants. Client-side preview; the platform decides at request time."
13886
+ })}
13887
+ `
13888
+ );
13889
+ return 0;
13890
+ }
13891
+ process.stdout.write(`
13892
+ ${principal} in '${opts.context}' \u2014 via ${via}
13893
+ `);
13894
+ if (profile.identityOverrides && Object.keys(profile.identityOverrides).length > 0) {
13895
+ process.stdout.write(` identity overrides: ${JSON.stringify(profile.identityOverrides)}
13896
+ `);
13897
+ }
13898
+ process.stdout.write("\n Clauses (a record is accessible when ANY clause grants it):\n");
13899
+ clauses.forEach((c, i) => {
13900
+ process.stdout.write(` ${i + 1}. [${c.actions.join(", ")}] where ${renderDataScope(c.dataScope)}
13901
+ `);
13902
+ });
13903
+ process.stdout.write(
13904
+ "\n" + formatTable(rows, [
13905
+ { header: "record owned by\u2026", get: (r) => r.archetype },
13906
+ { header: "granted?", get: (r) => r.clauses.length ? `via clause ${r.clauses.join(", ")}` : "NO" },
13907
+ { header: "actions", get: (r) => r.actions.length ? r.actions.join(", ") : "\u2014" }
13908
+ ]) + "\n\n NOTE: this matrix models the OWNERSHIP dimensions only. Per-type qualifiers narrow further:\n a `records:<ops>:<type>` action reaches only that type, and search/RAG visibility is bounded\n per row by the SAME clause's typed read grants \u2014 a row shown with `search:r` is searchable\n only for the content types that clause can read.\n Preview only \u2014 computed client-side from the documented matching rules; the platform decides at request time.\n"
13909
+ );
13910
+ return 0;
13911
+ } catch (err) {
13912
+ process.stderr.write(` \u2716 access explain failed: ${formatError(err)}
13913
+ `);
13914
+ return 1;
13915
+ }
13916
+ });
13917
+ }
13918
+
13336
13919
  // src/commands/role.ts
13337
13920
  function renderRole(r) {
13338
13921
  return formatKeyValues([
@@ -13349,6 +13932,16 @@ async function runRoleCreate(opts, deps = {}) {
13349
13932
  process.stderr.write(" \u2716 --actions must list at least one action (e.g. records:cru,search:r).\n");
13350
13933
  return 2;
13351
13934
  }
13935
+ let dataScope;
13936
+ if (opts.dataScope !== void 0) {
13937
+ try {
13938
+ dataScope = parseJsonObjectFlag("--data-scope", opts.dataScope);
13939
+ } catch (err) {
13940
+ process.stderr.write(` \u2716 ${formatError(err)}
13941
+ `);
13942
+ return 2;
13943
+ }
13944
+ }
13352
13945
  return withAuth(opts, deps, async (auth) => {
13353
13946
  const client = auth.client;
13354
13947
  try {
@@ -13358,7 +13951,11 @@ async function runRoleCreate(opts, deps = {}) {
13358
13951
  roleId: opts.roleId,
13359
13952
  name: opts.name,
13360
13953
  description: opts.description,
13361
- scopes: [{ allowed_actions: actions }]
13954
+ // data_scope is included only when supplied — the clause wire shape
13955
+ // stays byte-identical for unscoped roles.
13956
+ scopes: [
13957
+ dataScope ? { allowed_actions: actions, data_scope: dataScope } : { allowed_actions: actions }
13958
+ ]
13362
13959
  }
13363
13960
  });
13364
13961
  if (opts.json) {
@@ -13990,7 +14587,12 @@ async function runTeardown(client, blueprint, machineId, snapshot, result) {
13990
14587
  return steps;
13991
14588
  }
13992
14589
  async function runBlueprintHarness(client, input, deps) {
13993
- const { blueprint, tenantId, machineId, vectrosDir, keep = false, log, remintForContext } = input;
14590
+ const { tenantId, machineId, vectrosDir, keep = false, log, remintForContext } = input;
14591
+ const blueprint = await resolveApplyTimeIdentities(
14592
+ input.blueprint,
14593
+ client,
14594
+ log
14595
+ );
13994
14596
  const remint = remintForContext ?? (async () => client);
13995
14597
  let snapshot;
13996
14598
  if (remintForContext) {
@@ -14164,11 +14766,461 @@ async function runBlueprintTest(fileOrName, opts, deps = {}) {
14164
14766
  });
14165
14767
  }
14166
14768
 
14769
+ // src/keyring.ts
14770
+ var import_node_fs4 = require("fs");
14771
+ var import_node_path5 = __toESM(require("path"));
14772
+ var import_node_crypto4 = require("crypto");
14773
+ var KEYRING_FILE = "keyring.json";
14774
+ var SALT_FILE = "keyring.salt";
14775
+ var SECRETS_DIR = "secrets";
14776
+ var ALGO = "aes-256-gcm";
14777
+ var IV_LEN = 12;
14778
+ var TAG_LEN = 16;
14779
+ function keyringPath(dir = vectrosHomeDir()) {
14780
+ return import_node_path5.default.join(dir, KEYRING_FILE);
14781
+ }
14782
+ var CryptoFileSecretStore = class {
14783
+ dir;
14784
+ keyPromise;
14785
+ constructor(dir = vectrosHomeDir()) {
14786
+ this.dir = dir;
14787
+ }
14788
+ secretsDir() {
14789
+ return import_node_path5.default.join(this.dir, SECRETS_DIR);
14790
+ }
14791
+ secretPath(ref) {
14792
+ return import_node_path5.default.join(this.secretsDir(), `${encodeURIComponent(ref)}.enc`);
14793
+ }
14794
+ /** Resolve (and cache) the machine-derived AES key, creating the salt once. */
14795
+ async encryptionKey() {
14796
+ if (!this.keyPromise) {
14797
+ this.keyPromise = (async () => {
14798
+ const machineId = await resolveMachineId(this.dir);
14799
+ const salt = await this.readOrCreateSalt();
14800
+ return (0, import_node_crypto4.scryptSync)(machineId, salt, 32);
14801
+ })();
14802
+ }
14803
+ return this.keyPromise;
14804
+ }
14805
+ async readOrCreateSalt() {
14806
+ const saltPath = import_node_path5.default.join(this.dir, SALT_FILE);
14807
+ try {
14808
+ const existing = await import_node_fs4.promises.readFile(saltPath);
14809
+ if (existing.length >= 16) return existing;
14810
+ } catch {
14811
+ }
14812
+ const salt = (0, import_node_crypto4.randomBytes)(16);
14813
+ await import_node_fs4.promises.mkdir(this.dir, { recursive: true });
14814
+ await import_node_fs4.promises.writeFile(saltPath, salt, { mode: 384 });
14815
+ return salt;
14816
+ }
14817
+ async setSecret(ref, secret) {
14818
+ const key = await this.encryptionKey();
14819
+ const iv = (0, import_node_crypto4.randomBytes)(IV_LEN);
14820
+ const cipher = (0, import_node_crypto4.createCipheriv)(ALGO, key, iv);
14821
+ const enc = Buffer.concat([cipher.update(secret, "utf8"), cipher.final()]);
14822
+ const tag = cipher.getAuthTag();
14823
+ const blob = Buffer.concat([iv, tag, enc]);
14824
+ await import_node_fs4.promises.mkdir(this.secretsDir(), { recursive: true });
14825
+ await import_node_fs4.promises.writeFile(this.secretPath(ref), blob.toString("base64"), {
14826
+ encoding: "utf8",
14827
+ mode: 384
14828
+ });
14829
+ }
14830
+ async getSecret(ref) {
14831
+ let blob;
14832
+ try {
14833
+ blob = Buffer.from((await import_node_fs4.promises.readFile(this.secretPath(ref), "utf8")).trim(), "base64");
14834
+ } catch {
14835
+ return void 0;
14836
+ }
14837
+ if (blob.length <= IV_LEN + TAG_LEN) return void 0;
14838
+ try {
14839
+ const key = await this.encryptionKey();
14840
+ const iv = blob.subarray(0, IV_LEN);
14841
+ const tag = blob.subarray(IV_LEN, IV_LEN + TAG_LEN);
14842
+ const enc = blob.subarray(IV_LEN + TAG_LEN);
14843
+ const decipher = (0, import_node_crypto4.createDecipheriv)(ALGO, key, iv);
14844
+ decipher.setAuthTag(tag);
14845
+ return Buffer.concat([decipher.update(enc), decipher.final()]).toString("utf8");
14846
+ } catch {
14847
+ return void 0;
14848
+ }
14849
+ }
14850
+ async deleteSecret(ref) {
14851
+ try {
14852
+ await import_node_fs4.promises.rm(this.secretPath(ref));
14853
+ } catch {
14854
+ }
14855
+ }
14856
+ };
14857
+ async function readKeyring(dir = vectrosHomeDir()) {
14858
+ let raw;
14859
+ try {
14860
+ raw = await import_node_fs4.promises.readFile(keyringPath(dir), "utf8");
14861
+ } catch (err) {
14862
+ if (err.code === "ENOENT") return { version: 1, entries: [] };
14863
+ throw err;
14864
+ }
14865
+ let parsed;
14866
+ try {
14867
+ parsed = JSON.parse(raw);
14868
+ } catch {
14869
+ throw new Error(`Keyring index at ${keyringPath(dir)} is corrupt (invalid JSON). Fix or remove the file.`);
14870
+ }
14871
+ if (!Array.isArray(parsed.entries)) {
14872
+ throw new Error(`Keyring index at ${keyringPath(dir)} is malformed (no entries array). Fix or remove the file.`);
14873
+ }
14874
+ return { version: 1, active: parsed.active, entries: parsed.entries };
14875
+ }
14876
+ async function writeKeyring(kr, dir = vectrosHomeDir()) {
14877
+ await import_node_fs4.promises.mkdir(dir, { recursive: true });
14878
+ const finalPath = keyringPath(dir);
14879
+ const tmpPath = `${finalPath}.${process.pid}.${(0, import_node_crypto4.randomBytes)(4).toString("hex")}.tmp`;
14880
+ await import_node_fs4.promises.writeFile(tmpPath, `${JSON.stringify(kr, null, 2)}
14881
+ `, { encoding: "utf8", mode: 384 });
14882
+ await import_node_fs4.promises.rename(tmpPath, finalPath);
14883
+ }
14884
+ var Keyring = class {
14885
+ dir;
14886
+ store;
14887
+ constructor(deps = {}) {
14888
+ this.dir = deps.dir ?? vectrosHomeDir();
14889
+ this.store = deps.store ?? new CryptoFileSecretStore(this.dir);
14890
+ }
14891
+ /**
14892
+ * This machine's stable id — used to namespace a scoped-key NAME so each
14893
+ * machine holds its OWN independently-rotatable `ssk_*` under one shared
14894
+ * (idempotent) AccessProfile (see `machine-id.ts`). Resolved from the SAME
14895
+ * dir the secrets live in, so a test keyring rooted at a temp dir stays
14896
+ * self-consistent.
14897
+ */
14898
+ async machineId() {
14899
+ return resolveMachineId(this.dir);
14900
+ }
14901
+ async list() {
14902
+ return (await readKeyring(this.dir)).entries;
14903
+ }
14904
+ async get(alias) {
14905
+ return (await this.list()).find((e) => e.alias === alias);
14906
+ }
14907
+ async activeAlias() {
14908
+ return (await readKeyring(this.dir)).active;
14909
+ }
14910
+ async getActive() {
14911
+ const kr = await readKeyring(this.dir);
14912
+ return kr.active ? kr.entries.find((e) => e.alias === kr.active) : void 0;
14913
+ }
14914
+ /**
14915
+ * Upsert an entry (by alias) and store its secret. Activates it by default —
14916
+ * `join` wants the freshly-created identity to become current.
14917
+ */
14918
+ async add(entry, secret, opts = {}) {
14919
+ const kr = await readKeyring(this.dir);
14920
+ const prior = kr.entries.find((e) => e.alias === entry.alias);
14921
+ await this.store.setSecret(entry.secretRef, secret);
14922
+ if (prior && prior.secretRef !== entry.secretRef) {
14923
+ await this.store.deleteSecret(prior.secretRef);
14924
+ }
14925
+ const entries = kr.entries.filter((e) => e.alias !== entry.alias);
14926
+ entries.push(entry);
14927
+ const active = opts.activate === false ? kr.active : entry.alias;
14928
+ await writeKeyring({ version: 1, active, entries }, this.dir);
14929
+ }
14930
+ /** Point the active alias at an existing entry. Throws if the alias is unknown. */
14931
+ async setActive(alias) {
14932
+ const kr = await readKeyring(this.dir);
14933
+ if (!kr.entries.some((e) => e.alias === alias)) {
14934
+ throw new Error(`No keyring entry named '${alias}'. Run \`vectros keyring list\` to see entries.`);
14935
+ }
14936
+ await writeKeyring({ ...kr, version: 1, active: alias }, this.dir);
14937
+ }
14938
+ /** Remove an entry + its secret. Clears the active pointer if it named this alias. Returns true if removed. */
14939
+ async remove(alias) {
14940
+ const kr = await readKeyring(this.dir);
14941
+ const entry = kr.entries.find((e) => e.alias === alias);
14942
+ if (!entry) return false;
14943
+ await this.store.deleteSecret(entry.secretRef);
14944
+ const entries = kr.entries.filter((e) => e.alias !== alias);
14945
+ const active = kr.active === alias ? void 0 : kr.active;
14946
+ await writeKeyring({ version: 1, active, entries }, this.dir);
14947
+ return true;
14948
+ }
14949
+ /** The raw secret for a named entry (via the secret store), or undefined. */
14950
+ async resolveSecret(alias) {
14951
+ const entry = await this.get(alias);
14952
+ return entry ? this.store.getSecret(entry.secretRef) : void 0;
14953
+ }
14954
+ /** The raw secret for the active entry, or undefined if none is active. */
14955
+ async resolveActiveSecret() {
14956
+ const entry = await this.getActive();
14957
+ return entry ? this.store.getSecret(entry.secretRef) : void 0;
14958
+ }
14959
+ };
14960
+
14961
+ // src/commands/join.ts
14962
+ var SELF_ALIAS = "me";
14963
+ async function resolvePrincipal(as, auth) {
14964
+ const raw = as ?? SELF_ALIAS;
14965
+ if (raw === SELF_ALIAS) {
14966
+ const self2 = await resolveSelfPrincipal(auth.client, auth.cognitoToken);
14967
+ if (!self2) {
14968
+ throw new Error(
14969
+ "Could not resolve your own user id from your login. Pass --as usr_<id> explicitly (find it with `vectros identity list --type user`)."
14970
+ );
14971
+ }
14972
+ return self2.principalId;
14973
+ }
14974
+ if (!raw.startsWith("usr_")) {
14975
+ throw new Error(`--as must be 'me' or 'usr_<id>' (got '${raw}').`);
14976
+ }
14977
+ return raw;
14978
+ }
14979
+ async function issueOrRotateKey(client, tenantId, context, userId, keyName) {
14980
+ const first = await client.auth.createScopedKey({ keyName, tenantId, contextId: context, userId });
14981
+ if (first.rawKey) return first;
14982
+ const existing = (await drainPages(() => client.auth.listScopedKeys())).find(
14983
+ (k) => k.contextId === context && k.userId === userId && k.keyName === keyName && k.status === "active"
14984
+ );
14985
+ if (existing?.keyId) await client.auth.revokeScopedKey({ keyId: existing.keyId });
14986
+ const rotated = await client.auth.createScopedKey({ keyName, tenantId, contextId: context, userId });
14987
+ if (!rotated.rawKey) {
14988
+ throw new Error(
14989
+ "The key already exists and its secret could not be re-minted (the prior key may not have cleared yet). Retry shortly."
14990
+ );
14991
+ }
14992
+ return rotated;
14993
+ }
14994
+ async function runJoin(opts, deps = {}) {
14995
+ const hasRole = Boolean(opts.role);
14996
+ const hasActions = Boolean(opts.actions);
14997
+ if (hasRole === hasActions) {
14998
+ process.stderr.write(" \u2716 Provide exactly one of --role <roleId> or --actions <csv>.\n");
14999
+ return 2;
15000
+ }
15001
+ if (hasActions && splitCsv(opts.actions).length === 0) {
15002
+ process.stderr.write(" \u2716 --actions needs at least one action (an empty/blank list grants nothing).\n");
15003
+ return 2;
15004
+ }
15005
+ if (opts.as && opts.as !== SELF_ALIAS && !opts.as.startsWith("usr_")) {
15006
+ process.stderr.write(` \u2716 --as must be 'me' or 'usr_<id>' (got '${opts.as}').
15007
+ `);
15008
+ return 2;
15009
+ }
15010
+ const context = opts.contextArg;
15011
+ const alias = opts.alias ?? context;
15012
+ const keyring = deps.keyring ?? new Keyring();
15013
+ return withAuth({ ...opts, context }, deps, async (auth) => {
15014
+ const client = auth.client;
15015
+ try {
15016
+ const tenantId = requireTenantId(auth);
15017
+ const principalId = await resolvePrincipal(opts.as, auth);
15018
+ const userId = normalizePrincipalUserId(principalId);
15019
+ const scopes = hasActions ? [{ allowed_actions: splitCsv(opts.actions) }] : void 0;
15020
+ await client.auth.createAccessProfile({
15021
+ contextId: context,
15022
+ upsert: true,
15023
+ body: {
15024
+ principalId,
15025
+ roleId: opts.role,
15026
+ scopes,
15027
+ identityOverrides: opts.org ? { orgId: opts.org } : void 0
15028
+ }
15029
+ });
15030
+ const existingEntry = await keyring.get(alias);
15031
+ const heldSecret = existingEntry ? await keyring.resolveSecret(alias) : void 0;
15032
+ const canReuse = existingEntry && heldSecret && existingEntry.context === context && existingEntry.principalId === principalId && existingEntry.env === auth.env;
15033
+ if (canReuse && existingEntry) {
15034
+ await keyring.setActive(alias);
15035
+ emitJoined(alias, context, principalId, opts.role, existingEntry.keyId, true);
15036
+ return 0;
15037
+ }
15038
+ if (existingEntry) {
15039
+ process.stdout.write(
15040
+ `
15041
+ \u26A0 Replacing keyring entry '${alias}' (was ${existingEntry.principalId} in '${existingEntry.context}' @ ${existingEntry.env}). Pass --alias to keep both.
15042
+ `
15043
+ );
15044
+ }
15045
+ const keyName = keyNameFor(context, await keyring.machineId());
15046
+ const key = await issueOrRotateKey(client, tenantId, context, userId, keyName);
15047
+ if (!key.keyId) {
15048
+ throw new Error(
15049
+ "The minted key has no id \u2014 it could not be recorded or later rotated. Retry, or use `vectros key issue`."
15050
+ );
15051
+ }
15052
+ const entry = {
15053
+ alias,
15054
+ context,
15055
+ principalId,
15056
+ role: opts.role,
15057
+ keyId: key.keyId,
15058
+ env: auth.env,
15059
+ secretRef: key.keyId,
15060
+ createdAt: nowIso()
15061
+ };
15062
+ await keyring.add(entry, key.rawKey, { activate: true });
15063
+ emitJoined(alias, context, principalId, opts.role, entry.keyId, false);
15064
+ return 0;
15065
+ } catch (err) {
15066
+ process.stderr.write(` \u2716 join failed: ${formatError(err)}
15067
+ `);
15068
+ return 1;
15069
+ }
15070
+ });
15071
+ }
15072
+ function nowIso() {
15073
+ return (/* @__PURE__ */ new Date()).toISOString();
15074
+ }
15075
+ function emitJoined(alias, context, principalId, role, keyId, reused) {
15076
+ process.stdout.write(
15077
+ `
15078
+ \u2714 Joined '${context}' as ${principalId}` + (reused ? " (existing key reused)" : "") + "\n" + formatKeyValues([
15079
+ ["alias", alias],
15080
+ ["context", context],
15081
+ ["principal", principalId],
15082
+ ["role", role],
15083
+ ["key id", keyId]
15084
+ ]) + `
15085
+
15086
+ Now active. Use \`vectros switch ${alias}\` to return to it, \`vectros keyring show --format env\` to export the key for the MCP server.
15087
+ `
15088
+ );
15089
+ }
15090
+
15091
+ // src/commands/keyring.ts
15092
+ async function runKeyringList(opts, deps = {}) {
15093
+ const keyring = deps.keyring ?? new Keyring();
15094
+ try {
15095
+ const entries = await keyring.list();
15096
+ const active = await keyring.activeAlias();
15097
+ if (opts.json) {
15098
+ process.stdout.write(`${formatJson({ active, entries })}
15099
+ `);
15100
+ return 0;
15101
+ }
15102
+ if (entries.length === 0) {
15103
+ process.stdout.write("\n (no keyring entries yet \u2014 run `vectros join <context>` to add one)\n");
15104
+ return 0;
15105
+ }
15106
+ process.stdout.write(
15107
+ "\n" + formatTable(entries, [
15108
+ { header: "", get: (e) => e.alias === active ? "*" : "" },
15109
+ { header: "alias", get: (e) => e.alias },
15110
+ { header: "context", get: (e) => e.context },
15111
+ { header: "principal", get: (e) => e.principalId },
15112
+ { header: "role", get: (e) => e.role },
15113
+ { header: "env", get: (e) => e.env },
15114
+ { header: "key id", get: (e) => e.keyId }
15115
+ ]) + "\n"
15116
+ );
15117
+ return 0;
15118
+ } catch (err) {
15119
+ process.stderr.write(` \u2716 keyring list failed: ${formatError(err)}
15120
+ `);
15121
+ return 1;
15122
+ }
15123
+ }
15124
+ async function runKeyringUse(alias, opts, deps = {}) {
15125
+ const keyring = deps.keyring ?? new Keyring();
15126
+ try {
15127
+ await keyring.setActive(alias);
15128
+ if (opts.json) {
15129
+ process.stdout.write(`${formatJson({ active: alias })}
15130
+ `);
15131
+ return 0;
15132
+ }
15133
+ process.stdout.write(`
15134
+ \u2714 Active identity is now '${alias}'.
15135
+ `);
15136
+ return 0;
15137
+ } catch (err) {
15138
+ process.stderr.write(` \u2716 ${formatError(err)}
15139
+ `);
15140
+ return 2;
15141
+ }
15142
+ }
15143
+ async function runKeyringRemove(alias, opts, deps = {}) {
15144
+ const keyring = deps.keyring ?? new Keyring();
15145
+ try {
15146
+ const removed = await keyring.remove(alias);
15147
+ if (!removed) {
15148
+ process.stderr.write(` \u2716 No keyring entry named '${alias}'.
15149
+ `);
15150
+ return 2;
15151
+ }
15152
+ if (opts.json) {
15153
+ process.stdout.write(`${formatJson({ removed: alias })}
15154
+ `);
15155
+ return 0;
15156
+ }
15157
+ process.stdout.write(`
15158
+ \u2714 Removed '${alias}' from the keyring.
15159
+ `);
15160
+ return 0;
15161
+ } catch (err) {
15162
+ process.stderr.write(` \u2716 keyring remove failed: ${formatError(err)}
15163
+ `);
15164
+ return 1;
15165
+ }
15166
+ }
15167
+ async function runKeyringShow(opts, deps = {}) {
15168
+ const keyring = deps.keyring ?? new Keyring();
15169
+ try {
15170
+ const entry = opts.alias ? await keyring.get(opts.alias) : await keyring.getActive();
15171
+ if (!entry) {
15172
+ process.stderr.write(
15173
+ opts.alias ? ` \u2716 No keyring entry named '${opts.alias}'.
15174
+ ` : " \u2716 No active identity. Run `vectros join <context>` or `vectros switch <alias>`.\n"
15175
+ );
15176
+ return 2;
15177
+ }
15178
+ const secret = await keyring.resolveSecret(entry.alias);
15179
+ if (!secret) {
15180
+ process.stderr.write(
15181
+ ` \u2716 The stored secret for '${entry.alias}' is missing or unreadable. Re-issue it with \`vectros join ${entry.context} --alias ${entry.alias}\`.
15182
+ `
15183
+ );
15184
+ return 1;
15185
+ }
15186
+ switch (opts.format) {
15187
+ case "raw":
15188
+ process.stdout.write(`${secret}
15189
+ `);
15190
+ return 0;
15191
+ case "env":
15192
+ process.stdout.write(`VECTROS_API_KEY=${secret}
15193
+ `);
15194
+ return 0;
15195
+ case "human":
15196
+ process.stdout.write(
15197
+ "\n" + formatKeyValues([
15198
+ ["alias", entry.alias],
15199
+ ["context", entry.context],
15200
+ ["principal", entry.principalId],
15201
+ ["role", entry.role],
15202
+ ["env", entry.env],
15203
+ ["key id", entry.keyId]
15204
+ ]) + `
15205
+
15206
+ VECTROS_API_KEY=${secret}
15207
+ \u26A0 This is a live credential \u2014 treat it like a password.
15208
+ `
15209
+ );
15210
+ return 0;
15211
+ }
15212
+ } catch (err) {
15213
+ process.stderr.write(` \u2716 keyring show failed: ${formatError(err)}
15214
+ `);
15215
+ return 1;
15216
+ }
15217
+ }
15218
+
14167
15219
  // src/build-info.ts
14168
15220
  var BUILD_INFO = {
14169
- cli: true ? "0.7.0" : "dev",
14170
- sdk: true ? "0.33.0" : "dev",
14171
- blueprints: true ? "0.6.5" : "dev"
15221
+ cli: true ? "0.9.0" : "dev",
15222
+ sdk: true ? "0.34.0" : "dev",
15223
+ blueprints: true ? "0.7.0" : "dev"
14172
15224
  };
14173
15225
  function formatBuildInfo() {
14174
15226
  return `vectros ${BUILD_INFO.cli} (sdk ${BUILD_INFO.sdk}, blueprints ${BUILD_INFO.blueprints})`;
@@ -14238,8 +15290,56 @@ function buildProgram(deps = {}) {
14238
15290
  registerAccessCommands(program, deps);
14239
15291
  registerRoleCommands(program, deps);
14240
15292
  registerContextCommands(program, deps);
15293
+ registerJoinCommand(program, deps);
15294
+ registerKeyringCommands(program);
14241
15295
  return program;
14242
15296
  }
15297
+ function registerJoinCommand(program, deps) {
15298
+ const join4 = program.command("join").description("Enroll a principal in a context (grant + key) and keep the key in the keyring.").argument("<context>", "the app context to join").option("--role <roleId>", "reference a Role to grant (XOR --actions)").option("--actions <csv>", "inline single-clause actions to grant, comma-separated (XOR --role)").option("--as <principal>", "who to enroll: 'me' (default) or an explicit 'usr_<id>'", "me").option("--org <orgId>", "identity override: the org the member acts under (enables org/team-scoped writes)").option("--alias <name>", "keyring entry name (defaults to the context id)").option("--tenant <tenant>", "target tenant: 'test' or 'live' (default live)");
15299
+ addAuthOptions(join4).action(
15300
+ async (contextArg, flags) => {
15301
+ if (flags.tenant !== void 0 && flags.tenant !== "test" && flags.tenant !== "live") {
15302
+ process.stderr.write(` \u2716 --tenant must be 'test' or 'live' (got '${flags.tenant}')
15303
+ `);
15304
+ process.exit(2);
15305
+ }
15306
+ process.exit(
15307
+ await runJoin(
15308
+ {
15309
+ ...toCommonOptions(flags),
15310
+ contextArg,
15311
+ role: flags.role,
15312
+ actions: flags.actions,
15313
+ as: flags.as,
15314
+ org: flags.org,
15315
+ alias: flags.alias,
15316
+ tenant: flags.tenant
15317
+ },
15318
+ deps
15319
+ )
15320
+ );
15321
+ }
15322
+ );
15323
+ }
15324
+ function registerKeyringCommands(program) {
15325
+ const KEYRING_SHOW_FORMATS = ["human", "env", "raw"];
15326
+ const keyring = program.command("keyring").description("Inspect and manage the local multi-principal keyring.");
15327
+ keyring.command("list").description("List keyring entries and which one is active.").option("--json", "emit machine-readable JSON").action(async (flags) => {
15328
+ process.exit(await runKeyringList({ json: Boolean(flags.json) }));
15329
+ });
15330
+ keyring.command("use").description("Make an entry the active identity.").argument("<alias>", "the keyring entry to activate").option("--json", "emit machine-readable JSON").action(async (alias, flags) => {
15331
+ process.exit(await runKeyringUse(alias, { json: Boolean(flags.json) }));
15332
+ });
15333
+ keyring.command("show").description("Reveal the active (or a named) entry\u2019s key.").option("--alias <name>", "show this entry instead of the active one").addOption(new import_commander.Option("--format <fmt>", "how to emit the key").choices(KEYRING_SHOW_FORMATS).default("human")).action(async (flags) => {
15334
+ process.exit(await runKeyringShow({ alias: flags.alias, format: flags.format }));
15335
+ });
15336
+ keyring.command("remove").description("Drop an entry and its stored secret.").argument("<alias>", "the keyring entry to remove").option("--json", "emit machine-readable JSON").action(async (alias, flags) => {
15337
+ process.exit(await runKeyringRemove(alias, { json: Boolean(flags.json) }));
15338
+ });
15339
+ program.command("switch").description("Shortcut for `keyring use` \u2014 flip the active identity.").argument("<alias>", "the keyring entry to activate").option("--json", "emit machine-readable JSON").action(async (alias, flags) => {
15340
+ process.exit(await runKeyringUse(alias, { json: Boolean(flags.json) }));
15341
+ });
15342
+ }
14243
15343
  function registerContextCommands(program, deps) {
14244
15344
  const context = program.command("context").description("Manage app contexts (the namespace all data lives in).");
14245
15345
  const create = context.command("create").description("Create (idempotent) an app context.").argument("<contextId>", "context id (3-31 chars; lowercase letter + lowercase/digit/dash)").option("--name <name>", "human-readable name").option("--tenant <tenant>", "tenant to create the context in: 'test' or 'live' (default live)");
@@ -14314,7 +15414,10 @@ function registerContextCommands(program, deps) {
14314
15414
  }
14315
15415
  function registerRoleCommands(program, deps) {
14316
15416
  const role = program.command("role").description("Manage context-scoped roles (reusable scope rules; bind via `access grant --role`).");
14317
- const create = role.command("create").description("Create (idempotent by roleId) a single-clause role from --actions.").requiredOption("--context <c>", "app context").requiredOption("--role-id <id>", "role id (3-31 chars; lowercase letter + lowercase/digit/dash)").requiredOption("--name <name>", "human-readable name").requiredOption("--actions <csv>", "allowed actions, comma-separated (e.g. records:cru,search:r)").option("--description <text>", "free-text description");
15417
+ const create = role.command("create").description("Create (idempotent by roleId) a single-clause role from --actions.").requiredOption("--context <c>", "app context").requiredOption("--role-id <id>", "role id (3-31 chars; lowercase letter + lowercase/digit/dash)").requiredOption("--name <name>", "human-readable name").requiredOption("--actions <csv>", "allowed actions, comma-separated (e.g. records:cru,search:r)").option(
15418
+ "--data-scope <json>",
15419
+ `confine the clause to owned data \u2014 JSON map of dimension \u2192 allowed values, e.g. '{"userId":["\${{ self.userId }}"]}' or '{"scope:org":["<orgId>", null]}'`
15420
+ ).option("--description <text>", "free-text description");
14318
15421
  addCommonAuthOptions(create).action(
14319
15422
  async (flags) => {
14320
15423
  process.exit(
@@ -14324,6 +15427,7 @@ function registerRoleCommands(program, deps) {
14324
15427
  roleId: flags.roleId,
14325
15428
  name: flags.name,
14326
15429
  actions: flags.actions,
15430
+ dataScope: flags.dataScope,
14327
15431
  description: flags.description
14328
15432
  }, deps)
14329
15433
  );
@@ -14344,7 +15448,7 @@ function registerRoleCommands(program, deps) {
14344
15448
  }
14345
15449
  function registerAccessCommands(program, deps) {
14346
15450
  const access2 = program.command("access").description("Manage access bindings: principal \u2194 context \u2194 role/scopes.");
14347
- const grant = access2.command("grant").description("Bind a principal to a context via a role or inline actions.").requiredOption("--principal <p>", "principal: 'usr_<userId>', 'key_<keyId>', or 'me' (your own user)").requiredOption("--context <c>", "app context").option("--role <roleId>", "reference a Role (XOR --actions)").option("--actions <csv>", "inline single-clause actions, comma-separated (XOR --role)");
15451
+ const grant = access2.command("grant").description("Bind a principal to a context via a role or inline actions.").requiredOption("--principal <p>", "principal: 'usr_<userId>', 'key_<keyId>', or 'me' (your own user)").requiredOption("--context <c>", "app context").option("--role <roleId>", "reference a Role (XOR --actions)").option("--actions <csv>", "inline single-clause actions, comma-separated (XOR --role)").option("--org <orgId>", "identity override: the org this principal acts under (enables org/team-scoped writes)").option("--identity-overrides <json>", "general identity overrides as a JSON object (merged over --org)");
14348
15452
  addCommonAuthOptions(grant).action(
14349
15453
  async (flags) => {
14350
15454
  process.exit(
@@ -14353,7 +15457,9 @@ function registerAccessCommands(program, deps) {
14353
15457
  principal: flags.principal,
14354
15458
  context: flags.context,
14355
15459
  role: flags.role,
14356
- actions: flags.actions
15460
+ actions: flags.actions,
15461
+ org: flags.org,
15462
+ identityOverrides: flags.identityOverrides
14357
15463
  }, deps)
14358
15464
  );
14359
15465
  }
@@ -14370,6 +15476,12 @@ function registerAccessCommands(program, deps) {
14370
15476
  addCommonAuthOptions(get).action(async (flags) => {
14371
15477
  process.exit(await runAccessGet({ ...toCommonOptions(flags), principal: flags.principal, context: flags.context }, deps));
14372
15478
  });
15479
+ const explain = access2.command("explain").description("Show what a principal\u2019s binding grants: its clauses + a concrete access matrix.").requiredOption("--principal <p>", "principal: 'usr_<userId>', 'key_<keyId>', or 'me' (your own user)").requiredOption("--context <c>", "app context");
15480
+ addCommonAuthOptions(explain).action(async (flags) => {
15481
+ process.exit(
15482
+ await runAccessExplain({ ...toCommonOptions(flags), principal: flags.principal, context: flags.context }, deps)
15483
+ );
15484
+ });
14373
15485
  }
14374
15486
  function dimensionOption() {
14375
15487
  return new import_commander.Option("--type <dim>", "principal dimension").choices(DIMENSIONS).makeOptionMandatory();