@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.mjs CHANGED
@@ -6109,7 +6109,7 @@ var AuthClient = class {
6109
6109
  return handleNonStatusCodeError(_response.error, _response.rawResponse, "POST", "/v1/auth/token");
6110
6110
  }
6111
6111
  /**
6112
- * 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.
6112
+ * 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.
6113
6113
  *
6114
6114
  * @param {Vectros.CreateInviteRequest} request
6115
6115
  * @param {AuthClient.RequestOptions} requestOptions - Request-specific configuration.
@@ -6180,7 +6180,7 @@ var AuthClient = class {
6180
6180
  return handleNonStatusCodeError(_response.error, _response.rawResponse, "POST", "/v1/users/invite");
6181
6181
  }
6182
6182
  /**
6183
- * 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.
6183
+ * 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.
6184
6184
  *
6185
6185
  * @param {Vectros.CreateInviteRequest} request
6186
6186
  * @param {AuthClient.RequestOptions} requestOptions - Request-specific configuration.
@@ -7053,7 +7053,21 @@ var DocumentsClient = class {
7053
7053
  return HttpResponsePromise.fromPromise(this.__lookupDocuments(request, requestOptions));
7054
7054
  }
7055
7055
  async __lookupDocuments(request, requestOptions) {
7056
- const { type: type_, field, value, from: from_, to, prefix, startFrom, limit, order, userId, scope } = request;
7056
+ const {
7057
+ type: type_,
7058
+ field,
7059
+ value,
7060
+ from: from_,
7061
+ to,
7062
+ prefix,
7063
+ sortFrom,
7064
+ sortTo,
7065
+ startFrom,
7066
+ limit,
7067
+ order,
7068
+ userId,
7069
+ scope
7070
+ } = request;
7057
7071
  const _queryParams = {
7058
7072
  type: type_,
7059
7073
  field,
@@ -7061,6 +7075,8 @@ var DocumentsClient = class {
7061
7075
  from: from_,
7062
7076
  to,
7063
7077
  prefix,
7078
+ sortFrom,
7079
+ sortTo,
7064
7080
  startFrom,
7065
7081
  limit,
7066
7082
  order: order != null ? order : void 0,
@@ -11032,7 +11048,21 @@ var BlueprintSchemaSchema = z.object({
11032
11048
  // (it becomes that name's shared base). Must point directly at the base (one
11033
11049
  // hop); immutable once set. Mirrors the `basedOn` field on the platform's
11034
11050
  // schema-create request.
11035
- basedOn: z.string().min(1).optional()
11051
+ basedOn: z.string().min(1).optional(),
11052
+ // ADVISORY ONLY — not sent to the platform, never runtime-enforced. The
11053
+ // ownership dimensions this schema's records are typically scoped by
11054
+ // (e.g. ['org', 'client']), declared so the CLI's blueprint lint can warn
11055
+ // when a role clause grants r/u/d on this type but its `dataScope` names
11056
+ // only SOME of these dims — a schema author's easiest way to leave a
11057
+ // dimension unintentionally unconstrained, since reads (unlike creates)
11058
+ // never require full-dimension `dataScope` coverage — this helps an
11059
+ // author NOTICE that read/write asymmetry, not change it; the asymmetry
11060
+ // itself is a ratified, deliberate platform behavior.
11061
+ // Same grammar as a `dataScope` key with the `scope:` prefix stripped:
11062
+ // a bare namespace name, or 'userId' for the principal dimension.
11063
+ // Structural-only here (validated below by `lintExpectedScopeDims`); the
11064
+ // cross-referencing lint against role clauses lives in `packages/cli`.
11065
+ expectedScopeDims: z.array(z.string().min(1)).max(MAX_SCOPE_DIMENSIONS).optional()
11036
11066
  }).strict().superRefine((schema, ctx) => {
11037
11067
  const hasComposite = (schema.lookupFields ?? []).some(
11038
11068
  (lf) => typeof lf === "object" && lf.fieldNames !== void 0
@@ -11083,8 +11113,8 @@ var BlueprintAccessProfileSchema = z.object({
11083
11113
  //
11084
11114
  // Optional now: mutually exclusive with `roleIds` below (the
11085
11115
  // `.superRefine` on this schema enforces the XOR, mirroring the platform's
11086
- // own `AccessProfileDB.validateScopesXorRole`). Exactly one of the two
11087
- // authors this profile's grant.
11116
+ // own access-profile scopes/role-composition XOR check). Exactly one of
11117
+ // the two authors this profile's grant.
11088
11118
  allowedActions: z.array(z.string().min(1)).min(1).optional(),
11089
11119
  // Additive role composition: this profile's effective grant is
11090
11120
  // each named role's own clauses, concatenated in the order listed, instead of
@@ -11687,6 +11717,35 @@ function lintDataScopeKeys(value, ctx) {
11687
11717
  }
11688
11718
  }
11689
11719
  }
11720
+ function lintExpectedScopeDims(value, ctx) {
11721
+ const bp = value;
11722
+ (bp.schemas ?? []).forEach((schema, i) => {
11723
+ const dims = schema.expectedScopeDims;
11724
+ if (!dims) return;
11725
+ const seen = /* @__PURE__ */ new Set();
11726
+ dims.forEach((raw, j) => {
11727
+ const dim = String(raw);
11728
+ const path4 = ["schemas", i, "expectedScopeDims", j];
11729
+ if (seen.has(dim)) {
11730
+ ctx.addIssue({
11731
+ code: z.ZodIssueCode.custom,
11732
+ path: path4,
11733
+ message: `schema '${schema.typeName}': expectedScopeDims lists '${dim}' more than once.`
11734
+ });
11735
+ }
11736
+ seen.add(dim);
11737
+ if (dim === "userId") return;
11738
+ const nsError = scopeNamespaceGrammarError(dim);
11739
+ if (nsError !== null) {
11740
+ ctx.addIssue({
11741
+ code: z.ZodIssueCode.custom,
11742
+ path: path4,
11743
+ message: `schema '${schema.typeName}': expectedScopeDims['${dim}'] ${nsError}`
11744
+ });
11745
+ }
11746
+ });
11747
+ });
11748
+ }
11690
11749
  function lintDataScopeRefs(value, ctx) {
11691
11750
  const bp = value;
11692
11751
  const declared = new Set(Object.keys(bp.fragments ?? {}));
@@ -11879,6 +11938,15 @@ var BlueprintSchema = z.object({
11879
11938
  }),
11880
11939
  /** Human-readable app-context name; defaults to `MCP — <name>` (see {@link contextNameOf}) when absent. */
11881
11940
  contextName: z.string().min(1).optional(),
11941
+ /**
11942
+ * The deploying organization's own display name — distinct from {@link contextName}.
11943
+ * `contextName` is the blueprint author's fixed identity for the app (the same for every
11944
+ * deployer); `companyName` is meant to vary per install, typically templated from a
11945
+ * deployer-supplied `${{ inputs.x }}` value rather than hardcoded. Used for branding on
11946
+ * platform-sent correspondence (e.g. invite emails) alongside `contextName` — not a
11947
+ * replacement for it. See {@link companyNameOf}.
11948
+ */
11949
+ companyName: z.string().min(1).optional(),
11882
11950
  schemas: z.array(BlueprintSchemaSchema).default([]),
11883
11951
  accessProfile: BlueprintAccessProfileSchema,
11884
11952
  servicePrincipal: BlueprintServicePrincipalSchema,
@@ -11927,6 +11995,7 @@ var BlueprintSchema = z.object({
11927
11995
  lintIdentityOverrides(bp, ctx);
11928
11996
  lintNamespacedScopeArrays(bp, ctx);
11929
11997
  lintDataScopeKeys(bp, ctx);
11998
+ lintExpectedScopeDims(bp, ctx);
11930
11999
  lintDataScopeRefs(bp, ctx);
11931
12000
  lintRoleAssumable(bp, ctx);
11932
12001
  lintAccessProfileRoleIds(bp, ctx);
@@ -14109,6 +14178,7 @@ var ENV_BASE_URLS = {
14109
14178
  // `--base-url` or VECTROS_API_BASE_URL; verify at smoke time.
14110
14179
  staging: "https://api.staging.vectros.ai"
14111
14180
  };
14181
+ var AUTH_FLAG_NAMES = ["--env", "--base-url", "--token", "--use-scoped-token"];
14112
14182
  function envToBaseUrl(env, override) {
14113
14183
  return validateBaseUrl(override ?? ENV_BASE_URLS[env]);
14114
14184
  }
@@ -14615,7 +14685,7 @@ ${summary}`
14615
14685
  this.rejected = rejected;
14616
14686
  }
14617
14687
  };
14618
- var GRANTED_CAPABILITIES_ALLOWLIST = ["member-lifecycle"];
14688
+ var GRANTED_CAPABILITIES_ALLOWLIST = ["member-lifecycle", "delegate-principal-stamp"];
14619
14689
  var GRANTED_CAPABILITIES_SET = new Set(GRANTED_CAPABILITIES_ALLOWLIST);
14620
14690
  var CAPABILITIES_HINT = GRANTED_CAPABILITIES_ALLOWLIST.join("/");
14621
14691
  var CapabilityGateError = class extends Error {
@@ -14892,7 +14962,8 @@ function makeIdentityResolver(client, log) {
14892
14962
  return id;
14893
14963
  };
14894
14964
  }
14895
- async function resolveApplyTimeIdentities(blueprint, client, log) {
14965
+ async function resolveApplyTimeIdentities(blueprint, provisioned, bootstrapClient, log) {
14966
+ const client = provisioned.ctxBootstrapClient ?? bootstrapClient;
14896
14967
  const resolver = makeIdentityResolver(client, log);
14897
14968
  const resolved = await resolveBlueprintIdentities(blueprint, resolver);
14898
14969
  return parseBlueprint(resolved);
@@ -15160,21 +15231,13 @@ async function runLoader(client, input) {
15160
15231
  machineId,
15161
15232
  rotate = false,
15162
15233
  skipSeed = false,
15163
- allowTenantWideNamespaces = false,
15234
+ seedIndexPoll,
15235
+ provisioned,
15164
15236
  readExistingKey,
15165
15237
  persistMintedKey,
15166
- confirmExistingPrincipal,
15167
15238
  log
15168
15239
  } = input;
15169
15240
  assertNoUnresolvedIdentities(blueprint);
15170
- const provisioned = input.provisioned ?? await provisionContext(client, {
15171
- blueprint,
15172
- allowTenantWideNamespaces,
15173
- remintBootstrapForContext: input.remintBootstrapForContext,
15174
- readExistingKey,
15175
- confirmExistingPrincipal,
15176
- log
15177
- });
15178
15241
  const { contextAction, servicePrincipalUserId: userId, userAction, namespaceActions } = provisioned;
15179
15242
  assertBlueprintGates(blueprint);
15180
15243
  const needsContextPinnedBootstrapForIssuers = (blueprint.issuers ?? []).length > 0;
@@ -15353,6 +15416,7 @@ async function runLoader(client, input) {
15353
15416
  const seededRecordIds = [];
15354
15417
  const seededDocumentIds = [];
15355
15418
  const seedIndexFailures = [];
15419
+ const pendingIndexSeeds = [];
15356
15420
  const noteIfIndexFailed = (created, rec, surface) => {
15357
15421
  if (created.indexStatus !== "FAILED") return;
15358
15422
  seedIndexFailures.push({
@@ -15387,6 +15451,9 @@ async function runLoader(client, input) {
15387
15451
  }
15388
15452
  });
15389
15453
  if (created.id) seededDocumentIds.push(created.id);
15454
+ if (created.id && created.indexStatus === "PENDING_INDEX") {
15455
+ pendingIndexSeeds.push({ id: created.id, externalId: rec.externalId, typeName: rec.typeName, surface: "document" });
15456
+ }
15390
15457
  noteIfIndexFailed(created, rec, "document");
15391
15458
  } else {
15392
15459
  const created = await ctxClient.records.createRecord({
@@ -15399,10 +15466,30 @@ async function runLoader(client, input) {
15399
15466
  }
15400
15467
  });
15401
15468
  if (created.id) seededRecordIds.push(created.id);
15469
+ if (created.id && created.indexStatus === "PENDING_INDEX") {
15470
+ pendingIndexSeeds.push({ id: created.id, externalId: rec.externalId, typeName: rec.typeName, surface: "record" });
15471
+ }
15402
15472
  noteIfIndexFailed(created, rec, "record");
15403
15473
  }
15404
15474
  seededCount++;
15405
15475
  }
15476
+ const pollAttempts = seedIndexPoll?.attempts ?? 3;
15477
+ const pollDelayMs = seedIndexPoll?.delayMs ?? 500;
15478
+ if (pollAttempts > 0 && pendingIndexSeeds.length > 0) {
15479
+ await Promise.all(
15480
+ pendingIndexSeeds.map(async (seed) => {
15481
+ for (let attempt = 0; attempt < pollAttempts; attempt++) {
15482
+ await new Promise((r) => setTimeout(r, pollDelayMs));
15483
+ const readBack = seed.surface === "document" ? await ctxClient.documents.getDocument({ id: seed.id }) : await ctxClient.records.getRecord({ id: seed.id });
15484
+ if (readBack.indexStatus === "FAILED") {
15485
+ noteIfIndexFailed(readBack, seed, seed.surface);
15486
+ return;
15487
+ }
15488
+ if (readBack.indexStatus !== "PENDING_INDEX") return;
15489
+ }
15490
+ })
15491
+ );
15492
+ }
15406
15493
  return {
15407
15494
  contextId: blueprint.contextId,
15408
15495
  servicePrincipalUserId: userId,
@@ -15582,6 +15669,11 @@ function buildClaudeCodeAddCommand(entry, serverKey = "vectros") {
15582
15669
  const envFlags = Object.entries(entry.env ?? {}).map(([k, v]) => `-e ${k}=${v}`);
15583
15670
  return ["claude", "mcp", "add", serverKey, ...envFlags, "--", entry.command, ...entry.args].join(" ");
15584
15671
  }
15672
+ function buildRestartHint(blueprintName, isCode) {
15673
+ const target = isCode ? "Reopen Claude Code in this project" : "Restart your MCP client";
15674
+ return ` ${target}, then ask Claude about your '${blueprintName}' data.
15675
+ `;
15676
+ }
15585
15677
  function mergeServerEntry(existing, entry, serverKey = "vectros") {
15586
15678
  const base = existing ? { ...existing } : {};
15587
15679
  const servers = { ...base.mcpServers ?? {} };
@@ -15921,11 +16013,19 @@ var CryptoFileSecretStore = class {
15921
16013
  * sub-millisecond in practice; the budget is generous for a loaded machine)
15922
16014
  * before deciding it is a crash remnant. A SHORT-but-nonempty winner is never
15923
16015
  * a write in flight — the 16 salt bytes land in one write — so it is refused
15924
- * immediately, exactly like the top-of-function read's twin check.
16016
+ * immediately, exactly like the top-of-function read's twin check. A winner
16017
+ * that goes MISSING mid-poll (ENOENT) restarts creation from the top rather
16018
+ * than surfacing the raw ENOENT — see the catch below.
15925
16019
  */
15926
16020
  async adoptSalt(saltPath) {
15927
16021
  for (let attempt = 0; ; attempt++) {
15928
- const winner = await fs3.readFile(saltPath);
16022
+ let winner;
16023
+ try {
16024
+ winner = await fs3.readFile(saltPath);
16025
+ } catch (err) {
16026
+ if (err.code === "ENOENT") return this.readOrCreateSalt();
16027
+ throw err;
16028
+ }
15929
16029
  if (winner.length >= 16) return winner;
15930
16030
  if (winner.length > 0) throw corruptSaltError(saltPath, winner.length);
15931
16031
  if (attempt >= 24) break;
@@ -17764,7 +17864,7 @@ function applyOutcomeSummary(result) {
17764
17864
  const parts = [
17765
17865
  reconcileTally(result.schemaActions, "schema"),
17766
17866
  reconcileTally(result.roleActions, "role"),
17767
- // #1023 — the platform's PUT /v1/auth/issuers/{issuerId} makes an issuer's
17867
+ // The platform's issuer partial-update endpoint makes an issuer's
17768
17868
  // 'reconciled' mean the same "updated to match" as every other kind here.
17769
17869
  reconcileTally(result.issuerActions, "issuer"),
17770
17870
  namespaceTally(result.namespaceActions),
@@ -18155,11 +18255,7 @@ ${renderPlan(blueprint, planSteps)}
18155
18255
  confirmExistingPrincipal,
18156
18256
  log
18157
18257
  });
18158
- const resolvedBlueprint = await resolveIdentities(
18159
- blueprint,
18160
- provisioned.ctxBootstrapClient ?? client,
18161
- log
18162
- );
18258
+ const resolvedBlueprint = await resolveIdentities(blueprint, provisioned, client, log);
18163
18259
  const machineId = await resolveMachineId();
18164
18260
  const result = await loader(client, {
18165
18261
  blueprint: resolvedBlueprint,
@@ -18167,7 +18263,6 @@ ${renderPlan(blueprint, planSteps)}
18167
18263
  machineId,
18168
18264
  rotate: args.rotate,
18169
18265
  skipSeed: noSeed,
18170
- allowTenantWideNamespaces: args.allowTenantWideNamespaces,
18171
18266
  readExistingKey,
18172
18267
  persistMintedKey,
18173
18268
  provisioned,
@@ -18178,7 +18273,6 @@ ${renderPlan(blueprint, planSteps)}
18178
18273
  tenant: args.tenant,
18179
18274
  mintContext
18180
18275
  }),
18181
- confirmExistingPrincipal,
18182
18276
  log
18183
18277
  });
18184
18278
  process.stdout.write(
@@ -18234,10 +18328,13 @@ ${codeAddHint}`);
18234
18328
  }
18235
18329
  try {
18236
18330
  const merged = await safeMergeConfigFile({ configPath, entry });
18237
- 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';
18331
+ const restartHint = buildRestartHint(blueprint.name, isCode);
18238
18332
  process.stdout.write(
18239
18333
  ` \u2714 Config ${merged.action}${merged.backupPath ? ` (backup: ${merged.backupPath})` : ""}
18240
- ` + restartHint
18334
+ ` + restartHint + // The `claude mcp add` line is otherwise only shown on --no-merge / a
18335
+ // declined / a failed merge — surface it here too so a successful
18336
+ // auto-merge doesn't hide the idiomatic install command entirely.
18337
+ codeAddHint
18241
18338
  );
18242
18339
  } catch (err) {
18243
18340
  process.stderr.write(` \u26A0 Could not merge config (${err instanceof Error ? err.message : String(err)}).
@@ -18421,8 +18518,12 @@ function compositeLegFindings(schema, lf, fieldsById, rangeEnabledFieldNames) {
18421
18518
  function lintBlueprint(bp) {
18422
18519
  const findings = [];
18423
18520
  const typeNames = /* @__PURE__ */ new Set();
18521
+ const expectedDimsByTypeName = /* @__PURE__ */ new Map();
18424
18522
  for (const schema of bp.schemas) {
18425
18523
  typeNames.add(schema.typeName);
18524
+ if (schema.expectedScopeDims && schema.expectedScopeDims.length > 0) {
18525
+ expectedDimsByTypeName.set(schema.typeName, schema.expectedScopeDims);
18526
+ }
18426
18527
  const fieldIds = /* @__PURE__ */ new Set();
18427
18528
  const fieldsById = /* @__PURE__ */ new Map();
18428
18529
  for (const f of schema.fields) {
@@ -18503,6 +18604,9 @@ function lintBlueprint(bp) {
18503
18604
  if (bp.accessProfile.allowedActions !== void 0) {
18504
18605
  const reached = ownerScopedReachWithoutDataScope(bp.accessProfile.allowedActions, ds);
18505
18606
  if (reached.length > 0) findings.push(unconstrainedReachFinding("accessProfile", reached));
18607
+ findings.push(
18608
+ ...partialCoverageFindings("accessProfile", bp.accessProfile.allowedActions, ds, expectedDimsByTypeName)
18609
+ );
18506
18610
  }
18507
18611
  for (const roleId of Object.keys(bp.roles ?? {})) {
18508
18612
  if (!ROLE_ID_RE.test(roleId)) {
@@ -18520,6 +18624,9 @@ function lintBlueprint(bp) {
18520
18624
  if (reached.length > 0) {
18521
18625
  findings.push(unconstrainedReachFinding(`role '${roleId}' clause ${i}`, reached));
18522
18626
  }
18627
+ findings.push(
18628
+ ...partialCoverageFindings(`role '${roleId}' clause ${i}`, clause.allowedActions, cds, expectedDimsByTypeName)
18629
+ );
18523
18630
  if (!cds) return;
18524
18631
  for (const [key, values] of Object.entries(cds)) {
18525
18632
  for (const value of values) {
@@ -18596,6 +18703,32 @@ function unconstrainedReachFinding(where, resources) {
18596
18703
  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.`
18597
18704
  };
18598
18705
  }
18706
+ function dimToDataScopeKey(dim) {
18707
+ return dim === "userId" ? "userId" : `scope:${dim}`;
18708
+ }
18709
+ function partialCoverageFindings(where, allowedActions, dataScope, expectedDimsByTypeName) {
18710
+ const findings = [];
18711
+ const dataScopeKeys = new Set(Object.keys(dataScope ?? {}));
18712
+ for (const action of allowedActions) {
18713
+ const parts = action.split(":");
18714
+ const [resource, ops] = parts;
18715
+ if (resource !== "records" && resource !== "documents" || !ops) continue;
18716
+ const qualifier = parts.length > 2 ? parts.slice(2).join(":") : void 0;
18717
+ if (qualifier === void 0) continue;
18718
+ const dims = expectedDimsByTypeName.get(qualifier);
18719
+ if (!dims || dims.length === 0) continue;
18720
+ if (!(ops === "*" || [...ops].some((op) => op === "r" || op === "u" || op === "d"))) continue;
18721
+ const covered = dims.filter((d) => dataScopeKeys.has(dimToDataScopeKey(d)));
18722
+ if (covered.length === 0 || covered.length === dims.length) continue;
18723
+ const missing = dims.filter((d) => !covered.includes(d));
18724
+ findings.push({
18725
+ severity: "warning",
18726
+ rule: "partial-datascope-dimension-coverage",
18727
+ 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).`
18728
+ });
18729
+ }
18730
+ return findings;
18731
+ }
18599
18732
  function lintPasses(findings) {
18600
18733
  return !findings.some((f) => f.severity === "error");
18601
18734
  }
@@ -18687,6 +18820,10 @@ function buildScaffold(name, from) {
18687
18820
  }
18688
18821
 
18689
18822
  // src/cli-blueprint.ts
18823
+ function noEffectOnBlueprint(flag, sub) {
18824
+ const cmd = sub ? `blueprint ${sub}` : "blueprint";
18825
+ 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'?`;
18826
+ }
18690
18827
  function parseBlueprintArgs(argv) {
18691
18828
  const args = { help: false, force: false, set: [] };
18692
18829
  const positionals = [];
@@ -18713,6 +18850,9 @@ function parseBlueprintArgs(argv) {
18713
18850
  args.values = argv[++i];
18714
18851
  break;
18715
18852
  default:
18853
+ if (AUTH_FLAG_NAMES.includes(a)) {
18854
+ throw new Error(noEffectOnBlueprint(a, positionals[0]));
18855
+ }
18716
18856
  if (a.startsWith("-")) throw new Error(`Unknown blueprint flag: ${a}`);
18717
18857
  positionals.push(a);
18718
18858
  }
@@ -19505,6 +19645,51 @@ ${list}
19505
19645
  });
19506
19646
  }
19507
19647
 
19648
+ // src/dev-api.ts
19649
+ var DeveloperApiError = class extends Error {
19650
+ statusCode;
19651
+ constructor(message, statusCode) {
19652
+ super(message);
19653
+ this.name = "DeveloperApiError";
19654
+ this.statusCode = statusCode;
19655
+ }
19656
+ };
19657
+ async function fetchDeveloperUsersPage(opts) {
19658
+ const { cognitoToken, apiBaseUrl, tenant, startFrom, limit, fetchImpl = fetch } = opts;
19659
+ const base = apiBaseUrl.replace(/\/$/, "");
19660
+ let url = `${base}/developer/users?tenant=${encodeURIComponent(tenant)}`;
19661
+ if (startFrom) url += `&startFrom=${encodeURIComponent(startFrom)}`;
19662
+ if (limit !== void 0) url += `&limit=${encodeURIComponent(String(limit))}`;
19663
+ let res;
19664
+ try {
19665
+ res = await fetchImpl(url, {
19666
+ method: "GET",
19667
+ headers: { Authorization: `Bearer ${cognitoToken}` }
19668
+ });
19669
+ } catch (err) {
19670
+ throw new DeveloperApiError(
19671
+ `Request to /developer/users failed: ${err instanceof Error ? err.message : String(err)}`
19672
+ );
19673
+ }
19674
+ if (!res.ok) {
19675
+ const body = await res.text().catch(() => "");
19676
+ const hint = res.status === 403 ? " (this command requires an account owner login \u2014 sign in as the owner and retry)" : "";
19677
+ throw new DeveloperApiError(
19678
+ `GET /developer/users failed: HTTP ${res.status}${hint}${body ? ` \u2014 ${body}` : ""}`,
19679
+ res.status
19680
+ );
19681
+ }
19682
+ let parsed;
19683
+ try {
19684
+ parsed = await res.json();
19685
+ } catch {
19686
+ throw new DeveloperApiError("GET /developer/users returned a non-JSON body");
19687
+ }
19688
+ const data = Array.isArray(parsed.data) ? parsed.data : [];
19689
+ const nextCursor = typeof parsed.nextCursor === "string" ? parsed.nextCursor : null;
19690
+ return { data, nextCursor };
19691
+ }
19692
+
19508
19693
  // src/commands/identity.ts
19509
19694
  function isUserDimension(type) {
19510
19695
  return type === "user";
@@ -19659,6 +19844,57 @@ async function runIdentityList(opts, deps = {}) {
19659
19844
  }
19660
19845
  });
19661
19846
  }
19847
+ function toIdentityView(row) {
19848
+ const str = (v) => typeof v === "string" ? v : void 0;
19849
+ return {
19850
+ id: str(row.id),
19851
+ externalId: str(row.externalId),
19852
+ email: str(row.email),
19853
+ status: str(row.status),
19854
+ type: str(row.type),
19855
+ createdAt: str(row.createdAt)
19856
+ };
19857
+ }
19858
+ async function runIdentityListAll(opts, deps = {}) {
19859
+ const fetchImpl = deps.fetchImpl ?? fetch;
19860
+ return withAuth({ ...opts, provisioning: true }, deps, async (auth) => {
19861
+ try {
19862
+ const tenant = opts.tenant ?? "live";
19863
+ const fetchPage = async (startFrom) => {
19864
+ const page = await fetchDeveloperUsersPage({
19865
+ cognitoToken: auth.cognitoToken,
19866
+ apiBaseUrl: auth.baseUrl,
19867
+ tenant,
19868
+ startFrom,
19869
+ limit: opts.limit,
19870
+ fetchImpl
19871
+ });
19872
+ return { data: page.data.map(toIdentityView), nextCursor: page.nextCursor };
19873
+ };
19874
+ let rows = opts.limit !== void 0 ? (await fetchPage()).data ?? [] : await drainPages(fetchPage);
19875
+ if (opts.externalId) rows = rows.filter((r) => r.externalId === opts.externalId);
19876
+ if (opts.json) {
19877
+ process.stdout.write(`${formatJson(rows)}
19878
+ `);
19879
+ return 0;
19880
+ }
19881
+ process.stdout.write(
19882
+ "\n" + formatTable(rows, [
19883
+ { header: "id", get: (r) => r.id },
19884
+ { header: "external id", get: (r) => r.externalId },
19885
+ { header: "email", get: (r) => r.email },
19886
+ { header: "kind", get: (r) => r.type },
19887
+ { header: "status", get: (r) => r.status }
19888
+ ]) + "\n"
19889
+ );
19890
+ return 0;
19891
+ } catch (err) {
19892
+ process.stderr.write(` \u2716 identity list-all failed: ${formatError(err)}
19893
+ `);
19894
+ return 1;
19895
+ }
19896
+ });
19897
+ }
19662
19898
  async function runIdentityGet(opts, deps = {}) {
19663
19899
  return withAuth(opts, deps, async (auth) => {
19664
19900
  const client = auth.client;
@@ -20262,6 +20498,16 @@ async function runRoleCreate(opts, deps = {}) {
20262
20498
  dataScope = parseJsonObjectFlag("--data-scope", opts.dataScope);
20263
20499
  } catch (err) {
20264
20500
  process.stderr.write(` \u2716 ${formatError(err)}
20501
+ `);
20502
+ return 2;
20503
+ }
20504
+ }
20505
+ let assumable;
20506
+ if (opts.assumable !== void 0) {
20507
+ try {
20508
+ assumable = parseJsonObjectFlag("--assumable", opts.assumable);
20509
+ } catch (err) {
20510
+ process.stderr.write(` \u2716 ${formatError(err)}
20265
20511
  `);
20266
20512
  return 2;
20267
20513
  }
@@ -20279,7 +20525,12 @@ async function runRoleCreate(opts, deps = {}) {
20279
20525
  // stays byte-identical for unscoped roles.
20280
20526
  scopes: [
20281
20527
  dataScope ? { allowed_actions: actions, data_scope: dataScope } : { allowed_actions: actions }
20282
- ]
20528
+ ],
20529
+ // Included only when supplied — omitting it (rather than sending an
20530
+ // empty object) keeps "no assume grant" and "explicitly empty grant"
20531
+ // indistinguishable on the wire, matching the platform's own
20532
+ // "omit for the safe default" contract.
20533
+ assumable
20283
20534
  }
20284
20535
  });
20285
20536
  if (opts.json) {
@@ -20519,6 +20770,19 @@ ${renderContext(ctx)}
20519
20770
  }
20520
20771
 
20521
20772
  // src/commands/issuers.ts
20773
+ import * as readline4 from "readline/promises";
20774
+ function parseSelfSignupPolicies(raw) {
20775
+ if (!raw || raw.length === 0) return void 0;
20776
+ return raw.map((entry) => {
20777
+ const sep = entry.indexOf(":");
20778
+ if (sep <= 0 || sep === entry.length - 1) {
20779
+ throw new Error(
20780
+ `--self-signup-policy must be '<signup_type>:<roleId>' (e.g. 'default:member'); got '${entry}'.`
20781
+ );
20782
+ }
20783
+ return { signup_type: entry.slice(0, sep), role_id: entry.slice(sep + 1) };
20784
+ });
20785
+ }
20522
20786
  function renderSelfSignup(policies) {
20523
20787
  if (!policies || policies.length === 0) return "\u2014";
20524
20788
  return policies.map((p) => `${p.signup_type} \u2192 ${p.role_id}`).join(", ");
@@ -20586,6 +20850,308 @@ ${renderIssuer(issuer)}
20586
20850
  }
20587
20851
  });
20588
20852
  }
20853
+ async function runIssuersRegister(opts, deps = {}) {
20854
+ let selfSignupPolicies;
20855
+ try {
20856
+ selfSignupPolicies = parseSelfSignupPolicies(opts.selfSignupPolicy);
20857
+ } catch (err) {
20858
+ process.stderr.write(` \u2716 ${formatError(err)}
20859
+ `);
20860
+ return 2;
20861
+ }
20862
+ return withAuth({ ...opts, provisioning: true, context: opts.context }, deps, async (auth) => {
20863
+ const client = auth.client;
20864
+ try {
20865
+ const issuer = await client.auth.registerIssuer({
20866
+ issuerId: opts.issuerId,
20867
+ issuer: opts.issuer,
20868
+ jwksUri: opts.jwksUri,
20869
+ audience: opts.audience,
20870
+ contextId: opts.context,
20871
+ subClaim: opts.subClaim,
20872
+ emailClaim: opts.emailClaim,
20873
+ userinfoUri: opts.userinfoUri,
20874
+ selfSignupPolicies
20875
+ });
20876
+ if (opts.json) {
20877
+ process.stdout.write(`${formatJson(issuer)}
20878
+ `);
20879
+ return 0;
20880
+ }
20881
+ const verb = issuer.created === false ? "already registered \u2014 returned unchanged" : "registered";
20882
+ process.stdout.write(`
20883
+ \u2714 issuer '${opts.issuerId}' ${verb}
20884
+ ${renderIssuer(issuer)}
20885
+ `);
20886
+ return 0;
20887
+ } catch (err) {
20888
+ process.stderr.write(formatCliError(err, { action: "issuers register failed" }));
20889
+ return 1;
20890
+ }
20891
+ });
20892
+ }
20893
+ async function runIssuersUpdate(opts, deps = {}) {
20894
+ let selfSignupPolicies;
20895
+ try {
20896
+ selfSignupPolicies = parseSelfSignupPolicies(opts.selfSignupPolicy);
20897
+ } catch (err) {
20898
+ process.stderr.write(` \u2716 ${formatError(err)}
20899
+ `);
20900
+ return 2;
20901
+ }
20902
+ if (opts.clearSelfSignupPolicies && selfSignupPolicies && selfSignupPolicies.length > 0) {
20903
+ process.stderr.write(
20904
+ " \u2716 --clear-self-signup-policies and --self-signup-policy are mutually exclusive.\n"
20905
+ );
20906
+ return 2;
20907
+ }
20908
+ return withAuth({ ...opts, provisioning: true, context: opts.context }, deps, async (auth) => {
20909
+ const client = auth.client;
20910
+ try {
20911
+ const issuer = await client.auth.updateIssuer({
20912
+ issuerId: opts.issuerId,
20913
+ subClaim: opts.subClaim,
20914
+ emailClaim: opts.emailClaim,
20915
+ userinfoUri: opts.userinfoUri,
20916
+ status: opts.status,
20917
+ selfSignupPolicies: opts.clearSelfSignupPolicies ? [] : selfSignupPolicies
20918
+ });
20919
+ if (opts.json) {
20920
+ process.stdout.write(`${formatJson(issuer)}
20921
+ `);
20922
+ return 0;
20923
+ }
20924
+ process.stdout.write(`
20925
+ \u2714 issuer '${opts.issuerId}' updated
20926
+ ${renderIssuer(issuer)}
20927
+ `);
20928
+ return 0;
20929
+ } catch (err) {
20930
+ process.stderr.write(formatCliError(err, { action: "issuers update failed" }));
20931
+ return 1;
20932
+ }
20933
+ });
20934
+ }
20935
+ async function runIssuersDelete(opts, deps = {}) {
20936
+ const { issuerId, context } = opts;
20937
+ if (!opts.force) {
20938
+ const isTty = deps.isTty ?? Boolean(process.stdin.isTTY);
20939
+ if (!isTty) {
20940
+ process.stderr.write(
20941
+ ` \u2716 issuers delete is destructive and needs an interactive confirmation.
20942
+ Re-run with --force to delete '${issuerId}' without a prompt.
20943
+ `
20944
+ );
20945
+ return 2;
20946
+ }
20947
+ const promptCtx = deps.createPrompt ? deps.createPrompt() : (() => {
20948
+ const rl = readline4.createInterface({ input: process.stdin, output: process.stdout });
20949
+ return { prompt: (q) => rl.question(q), close: () => rl.close() };
20950
+ })();
20951
+ try {
20952
+ process.stdout.write(
20953
+ `
20954
+ \u26A0 Deregistering issuer '${issuerId}' from context '${context}' removes it as a token-exchange
20955
+ trust anchor. The platform refuses this while any user was ever created or matched
20956
+ through it \u2014 deactivate those users or register a replacement issuer first if you
20957
+ intend to cut off access.
20958
+
20959
+ `
20960
+ );
20961
+ const typed = await promptCtx.prompt(` Type the issuer id ('${issuerId}') to confirm: `);
20962
+ if (typed.trim() !== issuerId) {
20963
+ process.stderr.write(` \u2716 Confirmation did not match \u2014 nothing deleted.
20964
+ `);
20965
+ return 1;
20966
+ }
20967
+ } finally {
20968
+ promptCtx.close();
20969
+ }
20970
+ }
20971
+ return withAuth({ ...opts, provisioning: true, context }, deps, async (auth) => {
20972
+ const client = auth.client;
20973
+ try {
20974
+ await client.auth.deleteIssuer({ issuerId });
20975
+ if (opts.json) {
20976
+ process.stdout.write(`${formatJson({ issuerId, deleted: true })}
20977
+ `);
20978
+ return 0;
20979
+ }
20980
+ process.stdout.write(`
20981
+ \u2714 issuer '${issuerId}' deregistered from '${context}'
20982
+ `);
20983
+ return 0;
20984
+ } catch (err) {
20985
+ process.stderr.write(formatCliError(err, { action: "issuers delete failed" }));
20986
+ return 1;
20987
+ }
20988
+ });
20989
+ }
20990
+
20991
+ // src/commands/namespace.ts
20992
+ function renderNamespace(n) {
20993
+ return formatKeyValues([
20994
+ ["namespace", n.namespace],
20995
+ ["contextId", n.contextId ?? "(tenant-wide)"],
20996
+ ["entityBacked", n.entityBacked === void 0 ? void 0 : String(n.entityBacked)],
20997
+ ["reserved", n.reserved === void 0 ? void 0 : String(n.reserved)],
20998
+ ["specificityRank", n.specificityRank === void 0 ? void 0 : String(n.specificityRank)],
20999
+ ["defaultSchemaId", n.defaultSchemaId],
21000
+ ["createdAt", n.createdAt]
21001
+ ]);
21002
+ }
21003
+ async function runNamespaceList(opts, deps = {}) {
21004
+ return withAuth({ ...opts, context: opts.context }, deps, async (auth) => {
21005
+ const client = auth.client;
21006
+ try {
21007
+ const rows = await drainPages(
21008
+ (startFrom) => client.identity.listNamespaces({
21009
+ limit: PAGE_LIMIT,
21010
+ ...opts.context ? { contextId: opts.context } : {},
21011
+ ...startFrom ? { startFrom } : {}
21012
+ })
21013
+ );
21014
+ if (opts.json) {
21015
+ process.stdout.write(`${formatJson(rows)}
21016
+ `);
21017
+ return 0;
21018
+ }
21019
+ process.stdout.write(
21020
+ "\n" + formatTable(rows, [
21021
+ { header: "namespace", get: (n) => n.namespace },
21022
+ { header: "contextId", get: (n) => n.contextId ?? "(tenant-wide)" },
21023
+ {
21024
+ header: "entityBacked",
21025
+ get: (n) => n.entityBacked === void 0 ? void 0 : String(n.entityBacked)
21026
+ },
21027
+ {
21028
+ header: "specificityRank",
21029
+ get: (n) => n.specificityRank === void 0 ? void 0 : String(n.specificityRank)
21030
+ }
21031
+ ]) + "\n"
21032
+ );
21033
+ return 0;
21034
+ } catch (err) {
21035
+ process.stderr.write(formatCliError(err, { action: "namespace list failed" }));
21036
+ return 1;
21037
+ }
21038
+ });
21039
+ }
21040
+ async function runNamespaceGet(opts, deps = {}) {
21041
+ return withAuth({ ...opts, context: opts.context }, deps, async (auth) => {
21042
+ const client = auth.client;
21043
+ try {
21044
+ const ns = await client.identity.getNamespace({
21045
+ namespace: opts.namespace,
21046
+ ...opts.context ? { contextId: opts.context } : {}
21047
+ });
21048
+ if (opts.json) {
21049
+ process.stdout.write(`${formatJson(ns)}
21050
+ `);
21051
+ return 0;
21052
+ }
21053
+ process.stdout.write(`
21054
+ ${renderNamespace(ns)}
21055
+ `);
21056
+ return 0;
21057
+ } catch (err) {
21058
+ process.stderr.write(formatCliError(err, { action: "namespace get failed" }));
21059
+ return 1;
21060
+ }
21061
+ });
21062
+ }
21063
+ async function runNamespaceRegister(opts, deps = {}) {
21064
+ return withAuth({ ...opts, provisioning: true, context: opts.context }, deps, async (auth) => {
21065
+ const client = auth.client;
21066
+ try {
21067
+ const ns = await client.identity.registerNamespace({
21068
+ contextId: opts.context,
21069
+ body: {
21070
+ namespace: opts.namespace,
21071
+ specificityRank: opts.specificityRank,
21072
+ entityBacked: opts.entityBacked,
21073
+ defaultSchemaId: opts.defaultSchemaId
21074
+ }
21075
+ });
21076
+ if (opts.json) {
21077
+ process.stdout.write(`${formatJson(ns)}
21078
+ `);
21079
+ return 0;
21080
+ }
21081
+ process.stdout.write(`
21082
+ \u2714 namespace '${opts.namespace}' registered
21083
+ ${renderNamespace(ns)}
21084
+ `);
21085
+ return 0;
21086
+ } catch (err) {
21087
+ process.stderr.write(formatCliError(err, { action: "namespace register failed" }));
21088
+ return 1;
21089
+ }
21090
+ });
21091
+ }
21092
+
21093
+ // src/commands/assume.ts
21094
+ function parseAssumeScopes(raw) {
21095
+ const out = {};
21096
+ for (const entry of raw) {
21097
+ const sep = entry.indexOf(":");
21098
+ if (sep <= 0 || sep === entry.length - 1) {
21099
+ throw new Error(`--scope must be '<namespace>:<value>' (e.g. 'org:orgB'); got '${entry}'.`);
21100
+ }
21101
+ const ns = entry.slice(0, sep);
21102
+ const value = entry.slice(sep + 1);
21103
+ const valueError = valueGrammarError(value);
21104
+ if (valueError) throw new Error(`--scope value ${valueError}`);
21105
+ const key = `scope:${ns}`;
21106
+ if (Object.prototype.hasOwnProperty.call(out, key)) {
21107
+ throw new Error(`--scope names namespace '${ns}' more than once \u2014 pass one value per namespace.`);
21108
+ }
21109
+ out[key] = value;
21110
+ }
21111
+ return out;
21112
+ }
21113
+ function renderAssumeToken(t) {
21114
+ return formatKeyValues([
21115
+ ["access_token", t.access_token],
21116
+ ["token_type", t.token_type],
21117
+ ["expires_in", t.expires_in === void 0 ? void 0 : `${t.expires_in}s`]
21118
+ ]);
21119
+ }
21120
+ async function runAssume(opts, deps = {}) {
21121
+ if (!opts.scope || opts.scope.length === 0) {
21122
+ process.stderr.write(" \u2716 --scope must be given at least once (e.g. --scope org:orgB).\n");
21123
+ return 2;
21124
+ }
21125
+ let scopes;
21126
+ try {
21127
+ scopes = parseAssumeScopes(opts.scope);
21128
+ } catch (err) {
21129
+ process.stderr.write(` \u2716 ${formatError(err)}
21130
+ `);
21131
+ return 2;
21132
+ }
21133
+ return withAuth({ ...opts, context: opts.context }, deps, async (auth) => {
21134
+ const client = auth.client;
21135
+ try {
21136
+ const result = await client.auth.assumeToken(scopes);
21137
+ if (opts.json) {
21138
+ process.stdout.write(`${formatJson(result)}
21139
+ `);
21140
+ return 0;
21141
+ }
21142
+ process.stdout.write(
21143
+ `
21144
+ \u2714 assumed ${Object.keys(scopes).join(", ")}
21145
+ ${renderAssumeToken(result)}
21146
+ `
21147
+ );
21148
+ return 0;
21149
+ } catch (err) {
21150
+ process.stderr.write(formatCliError(err, { action: "assume failed" }));
21151
+ return 1;
21152
+ }
21153
+ });
21154
+ }
20589
21155
 
20590
21156
  // src/commands/login.ts
20591
21157
  import { createServer } from "http";
@@ -21050,7 +21616,8 @@ async function runBlueprintHarness(client, input, deps) {
21050
21616
  });
21051
21617
  const blueprint = await resolveApplyTimeIdentities(
21052
21618
  input.blueprint,
21053
- provisioned.ctxBootstrapClient ?? client,
21619
+ provisioned,
21620
+ client,
21054
21621
  log
21055
21622
  );
21056
21623
  const remint = remintForContext ?? (async () => client);
@@ -21463,9 +22030,9 @@ function emitJoined(opts) {
21463
22030
 
21464
22031
  // src/build-info.ts
21465
22032
  var BUILD_INFO = {
21466
- cli: true ? "0.19.0" : "dev",
21467
- sdk: true ? "0.41.0" : "dev",
21468
- blueprints: true ? "0.16.0" : "dev"
22033
+ cli: true ? "0.20.0" : "dev",
22034
+ sdk: true ? "0.42.0" : "dev",
22035
+ blueprints: true ? "0.17.0" : "dev"
21469
22036
  };
21470
22037
  function formatBuildInfo() {
21471
22038
  return `vectros ${BUILD_INFO.cli} (sdk ${BUILD_INFO.sdk}, blueprints ${BUILD_INFO.blueprints})`;
@@ -21546,10 +22113,22 @@ function buildProgram(deps = {}) {
21546
22113
  registerRoleCommands(program, deps);
21547
22114
  registerContextCommands(program, deps);
21548
22115
  registerIssuersCommands(program, deps);
22116
+ registerNamespaceCommands(program, deps);
22117
+ registerAssumeCommand(program, deps);
21549
22118
  registerJoinCommand(program, deps);
21550
22119
  registerKeyringCommands(program);
21551
22120
  return program;
21552
22121
  }
22122
+ function registerAssumeCommand(program, deps) {
22123
+ 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, []);
22124
+ addCommonAuthOptions(assume).action(
22125
+ async (flags) => {
22126
+ process.exit(
22127
+ await runAssume({ ...toCommonOptions(flags), context: flags.context, scope: flags.scope ?? [] }, deps)
22128
+ );
22129
+ }
22130
+ );
22131
+ }
21553
22132
  function registerJoinCommand(program, deps) {
21554
22133
  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(
21555
22134
  "--activate",
@@ -21686,7 +22265,7 @@ function registerContextCommands(program, deps) {
21686
22265
  );
21687
22266
  }
21688
22267
  function registerIssuersCommands(program, deps) {
21689
- const issuers = program.command("issuers").description("Inspect registered third-party IdP issuers (token-exchange trust anchors).");
22268
+ const issuers = program.command("issuers").description("Manage registered third-party IdP issuers (token-exchange trust anchors).");
21690
22269
  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)");
21691
22270
  addCommonAuthOptions(list).action(
21692
22271
  async (flags) => {
@@ -21705,12 +22284,125 @@ function registerIssuersCommands(program, deps) {
21705
22284
  );
21706
22285
  }
21707
22286
  );
22287
+ 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(
22288
+ "--self-signup-policy <type:roleId>",
22289
+ "opt-in self-service signup entry, repeatable (e.g. default:member) \u2014 no entry may target an elevated role",
22290
+ collect,
22291
+ []
22292
+ ).option("--tenant <tenant>", "tenant the context lives in: 'test' or 'live' (default live)");
22293
+ addCommonAuthOptions(register).action(
22294
+ async (flags) => {
22295
+ const tenant = parseTenantFlag(flags.tenant);
22296
+ process.exit(
22297
+ await runIssuersRegister(
22298
+ {
22299
+ ...toCommonOptions(flags),
22300
+ context: flags.context,
22301
+ issuerId: flags.issuerId,
22302
+ issuer: flags.issuer,
22303
+ jwksUri: flags.jwksUri,
22304
+ audience: flags.audience,
22305
+ subClaim: flags.subClaim,
22306
+ emailClaim: flags.emailClaim,
22307
+ userinfoUri: flags.userinfoUri,
22308
+ selfSignupPolicy: flags.selfSignupPolicy,
22309
+ tenant
22310
+ },
22311
+ deps
22312
+ )
22313
+ );
22314
+ }
22315
+ );
22316
+ 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(
22317
+ "--self-signup-policy <type:roleId>",
22318
+ "replace self-signup policies, repeatable (e.g. default:member) \u2014 omit entirely to leave unchanged",
22319
+ collect,
22320
+ []
22321
+ ).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)");
22322
+ addCommonAuthOptions(update).action(
22323
+ async (issuerId, flags) => {
22324
+ const tenant = parseTenantFlag(flags.tenant);
22325
+ process.exit(
22326
+ await runIssuersUpdate(
22327
+ {
22328
+ ...toCommonOptions(flags),
22329
+ context: flags.context,
22330
+ issuerId,
22331
+ subClaim: flags.subClaim,
22332
+ emailClaim: flags.emailClaim,
22333
+ userinfoUri: flags.userinfoUri,
22334
+ status: flags.status,
22335
+ selfSignupPolicy: flags.selfSignupPolicy,
22336
+ clearSelfSignupPolicies: Boolean(flags.clearSelfSignupPolicies),
22337
+ tenant
22338
+ },
22339
+ deps
22340
+ )
22341
+ );
22342
+ }
22343
+ );
22344
+ 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)");
22345
+ addCommonAuthOptions(del).action(
22346
+ async (issuerId, flags) => {
22347
+ const tenant = parseTenantFlag(flags.tenant);
22348
+ process.exit(
22349
+ await runIssuersDelete(
22350
+ { ...toCommonOptions(flags), context: flags.context, issuerId, force: Boolean(flags.force), tenant },
22351
+ deps
22352
+ )
22353
+ );
22354
+ }
22355
+ );
22356
+ }
22357
+ function registerNamespaceCommands(program, deps) {
22358
+ const namespace = program.command("namespace").description("Manage scope-namespace registrations (whether scope:<namespace> values resolve to identity entities).");
22359
+ 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");
22360
+ addCommonAuthOptions(register).action(
22361
+ async (name, flags) => {
22362
+ const specificityRank = Number(flags.specificityRank);
22363
+ if (!Number.isInteger(specificityRank) || specificityRank < 0 || specificityRank > 1e6) {
22364
+ process.stderr.write(
22365
+ ` \u2716 --specificity-rank must be an integer between 0 and 1000000 (got '${flags.specificityRank}').
22366
+ `
22367
+ );
22368
+ process.exit(2);
22369
+ }
22370
+ process.exit(
22371
+ await runNamespaceRegister(
22372
+ {
22373
+ ...toCommonOptions(flags),
22374
+ namespace: name,
22375
+ context: flags.context,
22376
+ specificityRank,
22377
+ entityBacked: Boolean(flags.entityBacked),
22378
+ defaultSchemaId: flags.defaultSchemaId
22379
+ },
22380
+ deps
22381
+ )
22382
+ );
22383
+ }
22384
+ );
22385
+ 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");
22386
+ addCommonAuthOptions(list).action(async (flags) => {
22387
+ process.exit(await runNamespaceList({ ...toCommonOptions(flags), context: flags.context }, deps));
22388
+ });
22389
+ 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");
22390
+ addCommonAuthOptions(get).action(
22391
+ async (name, flags) => {
22392
+ process.exit(
22393
+ await runNamespaceGet({ ...toCommonOptions(flags), namespace: name, context: flags.context }, deps)
22394
+ );
22395
+ }
22396
+ );
21708
22397
  }
21709
22398
  function registerRoleCommands(program, deps) {
21710
22399
  const role = program.command("role").description("Manage context-scoped roles (reusable scope rules; bind via `access grant --role`).");
21711
22400
  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(
21712
22401
  "--data-scope <json>",
21713
22402
  `confine the clause to owned data \u2014 JSON map of dimension \u2192 allowed values, e.g. '{"userId":["\${{ self.userId }}"]}' or '{"scope:org":["<orgId>", null]}'`
22403
+ ).option(
22404
+ "--assumable <json>",
22405
+ `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)`
21714
22406
  ).option("--description <text>", "free-text description");
21715
22407
  addCommonAuthOptions(create).action(
21716
22408
  async (flags) => {
@@ -21722,6 +22414,7 @@ function registerRoleCommands(program, deps) {
21722
22414
  name: flags.name,
21723
22415
  actions: flags.actions,
21724
22416
  dataScope: flags.dataScope,
22417
+ assumable: flags.assumable,
21725
22418
  description: flags.description
21726
22419
  }, deps)
21727
22420
  );
@@ -21815,6 +22508,20 @@ function registerIdentityCommands(program, deps) {
21815
22508
  );
21816
22509
  }
21817
22510
  );
22511
+ const listAll = identity.command("list-all").description(
22512
+ "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."
22513
+ ).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)");
22514
+ addCommonAuthOptions(listAll).action(
22515
+ async (flags) => {
22516
+ const tenant = parseTenantFlag(flags.tenant);
22517
+ process.exit(
22518
+ await runIdentityListAll(
22519
+ { ...toCommonOptions(flags), externalId: flags.externalId, limit: flags.limit, tenant },
22520
+ deps
22521
+ )
22522
+ );
22523
+ }
22524
+ );
21818
22525
  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)");
21819
22526
  addCommonAuthOptions(get).action(async (flags) => {
21820
22527
  process.exit(await runIdentityGet({ ...toCommonOptions(flags), type: flags.type, id: flags.id }, deps));