@vectros-ai/cli 0.19.0 → 0.20.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
@@ -6104,7 +6104,7 @@ var AuthClient = class {
6104
6104
  return handleNonStatusCodeError(_response.error, _response.rawResponse, "POST", "/v1/auth/token");
6105
6105
  }
6106
6106
  /**
6107
- * Invite a new member to one of your app contexts by email, OR grant an existing member access to an additional app context by inviting their same email again. Idempotent on the combination of context and email: re-inviting the same email into the SAME context rotates the token and resends the invitation rather than creating a duplicate — this requires the `users:r` and `users:u` scopes in addition to `users:c`, because resending rotates a credential on an existing invitation and invalidates any link already sent. Without them the collision returns 409 instead, with no invitation details and no change to the outstanding invitation. Inviting the SAME email into a DIFFERENT app context in this tenant, where that email already resolves to an existing member: if that member is active AND already has (or, once accepted, will have) a credential that works for the new context's own identity provider, this immediately grants them access to the new context (no email is sent — there is nothing to accept, `emailSent` is false) — this additionally requires the `users:r` scope (no `users:u`, since nothing is mutated), because the response names the existing member's userId, a fact about them your credential could not otherwise learn through this endpoint. If that active member's ONLY existing credential is for a DIFFERENT identity provider than the one the new context uses, a normal, independent invitation is created instead (its own new member id, a real token/accept link) — attaching them silently would leave no way for them to ever actually sign in to that context. If the existing member's original invitation is still pending, this attaches the new context's access to that same outstanding invitation and rotates its token (`users:r`+`users:u`, same as an ordinary resend — both the disclosure and the credential rotation apply here). A SUSPENDED member's email does not get new-context access this way — reactivate them explicitly first. Returns HTTP 201 in every one of those cases. Returns 409 if that email already belongs to an active or suspended member of THIS specific app context, already has a PENDING invitation for THIS specific app context, or resolves to an existing member elsewhere in the tenant and your token lacks the additional scope the grant/attach requires (`users:r`, or `users:r`+`users:u` for the still-pending case). An email that already has an identity in your OTHER tenant (test vs. live) is not a collision either — it creates an additional, independent membership in this tenant for that same identity. When `sendEmail` is false, the response includes the raw token and a ready-to-use accept link so you can deliver the invitation through your own email provider. Requires the `users:c` scope.
6107
+ * Invite a new member to one of your app contexts by email, OR grant an existing member access to an additional app context by inviting their same email again. Idempotent on the combination of context and email: re-inviting the same email into the SAME context rotates the token and resends the invitation rather than creating a duplicate — this requires the `users:r` and `users:u` scopes in addition to `users:c`, because resending rotates a credential on an existing invitation and invalidates any link already sent. Without them the collision returns 409 instead, with no invitation details and no change to the outstanding invitation. Inviting the SAME email into a DIFFERENT app context in this tenant, where that email already resolves to an existing member: if that member is active AND already has (or, once accepted, will have) a credential that works for the new context's own identity provider, this immediately grants them access to the new context (no email is sent — there is nothing to accept, `emailSent` is false) — this additionally requires the `users:r` scope (no `users:u`, since nothing is mutated), because the response names the existing member's userId, a fact about them your credential could not otherwise learn through this endpoint. If that active member's ONLY existing credential is for a DIFFERENT identity provider than the one the new context uses, a normal, independent invitation is created instead (its own new member id, a real token/accept link) — attaching them silently would leave no way for them to ever actually sign in to that context. If the existing member's original invitation is still pending, this attaches the new context's access to that same outstanding invitation and rotates its token (`users:r`+`users:u`, same as an ordinary resend — both the disclosure and the credential rotation apply here). A SUSPENDED member's email does not get new-context access this way — reactivate them explicitly first. Returns HTTP 201 in every one of those cases. Returns 409 if that email already belongs to an active or suspended member of THIS specific app context, already has a PENDING invitation for THIS specific app context, or resolves to an existing member elsewhere in the tenant and your token lacks the additional scope the grant/attach requires (`users:r`, or `users:r`+`users:u` for the still-pending case). An email that already has an identity in your OTHER tenant (test vs. live) is not a collision either — it creates an additional, independent membership in this tenant for that same identity. When `sendEmail` is false, the response includes the raw token and a ready-to-use accept link so you can deliver the invitation through your own email provider. Requires the `users:c` scope — or, for a credential scoped to a single app context, the `member-lifecycle` capability plus the matching `profiles:c`/`profiles:r`/`profiles:u` grant(s) in that context, as an equally sufficient alternative to every `users:c`/`users:r`/`users:u` requirement in this description.
6108
6108
  *
6109
6109
  * @param {Vectros.CreateInviteRequest} request
6110
6110
  * @param {AuthClient.RequestOptions} requestOptions - Request-specific configuration.
@@ -6175,7 +6175,7 @@ var AuthClient = class {
6175
6175
  return handleNonStatusCodeError(_response.error, _response.rawResponse, "POST", "/v1/users/invite");
6176
6176
  }
6177
6177
  /**
6178
- * Resend an outstanding invitation, identified by its email and app context. Rotates the invitation token and extends its expiry, then (when `sendEmail` is true) re-delivers the email. Rotating the token invalidates any previously issued link for this invitation, so only the newest link works. The invitee's pending permissions are left unchanged. Because this rotates a credential on an existing invitation, it requires the `users:c`, `users:r` and `users:u` scopes.
6178
+ * Resend an outstanding invitation, identified by its email and app context. Rotates the invitation token and extends its expiry, then (when `sendEmail` is true) re-delivers the email. Rotating the token invalidates any previously issued link for this invitation, so only the newest link works. The invitee's pending permissions are left unchanged. Because this rotates a credential on an existing invitation, it requires the `users:c`, `users:r` and `users:u` scopes — or, for a credential scoped to a single app context, the `member-lifecycle` capability plus the matching `profiles:c`/`profiles:r`/`profiles:u` grant(s) in that context.
6179
6179
  *
6180
6180
  * @param {Vectros.CreateInviteRequest} request
6181
6181
  * @param {AuthClient.RequestOptions} requestOptions - Request-specific configuration.
@@ -7048,7 +7048,21 @@ var DocumentsClient = class {
7048
7048
  return HttpResponsePromise.fromPromise(this.__lookupDocuments(request, requestOptions));
7049
7049
  }
7050
7050
  async __lookupDocuments(request, requestOptions) {
7051
- const { type: type_, field, value, from: from_, to, prefix, startFrom, limit, order, userId, scope } = request;
7051
+ const {
7052
+ type: type_,
7053
+ field,
7054
+ value,
7055
+ from: from_,
7056
+ to,
7057
+ prefix,
7058
+ sortFrom,
7059
+ sortTo,
7060
+ startFrom,
7061
+ limit,
7062
+ order,
7063
+ userId,
7064
+ scope
7065
+ } = request;
7052
7066
  const _queryParams = {
7053
7067
  type: type_,
7054
7068
  field,
@@ -7056,6 +7070,8 @@ var DocumentsClient = class {
7056
7070
  from: from_,
7057
7071
  to,
7058
7072
  prefix,
7073
+ sortFrom,
7074
+ sortTo,
7059
7075
  startFrom,
7060
7076
  limit,
7061
7077
  order: order != null ? order : void 0,
@@ -11027,7 +11043,21 @@ var BlueprintSchemaSchema = import_zod.z.object({
11027
11043
  // (it becomes that name's shared base). Must point directly at the base (one
11028
11044
  // hop); immutable once set. Mirrors the `basedOn` field on the platform's
11029
11045
  // schema-create request.
11030
- basedOn: import_zod.z.string().min(1).optional()
11046
+ basedOn: import_zod.z.string().min(1).optional(),
11047
+ // ADVISORY ONLY — not sent to the platform, never runtime-enforced. The
11048
+ // ownership dimensions this schema's records are typically scoped by
11049
+ // (e.g. ['org', 'client']), declared so the CLI's blueprint lint can warn
11050
+ // when a role clause grants r/u/d on this type but its `dataScope` names
11051
+ // only SOME of these dims — a schema author's easiest way to leave a
11052
+ // dimension unintentionally unconstrained, since reads (unlike creates)
11053
+ // never require full-dimension `dataScope` coverage — this helps an
11054
+ // author NOTICE that read/write asymmetry, not change it; the asymmetry
11055
+ // itself is a ratified, deliberate platform behavior.
11056
+ // Same grammar as a `dataScope` key with the `scope:` prefix stripped:
11057
+ // a bare namespace name, or 'userId' for the principal dimension.
11058
+ // Structural-only here (validated below by `lintExpectedScopeDims`); the
11059
+ // cross-referencing lint against role clauses lives in `packages/cli`.
11060
+ expectedScopeDims: import_zod.z.array(import_zod.z.string().min(1)).max(MAX_SCOPE_DIMENSIONS).optional()
11031
11061
  }).strict().superRefine((schema, ctx) => {
11032
11062
  const hasComposite = (schema.lookupFields ?? []).some(
11033
11063
  (lf) => typeof lf === "object" && lf.fieldNames !== void 0
@@ -11078,8 +11108,8 @@ var BlueprintAccessProfileSchema = import_zod.z.object({
11078
11108
  //
11079
11109
  // Optional now: mutually exclusive with `roleIds` below (the
11080
11110
  // `.superRefine` on this schema enforces the XOR, mirroring the platform's
11081
- // own `AccessProfileDB.validateScopesXorRole`). Exactly one of the two
11082
- // authors this profile's grant.
11111
+ // own access-profile scopes/role-composition XOR check). Exactly one of
11112
+ // the two authors this profile's grant.
11083
11113
  allowedActions: import_zod.z.array(import_zod.z.string().min(1)).min(1).optional(),
11084
11114
  // Additive role composition: this profile's effective grant is
11085
11115
  // each named role's own clauses, concatenated in the order listed, instead of
@@ -11682,6 +11712,35 @@ function lintDataScopeKeys(value, ctx) {
11682
11712
  }
11683
11713
  }
11684
11714
  }
11715
+ function lintExpectedScopeDims(value, ctx) {
11716
+ const bp = value;
11717
+ (bp.schemas ?? []).forEach((schema, i) => {
11718
+ const dims = schema.expectedScopeDims;
11719
+ if (!dims) return;
11720
+ const seen = /* @__PURE__ */ new Set();
11721
+ dims.forEach((raw, j) => {
11722
+ const dim = String(raw);
11723
+ const path4 = ["schemas", i, "expectedScopeDims", j];
11724
+ if (seen.has(dim)) {
11725
+ ctx.addIssue({
11726
+ code: import_zod.z.ZodIssueCode.custom,
11727
+ path: path4,
11728
+ message: `schema '${schema.typeName}': expectedScopeDims lists '${dim}' more than once.`
11729
+ });
11730
+ }
11731
+ seen.add(dim);
11732
+ if (dim === "userId") return;
11733
+ const nsError = scopeNamespaceGrammarError(dim);
11734
+ if (nsError !== null) {
11735
+ ctx.addIssue({
11736
+ code: import_zod.z.ZodIssueCode.custom,
11737
+ path: path4,
11738
+ message: `schema '${schema.typeName}': expectedScopeDims['${dim}'] ${nsError}`
11739
+ });
11740
+ }
11741
+ });
11742
+ });
11743
+ }
11685
11744
  function lintDataScopeRefs(value, ctx) {
11686
11745
  const bp = value;
11687
11746
  const declared = new Set(Object.keys(bp.fragments ?? {}));
@@ -11874,6 +11933,15 @@ var BlueprintSchema = import_zod.z.object({
11874
11933
  }),
11875
11934
  /** Human-readable app-context name; defaults to `MCP — <name>` (see {@link contextNameOf}) when absent. */
11876
11935
  contextName: import_zod.z.string().min(1).optional(),
11936
+ /**
11937
+ * The deploying organization's own display name — distinct from {@link contextName}.
11938
+ * `contextName` is the blueprint author's fixed identity for the app (the same for every
11939
+ * deployer); `companyName` is meant to vary per install, typically templated from a
11940
+ * deployer-supplied `${{ inputs.x }}` value rather than hardcoded. Used for branding on
11941
+ * platform-sent correspondence (e.g. invite emails) alongside `contextName` — not a
11942
+ * replacement for it. See {@link companyNameOf}.
11943
+ */
11944
+ companyName: import_zod.z.string().min(1).optional(),
11877
11945
  schemas: import_zod.z.array(BlueprintSchemaSchema).default([]),
11878
11946
  accessProfile: BlueprintAccessProfileSchema,
11879
11947
  servicePrincipal: BlueprintServicePrincipalSchema,
@@ -11922,6 +11990,7 @@ var BlueprintSchema = import_zod.z.object({
11922
11990
  lintIdentityOverrides(bp, ctx);
11923
11991
  lintNamespacedScopeArrays(bp, ctx);
11924
11992
  lintDataScopeKeys(bp, ctx);
11993
+ lintExpectedScopeDims(bp, ctx);
11925
11994
  lintDataScopeRefs(bp, ctx);
11926
11995
  lintRoleAssumable(bp, ctx);
11927
11996
  lintAccessProfileRoleIds(bp, ctx);
@@ -14104,6 +14173,7 @@ var ENV_BASE_URLS = {
14104
14173
  // `--base-url` or VECTROS_API_BASE_URL; verify at smoke time.
14105
14174
  staging: "https://api.staging.vectros.ai"
14106
14175
  };
14176
+ var AUTH_FLAG_NAMES = ["--env", "--base-url", "--token", "--use-scoped-token"];
14107
14177
  function envToBaseUrl(env, override) {
14108
14178
  return validateBaseUrl(override ?? ENV_BASE_URLS[env]);
14109
14179
  }
@@ -14610,7 +14680,7 @@ ${summary}`
14610
14680
  this.rejected = rejected;
14611
14681
  }
14612
14682
  };
14613
- var GRANTED_CAPABILITIES_ALLOWLIST = ["member-lifecycle"];
14683
+ var GRANTED_CAPABILITIES_ALLOWLIST = ["member-lifecycle", "delegate-principal-stamp"];
14614
14684
  var GRANTED_CAPABILITIES_SET = new Set(GRANTED_CAPABILITIES_ALLOWLIST);
14615
14685
  var CAPABILITIES_HINT = GRANTED_CAPABILITIES_ALLOWLIST.join("/");
14616
14686
  var CapabilityGateError = class extends Error {
@@ -14887,7 +14957,8 @@ function makeIdentityResolver(client, log) {
14887
14957
  return id;
14888
14958
  };
14889
14959
  }
14890
- async function resolveApplyTimeIdentities(blueprint, client, log) {
14960
+ async function resolveApplyTimeIdentities(blueprint, provisioned, bootstrapClient, log) {
14961
+ const client = provisioned.ctxBootstrapClient ?? bootstrapClient;
14891
14962
  const resolver = makeIdentityResolver(client, log);
14892
14963
  const resolved = await resolveBlueprintIdentities(blueprint, resolver);
14893
14964
  return parseBlueprint(resolved);
@@ -15155,21 +15226,13 @@ async function runLoader(client, input) {
15155
15226
  machineId,
15156
15227
  rotate = false,
15157
15228
  skipSeed = false,
15158
- allowTenantWideNamespaces = false,
15229
+ seedIndexPoll,
15230
+ provisioned,
15159
15231
  readExistingKey,
15160
15232
  persistMintedKey,
15161
- confirmExistingPrincipal,
15162
15233
  log
15163
15234
  } = input;
15164
15235
  assertNoUnresolvedIdentities(blueprint);
15165
- const provisioned = input.provisioned ?? await provisionContext(client, {
15166
- blueprint,
15167
- allowTenantWideNamespaces,
15168
- remintBootstrapForContext: input.remintBootstrapForContext,
15169
- readExistingKey,
15170
- confirmExistingPrincipal,
15171
- log
15172
- });
15173
15236
  const { contextAction, servicePrincipalUserId: userId, userAction, namespaceActions } = provisioned;
15174
15237
  assertBlueprintGates(blueprint);
15175
15238
  const needsContextPinnedBootstrapForIssuers = (blueprint.issuers ?? []).length > 0;
@@ -15348,6 +15411,7 @@ async function runLoader(client, input) {
15348
15411
  const seededRecordIds = [];
15349
15412
  const seededDocumentIds = [];
15350
15413
  const seedIndexFailures = [];
15414
+ const pendingIndexSeeds = [];
15351
15415
  const noteIfIndexFailed = (created, rec, surface) => {
15352
15416
  if (created.indexStatus !== "FAILED") return;
15353
15417
  seedIndexFailures.push({
@@ -15382,6 +15446,9 @@ async function runLoader(client, input) {
15382
15446
  }
15383
15447
  });
15384
15448
  if (created.id) seededDocumentIds.push(created.id);
15449
+ if (created.id && created.indexStatus === "PENDING_INDEX") {
15450
+ pendingIndexSeeds.push({ id: created.id, externalId: rec.externalId, typeName: rec.typeName, surface: "document" });
15451
+ }
15385
15452
  noteIfIndexFailed(created, rec, "document");
15386
15453
  } else {
15387
15454
  const created = await ctxClient.records.createRecord({
@@ -15394,10 +15461,30 @@ async function runLoader(client, input) {
15394
15461
  }
15395
15462
  });
15396
15463
  if (created.id) seededRecordIds.push(created.id);
15464
+ if (created.id && created.indexStatus === "PENDING_INDEX") {
15465
+ pendingIndexSeeds.push({ id: created.id, externalId: rec.externalId, typeName: rec.typeName, surface: "record" });
15466
+ }
15397
15467
  noteIfIndexFailed(created, rec, "record");
15398
15468
  }
15399
15469
  seededCount++;
15400
15470
  }
15471
+ const pollAttempts = seedIndexPoll?.attempts ?? 3;
15472
+ const pollDelayMs = seedIndexPoll?.delayMs ?? 500;
15473
+ if (pollAttempts > 0 && pendingIndexSeeds.length > 0) {
15474
+ await Promise.all(
15475
+ pendingIndexSeeds.map(async (seed) => {
15476
+ for (let attempt = 0; attempt < pollAttempts; attempt++) {
15477
+ await new Promise((r) => setTimeout(r, pollDelayMs));
15478
+ const readBack = seed.surface === "document" ? await ctxClient.documents.getDocument({ id: seed.id }) : await ctxClient.records.getRecord({ id: seed.id });
15479
+ if (readBack.indexStatus === "FAILED") {
15480
+ noteIfIndexFailed(readBack, seed, seed.surface);
15481
+ return;
15482
+ }
15483
+ if (readBack.indexStatus !== "PENDING_INDEX") return;
15484
+ }
15485
+ })
15486
+ );
15487
+ }
15401
15488
  return {
15402
15489
  contextId: blueprint.contextId,
15403
15490
  servicePrincipalUserId: userId,
@@ -15577,6 +15664,11 @@ function buildClaudeCodeAddCommand(entry, serverKey = "vectros") {
15577
15664
  const envFlags = Object.entries(entry.env ?? {}).map(([k, v]) => `-e ${k}=${v}`);
15578
15665
  return ["claude", "mcp", "add", serverKey, ...envFlags, "--", entry.command, ...entry.args].join(" ");
15579
15666
  }
15667
+ function buildRestartHint(blueprintName, isCode) {
15668
+ const target = isCode ? "Reopen Claude Code in this project" : "Restart your MCP client";
15669
+ return ` ${target}, then ask Claude about your '${blueprintName}' data.
15670
+ `;
15671
+ }
15580
15672
  function mergeServerEntry(existing, entry, serverKey = "vectros") {
15581
15673
  const base = existing ? { ...existing } : {};
15582
15674
  const servers = { ...base.mcpServers ?? {} };
@@ -15916,11 +16008,19 @@ var CryptoFileSecretStore = class {
15916
16008
  * sub-millisecond in practice; the budget is generous for a loaded machine)
15917
16009
  * before deciding it is a crash remnant. A SHORT-but-nonempty winner is never
15918
16010
  * a write in flight — the 16 salt bytes land in one write — so it is refused
15919
- * immediately, exactly like the top-of-function read's twin check.
16011
+ * immediately, exactly like the top-of-function read's twin check. A winner
16012
+ * that goes MISSING mid-poll (ENOENT) restarts creation from the top rather
16013
+ * than surfacing the raw ENOENT — see the catch below.
15920
16014
  */
15921
16015
  async adoptSalt(saltPath) {
15922
16016
  for (let attempt = 0; ; attempt++) {
15923
- const winner = await import_node_fs3.promises.readFile(saltPath);
16017
+ let winner;
16018
+ try {
16019
+ winner = await import_node_fs3.promises.readFile(saltPath);
16020
+ } catch (err) {
16021
+ if (err.code === "ENOENT") return this.readOrCreateSalt();
16022
+ throw err;
16023
+ }
15924
16024
  if (winner.length >= 16) return winner;
15925
16025
  if (winner.length > 0) throw corruptSaltError(saltPath, winner.length);
15926
16026
  if (attempt >= 24) break;
@@ -17759,7 +17859,7 @@ function applyOutcomeSummary(result) {
17759
17859
  const parts = [
17760
17860
  reconcileTally(result.schemaActions, "schema"),
17761
17861
  reconcileTally(result.roleActions, "role"),
17762
- // #1023 — the platform's PUT /v1/auth/issuers/{issuerId} makes an issuer's
17862
+ // The platform's issuer partial-update endpoint makes an issuer's
17763
17863
  // 'reconciled' mean the same "updated to match" as every other kind here.
17764
17864
  reconcileTally(result.issuerActions, "issuer"),
17765
17865
  namespaceTally(result.namespaceActions),
@@ -18150,11 +18250,7 @@ ${renderPlan(blueprint, planSteps)}
18150
18250
  confirmExistingPrincipal,
18151
18251
  log
18152
18252
  });
18153
- const resolvedBlueprint = await resolveIdentities(
18154
- blueprint,
18155
- provisioned.ctxBootstrapClient ?? client,
18156
- log
18157
- );
18253
+ const resolvedBlueprint = await resolveIdentities(blueprint, provisioned, client, log);
18158
18254
  const machineId = await resolveMachineId();
18159
18255
  const result = await loader(client, {
18160
18256
  blueprint: resolvedBlueprint,
@@ -18162,7 +18258,6 @@ ${renderPlan(blueprint, planSteps)}
18162
18258
  machineId,
18163
18259
  rotate: args.rotate,
18164
18260
  skipSeed: noSeed,
18165
- allowTenantWideNamespaces: args.allowTenantWideNamespaces,
18166
18261
  readExistingKey,
18167
18262
  persistMintedKey,
18168
18263
  provisioned,
@@ -18173,7 +18268,6 @@ ${renderPlan(blueprint, planSteps)}
18173
18268
  tenant: args.tenant,
18174
18269
  mintContext
18175
18270
  }),
18176
- confirmExistingPrincipal,
18177
18271
  log
18178
18272
  });
18179
18273
  process.stdout.write(
@@ -18229,10 +18323,13 @@ ${codeAddHint}`);
18229
18323
  }
18230
18324
  try {
18231
18325
  const merged = await safeMergeConfigFile({ configPath, entry });
18232
- const restartHint = isCode ? ' Reopen Claude Code in this project, then try: "list my tasks" / "add a task to \u2026"\n' : ' Restart your MCP client, then try: "list my tasks" / "add a task to \u2026"\n';
18326
+ const restartHint = buildRestartHint(blueprint.name, isCode);
18233
18327
  process.stdout.write(
18234
18328
  ` \u2714 Config ${merged.action}${merged.backupPath ? ` (backup: ${merged.backupPath})` : ""}
18235
- ` + restartHint
18329
+ ` + restartHint + // The `claude mcp add` line is otherwise only shown on --no-merge / a
18330
+ // declined / a failed merge — surface it here too so a successful
18331
+ // auto-merge doesn't hide the idiomatic install command entirely.
18332
+ codeAddHint
18236
18333
  );
18237
18334
  } catch (err) {
18238
18335
  process.stderr.write(` \u26A0 Could not merge config (${err instanceof Error ? err.message : String(err)}).
@@ -18416,8 +18513,12 @@ function compositeLegFindings(schema, lf, fieldsById, rangeEnabledFieldNames) {
18416
18513
  function lintBlueprint(bp) {
18417
18514
  const findings = [];
18418
18515
  const typeNames = /* @__PURE__ */ new Set();
18516
+ const expectedDimsByTypeName = /* @__PURE__ */ new Map();
18419
18517
  for (const schema of bp.schemas) {
18420
18518
  typeNames.add(schema.typeName);
18519
+ if (schema.expectedScopeDims && schema.expectedScopeDims.length > 0) {
18520
+ expectedDimsByTypeName.set(schema.typeName, schema.expectedScopeDims);
18521
+ }
18421
18522
  const fieldIds = /* @__PURE__ */ new Set();
18422
18523
  const fieldsById = /* @__PURE__ */ new Map();
18423
18524
  for (const f of schema.fields) {
@@ -18498,6 +18599,9 @@ function lintBlueprint(bp) {
18498
18599
  if (bp.accessProfile.allowedActions !== void 0) {
18499
18600
  const reached = ownerScopedReachWithoutDataScope(bp.accessProfile.allowedActions, ds);
18500
18601
  if (reached.length > 0) findings.push(unconstrainedReachFinding("accessProfile", reached));
18602
+ findings.push(
18603
+ ...partialCoverageFindings("accessProfile", bp.accessProfile.allowedActions, ds, expectedDimsByTypeName)
18604
+ );
18501
18605
  }
18502
18606
  for (const roleId of Object.keys(bp.roles ?? {})) {
18503
18607
  if (!ROLE_ID_RE.test(roleId)) {
@@ -18515,6 +18619,9 @@ function lintBlueprint(bp) {
18515
18619
  if (reached.length > 0) {
18516
18620
  findings.push(unconstrainedReachFinding(`role '${roleId}' clause ${i}`, reached));
18517
18621
  }
18622
+ findings.push(
18623
+ ...partialCoverageFindings(`role '${roleId}' clause ${i}`, clause.allowedActions, cds, expectedDimsByTypeName)
18624
+ );
18518
18625
  if (!cds) return;
18519
18626
  for (const [key, values] of Object.entries(cds)) {
18520
18627
  for (const value of values) {
@@ -18591,6 +18698,32 @@ function unconstrainedReachFinding(where, resources) {
18591
18698
  message: `${where}: no dataScope at all, but allowedActions grants r/u/d on ${resources.join("/")} \u2014 this reaches EVERY row of EVERY owner on ${resources.length > 1 ? "those resources" : "that resource"}, not just "your own org" or any other value you may have intended. If that's deliberate (a genuinely shared/tenant-wide resource), you can ignore this; otherwise add a dataScope entry.`
18592
18699
  };
18593
18700
  }
18701
+ function dimToDataScopeKey(dim) {
18702
+ return dim === "userId" ? "userId" : `scope:${dim}`;
18703
+ }
18704
+ function partialCoverageFindings(where, allowedActions, dataScope, expectedDimsByTypeName) {
18705
+ const findings = [];
18706
+ const dataScopeKeys = new Set(Object.keys(dataScope ?? {}));
18707
+ for (const action of allowedActions) {
18708
+ const parts = action.split(":");
18709
+ const [resource, ops] = parts;
18710
+ if (resource !== "records" && resource !== "documents" || !ops) continue;
18711
+ const qualifier = parts.length > 2 ? parts.slice(2).join(":") : void 0;
18712
+ if (qualifier === void 0) continue;
18713
+ const dims = expectedDimsByTypeName.get(qualifier);
18714
+ if (!dims || dims.length === 0) continue;
18715
+ if (!(ops === "*" || [...ops].some((op) => op === "r" || op === "u" || op === "d"))) continue;
18716
+ const covered = dims.filter((d) => dataScopeKeys.has(dimToDataScopeKey(d)));
18717
+ if (covered.length === 0 || covered.length === dims.length) continue;
18718
+ const missing = dims.filter((d) => !covered.includes(d));
18719
+ findings.push({
18720
+ severity: "warning",
18721
+ rule: "partial-datascope-dimension-coverage",
18722
+ message: `${where}: ${action} covers dataScope dim${covered.length > 1 ? "s" : ""} [${covered.join(", ")}] of '${qualifier}'s declared expectedScopeDims [${dims.join(", ")}] \u2014 the ${missing.join("/")} dimension${missing.length > 1 ? "s are" : " is"} UNCONSTRAINED for this grant (every value reaches, not just the one(s) you may have intended). If that's deliberate, you can ignore this; otherwise add a dataScope entry for the missing dimension(s).`
18723
+ });
18724
+ }
18725
+ return findings;
18726
+ }
18594
18727
  function lintPasses(findings) {
18595
18728
  return !findings.some((f) => f.severity === "error");
18596
18729
  }
@@ -18682,6 +18815,10 @@ function buildScaffold(name, from) {
18682
18815
  }
18683
18816
 
18684
18817
  // src/cli-blueprint.ts
18818
+ function noEffectOnBlueprint(flag, sub) {
18819
+ const cmd = sub ? `blueprint ${sub}` : "blueprint";
18820
+ return `'${flag}' has no effect on '${cmd}' \u2014 blueprint validate/plan/list/init are creds-free and never call the API, so there's no environment/credential to select. Did you mean 'vectros bootstrap'?`;
18821
+ }
18685
18822
  function parseBlueprintArgs(argv) {
18686
18823
  const args = { help: false, force: false, set: [] };
18687
18824
  const positionals = [];
@@ -18708,6 +18845,9 @@ function parseBlueprintArgs(argv) {
18708
18845
  args.values = argv[++i];
18709
18846
  break;
18710
18847
  default:
18848
+ if (AUTH_FLAG_NAMES.includes(a)) {
18849
+ throw new Error(noEffectOnBlueprint(a, positionals[0]));
18850
+ }
18711
18851
  if (a.startsWith("-")) throw new Error(`Unknown blueprint flag: ${a}`);
18712
18852
  positionals.push(a);
18713
18853
  }
@@ -19500,6 +19640,51 @@ ${list}
19500
19640
  });
19501
19641
  }
19502
19642
 
19643
+ // src/dev-api.ts
19644
+ var DeveloperApiError = class extends Error {
19645
+ statusCode;
19646
+ constructor(message, statusCode) {
19647
+ super(message);
19648
+ this.name = "DeveloperApiError";
19649
+ this.statusCode = statusCode;
19650
+ }
19651
+ };
19652
+ async function fetchDeveloperUsersPage(opts) {
19653
+ const { cognitoToken, apiBaseUrl, tenant, startFrom, limit, fetchImpl = fetch } = opts;
19654
+ const base = apiBaseUrl.replace(/\/$/, "");
19655
+ let url = `${base}/developer/users?tenant=${encodeURIComponent(tenant)}`;
19656
+ if (startFrom) url += `&startFrom=${encodeURIComponent(startFrom)}`;
19657
+ if (limit !== void 0) url += `&limit=${encodeURIComponent(String(limit))}`;
19658
+ let res;
19659
+ try {
19660
+ res = await fetchImpl(url, {
19661
+ method: "GET",
19662
+ headers: { Authorization: `Bearer ${cognitoToken}` }
19663
+ });
19664
+ } catch (err) {
19665
+ throw new DeveloperApiError(
19666
+ `Request to /developer/users failed: ${err instanceof Error ? err.message : String(err)}`
19667
+ );
19668
+ }
19669
+ if (!res.ok) {
19670
+ const body = await res.text().catch(() => "");
19671
+ const hint = res.status === 403 ? " (this command requires an account owner login \u2014 sign in as the owner and retry)" : "";
19672
+ throw new DeveloperApiError(
19673
+ `GET /developer/users failed: HTTP ${res.status}${hint}${body ? ` \u2014 ${body}` : ""}`,
19674
+ res.status
19675
+ );
19676
+ }
19677
+ let parsed;
19678
+ try {
19679
+ parsed = await res.json();
19680
+ } catch {
19681
+ throw new DeveloperApiError("GET /developer/users returned a non-JSON body");
19682
+ }
19683
+ const data = Array.isArray(parsed.data) ? parsed.data : [];
19684
+ const nextCursor = typeof parsed.nextCursor === "string" ? parsed.nextCursor : null;
19685
+ return { data, nextCursor };
19686
+ }
19687
+
19503
19688
  // src/commands/identity.ts
19504
19689
  function isUserDimension(type) {
19505
19690
  return type === "user";
@@ -19654,6 +19839,57 @@ async function runIdentityList(opts, deps = {}) {
19654
19839
  }
19655
19840
  });
19656
19841
  }
19842
+ function toIdentityView(row) {
19843
+ const str = (v) => typeof v === "string" ? v : void 0;
19844
+ return {
19845
+ id: str(row.id),
19846
+ externalId: str(row.externalId),
19847
+ email: str(row.email),
19848
+ status: str(row.status),
19849
+ type: str(row.type),
19850
+ createdAt: str(row.createdAt)
19851
+ };
19852
+ }
19853
+ async function runIdentityListAll(opts, deps = {}) {
19854
+ const fetchImpl = deps.fetchImpl ?? fetch;
19855
+ return withAuth({ ...opts, provisioning: true }, deps, async (auth) => {
19856
+ try {
19857
+ const tenant = opts.tenant ?? "live";
19858
+ const fetchPage = async (startFrom) => {
19859
+ const page = await fetchDeveloperUsersPage({
19860
+ cognitoToken: auth.cognitoToken,
19861
+ apiBaseUrl: auth.baseUrl,
19862
+ tenant,
19863
+ startFrom,
19864
+ limit: opts.limit,
19865
+ fetchImpl
19866
+ });
19867
+ return { data: page.data.map(toIdentityView), nextCursor: page.nextCursor };
19868
+ };
19869
+ let rows = opts.limit !== void 0 ? (await fetchPage()).data ?? [] : await drainPages(fetchPage);
19870
+ if (opts.externalId) rows = rows.filter((r) => r.externalId === opts.externalId);
19871
+ if (opts.json) {
19872
+ process.stdout.write(`${formatJson(rows)}
19873
+ `);
19874
+ return 0;
19875
+ }
19876
+ process.stdout.write(
19877
+ "\n" + formatTable(rows, [
19878
+ { header: "id", get: (r) => r.id },
19879
+ { header: "external id", get: (r) => r.externalId },
19880
+ { header: "email", get: (r) => r.email },
19881
+ { header: "kind", get: (r) => r.type },
19882
+ { header: "status", get: (r) => r.status }
19883
+ ]) + "\n"
19884
+ );
19885
+ return 0;
19886
+ } catch (err) {
19887
+ process.stderr.write(` \u2716 identity list-all failed: ${formatError(err)}
19888
+ `);
19889
+ return 1;
19890
+ }
19891
+ });
19892
+ }
19657
19893
  async function runIdentityGet(opts, deps = {}) {
19658
19894
  return withAuth(opts, deps, async (auth) => {
19659
19895
  const client = auth.client;
@@ -20257,6 +20493,16 @@ async function runRoleCreate(opts, deps = {}) {
20257
20493
  dataScope = parseJsonObjectFlag("--data-scope", opts.dataScope);
20258
20494
  } catch (err) {
20259
20495
  process.stderr.write(` \u2716 ${formatError(err)}
20496
+ `);
20497
+ return 2;
20498
+ }
20499
+ }
20500
+ let assumable;
20501
+ if (opts.assumable !== void 0) {
20502
+ try {
20503
+ assumable = parseJsonObjectFlag("--assumable", opts.assumable);
20504
+ } catch (err) {
20505
+ process.stderr.write(` \u2716 ${formatError(err)}
20260
20506
  `);
20261
20507
  return 2;
20262
20508
  }
@@ -20274,7 +20520,12 @@ async function runRoleCreate(opts, deps = {}) {
20274
20520
  // stays byte-identical for unscoped roles.
20275
20521
  scopes: [
20276
20522
  dataScope ? { allowed_actions: actions, data_scope: dataScope } : { allowed_actions: actions }
20277
- ]
20523
+ ],
20524
+ // Included only when supplied — omitting it (rather than sending an
20525
+ // empty object) keeps "no assume grant" and "explicitly empty grant"
20526
+ // indistinguishable on the wire, matching the platform's own
20527
+ // "omit for the safe default" contract.
20528
+ assumable
20278
20529
  }
20279
20530
  });
20280
20531
  if (opts.json) {
@@ -20514,6 +20765,19 @@ ${renderContext(ctx)}
20514
20765
  }
20515
20766
 
20516
20767
  // src/commands/issuers.ts
20768
+ var readline4 = __toESM(require("readline/promises"));
20769
+ function parseSelfSignupPolicies(raw) {
20770
+ if (!raw || raw.length === 0) return void 0;
20771
+ return raw.map((entry) => {
20772
+ const sep = entry.indexOf(":");
20773
+ if (sep <= 0 || sep === entry.length - 1) {
20774
+ throw new Error(
20775
+ `--self-signup-policy must be '<signup_type>:<roleId>' (e.g. 'default:member'); got '${entry}'.`
20776
+ );
20777
+ }
20778
+ return { signup_type: entry.slice(0, sep), role_id: entry.slice(sep + 1) };
20779
+ });
20780
+ }
20517
20781
  function renderSelfSignup(policies) {
20518
20782
  if (!policies || policies.length === 0) return "\u2014";
20519
20783
  return policies.map((p) => `${p.signup_type} \u2192 ${p.role_id}`).join(", ");
@@ -20581,6 +20845,308 @@ ${renderIssuer(issuer)}
20581
20845
  }
20582
20846
  });
20583
20847
  }
20848
+ async function runIssuersRegister(opts, deps = {}) {
20849
+ let selfSignupPolicies;
20850
+ try {
20851
+ selfSignupPolicies = parseSelfSignupPolicies(opts.selfSignupPolicy);
20852
+ } catch (err) {
20853
+ process.stderr.write(` \u2716 ${formatError(err)}
20854
+ `);
20855
+ return 2;
20856
+ }
20857
+ return withAuth({ ...opts, provisioning: true, context: opts.context }, deps, async (auth) => {
20858
+ const client = auth.client;
20859
+ try {
20860
+ const issuer = await client.auth.registerIssuer({
20861
+ issuerId: opts.issuerId,
20862
+ issuer: opts.issuer,
20863
+ jwksUri: opts.jwksUri,
20864
+ audience: opts.audience,
20865
+ contextId: opts.context,
20866
+ subClaim: opts.subClaim,
20867
+ emailClaim: opts.emailClaim,
20868
+ userinfoUri: opts.userinfoUri,
20869
+ selfSignupPolicies
20870
+ });
20871
+ if (opts.json) {
20872
+ process.stdout.write(`${formatJson(issuer)}
20873
+ `);
20874
+ return 0;
20875
+ }
20876
+ const verb = issuer.created === false ? "already registered \u2014 returned unchanged" : "registered";
20877
+ process.stdout.write(`
20878
+ \u2714 issuer '${opts.issuerId}' ${verb}
20879
+ ${renderIssuer(issuer)}
20880
+ `);
20881
+ return 0;
20882
+ } catch (err) {
20883
+ process.stderr.write(formatCliError(err, { action: "issuers register failed" }));
20884
+ return 1;
20885
+ }
20886
+ });
20887
+ }
20888
+ async function runIssuersUpdate(opts, deps = {}) {
20889
+ let selfSignupPolicies;
20890
+ try {
20891
+ selfSignupPolicies = parseSelfSignupPolicies(opts.selfSignupPolicy);
20892
+ } catch (err) {
20893
+ process.stderr.write(` \u2716 ${formatError(err)}
20894
+ `);
20895
+ return 2;
20896
+ }
20897
+ if (opts.clearSelfSignupPolicies && selfSignupPolicies && selfSignupPolicies.length > 0) {
20898
+ process.stderr.write(
20899
+ " \u2716 --clear-self-signup-policies and --self-signup-policy are mutually exclusive.\n"
20900
+ );
20901
+ return 2;
20902
+ }
20903
+ return withAuth({ ...opts, provisioning: true, context: opts.context }, deps, async (auth) => {
20904
+ const client = auth.client;
20905
+ try {
20906
+ const issuer = await client.auth.updateIssuer({
20907
+ issuerId: opts.issuerId,
20908
+ subClaim: opts.subClaim,
20909
+ emailClaim: opts.emailClaim,
20910
+ userinfoUri: opts.userinfoUri,
20911
+ status: opts.status,
20912
+ selfSignupPolicies: opts.clearSelfSignupPolicies ? [] : selfSignupPolicies
20913
+ });
20914
+ if (opts.json) {
20915
+ process.stdout.write(`${formatJson(issuer)}
20916
+ `);
20917
+ return 0;
20918
+ }
20919
+ process.stdout.write(`
20920
+ \u2714 issuer '${opts.issuerId}' updated
20921
+ ${renderIssuer(issuer)}
20922
+ `);
20923
+ return 0;
20924
+ } catch (err) {
20925
+ process.stderr.write(formatCliError(err, { action: "issuers update failed" }));
20926
+ return 1;
20927
+ }
20928
+ });
20929
+ }
20930
+ async function runIssuersDelete(opts, deps = {}) {
20931
+ const { issuerId, context } = opts;
20932
+ if (!opts.force) {
20933
+ const isTty = deps.isTty ?? Boolean(process.stdin.isTTY);
20934
+ if (!isTty) {
20935
+ process.stderr.write(
20936
+ ` \u2716 issuers delete is destructive and needs an interactive confirmation.
20937
+ Re-run with --force to delete '${issuerId}' without a prompt.
20938
+ `
20939
+ );
20940
+ return 2;
20941
+ }
20942
+ const promptCtx = deps.createPrompt ? deps.createPrompt() : (() => {
20943
+ const rl = readline4.createInterface({ input: process.stdin, output: process.stdout });
20944
+ return { prompt: (q) => rl.question(q), close: () => rl.close() };
20945
+ })();
20946
+ try {
20947
+ process.stdout.write(
20948
+ `
20949
+ \u26A0 Deregistering issuer '${issuerId}' from context '${context}' removes it as a token-exchange
20950
+ trust anchor. The platform refuses this while any user was ever created or matched
20951
+ through it \u2014 deactivate those users or register a replacement issuer first if you
20952
+ intend to cut off access.
20953
+
20954
+ `
20955
+ );
20956
+ const typed = await promptCtx.prompt(` Type the issuer id ('${issuerId}') to confirm: `);
20957
+ if (typed.trim() !== issuerId) {
20958
+ process.stderr.write(` \u2716 Confirmation did not match \u2014 nothing deleted.
20959
+ `);
20960
+ return 1;
20961
+ }
20962
+ } finally {
20963
+ promptCtx.close();
20964
+ }
20965
+ }
20966
+ return withAuth({ ...opts, provisioning: true, context }, deps, async (auth) => {
20967
+ const client = auth.client;
20968
+ try {
20969
+ await client.auth.deleteIssuer({ issuerId });
20970
+ if (opts.json) {
20971
+ process.stdout.write(`${formatJson({ issuerId, deleted: true })}
20972
+ `);
20973
+ return 0;
20974
+ }
20975
+ process.stdout.write(`
20976
+ \u2714 issuer '${issuerId}' deregistered from '${context}'
20977
+ `);
20978
+ return 0;
20979
+ } catch (err) {
20980
+ process.stderr.write(formatCliError(err, { action: "issuers delete failed" }));
20981
+ return 1;
20982
+ }
20983
+ });
20984
+ }
20985
+
20986
+ // src/commands/namespace.ts
20987
+ function renderNamespace(n) {
20988
+ return formatKeyValues([
20989
+ ["namespace", n.namespace],
20990
+ ["contextId", n.contextId ?? "(tenant-wide)"],
20991
+ ["entityBacked", n.entityBacked === void 0 ? void 0 : String(n.entityBacked)],
20992
+ ["reserved", n.reserved === void 0 ? void 0 : String(n.reserved)],
20993
+ ["specificityRank", n.specificityRank === void 0 ? void 0 : String(n.specificityRank)],
20994
+ ["defaultSchemaId", n.defaultSchemaId],
20995
+ ["createdAt", n.createdAt]
20996
+ ]);
20997
+ }
20998
+ async function runNamespaceList(opts, deps = {}) {
20999
+ return withAuth({ ...opts, context: opts.context }, deps, async (auth) => {
21000
+ const client = auth.client;
21001
+ try {
21002
+ const rows = await drainPages(
21003
+ (startFrom) => client.identity.listNamespaces({
21004
+ limit: PAGE_LIMIT,
21005
+ ...opts.context ? { contextId: opts.context } : {},
21006
+ ...startFrom ? { startFrom } : {}
21007
+ })
21008
+ );
21009
+ if (opts.json) {
21010
+ process.stdout.write(`${formatJson(rows)}
21011
+ `);
21012
+ return 0;
21013
+ }
21014
+ process.stdout.write(
21015
+ "\n" + formatTable(rows, [
21016
+ { header: "namespace", get: (n) => n.namespace },
21017
+ { header: "contextId", get: (n) => n.contextId ?? "(tenant-wide)" },
21018
+ {
21019
+ header: "entityBacked",
21020
+ get: (n) => n.entityBacked === void 0 ? void 0 : String(n.entityBacked)
21021
+ },
21022
+ {
21023
+ header: "specificityRank",
21024
+ get: (n) => n.specificityRank === void 0 ? void 0 : String(n.specificityRank)
21025
+ }
21026
+ ]) + "\n"
21027
+ );
21028
+ return 0;
21029
+ } catch (err) {
21030
+ process.stderr.write(formatCliError(err, { action: "namespace list failed" }));
21031
+ return 1;
21032
+ }
21033
+ });
21034
+ }
21035
+ async function runNamespaceGet(opts, deps = {}) {
21036
+ return withAuth({ ...opts, context: opts.context }, deps, async (auth) => {
21037
+ const client = auth.client;
21038
+ try {
21039
+ const ns = await client.identity.getNamespace({
21040
+ namespace: opts.namespace,
21041
+ ...opts.context ? { contextId: opts.context } : {}
21042
+ });
21043
+ if (opts.json) {
21044
+ process.stdout.write(`${formatJson(ns)}
21045
+ `);
21046
+ return 0;
21047
+ }
21048
+ process.stdout.write(`
21049
+ ${renderNamespace(ns)}
21050
+ `);
21051
+ return 0;
21052
+ } catch (err) {
21053
+ process.stderr.write(formatCliError(err, { action: "namespace get failed" }));
21054
+ return 1;
21055
+ }
21056
+ });
21057
+ }
21058
+ async function runNamespaceRegister(opts, deps = {}) {
21059
+ return withAuth({ ...opts, provisioning: true, context: opts.context }, deps, async (auth) => {
21060
+ const client = auth.client;
21061
+ try {
21062
+ const ns = await client.identity.registerNamespace({
21063
+ contextId: opts.context,
21064
+ body: {
21065
+ namespace: opts.namespace,
21066
+ specificityRank: opts.specificityRank,
21067
+ entityBacked: opts.entityBacked,
21068
+ defaultSchemaId: opts.defaultSchemaId
21069
+ }
21070
+ });
21071
+ if (opts.json) {
21072
+ process.stdout.write(`${formatJson(ns)}
21073
+ `);
21074
+ return 0;
21075
+ }
21076
+ process.stdout.write(`
21077
+ \u2714 namespace '${opts.namespace}' registered
21078
+ ${renderNamespace(ns)}
21079
+ `);
21080
+ return 0;
21081
+ } catch (err) {
21082
+ process.stderr.write(formatCliError(err, { action: "namespace register failed" }));
21083
+ return 1;
21084
+ }
21085
+ });
21086
+ }
21087
+
21088
+ // src/commands/assume.ts
21089
+ function parseAssumeScopes(raw) {
21090
+ const out = {};
21091
+ for (const entry of raw) {
21092
+ const sep = entry.indexOf(":");
21093
+ if (sep <= 0 || sep === entry.length - 1) {
21094
+ throw new Error(`--scope must be '<namespace>:<value>' (e.g. 'org:orgB'); got '${entry}'.`);
21095
+ }
21096
+ const ns = entry.slice(0, sep);
21097
+ const value = entry.slice(sep + 1);
21098
+ const valueError = valueGrammarError(value);
21099
+ if (valueError) throw new Error(`--scope value ${valueError}`);
21100
+ const key = `scope:${ns}`;
21101
+ if (Object.prototype.hasOwnProperty.call(out, key)) {
21102
+ throw new Error(`--scope names namespace '${ns}' more than once \u2014 pass one value per namespace.`);
21103
+ }
21104
+ out[key] = value;
21105
+ }
21106
+ return out;
21107
+ }
21108
+ function renderAssumeToken(t) {
21109
+ return formatKeyValues([
21110
+ ["access_token", t.access_token],
21111
+ ["token_type", t.token_type],
21112
+ ["expires_in", t.expires_in === void 0 ? void 0 : `${t.expires_in}s`]
21113
+ ]);
21114
+ }
21115
+ async function runAssume(opts, deps = {}) {
21116
+ if (!opts.scope || opts.scope.length === 0) {
21117
+ process.stderr.write(" \u2716 --scope must be given at least once (e.g. --scope org:orgB).\n");
21118
+ return 2;
21119
+ }
21120
+ let scopes;
21121
+ try {
21122
+ scopes = parseAssumeScopes(opts.scope);
21123
+ } catch (err) {
21124
+ process.stderr.write(` \u2716 ${formatError(err)}
21125
+ `);
21126
+ return 2;
21127
+ }
21128
+ return withAuth({ ...opts, context: opts.context }, deps, async (auth) => {
21129
+ const client = auth.client;
21130
+ try {
21131
+ const result = await client.auth.assumeToken(scopes);
21132
+ if (opts.json) {
21133
+ process.stdout.write(`${formatJson(result)}
21134
+ `);
21135
+ return 0;
21136
+ }
21137
+ process.stdout.write(
21138
+ `
21139
+ \u2714 assumed ${Object.keys(scopes).join(", ")}
21140
+ ${renderAssumeToken(result)}
21141
+ `
21142
+ );
21143
+ return 0;
21144
+ } catch (err) {
21145
+ process.stderr.write(formatCliError(err, { action: "assume failed" }));
21146
+ return 1;
21147
+ }
21148
+ });
21149
+ }
20584
21150
 
20585
21151
  // src/commands/login.ts
20586
21152
  var import_node_http = require("http");
@@ -21045,7 +21611,8 @@ async function runBlueprintHarness(client, input, deps) {
21045
21611
  });
21046
21612
  const blueprint = await resolveApplyTimeIdentities(
21047
21613
  input.blueprint,
21048
- provisioned.ctxBootstrapClient ?? client,
21614
+ provisioned,
21615
+ client,
21049
21616
  log
21050
21617
  );
21051
21618
  const remint = remintForContext ?? (async () => client);
@@ -21458,9 +22025,9 @@ function emitJoined(opts) {
21458
22025
 
21459
22026
  // src/build-info.ts
21460
22027
  var BUILD_INFO = {
21461
- cli: true ? "0.19.0" : "dev",
21462
- sdk: true ? "0.41.0" : "dev",
21463
- blueprints: true ? "0.16.0" : "dev"
22028
+ cli: true ? "0.20.0" : "dev",
22029
+ sdk: true ? "0.42.0" : "dev",
22030
+ blueprints: true ? "0.17.0" : "dev"
21464
22031
  };
21465
22032
  function formatBuildInfo() {
21466
22033
  return `vectros ${BUILD_INFO.cli} (sdk ${BUILD_INFO.sdk}, blueprints ${BUILD_INFO.blueprints})`;
@@ -21541,10 +22108,22 @@ function buildProgram(deps = {}) {
21541
22108
  registerRoleCommands(program, deps);
21542
22109
  registerContextCommands(program, deps);
21543
22110
  registerIssuersCommands(program, deps);
22111
+ registerNamespaceCommands(program, deps);
22112
+ registerAssumeCommand(program, deps);
21544
22113
  registerJoinCommand(program, deps);
21545
22114
  registerKeyringCommands(program);
21546
22115
  return program;
21547
22116
  }
22117
+ function registerAssumeCommand(program, deps) {
22118
+ const assume = program.command("assume").description("Re-mint your own verb token with one or more identity.<namespace> values switched (needs a role's assumable grant).").requiredOption("--context <c>", "app context to operate in").option("--scope <ns:value>", "namespace value to assume, repeatable (e.g. org:orgB)", collect, []);
22119
+ addCommonAuthOptions(assume).action(
22120
+ async (flags) => {
22121
+ process.exit(
22122
+ await runAssume({ ...toCommonOptions(flags), context: flags.context, scope: flags.scope ?? [] }, deps)
22123
+ );
22124
+ }
22125
+ );
22126
+ }
21548
22127
  function registerJoinCommand(program, deps) {
21549
22128
  const join3 = 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 (repeatable \u2014 2+ compose additively; XOR --actions)", collect, []).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("--scope <ns:value>", "identity override: a scope the member acts under (repeatable, e.g. org:<id>)", collect, []).option("--alias <name>", "keyring entry name (defaults to the context id)").option("--tenant <tenant>", "target tenant: 'test' or 'live' (default live)").option(
21550
22129
  "--activate",
@@ -21681,7 +22260,7 @@ function registerContextCommands(program, deps) {
21681
22260
  );
21682
22261
  }
21683
22262
  function registerIssuersCommands(program, deps) {
21684
- const issuers = program.command("issuers").description("Inspect registered third-party IdP issuers (token-exchange trust anchors).");
22263
+ const issuers = program.command("issuers").description("Manage registered third-party IdP issuers (token-exchange trust anchors).");
21685
22264
  const list = issuers.command("list").description("List issuers registered in one app context.").requiredOption("--context <c>", "app context the issuer(s) are registered under").option("--tenant <tenant>", "tenant to read: 'test' or 'live' (default live)");
21686
22265
  addCommonAuthOptions(list).action(
21687
22266
  async (flags) => {
@@ -21700,12 +22279,125 @@ function registerIssuersCommands(program, deps) {
21700
22279
  );
21701
22280
  }
21702
22281
  );
22282
+ const register = issuers.command("register").description("Register (idempotent by issuerId) a trusted third-party IdP issuer.").requiredOption("--context <c>", "app context an exchanged token targets").requiredOption("--issuer-id <id>", "short slug for this issuer (3-31 chars; lowercase letter + lowercase/digit/dash)").requiredOption("--issuer <url>", "the IdP's iss claim value").requiredOption("--jwks-uri <url>", "the IdP's remote JWKS endpoint").requiredOption("--audience <aud>", "the required aud claim value (globally unique with --issuer)").option("--sub-claim <claim>", "claim carrying the subject identifier (default 'sub')").option("--email-claim <claim>", "claim carrying the subject's email (default 'email')").option("--userinfo-uri <url>", "OIDC userinfo endpoint, used as an email-resolution fallback").option(
22283
+ "--self-signup-policy <type:roleId>",
22284
+ "opt-in self-service signup entry, repeatable (e.g. default:member) \u2014 no entry may target an elevated role",
22285
+ collect,
22286
+ []
22287
+ ).option("--tenant <tenant>", "tenant the context lives in: 'test' or 'live' (default live)");
22288
+ addCommonAuthOptions(register).action(
22289
+ async (flags) => {
22290
+ const tenant = parseTenantFlag(flags.tenant);
22291
+ process.exit(
22292
+ await runIssuersRegister(
22293
+ {
22294
+ ...toCommonOptions(flags),
22295
+ context: flags.context,
22296
+ issuerId: flags.issuerId,
22297
+ issuer: flags.issuer,
22298
+ jwksUri: flags.jwksUri,
22299
+ audience: flags.audience,
22300
+ subClaim: flags.subClaim,
22301
+ emailClaim: flags.emailClaim,
22302
+ userinfoUri: flags.userinfoUri,
22303
+ selfSignupPolicy: flags.selfSignupPolicy,
22304
+ tenant
22305
+ },
22306
+ deps
22307
+ )
22308
+ );
22309
+ }
22310
+ );
22311
+ const update = issuers.command("update").description("Patch the safe, mutable fields of a registered issuer (trust-anchor fields are immutable).").argument("<issuerId>").requiredOption("--context <c>", "app context the issuer is registered under").option("--sub-claim <claim>", "update the subject-identifier claim").option("--email-claim <claim>", "update the subject-email claim").option("--userinfo-uri <url>", "update the OIDC userinfo fallback endpoint").option("--status <status>", "'active' or 'suspended' \u2014 a suspended issuer's tokens are rejected at exchange time").option(
22312
+ "--self-signup-policy <type:roleId>",
22313
+ "replace self-signup policies, repeatable (e.g. default:member) \u2014 omit entirely to leave unchanged",
22314
+ collect,
22315
+ []
22316
+ ).option("--clear-self-signup-policies", "disable self-signup entirely (sends an empty list; XOR --self-signup-policy)").option("--tenant <tenant>", "tenant the context lives in: 'test' or 'live' (default live)");
22317
+ addCommonAuthOptions(update).action(
22318
+ async (issuerId, flags) => {
22319
+ const tenant = parseTenantFlag(flags.tenant);
22320
+ process.exit(
22321
+ await runIssuersUpdate(
22322
+ {
22323
+ ...toCommonOptions(flags),
22324
+ context: flags.context,
22325
+ issuerId,
22326
+ subClaim: flags.subClaim,
22327
+ emailClaim: flags.emailClaim,
22328
+ userinfoUri: flags.userinfoUri,
22329
+ status: flags.status,
22330
+ selfSignupPolicy: flags.selfSignupPolicy,
22331
+ clearSelfSignupPolicies: Boolean(flags.clearSelfSignupPolicies),
22332
+ tenant
22333
+ },
22334
+ deps
22335
+ )
22336
+ );
22337
+ }
22338
+ );
22339
+ const del = issuers.command("delete").description("Deregister a trusted third-party IdP issuer (refused while any user is bound through it).").argument("<issuerId>").requiredOption("--context <c>", "app context the issuer is registered under").option("--force", "skip the interactive confirmation (required when stdin is not a TTY)").option("--tenant <tenant>", "tenant the context lives in: 'test' or 'live' (default live)");
22340
+ addCommonAuthOptions(del).action(
22341
+ async (issuerId, flags) => {
22342
+ const tenant = parseTenantFlag(flags.tenant);
22343
+ process.exit(
22344
+ await runIssuersDelete(
22345
+ { ...toCommonOptions(flags), context: flags.context, issuerId, force: Boolean(flags.force), tenant },
22346
+ deps
22347
+ )
22348
+ );
22349
+ }
22350
+ );
22351
+ }
22352
+ function registerNamespaceCommands(program, deps) {
22353
+ const namespace = program.command("namespace").description("Manage scope-namespace registrations (whether scope:<namespace> values resolve to identity entities).");
22354
+ const register = namespace.command("register").description("Register a new scope namespace (NOT idempotent \u2014 a repeat name 400s).").argument("<name>", "namespace name: 2-32 chars, a lowercase letter first, then lowercase/digit/dash/underscore").requiredOption("--context <c>", "app context to own this registration").requiredOption("--specificity-rank <n>", "position in the account specificity order (integer 0-1000000, unique)").option("--entity-backed", "values must resolve to an identity entity (create via `identity create --type <name>`)").option("--default-schema-id <id>", "default record schema bound to entities in this namespace");
22355
+ addCommonAuthOptions(register).action(
22356
+ async (name, flags) => {
22357
+ const specificityRank = Number(flags.specificityRank);
22358
+ if (!Number.isInteger(specificityRank) || specificityRank < 0 || specificityRank > 1e6) {
22359
+ process.stderr.write(
22360
+ ` \u2716 --specificity-rank must be an integer between 0 and 1000000 (got '${flags.specificityRank}').
22361
+ `
22362
+ );
22363
+ process.exit(2);
22364
+ }
22365
+ process.exit(
22366
+ await runNamespaceRegister(
22367
+ {
22368
+ ...toCommonOptions(flags),
22369
+ namespace: name,
22370
+ context: flags.context,
22371
+ specificityRank,
22372
+ entityBacked: Boolean(flags.entityBacked),
22373
+ defaultSchemaId: flags.defaultSchemaId
22374
+ },
22375
+ deps
22376
+ )
22377
+ );
22378
+ }
22379
+ );
22380
+ const list = namespace.command("list").description("List scope-namespace registrations \u2014 tenant-wide, or one context\u2019s own.").option("--context <c>", "list one app context\u2019s own registrations instead of the tenant-wide set");
22381
+ addCommonAuthOptions(list).action(async (flags) => {
22382
+ process.exit(await runNamespaceList({ ...toCommonOptions(flags), context: flags.context }, deps));
22383
+ });
22384
+ const get = namespace.command("get").description("Show one namespace registration \u2014 tenant-wide, or one context\u2019s own.").argument("<name>").option("--context <c>", "resolve for one app context\u2019s own registration instead of the tenant-wide one");
22385
+ addCommonAuthOptions(get).action(
22386
+ async (name, flags) => {
22387
+ process.exit(
22388
+ await runNamespaceGet({ ...toCommonOptions(flags), namespace: name, context: flags.context }, deps)
22389
+ );
22390
+ }
22391
+ );
21703
22392
  }
21704
22393
  function registerRoleCommands(program, deps) {
21705
22394
  const role = program.command("role").description("Manage context-scoped roles (reusable scope rules; bind via `access grant --role`).");
21706
22395
  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(
21707
22396
  "--data-scope <json>",
21708
22397
  `confine the clause to owned data \u2014 JSON map of dimension \u2192 allowed values, e.g. '{"userId":["\${{ self.userId }}"]}' or '{"scope:org":["<orgId>", null]}'`
22398
+ ).option(
22399
+ "--assumable <json>",
22400
+ `role-level \`vectros assume\` grant \u2014 JSON map of scope:<namespace> \u2192 allowed values (e.g. '{"scope:org":["orgB"]}'); independent of --data-scope, never data_scope's grammar (userId cannot be named)`
21709
22401
  ).option("--description <text>", "free-text description");
21710
22402
  addCommonAuthOptions(create).action(
21711
22403
  async (flags) => {
@@ -21717,6 +22409,7 @@ function registerRoleCommands(program, deps) {
21717
22409
  name: flags.name,
21718
22410
  actions: flags.actions,
21719
22411
  dataScope: flags.dataScope,
22412
+ assumable: flags.assumable,
21720
22413
  description: flags.description
21721
22414
  }, deps)
21722
22415
  );
@@ -21810,6 +22503,20 @@ function registerIdentityCommands(program, deps) {
21810
22503
  );
21811
22504
  }
21812
22505
  );
22506
+ const listAll = identity.command("list-all").description(
22507
+ "List every user in the tenant (owner login required), bypassing the app-context confinement 'identity list' is subject to \u2014 for recovering a user created but never granted access anywhere, which 'identity list' can never show."
22508
+ ).option("--external-id <id>", "filter to a stable id").option("--limit <n>", "page size (1\u2013100)", (v) => Number.parseInt(v, 10)).option("--tenant <tenant>", "tenant to list: 'test' or 'live' (default live)");
22509
+ addCommonAuthOptions(listAll).action(
22510
+ async (flags) => {
22511
+ const tenant = parseTenantFlag(flags.tenant);
22512
+ process.exit(
22513
+ await runIdentityListAll(
22514
+ { ...toCommonOptions(flags), externalId: flags.externalId, limit: flags.limit, tenant },
22515
+ deps
22516
+ )
22517
+ );
22518
+ }
22519
+ );
21813
22520
  const get = identity.command("get").description("Show one principal by its Vectros id.").addOption(dimensionOption()).requiredOption("--id <vectrosId>", "the Vectros-assigned id (from create/list)");
21814
22521
  addCommonAuthOptions(get).action(async (flags) => {
21815
22522
  process.exit(await runIdentityGet({ ...toCommonOptions(flags), type: flags.type, id: flags.id }, deps));