@vectros-ai/cli 0.24.0 → 0.25.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
@@ -1619,7 +1619,7 @@ var require_proper_lockfile = __commonJS({
1619
1619
  // src/cli-bootstrap.ts
1620
1620
  import * as readline2 from "readline/promises";
1621
1621
 
1622
- // node_modules/@vectros-ai/sdk/dist/index.mjs
1622
+ // ../../node_modules/@vectros-ai/sdk/dist/index.mjs
1623
1623
  var __defProp2 = Object.defineProperty;
1624
1624
  var __export = (target, all) => {
1625
1625
  for (var name in all)
@@ -4049,7 +4049,7 @@ var AuthClient = class {
4049
4049
  return handleNonStatusCodeError(_response.error, _response.rawResponse, "GET", "/v1/auth/jwks");
4050
4050
  }
4051
4051
  /**
4052
- * Returns a page of per-subject PHI read-access rows: who read which subject's PHI, when, against which record, and whether any sensitive value was actually revealed in plaintext. Metadata only — never the PHI itself. This is the disclosure-accounting surface from which a covered entity derives its HIPAA §164.528 accounting of disclosures. Provide at least one query axis: a subject (`subjectType` + `subjectId`) within a `contextId` for the primary accounting query; `resourceId` within a `contextId` for 'who read this record'; `callerKeyId` for 'what did this credential read' (account-wide forensic); or `contextId` alone to enumerate a whole context. `from`/`to` bound the time window. Results are scoped to your account, derived from your token — never from input. Requires the `access-log:r` scope.
4052
+ * Returns a page of per-subject PHI read-access rows: who read which subject's PHI, when, against which record, and whether any sensitive value was actually revealed in plaintext. Metadata only — never the PHI itself. Rows exist only where read-access logging is enabled. Provide at least one query axis: a subject (`subjectType` + `subjectId`) within a `contextId` for the primary accounting query; `resourceId` within a `contextId` for 'who read this record'; `callerKeyId` for 'what did this credential read' (account-wide forensic); or `contextId` alone to enumerate a whole context. `from`/`to` bound the time window. Results are scoped to your account, derived from your token — never from input. Requires the `access-log:r` scope.
4053
4053
  *
4054
4054
  * @param {Vectros.GetAccessLogRequest} request
4055
4055
  * @param {AuthClient.RequestOptions} requestOptions - Request-specific configuration.
@@ -5754,7 +5754,7 @@ var AuthClient = class {
5754
5754
  return handleNonStatusCodeError(_response.error, _response.rawResponse, "GET", "/v1/auth/issuers/{issuerId}");
5755
5755
  }
5756
5756
  /**
5757
- * Updates the mutable fields of a registered issuer: `subClaim`, `emailClaim`, `userinfoUri`, `capturedClaims`, `restrictedToDomain` (a new non-blank value must already be a VERIFIED domain for your account; an empty string clears it back to domain-less; see `IssuerUpdateRequest.restrictedToDomain` for the full semantics), `status` (`active`/`suspended` — a registration awaiting verification (`pending_verification`) accepts no status change until it is verified; a suspended issuer's tokens are rejected identically to an unregistered issuer at exchange time), and `selfSignupPolicies`. Fields omitted from the body are left unchanged (partial update). `issuer`, `jwksUri`, `audience`, and `contextId` are trust-anchor / routing-pin fields and are immutable via this route — supplying a value that differs from the current registration is rejected with 400; supplying the current value back is a no-op. Rotating a trust anchor requires deleting and re-registering the issuer, which is itself refused while any user is bound through it. **`subClaim` is identity-determining, not merely cosmetic**: it names which verified JWT claim is read as the federated user's identifier, so changing it on an issuer that already has bound users would silently re-identify (or, under self-signup, orphan) every one of them — changing it is therefore refused once any user has bound through this issuer, the same guard `DELETE` already applies, and is only free before the first real login. Requires a root API key or the bootstrap's provisioning capability, gated identically to every other operation on this surface. A credential confined to one app context may only update an issuer registered in that context; naming one registered in another context returns 404, identically to a nonexistent issuerId. A root API key may update any issuer.
5757
+ * Updates the mutable fields of a registered issuer: `subClaim`, `emailClaim`, `userinfoUri`, `capturedClaims`, `restrictedToDomain` (a new non-blank value must already be a VERIFIED domain for your account; an empty string clears it where permitted; see `IssuerUpdateRequest.restrictedToDomain` for the full semantics), `status` (`active`/`suspended` — a registration awaiting verification (`pending_verification`) accepts no status change until it is verified; a suspended issuer's tokens are rejected identically to an unregistered issuer at exchange time), and `selfSignupPolicies`. Fields omitted from the body are left unchanged (partial update). `issuer`, `jwksUri`, `audience`, and `contextId` are trust-anchor / routing-pin fields and are immutable via this route — supplying a value that differs from the current registration is rejected with 400; supplying the current value back is a no-op. Rotating a trust anchor requires deleting and re-registering the issuer, which is itself refused while any user is bound through it. **`subClaim` is identity-determining, not merely cosmetic**: it names which verified JWT claim is read as the federated user's identifier, so changing it on an issuer that already has bound users would silently re-identify (or, under self-signup, orphan) every one of them — changing it is therefore refused once any user has bound through this issuer, the same guard `DELETE` already applies, and is only free before the first real login. Requires a root API key or the bootstrap's provisioning capability, gated identically to every other operation on this surface. A credential confined to one app context may only update an issuer registered in that context; naming one registered in another context returns 404, identically to a nonexistent issuerId. A root API key may update any issuer.
5758
5758
  *
5759
5759
  * @param {Vectros.IssuerUpdateRequest} request
5760
5760
  * @param {AuthClient.RequestOptions} requestOptions - Request-specific configuration.
@@ -6564,7 +6564,7 @@ var ComplianceClient = class {
6564
6564
  this._options = normalizeClientOptionsWithAuth(options);
6565
6565
  }
6566
6566
  /**
6567
- * Submits a right-to-erasure request for a single end-subject (a user, or an identity entity in any namespace). Erasure removes exactly the data the subject solely owns across the declared contexts, plus its identity and lookup rows when all contexts are covered. It never touches another account's data and never cascades into another subject's data. The request is asynchronous: it returns 202 with a `requestId`; poll `GET /v1/erasure-requests/{id}` until the job completes to obtain the completion certificate. Requires a root API key — a scoped credential is rejected with 403.
6567
+ * Submits an erasure request for a single end-subject (a user, or an identity entity in any namespace). Erasure deletes the records, documents, folders and record schemas the subject solely owns across the declared contexts (and, for a user, its access profiles there), plus its identity and lookup rows when all contexts are covered. Rows co-owned with another owner scope or principal are kept; read-access log entries are not erased. The request is asynchronous: it returns 202 with a `requestId`; poll `GET /v1/erasure-requests/{id}` until the job completes to obtain the completion certificate. Requires a root API key — a scoped credential is rejected with 403.
6568
6568
  *
6569
6569
  * @param {Vectros.ErasureRequest} request
6570
6570
  * @param {ComplianceClient.RequestOptions} requestOptions - Request-specific configuration.
@@ -6627,7 +6627,7 @@ var ComplianceClient = class {
6627
6627
  return handleNonStatusCodeError(_response.error, _response.rawResponse, "POST", "/v1/erasure-requests");
6628
6628
  }
6629
6629
  /**
6630
- * Polls an erasure request by id. While the job is still running this returns its status only; once it completes, the response also includes the verifiable completion certificate (which contexts were swept, per-context deletion counts, whether the identity was kept, and reports of dangling references and shared rows left intact). Requires a root API key.
6630
+ * Polls an erasure request by id. While the job is still running this returns its status only; once it completes, the response also includes the completion certificate (which contexts were swept, per-context deletion counts, whether the identity was kept, and reports of dangling references and shared rows left intact). Requires a root API key.
6631
6631
  *
6632
6632
  * @param {Vectros.GetErasureRequestRequest} request
6633
6633
  * @param {ComplianceClient.RequestOptions} requestOptions - Request-specific configuration.
@@ -9446,6 +9446,8 @@ var InferenceClient = class {
9446
9446
  throw new PaymentRequiredError(_response.error.body, _response.rawResponse);
9447
9447
  case 403:
9448
9448
  throw new ForbiddenError(_response.error.body, _response.rawResponse);
9449
+ case 413:
9450
+ throw new ContentTooLargeError(_response.error.body, _response.rawResponse);
9449
9451
  case 429:
9450
9452
  throw new TooManyRequestsError(_response.error.body, _response.rawResponse);
9451
9453
  default:
@@ -9585,6 +9587,8 @@ var InferenceClient = class {
9585
9587
  throw new PaymentRequiredError(_response.error.body, _response.rawResponse);
9586
9588
  case 403:
9587
9589
  throw new ForbiddenError(_response.error.body, _response.rawResponse);
9590
+ case 413:
9591
+ throw new ContentTooLargeError(_response.error.body, _response.rawResponse);
9588
9592
  case 429:
9589
9593
  throw new TooManyRequestsError(_response.error.body, _response.rawResponse);
9590
9594
  case 500:
@@ -11882,7 +11886,7 @@ var RenderHintsSchema = z.object({
11882
11886
  helpText: z.string().optional(),
11883
11887
  // Marks this field as the record's headline (display) field — the linked
11884
11888
  // primary column in the records list + the title on the detail view. At most
11885
- // one per schema (the platform takes the first by order). Format passthrough →
11889
+ // one per schema (the platform stores the hint and does not enforce one; the reference UI uses the first by order). Format passthrough →
11886
11890
  // SchemaRequest.renderHints[fieldId].displayField.
11887
11891
  displayField: z.boolean().optional()
11888
11892
  }).strict();
@@ -13541,7 +13545,8 @@ var BlueprintSchema = z.object({
13541
13545
  * alongside app-context/service-principal creation and issuers, BEFORE
13542
13546
  * schemas/seed (an `entityBacked` namespace's entities, and an
13543
13547
  * `allowedSurfaces: ['entity']` schema, both need the registration to
13544
- * already exist). `org`/`client` need no entry — they exist already.
13548
+ * already exist). `org`/`client` are registered the same way as any other
13549
+ * namespace: signup does not register them, so declare an entry for each you use.
13545
13550
  */
13546
13551
  namespaces: z.array(BlueprintNamespaceSchema).optional(),
13547
13552
  /**
@@ -16505,6 +16510,25 @@ async function clearSession(baseDir) {
16505
16510
  return false;
16506
16511
  }
16507
16512
  }
16513
+ var TokenEndpointError = class extends Error {
16514
+ status;
16515
+ /** The OAuth `error` field of a JSON error body (e.g. `invalid_grant`), when there is one. */
16516
+ oauthError;
16517
+ constructor(status, text) {
16518
+ super(`token endpoint HTTP ${status}${text ? ` \u2014 ${stripControlCharacters(text, 500)}` : ""}`);
16519
+ this.name = "TokenEndpointError";
16520
+ this.status = status;
16521
+ this.oauthError = oauthErrorOf(text);
16522
+ }
16523
+ };
16524
+ function oauthErrorOf(text) {
16525
+ try {
16526
+ const parsed = JSON.parse(text);
16527
+ return typeof parsed.error === "string" ? parsed.error : void 0;
16528
+ } catch {
16529
+ return void 0;
16530
+ }
16531
+ }
16508
16532
  async function postToken(config, body, fetchImpl) {
16509
16533
  const res = await fetchImpl(tokenEndpoint(config), {
16510
16534
  method: "POST",
@@ -16512,9 +16536,7 @@ async function postToken(config, body, fetchImpl) {
16512
16536
  body: body.toString()
16513
16537
  });
16514
16538
  const text = await res.text().catch(() => "");
16515
- if (!res.ok) {
16516
- throw new Error(`token endpoint HTTP ${res.status}${text ? ` \u2014 ${stripControlCharacters(text, 500)}` : ""}`);
16517
- }
16539
+ if (!res.ok) throw new TokenEndpointError(res.status, text);
16518
16540
  try {
16519
16541
  return JSON.parse(text);
16520
16542
  } catch {
@@ -16530,31 +16552,54 @@ async function refreshTokens(config, refreshToken, fetchImpl = fetch) {
16530
16552
 
16531
16553
  // src/auth-context.ts
16532
16554
  var COGNITO_TOKEN_ENV = "VECTROS_BOOTSTRAP_TOKEN";
16555
+ function loginCommandFor(env) {
16556
+ return env === "staging" ? "vectros login --env staging" : "vectros login";
16557
+ }
16533
16558
  var MissingCredentialError = class extends Error {
16534
- constructor() {
16559
+ constructor(env) {
16535
16560
  super(
16536
- `No credential found. Set ${COGNITO_TOKEN_ENV} or pass --token (generate a token in the developer portal, or run \`vectros login\` once it ships).`
16561
+ `No credential found. Run \`${loginCommandFor(env)}\` to sign in, or set ${COGNITO_TOKEN_ENV} or pass --token (generate a token in the developer portal).`
16537
16562
  );
16538
16563
  this.name = "MissingCredentialError";
16539
16564
  }
16540
16565
  };
16566
+ var SessionExpiredError = class extends Error {
16567
+ constructor(env) {
16568
+ super(
16569
+ `Your Vectros login session has expired or was revoked. Run \`${loginCommandFor(env)}\` to sign in again.`
16570
+ );
16571
+ this.name = "SessionExpiredError";
16572
+ }
16573
+ };
16541
16574
  function resolveCognitoTokenSync(explicit) {
16542
16575
  if (explicit && explicit.trim()) return explicit.trim();
16543
16576
  const fromEnv = process.env[COGNITO_TOKEN_ENV];
16544
16577
  if (fromEnv && fromEnv.trim()) return fromEnv.trim();
16545
16578
  return void 0;
16546
16579
  }
16547
- async function resolveCognitoToken(env, explicit) {
16580
+ async function resolveCognitoToken(env, explicit, deps = { readSession, refreshTokens, saveSession }) {
16548
16581
  const sync = resolveCognitoTokenSync(explicit);
16549
16582
  if (sync) return sync;
16550
- const session = await readSession();
16583
+ const session = await deps.readSession();
16551
16584
  if (session && session.env === env) {
16552
16585
  const config = resolveCognitoConfig(env);
16553
- const tokens = await refreshTokens(config, session.refreshToken);
16586
+ let tokens;
16587
+ try {
16588
+ tokens = await deps.refreshTokens(config, session.refreshToken);
16589
+ } catch (err) {
16590
+ if (err instanceof TokenEndpointError && err.oauthError === "invalid_grant") throw new SessionExpiredError(env);
16591
+ throw err;
16592
+ }
16593
+ if (tokens.refresh_token && tokens.refresh_token !== session.refreshToken) {
16594
+ const stored = await deps.readSession().catch(() => void 0);
16595
+ if (stored?.refreshToken === session.refreshToken) {
16596
+ await deps.saveSession({ ...session, refreshToken: tokens.refresh_token }).catch(() => void 0);
16597
+ }
16598
+ }
16554
16599
  const bearer = tokens.id_token ?? tokens.access_token;
16555
16600
  if (bearer) return bearer;
16556
16601
  }
16557
- throw new MissingCredentialError();
16602
+ throw new MissingCredentialError(env);
16558
16603
  }
16559
16604
  async function resolveAuthContext(opts) {
16560
16605
  const baseUrl = envToBaseUrl(opts.env, opts.baseUrl ?? process.env.VECTROS_API_BASE_URL);
@@ -17066,6 +17111,7 @@ function keyNameFor(pack, machineId) {
17066
17111
  }
17067
17112
 
17068
17113
  // src/identity-resolver.ts
17114
+ var IDENTITIES_TO_ENSURE_HEADING = " Identities to ensure (idempotent by external id; a non-user entity lives with its namespace):";
17069
17115
  function userPayload(decl) {
17070
17116
  const meta = decl.metadata;
17071
17117
  if (decl.displayName === void 0) return meta;
@@ -17206,7 +17252,7 @@ async function withAuth(opts, deps, body) {
17206
17252
  log
17207
17253
  });
17208
17254
  } catch (err) {
17209
- if (err instanceof MissingCredentialError || err instanceof InvalidBaseUrlError || err instanceof InvalidCognitoRegionError) {
17255
+ if (err instanceof MissingCredentialError || err instanceof SessionExpiredError || err instanceof InvalidBaseUrlError || err instanceof InvalidCognitoRegionError) {
17210
17256
  process.stderr.write(` \u2716 ${err.message}
17211
17257
  `);
17212
17258
  return 2;
@@ -17801,15 +17847,6 @@ async function provisionContext(client, input) {
17801
17847
  const { blueprint, allowTenantWideNamespaces = false, readExistingKey, confirmExistingPrincipal, log } = input;
17802
17848
  assertBlueprintGates(blueprint);
17803
17849
  assertTenantWideNamespacesAllowed(blueprint, allowTenantWideNamespaces);
17804
- const ctxResp = await client.auth.createAppContext({
17805
- body: {
17806
- contextId: blueprint.contextId,
17807
- name: contextNameOf(blueprint),
17808
- ...blueprint.identityProjectionClaims === void 0 ? {} : { identityProjectionClaims: blueprint.identityProjectionClaims }
17809
- }
17810
- });
17811
- const contextAction = actionOf(ctxResp.created);
17812
- log.debug({ component: "loader", contextId: blueprint.contextId, contextAction }, "app-context ensured");
17813
17850
  const user = await client.identity.createUser({
17814
17851
  body: {
17815
17852
  externalId: blueprint.servicePrincipal.externalId,
@@ -17850,6 +17887,15 @@ async function provisionContext(client, input) {
17850
17887
  "existing-principal collision confirmed (or unwired) \u2014 proceeding with any further write to this principal"
17851
17888
  );
17852
17889
  }
17890
+ const ctxResp = await client.auth.createAppContext({
17891
+ body: {
17892
+ contextId: blueprint.contextId,
17893
+ name: contextNameOf(blueprint),
17894
+ ...blueprint.identityProjectionClaims === void 0 ? {} : { identityProjectionClaims: blueprint.identityProjectionClaims }
17895
+ }
17896
+ });
17897
+ const contextAction = actionOf(ctxResp.created);
17898
+ log.debug({ component: "loader", contextId: blueprint.contextId, contextAction }, "app-context ensured");
17853
17899
  const needsContextPinnedBootstrap = (blueprint.namespaces ?? []).length > 0;
17854
17900
  const ctxBootstrapClient = needsContextPinnedBootstrap ? input.remintBootstrapForContext ? await input.remintBootstrapForContext(blueprint.contextId) : client : void 0;
17855
17901
  if (ctxBootstrapClient && input.remintBootstrapForContext) {
@@ -18358,14 +18404,14 @@ function renderPlanDataScope(ds) {
18358
18404
  function buildPlan(blueprint, opts = {}) {
18359
18405
  const steps = [];
18360
18406
  const projection = blueprint.identityProjectionClaims?.length ? `, identity projection: [${blueprint.identityProjectionClaims.join(", ")}]` : blueprint.identityProjectionClaims ? ", identity projection: disabled (empty list)" : "";
18361
- steps.push({
18362
- kind: "context",
18363
- summary: `context '${blueprint.contextId}' (${contextNameOf(blueprint)}${projection})`
18364
- });
18365
18407
  steps.push({
18366
18408
  kind: "principal",
18367
18409
  summary: `principal '${blueprint.servicePrincipal.externalId}' (SERVICE)`
18368
18410
  });
18411
+ steps.push({
18412
+ kind: "context",
18413
+ summary: `context '${blueprint.contextId}' (${contextNameOf(blueprint)}${projection})`
18414
+ });
18369
18415
  for (const ns of blueprint.namespaces ?? []) {
18370
18416
  const ownerNote = ns.tenantWide ? "TENANT-WIDE \u2014 visible to every context" : `owned by '${blueprint.contextId}'`;
18371
18417
  const entityNote = ns.entityBacked ? ", entity-backed" : "";
@@ -18920,9 +18966,8 @@ var OsKeychainSecretStore = class {
18920
18966
  * enumeration, and `deleteCredential` on a nonexistent ref returns the same
18921
18967
  * meaningless `false` as Linux. So the `.some(c => c.account === ref)` comparison
18922
18968
  * is sound on both measured platforms. Still unmeasured: a CI lane that runs this
18923
- * against the real Windows store on every MR — the dev-machine probe is a point
18924
- * measurement, not a regression gate. (Tracked internally; this file ships to
18925
- * public npm, so no tracker number here.)
18969
+ * against the real Windows store on every change — the dev-machine probe is a point
18970
+ * measurement, not a regression gate.
18926
18971
  *
18927
18972
  * ⚠️ THIS METHOD'S FAIL-CLOSED GUARANTEE IS AN UPSTREAM BEHAVIOURAL CONTRACT, NOT
18928
18973
  * SOMETHING THIS CODE ENFORCES. `@napi-rs/keyring` is pinned to an EXACT version
@@ -19078,7 +19123,7 @@ var CryptoFileSecretStore = class {
19078
19123
  async setSecret(ref, secret) {
19079
19124
  const key = await this.encryptionKey();
19080
19125
  const iv = randomBytes2(IV_LEN);
19081
- const cipher = createCipheriv(ALGO, key, iv);
19126
+ const cipher = createCipheriv(ALGO, key, iv, { authTagLength: TAG_LEN });
19082
19127
  const enc = Buffer.concat([cipher.update(secret, "utf8"), cipher.final()]);
19083
19128
  const tag = cipher.getAuthTag();
19084
19129
  const blob = Buffer.concat([iv, tag, enc]);
@@ -19101,7 +19146,7 @@ var CryptoFileSecretStore = class {
19101
19146
  const iv = blob.subarray(0, IV_LEN);
19102
19147
  const tag = blob.subarray(IV_LEN, IV_LEN + TAG_LEN);
19103
19148
  const enc = blob.subarray(IV_LEN + TAG_LEN);
19104
- const decipher = createDecipheriv(ALGO, key, iv);
19149
+ const decipher = createDecipheriv(ALGO, key, iv, { authTagLength: TAG_LEN });
19105
19150
  decipher.setAuthTag(tag);
19106
19151
  return Buffer.concat([decipher.update(enc), decipher.final()]).toString("utf8");
19107
19152
  } catch {
@@ -19301,7 +19346,7 @@ async function withIndexLock(dir, fn, timings = LOCK_OPTS) {
19301
19346
  // judged: two waiters can both condemn one crash remnant, and the second's
19302
19347
  // already-decided rmdir destroys the first's FRESH lock. Nothing freezes.
19303
19348
  //
19304
- // MEASURED against this exact config (probes recorded in the MR):
19349
+ // MEASURED against this exact config (probe results below):
19305
19350
  // real OS processes, no injection : 0/25 and 1/25 (two independent runs)
19306
19351
  // in-process, 5 waiters : ~3% per round
19307
19352
  // rival's rmdir lagged one turn : 19/25
@@ -21134,7 +21179,7 @@ async function resolveBridgeCognitoToken(args, prompt, isTty, resolveCognito = r
21134
21179
  }
21135
21180
  if (!isTty) {
21136
21181
  throw new Error(
21137
- "No bridge token: set VECTROS_BOOTSTRAP_TOKEN, pass --token, or run `vectros login` first (required in non-interactive mode)."
21182
+ `No bridge token: set VECTROS_BOOTSTRAP_TOKEN, pass --token, or run \`${loginCommandFor(args.env)}\` first (required in non-interactive mode).`
21138
21183
  );
21139
21184
  }
21140
21185
  process.stdout.write(
@@ -21243,7 +21288,9 @@ ${renderPlan(blueprint, planSteps)}
21243
21288
  `);
21244
21289
  const declaredIdentities = Object.entries(blueprint.identities ?? {});
21245
21290
  if (declaredIdentities.length > 0) {
21246
- process.stdout.write("\n Identities to ensure (tenant-wide, idempotent by external id):\n");
21291
+ process.stdout.write(`
21292
+ ${IDENTITIES_TO_ENSURE_HEADING}
21293
+ `);
21247
21294
  for (const [name, decl] of declaredIdentities) {
21248
21295
  process.stdout.write(` ~ ${name} (${decl.kind}) \u2014 external id '${decl.externalId}'
21249
21296
  `);
@@ -21474,6 +21521,12 @@ ${codeAddHint}`);
21474
21521
  }
21475
21522
  return finishApply();
21476
21523
  } catch (err) {
21524
+ if (err instanceof SessionExpiredError) {
21525
+ process.stderr.write(`
21526
+ \u2716 ${err.message}
21527
+ `);
21528
+ return 2;
21529
+ }
21477
21530
  if (err instanceof LostKeyError) {
21478
21531
  process.stderr.write(`
21479
21532
  \u2716 ${err.message}
@@ -21845,7 +21898,9 @@ ${renderPlan(blueprint, buildPlan(blueprint, { noSeed, noKey: true }), { prune:
21845
21898
  process.stdout.write(applySeedNotice(blueprint, args.seed));
21846
21899
  const declaredIdentities = Object.entries(blueprint.identities ?? {});
21847
21900
  if (declaredIdentities.length > 0) {
21848
- process.stdout.write("\n Identities to ensure (tenant-wide, idempotent by external id):\n");
21901
+ process.stdout.write(`
21902
+ ${IDENTITIES_TO_ENSURE_HEADING}
21903
+ `);
21849
21904
  for (const [name, decl] of declaredIdentities) {
21850
21905
  process.stdout.write(` ~ ${name} (${decl.kind}) \u2014 external id '${decl.externalId}'
21851
21906
  `);
@@ -21978,7 +22033,7 @@ function reportApplyError(err) {
21978
22033
  If this is expected (a re-apply of a blueprint you manage), re-run with --confirm-existing-principal
21979
22034
  (with --yes in a pipeline, where it is standing consent), or confirm at the prompt.
21980
22035
  If you don't recognize this identity, investigate the blueprint's servicePrincipal.externalId first.
21981
- Only the app-context and the principal lookup had run; no grant or anything else was changed.
22036
+ Only the principal lookup had run; no context, grant or anything else was created or changed.
21982
22037
  `
21983
22038
  );
21984
22039
  return 3;
@@ -22331,10 +22386,14 @@ schemas:
22331
22386
  accessProfile:
22332
22387
  # SCOPE GATE: data-plane only (records/schemas/search/documents/folders/inference/entities),
22333
22388
  # PLUS profiles:r/u/d (bounded by the platform to redistributing access you already have,
22334
- # never granting more) and profiles:c:self (create a profile for yourself only \u2014 the
22335
- # platform resolves "self" from the caller, not from anything you declare here). A bare
22336
- # profiles:c, or profiles:c:usr_<id> naming another principal, is HARD-REJECTED \u2014 by
22337
- # design, no override. Same for keys/users/admin/namespaces/*.
22389
+ # never granting more), profiles:c:self (create a profile for yourself only \u2014 the
22390
+ # platform resolves "self" from the caller, not from anything you declare here) and
22391
+ # scripts:x:<script-name> (run one named script that this blueprint ships; there is no bare
22392
+ # or wildcard form).
22393
+ # A bare profiles:c is accepted ONLY when this same clause also declares the
22394
+ # member-lifecycle capability (see "capabilities" below); without it, and always for
22395
+ # profiles:c:usr_<id> naming another principal, it is REJECTED \u2014 no override. Every other
22396
+ # control-plane resource (keys/users/admin/namespaces/triggers/...) and "*" is REJECTED the same way.
22338
22397
  # Ops: c|r|u|d, combinable (records:cru). Prefer the narrowest set (least privilege).
22339
22398
  allowedActions: [records:r, records:c, records:u, search:r, schemas:r]
22340
22399
  # dataScope binds the key to specific owners. Include null to ALSO see
@@ -22350,8 +22409,10 @@ accessProfile:
22350
22409
  # above) unless every owner genuinely sharing every record is what you want.
22351
22410
  dataScope: {}
22352
22411
  # capabilities is a SEPARATE gated dimension from allowedActions \u2014 named platform
22353
- # effects a resource:ops verb can't express. Only 'member-lifecycle' is admitted
22354
- # today; every other name is HARD-REJECTED, same posture as the scope gate above.
22412
+ # effects a resource:ops verb can't express. Only 'member-lifecycle' and
22413
+ # 'delegate-principal-stamp' are admitted today; every other name is REJECTED, same
22414
+ # posture as the scope gate above. 'member-lifecycle' is also what lets this clause
22415
+ # carry a bare profiles:c.
22355
22416
  # capabilities: [member-lifecycle]
22356
22417
 
22357
22418
  servicePrincipal:
@@ -22438,8 +22499,10 @@ Usage:
22438
22499
 
22439
22500
  <file|name> is a path to your own blueprint (YAML .yaml/.yml or JSON .json) OR
22440
22501
  the name of a bundled blueprint. The same scope gate that protects bundled
22441
- blueprints applies to file blueprints \u2014 a forked or hand-written blueprint can
22442
- never request control-plane access.
22502
+ blueprints applies to file blueprints: beyond data-plane access, a forked or
22503
+ hand-written blueprint can request only a short allowlist of narrow exceptions
22504
+ (profiles:r/u/d, profiles:c:self, a bare profiles:c beside member-lifecycle,
22505
+ scripts:x:<script-name>); every other control-plane scope is rejected.
22443
22506
 
22444
22507
  A blueprint may declare a top-level 'inputs:' block and reference its values
22445
22508
  with \${{ inputs.<name> }} (and the built-ins \${{ vectros.context }} /
@@ -22551,7 +22614,9 @@ ${renderPlan(blueprint, steps)}
22551
22614
  `);
22552
22615
  const declaredIdentities = Object.entries(blueprint.identities ?? {});
22553
22616
  if (declaredIdentities.length > 0) {
22554
- process.stdout.write("\n Identities to ensure (tenant-wide, idempotent by external id):\n");
22617
+ process.stdout.write(`
22618
+ ${IDENTITIES_TO_ENSURE_HEADING}
22619
+ `);
22555
22620
  for (const [name, decl] of declaredIdentities) {
22556
22621
  process.stdout.write(` ~ ${name} (${decl.kind}) \u2014 external id '${decl.externalId}'
22557
22622
  `);
@@ -22685,6 +22750,89 @@ ${BLUEPRINT_HELP}
22685
22750
  // src/program.ts
22686
22751
  import { Command, Option } from "commander";
22687
22752
 
22753
+ // src/dev-api.ts
22754
+ var DeveloperApiError = class extends Error {
22755
+ statusCode;
22756
+ /** The platform's error body, when it answered with one (its message, request id and error code). */
22757
+ body;
22758
+ constructor(message, statusCode, body) {
22759
+ super(message);
22760
+ this.name = "DeveloperApiError";
22761
+ this.statusCode = statusCode;
22762
+ this.body = body;
22763
+ }
22764
+ };
22765
+ async function fetchDeveloperUsersPage(opts) {
22766
+ const { cognitoToken, apiBaseUrl, tenant, startFrom, limit, fetchImpl = fetch } = opts;
22767
+ const base = apiBaseUrl.replace(/\/$/, "");
22768
+ let url = `${base}/developer/users?tenant=${encodeURIComponent(tenant)}`;
22769
+ if (startFrom) url += `&startFrom=${encodeURIComponent(startFrom)}`;
22770
+ if (limit !== void 0) url += `&limit=${encodeURIComponent(String(limit))}`;
22771
+ let res;
22772
+ try {
22773
+ res = await fetchImpl(url, {
22774
+ method: "GET",
22775
+ headers: { Authorization: `Bearer ${cognitoToken}` }
22776
+ });
22777
+ } catch (err) {
22778
+ throw new DeveloperApiError(
22779
+ `Request to /developer/users failed: ${err instanceof Error ? err.message : String(err)}`
22780
+ );
22781
+ }
22782
+ if (!res.ok) {
22783
+ const body = await res.text().catch(() => "");
22784
+ const hint = res.status === 403 ? " (this command requires an account owner login \u2014 sign in as the owner and retry)" : "";
22785
+ throw new DeveloperApiError(
22786
+ `GET /developer/users failed: HTTP ${res.status}${hint}${body ? ` \u2014 ${body}` : ""}`,
22787
+ res.status
22788
+ );
22789
+ }
22790
+ let parsed;
22791
+ try {
22792
+ parsed = await res.json();
22793
+ } catch {
22794
+ throw new DeveloperApiError("GET /developer/users returned a non-JSON body");
22795
+ }
22796
+ const data = Array.isArray(parsed.data) ? parsed.data : [];
22797
+ const nextCursor = typeof parsed.nextCursor === "string" ? parsed.nextCursor : null;
22798
+ return { data, nextCursor };
22799
+ }
22800
+ async function fetchDeveloperMe(opts) {
22801
+ const { cognitoToken, apiBaseUrl, tenant, fetchImpl = fetch } = opts;
22802
+ const url = `${apiBaseUrl.replace(/\/$/, "")}/developer/me?tenant=${encodeURIComponent(tenant)}`;
22803
+ let res;
22804
+ try {
22805
+ res = await fetchImpl(url, { method: "GET", headers: { Authorization: `Bearer ${cognitoToken}` } });
22806
+ } catch (err) {
22807
+ throw new DeveloperApiError(
22808
+ `Request to /developer/me failed: ${err instanceof Error ? err.message : String(err)}`
22809
+ );
22810
+ }
22811
+ if (res.status === 404) return null;
22812
+ if (!res.ok) {
22813
+ const failure = await devRouteError(res, "GET /developer/me");
22814
+ throw new DeveloperApiError(
22815
+ `GET /developer/me failed: HTTP ${res.status}`,
22816
+ res.status,
22817
+ failure.body
22818
+ );
22819
+ }
22820
+ let parsed;
22821
+ try {
22822
+ parsed = await res.json();
22823
+ } catch {
22824
+ throw new DeveloperApiError("GET /developer/me returned a non-JSON body");
22825
+ }
22826
+ if (typeof parsed.principalId !== "string" || typeof parsed.userId !== "string") {
22827
+ throw new DeveloperApiError("GET /developer/me returned no principal");
22828
+ }
22829
+ return {
22830
+ principalId: parsed.principalId,
22831
+ userId: parsed.userId,
22832
+ tenantId: typeof parsed.tenantId === "string" ? parsed.tenantId : void 0
22833
+ };
22834
+ }
22835
+
22688
22836
  // src/self-principal.ts
22689
22837
  var PRINCIPAL_PREFIX_USER = "usr_";
22690
22838
  function emailFromJwt(token) {
@@ -22713,6 +22861,37 @@ async function resolveSelfPrincipal(client, cognitoToken) {
22713
22861
  if (!match?.id) return null;
22714
22862
  return { principalId: `${PRINCIPAL_PREFIX_USER}${match.id}`, userId: match.id, email };
22715
22863
  }
22864
+ async function resolveSelfPrincipalForTenant(auth, fetchImpl) {
22865
+ const tenant = auth.tenant ?? "live";
22866
+ let me;
22867
+ try {
22868
+ me = await fetchDeveloperMe({
22869
+ cognitoToken: auth.cognitoToken,
22870
+ apiBaseUrl: auth.baseUrl,
22871
+ tenant,
22872
+ ...fetchImpl ? { fetchImpl } : {}
22873
+ });
22874
+ } catch (err) {
22875
+ if (err instanceof DeveloperApiError && err.statusCode !== void 0) {
22876
+ if (err.body?.errorCode === "SESSION_REFRESH_REQUIRED") throw new SessionExpiredError(auth.env ?? "production");
22877
+ throw err;
22878
+ }
22879
+ if (tenant === "test") throw err;
22880
+ return resolveSelfPrincipal(auth.client, auth.cognitoToken);
22881
+ }
22882
+ if (me === null) return null;
22883
+ const forThisTenant = Boolean(auth.tenantId && me.tenantId && me.tenantId === auth.tenantId);
22884
+ if (forThisTenant && me.principalId.startsWith(PRINCIPAL_PREFIX_USER)) {
22885
+ return { principalId: me.principalId, userId: me.userId, email: emailFromJwt(auth.cognitoToken) ?? "" };
22886
+ }
22887
+ return tenant === "live" ? resolveSelfPrincipal(auth.client, auth.cognitoToken) : null;
22888
+ }
22889
+ function selfPrincipalUnresolvedMessage(tenant, flag) {
22890
+ if (tenant === "test") {
22891
+ return `Could not resolve your own user id in the test tenant: the platform did not report one for this login. Find it with \`vectros identity list-all --tenant test\` (an owner login) and pass ${flag} usr_<id> explicitly.`;
22892
+ }
22893
+ return `Could not resolve your own user id from your login. Pass ${flag} usr_<id> explicitly (find the id with \`vectros identity list --type user\`).`;
22894
+ }
22716
22895
 
22717
22896
  // src/commands/whoami.ts
22718
22897
  async function runWhoami(options, deps = {}) {
@@ -22720,7 +22899,10 @@ async function runWhoami(options, deps = {}) {
22720
22899
  const { identity, baseUrl } = auth;
22721
22900
  let self2 = null;
22722
22901
  try {
22723
- self2 = await resolveSelfPrincipal(auth.client, auth.cognitoToken);
22902
+ self2 = await resolveSelfPrincipalForTenant(
22903
+ { ...auth, client: auth.client },
22904
+ deps.fetchImpl
22905
+ );
22724
22906
  } catch {
22725
22907
  self2 = null;
22726
22908
  }
@@ -23126,51 +23308,6 @@ ${list}
23126
23308
  });
23127
23309
  }
23128
23310
 
23129
- // src/dev-api.ts
23130
- var DeveloperApiError = class extends Error {
23131
- statusCode;
23132
- constructor(message, statusCode) {
23133
- super(message);
23134
- this.name = "DeveloperApiError";
23135
- this.statusCode = statusCode;
23136
- }
23137
- };
23138
- async function fetchDeveloperUsersPage(opts) {
23139
- const { cognitoToken, apiBaseUrl, tenant, startFrom, limit, fetchImpl = fetch } = opts;
23140
- const base = apiBaseUrl.replace(/\/$/, "");
23141
- let url = `${base}/developer/users?tenant=${encodeURIComponent(tenant)}`;
23142
- if (startFrom) url += `&startFrom=${encodeURIComponent(startFrom)}`;
23143
- if (limit !== void 0) url += `&limit=${encodeURIComponent(String(limit))}`;
23144
- let res;
23145
- try {
23146
- res = await fetchImpl(url, {
23147
- method: "GET",
23148
- headers: { Authorization: `Bearer ${cognitoToken}` }
23149
- });
23150
- } catch (err) {
23151
- throw new DeveloperApiError(
23152
- `Request to /developer/users failed: ${err instanceof Error ? err.message : String(err)}`
23153
- );
23154
- }
23155
- if (!res.ok) {
23156
- const body = await res.text().catch(() => "");
23157
- const hint = res.status === 403 ? " (this command requires an account owner login \u2014 sign in as the owner and retry)" : "";
23158
- throw new DeveloperApiError(
23159
- `GET /developer/users failed: HTTP ${res.status}${hint}${body ? ` \u2014 ${body}` : ""}`,
23160
- res.status
23161
- );
23162
- }
23163
- let parsed;
23164
- try {
23165
- parsed = await res.json();
23166
- } catch {
23167
- throw new DeveloperApiError("GET /developer/users returned a non-JSON body");
23168
- }
23169
- const data = Array.isArray(parsed.data) ? parsed.data : [];
23170
- const nextCursor = typeof parsed.nextCursor === "string" ? parsed.nextCursor : null;
23171
- return { data, nextCursor };
23172
- }
23173
-
23174
23311
  // src/commands/identity.ts
23175
23312
  function isUserDimension(type) {
23176
23313
  return type === "user";
@@ -23466,14 +23603,13 @@ function assertPrincipalShape(principal) {
23466
23603
  }
23467
23604
  }
23468
23605
  var SELF_PRINCIPAL_ALIAS = "me";
23469
- async function resolvePrincipalArg(raw, auth) {
23606
+ async function resolvePrincipalArg(raw, auth, deps) {
23470
23607
  if (raw !== SELF_PRINCIPAL_ALIAS) return raw;
23471
- const self2 = await resolveSelfPrincipal(auth.client, auth.cognitoToken);
23472
- if (!self2) {
23473
- throw new Error(
23474
- "Could not resolve your own user id from your login. Pass --principal usr_<id> explicitly (find the id with `vectros identity list --type user`)."
23475
- );
23476
- }
23608
+ const self2 = await resolveSelfPrincipalForTenant(
23609
+ { ...auth, client: auth.client },
23610
+ deps.fetchImpl
23611
+ );
23612
+ if (!self2) throw new Error(selfPrincipalUnresolvedMessage(auth.tenant, "--principal"));
23477
23613
  return self2.principalId;
23478
23614
  }
23479
23615
  function roleSummary(p) {
@@ -23526,7 +23662,7 @@ async function runAccessGrant(opts, deps = {}) {
23526
23662
  return withAuth(opts, deps, async (auth) => {
23527
23663
  const client = auth.client;
23528
23664
  try {
23529
- const principal = await resolvePrincipalArg(opts.principal, auth);
23665
+ const principal = await resolvePrincipalArg(opts.principal, auth, deps);
23530
23666
  const scopes = hasActions ? [{ allowed_actions: splitCsv(opts.actions) }] : void 0;
23531
23667
  const profile = await client.auth.createAccessProfile({
23532
23668
  contextId: opts.context,
@@ -23536,8 +23672,10 @@ async function runAccessGrant(opts, deps = {}) {
23536
23672
  // Always send the plural `roleIds`, even for a single
23537
23673
  // `--role`: it is a strict superset of the deprecated singular
23538
23674
  // `roleId` (equivalent to `roleIds: ["<value>"]`), and the platform
23539
- // reads it uniformly regardless of how many roles compose.
23540
- roleIds: opts.role,
23675
+ // reads it uniformly regardless of how many roles compose. With
23676
+ // `--actions` there is no role: omit `roleIds` (the platform refuses an
23677
+ // empty list next to inline `scopes`).
23678
+ roleIds: hasRole ? opts.role : void 0,
23541
23679
  scopes,
23542
23680
  identityOverrides
23543
23681
  }
@@ -23574,7 +23712,7 @@ async function runAccessRevoke(opts, deps = {}) {
23574
23712
  return withAuth(opts, deps, async (auth) => {
23575
23713
  const client = auth.client;
23576
23714
  try {
23577
- const principal = await resolvePrincipalArg(opts.principal, auth);
23715
+ const principal = await resolvePrincipalArg(opts.principal, auth, deps);
23578
23716
  await client.auth.deleteAccessProfile({ contextId: opts.context, principalId: principal });
23579
23717
  process.stdout.write(` \u2714 Revoked ${principal} from '${opts.context}'
23580
23718
  `);
@@ -23603,7 +23741,7 @@ async function runAccessList(opts, deps = {}) {
23603
23741
  return withAuth(opts, deps, async (auth) => {
23604
23742
  const client = auth.client;
23605
23743
  try {
23606
- const principal = opts.principal ? await resolvePrincipalArg(opts.principal, auth) : void 0;
23744
+ const principal = opts.principal ? await resolvePrincipalArg(opts.principal, auth, deps) : void 0;
23607
23745
  const rows = opts.context ? await drainPages(
23608
23746
  (startFrom) => client.auth.listAccessProfiles({ contextId: opts.context, startFrom, limit: PAGE_LIMIT })
23609
23747
  ) : await drainPages(
@@ -23643,7 +23781,7 @@ async function runAccessGet(opts, deps = {}) {
23643
23781
  return withAuth(opts, deps, async (auth) => {
23644
23782
  const client = auth.client;
23645
23783
  try {
23646
- const principal = await resolvePrincipalArg(opts.principal, auth);
23784
+ const principal = await resolvePrincipalArg(opts.principal, auth, deps);
23647
23785
  const p = await client.auth.getAccessProfile({ contextId: opts.context, principalId: principal });
23648
23786
  if (opts.json) {
23649
23787
  process.stdout.write(`${formatJson(p)}
@@ -23865,11 +24003,13 @@ async function runAccessExplain(opts, deps = {}) {
23865
24003
  try {
23866
24004
  let principal = opts.principal;
23867
24005
  if (principal === SELF_PRINCIPAL_ALIAS2) {
23868
- const self2 = await resolveSelfPrincipal(auth.client, auth.cognitoToken);
24006
+ const self2 = await resolveSelfPrincipalForTenant(
24007
+ { ...auth, client: auth.client },
24008
+ deps.fetchImpl
24009
+ );
23869
24010
  if (!self2) {
23870
- process.stderr.write(
23871
- " \u2716 Could not resolve your own user id from your login. Pass --principal usr_<id> explicitly.\n"
23872
- );
24011
+ process.stderr.write(` \u2716 ${selfPrincipalUnresolvedMessage(auth.tenant, "--principal")}
24012
+ `);
23873
24013
  return 1;
23874
24014
  }
23875
24015
  principal = self2.principalId;
@@ -24248,10 +24388,17 @@ async function runContextDestroy(opts, deps = {}) {
24248
24388
  `);
24249
24389
  return 0;
24250
24390
  }
24391
+ const tenantFlag = auth.tenant === "test" ? " --tenant test" : "";
24251
24392
  process.stdout.write(
24252
24393
  `
24253
24394
  \u2714 teardown of '${contextId}' started \u2014 the context and its contents are being
24254
24395
  deleted asynchronously (it reports status 'purging' until the drain completes)
24396
+
24397
+ Not removed: a service principal a blueprint bootstrap created for this context (the
24398
+ externalId in the blueprint's servicePrincipal) belongs to the whole tenant, so it stays. To clean it up
24399
+ when it exists and nothing else uses it:
24400
+ vectros identity list-all${tenantFlag}
24401
+ vectros identity delete --type user --id <id>${tenantFlag}
24255
24402
  `
24256
24403
  );
24257
24404
  return 0;
@@ -24790,7 +24937,7 @@ ${renderIssuer(issuer)}
24790
24937
  import { createReadStream as createReadStream2 } from "fs";
24791
24938
  import * as readline6 from "readline/promises";
24792
24939
  var PROVIDER_TYPES = ["ANTHROPIC_NATIVE", "OPENAI_COMPATIBLE"];
24793
- var BILLING_MODES = ["PLATFORM_BILLED", "CUSTOMER_DIRECT"];
24940
+ var BILLING_MODES = ["CUSTOMER_DIRECT"];
24794
24941
  var STATUSES = ["ACTIVE", "DISABLED"];
24795
24942
  var MAX_SECRET_BYTES = 8 * 1024;
24796
24943
  async function readBoundedSecret(stream) {
@@ -24869,12 +25016,17 @@ function renderProviderConfig(c) {
24869
25016
  ["defaultProviderModelId", c.defaultProviderModelId],
24870
25017
  ["waiverType", c.waiverType],
24871
25018
  ["billingMode", c.billingMode],
24872
- ["platformTokenFactor", c.platformTokenFactor],
24873
- ["platformOutputWeight", c.platformOutputWeight],
24874
25019
  ["status", c.status],
24875
25020
  ["createdAt", c.createdAt]
24876
25021
  ]);
24877
25022
  }
25023
+ function billingModeError(value) {
25024
+ if (value === void 0 || BILLING_MODES.includes(value)) return void 0;
25025
+ if (value === "PLATFORM_BILLED") {
25026
+ return "--billing-mode PLATFORM_BILLED is retired: a provider you configure bills you directly, so CUSTOMER_DIRECT is the only billing mode.";
25027
+ }
25028
+ return invalidEnum("--billing-mode", value, BILLING_MODES);
25029
+ }
24878
25030
  function invalidEnum(flag, value, allowed) {
24879
25031
  return `${flag} must be one of ${allowed.join(", ")} (got '${value}').`;
24880
25032
  }
@@ -24885,8 +25037,9 @@ async function runProviderConfigCreate(opts, deps = {}) {
24885
25037
  `);
24886
25038
  return 2;
24887
25039
  }
24888
- if (!BILLING_MODES.includes(opts.billingMode)) {
24889
- process.stderr.write(` \u2716 ${invalidEnum("--billing-mode", opts.billingMode, BILLING_MODES)}
25040
+ const billingModeProblem = billingModeError(opts.billingMode);
25041
+ if (billingModeProblem !== void 0) {
25042
+ process.stderr.write(` \u2716 ${billingModeProblem}
24890
25043
  `);
24891
25044
  return 2;
24892
25045
  }
@@ -24895,19 +25048,6 @@ async function runProviderConfigCreate(opts, deps = {}) {
24895
25048
  `);
24896
25049
  return 2;
24897
25050
  }
24898
- if (opts.billingMode === "PLATFORM_BILLED") {
24899
- if (opts.platformTokenFactor === void 0 || opts.platformOutputWeight === void 0) {
24900
- process.stderr.write(
24901
- " \u2716 --platform-token-factor and --platform-output-weight are required when --billing-mode PLATFORM_BILLED.\n"
24902
- );
24903
- return 2;
24904
- }
24905
- } else if (opts.platformTokenFactor !== void 0 || opts.platformOutputWeight !== void 0) {
24906
- process.stderr.write(
24907
- " \u2716 --platform-token-factor/--platform-output-weight only apply with --billing-mode PLATFORM_BILLED.\n"
24908
- );
24909
- return 2;
24910
- }
24911
25051
  let apiKeySecret;
24912
25052
  try {
24913
25053
  apiKeySecret = await resolveApiKeySecret(opts.apiKeySecretFile, deps, "required");
@@ -24932,9 +25072,7 @@ async function runProviderConfigCreate(opts, deps = {}) {
24932
25072
  defaultProviderModelId: opts.defaultProviderModelId,
24933
25073
  apiKeySecret,
24934
25074
  waiverType: opts.waiverType,
24935
- billingMode: opts.billingMode,
24936
- platformTokenFactor: opts.platformTokenFactor,
24937
- platformOutputWeight: opts.platformOutputWeight,
25075
+ billingMode: opts.billingMode ?? "CUSTOMER_DIRECT",
24938
25076
  status: opts.status
24939
25077
  })
24940
25078
  });
@@ -25015,8 +25153,9 @@ async function runProviderConfigUpdate(opts, deps = {}) {
25015
25153
  `);
25016
25154
  return 2;
25017
25155
  }
25018
- if (opts.billingMode !== void 0 && !BILLING_MODES.includes(opts.billingMode)) {
25019
- process.stderr.write(` \u2716 ${invalidEnum("--billing-mode", opts.billingMode, BILLING_MODES)}
25156
+ const billingModeProblem = billingModeError(opts.billingMode);
25157
+ if (billingModeProblem !== void 0) {
25158
+ process.stderr.write(` \u2716 ${billingModeProblem}
25020
25159
  `);
25021
25160
  return 2;
25022
25161
  }
@@ -25049,8 +25188,6 @@ async function runProviderConfigUpdate(opts, deps = {}) {
25049
25188
  apiKeySecret,
25050
25189
  waiverType: opts.waiverType,
25051
25190
  billingMode: opts.billingMode,
25052
- platformTokenFactor: opts.platformTokenFactor,
25053
- platformOutputWeight: opts.platformOutputWeight,
25054
25191
  status: opts.status
25055
25192
  })
25056
25193
  });
@@ -26627,15 +26764,14 @@ async function runBlueprintTest(fileOrName, opts, deps = {}) {
26627
26764
 
26628
26765
  // src/commands/join.ts
26629
26766
  var SELF_ALIAS = "me";
26630
- async function resolvePrincipal(as, auth) {
26767
+ async function resolvePrincipal(as, auth, deps) {
26631
26768
  const raw = as ?? SELF_ALIAS;
26632
26769
  if (raw === SELF_ALIAS) {
26633
- const self2 = await resolveSelfPrincipal(auth.client, auth.cognitoToken);
26634
- if (!self2) {
26635
- throw new Error(
26636
- "Could not resolve your own user id from your login. Pass --as usr_<id> explicitly (find it with `vectros identity list --type user`)."
26637
- );
26638
- }
26770
+ const self2 = await resolveSelfPrincipalForTenant(
26771
+ { ...auth, client: auth.client },
26772
+ deps.fetchImpl
26773
+ );
26774
+ if (!self2) throw new Error(selfPrincipalUnresolvedMessage(auth.tenant, "--as"));
26639
26775
  return self2.principalId;
26640
26776
  }
26641
26777
  if (!raw.startsWith("usr_")) {
@@ -26705,7 +26841,7 @@ async function runJoin(opts, deps = {}) {
26705
26841
  const client = auth.client;
26706
26842
  try {
26707
26843
  const tenantId = requireTenantId(auth);
26708
- const principalId = await resolvePrincipal(opts.as, auth);
26844
+ const principalId = await resolvePrincipal(opts.as, auth, deps);
26709
26845
  const userId = normalizePrincipalUserId(principalId);
26710
26846
  const scopes = hasActions ? [{ allowed_actions: splitCsv(opts.actions) }] : void 0;
26711
26847
  await client.auth.createAccessProfile({
@@ -26716,7 +26852,9 @@ async function runJoin(opts, deps = {}) {
26716
26852
  // Always send the plural `roleIds`, even for a single
26717
26853
  // `--role`: a strict superset of the deprecated singular `roleId`,
26718
26854
  // read uniformly by the platform regardless of how many compose.
26719
- roleIds: opts.role,
26855
+ // With `--actions` there is no role: omit `roleIds` (the platform
26856
+ // refuses an empty list next to inline `scopes`).
26857
+ roleIds: hasRole ? opts.role : void 0,
26720
26858
  scopes,
26721
26859
  identityOverrides
26722
26860
  }
@@ -26803,9 +26941,9 @@ function emitJoined(opts) {
26803
26941
 
26804
26942
  // src/build-info.ts
26805
26943
  var BUILD_INFO = {
26806
- cli: true ? "0.24.0" : "dev",
26807
- sdk: true ? "0.46.0" : "dev",
26808
- blueprints: true ? "0.20.1" : "dev"
26944
+ cli: true ? "0.25.0" : "dev",
26945
+ sdk: true ? "0.47.0" : "dev",
26946
+ blueprints: true ? "0.20.2" : "dev"
26809
26947
  };
26810
26948
  function formatBuildInfo() {
26811
26949
  return `vectros ${BUILD_INFO.cli} (sdk ${BUILD_INFO.sdk}, blueprints ${BUILD_INFO.blueprints})`;
@@ -26842,18 +26980,6 @@ function parseTenantFlag(raw) {
26842
26980
  }
26843
26981
  return raw;
26844
26982
  }
26845
- var NUMBER_FLAG_INVALID = /* @__PURE__ */ Symbol("parseOptionalNumberFlag:invalid");
26846
- function parseOptionalNumberFlag(flag, raw) {
26847
- if (raw === void 0) return void 0;
26848
- const n = Number(raw);
26849
- if (!Number.isFinite(n)) {
26850
- process.stderr.write(` \u2716 ${flag} must be a number (got '${raw}')
26851
- `);
26852
- process.exitCode = 2;
26853
- return NUMBER_FLAG_INVALID;
26854
- }
26855
- return n;
26856
- }
26857
26983
  function toCommonOptions(flags) {
26858
26984
  return {
26859
26985
  env: flags.env,
@@ -27025,7 +27151,7 @@ function registerContextCommands(program, deps) {
27025
27151
  );
27026
27152
  });
27027
27153
  const destroy = context.command("destroy").description(
27028
- "Permanently destroy an app context and ALL of its data (irreversible; asks for confirmation)."
27154
+ "Permanently destroy an app context and ALL of its data (irreversible; asks for confirmation). A blueprint's service principal belongs to the whole tenant and is NOT removed: delete it with 'identity delete'."
27029
27155
  ).argument("<contextId>", "the app context to destroy").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)");
27030
27156
  addCommonAuthOptions(destroy).action(
27031
27157
  async (contextId, flags) => {
@@ -27180,15 +27306,11 @@ function registerIssuersCommands(program, deps) {
27180
27306
  }
27181
27307
  function registerProviderConfigCommands(program, deps) {
27182
27308
  const providerConfig = program.command("provider-config").description("Manage BYOM (bring-your-own-model) tenant-level provider configs.");
27183
- const create = providerConfig.command("create").description("Create a new BYOM provider config (not idempotent \u2014 a repeat --alias 400s).").requiredOption("--alias <alias>", "operator-facing name used to select this config; unique per tenant").requiredOption("--provider-type <type>", "'ANTHROPIC_NATIVE' or 'OPENAI_COMPATIBLE'").option("--endpoint <url>", "required for OPENAI_COMPATIBLE (no default); an optional override for ANTHROPIC_NATIVE").option("--declared-processing-region <region>", "customer-asserted, free-text, Vectros-unverified transparency field").option("--default-provider-model-id <id>", "convenience default only \u2014 a per-request model id, when the caller supplies one, always takes precedence").requiredOption("--api-key-secret-file <path>", "path to a file holding the provider credential ('-' for stdin) \u2014 never pass the secret as a bare flag value").requiredOption("--waiver-type <type>", "the waiver code this config is entitled under (e.g. byo-model-inference:customer-key)").requiredOption("--billing-mode <mode>", "'PLATFORM_BILLED' or 'CUSTOMER_DIRECT'").option("--platform-token-factor <n>", "required when --billing-mode PLATFORM_BILLED; refused otherwise").option("--platform-output-weight <n>", "required when --billing-mode PLATFORM_BILLED; refused otherwise").option("--status <status>", "'ACTIVE' or 'DISABLED' (default ACTIVE) \u2014 ACTIVE requires the tenant already hold a granted waiver for --waiver-type").option("--tenant <tenant>", "tenant to create the config in: 'test' or 'live' (default live)");
27309
+ const create = providerConfig.command("create").description("Create a new BYOM provider config (not idempotent \u2014 a repeat --alias 400s).").requiredOption("--alias <alias>", "operator-facing name used to select this config; unique per tenant; 1-64 lowercase letters, digits, - or _").requiredOption("--provider-type <type>", "'ANTHROPIC_NATIVE' or 'OPENAI_COMPATIBLE'").option("--endpoint <url>", "required for OPENAI_COMPATIBLE (no default); an optional override for ANTHROPIC_NATIVE; https on port 443").option("--declared-processing-region <region>", "customer-asserted, free-text, Vectros-unverified transparency field").option("--default-provider-model-id <id>", "convenience default only \u2014 a per-request model id, when the caller supplies one, always takes precedence").requiredOption("--api-key-secret-file <path>", "path to a file holding the provider credential ('-' for stdin) \u2014 never pass the secret as a bare flag value").requiredOption("--waiver-type <type>", "the waiver code this config is entitled under; the platform accepts byo-model-inference:customer-key for a config you set up yourself").option("--billing-mode <mode>", "only 'CUSTOMER_DIRECT' (your provider bills you), which is also the default; 'PLATFORM_BILLED' is retired").option("--status <status>", "'ACTIVE' or 'DISABLED' (default ACTIVE) \u2014 ACTIVE requires the tenant already hold a granted waiver for --waiver-type").option("--tenant <tenant>", "tenant to create the config in: 'test' or 'live' (default live)");
27184
27310
  addCommonAuthOptions(create).action(
27185
27311
  async (flags) => {
27186
27312
  const tenant = parseTenantFlag(flags.tenant);
27187
27313
  if (tenant === TENANT_FLAG_INVALID) return;
27188
- const platformTokenFactor = parseOptionalNumberFlag("--platform-token-factor", flags.platformTokenFactor);
27189
- if (platformTokenFactor === NUMBER_FLAG_INVALID) return;
27190
- const platformOutputWeight = parseOptionalNumberFlag("--platform-output-weight", flags.platformOutputWeight);
27191
- if (platformOutputWeight === NUMBER_FLAG_INVALID) return;
27192
27314
  process.exitCode = await runProviderConfigCreate(
27193
27315
  {
27194
27316
  ...toCommonOptions(flags),
@@ -27200,8 +27322,6 @@ function registerProviderConfigCommands(program, deps) {
27200
27322
  apiKeySecretFile: flags.apiKeySecretFile,
27201
27323
  waiverType: flags.waiverType,
27202
27324
  billingMode: flags.billingMode,
27203
- platformTokenFactor,
27204
- platformOutputWeight,
27205
27325
  status: flags.status,
27206
27326
  tenant
27207
27327
  },
@@ -27219,13 +27339,9 @@ function registerProviderConfigCommands(program, deps) {
27219
27339
  addCommonAuthOptions(get).action(async (id, flags) => {
27220
27340
  process.exitCode = await runProviderConfigGet({ ...toCommonOptions(flags), id }, deps);
27221
27341
  });
27222
- const update = providerConfig.command("update").description("Patch a provider config (partial update \u2014 an omitted field is left unchanged).").argument("<id>").option("--alias <alias>", "rename this config (still unique per tenant)").option("--provider-type <type>", "'ANTHROPIC_NATIVE' or 'OPENAI_COMPATIBLE'").option("--endpoint <url>", "update the provider endpoint").option("--declared-processing-region <region>", "update the transparency field").option("--default-provider-model-id <id>", "update the convenience default model id").option("--api-key-secret-file <path>", "rotate the provider credential from a file ('-' for stdin); omit to leave the stored secret unchanged").option("--waiver-type <type>", "update the waiver code this config is entitled under").option("--billing-mode <mode>", "'PLATFORM_BILLED' or 'CUSTOMER_DIRECT' \u2014 switching to CUSTOMER_DIRECT clears the platform pricing fields server-side").option("--platform-token-factor <n>", "update the platform pricing factor (PLATFORM_BILLED only)").option("--platform-output-weight <n>", "update the platform pricing weight (PLATFORM_BILLED only)").option("--status <status>", "'ACTIVE' or 'DISABLED' \u2014 activating requires a granted waiver for this config's waiverType");
27342
+ const update = providerConfig.command("update").description("Patch a provider config (partial update \u2014 an omitted field is left unchanged).").argument("<id>").option("--alias <alias>", "rename this config (unique per tenant); the platform refuses a new alias that is not 1-64 lowercase letters, digits, - or _ (an alias the config already holds is accepted)").option("--provider-type <type>", "'ANTHROPIC_NATIVE' or 'OPENAI_COMPATIBLE'; changing the type of an existing config needs --endpoint stated in the same command (--endpoint '' means the provider's default), or the platform answers 400").option("--endpoint <url>", "update the provider endpoint; https on port 443; changing the host needs --api-key-secret-file again; '' clears it (the provider's default), which also changes the host").option("--declared-processing-region <region>", "update the transparency field").option("--default-provider-model-id <id>", "update the convenience default model id").option("--api-key-secret-file <path>", "rotate the provider credential from a file ('-' for stdin); omit to leave the stored secret unchanged (required when --endpoint changes the host)").option("--waiver-type <type>", "update the waiver code this config is entitled under; the platform refuses a change to anything but byo-model-inference:customer-key (a value the config already holds is accepted)").option("--billing-mode <mode>", "only 'CUSTOMER_DIRECT' (your provider bills you); 'PLATFORM_BILLED' is retired. Omit it to leave the stored mode unchanged").option("--status <status>", "'ACTIVE' or 'DISABLED' \u2014 activating requires a granted waiver for this config's waiverType");
27223
27343
  addCommonAuthOptions(update).action(
27224
27344
  async (id, flags) => {
27225
- const platformTokenFactor = parseOptionalNumberFlag("--platform-token-factor", flags.platformTokenFactor);
27226
- if (platformTokenFactor === NUMBER_FLAG_INVALID) return;
27227
- const platformOutputWeight = parseOptionalNumberFlag("--platform-output-weight", flags.platformOutputWeight);
27228
- if (platformOutputWeight === NUMBER_FLAG_INVALID) return;
27229
27345
  process.exitCode = await runProviderConfigUpdate(
27230
27346
  {
27231
27347
  ...toCommonOptions(flags),
@@ -27238,8 +27354,6 @@ function registerProviderConfigCommands(program, deps) {
27238
27354
  apiKeySecretFile: flags.apiKeySecretFile,
27239
27355
  waiverType: flags.waiverType,
27240
27356
  billingMode: flags.billingMode,
27241
- platformTokenFactor,
27242
- platformOutputWeight,
27243
27357
  status: flags.status
27244
27358
  },
27245
27359
  deps
@@ -27507,12 +27621,15 @@ function dimensionOption() {
27507
27621
  ).makeOptionMandatory();
27508
27622
  }
27509
27623
  function registerIdentityCommands(program, deps) {
27510
- const identity = program.command("identity").description("Manage tenant-wide principals (users + namespaced entities) \u2014 reference, never embed.");
27511
- const create = identity.command("create").description("Create (idempotent by externalId) a principal.").addOption(dimensionOption()).requiredOption("--external-id <id>", "your stable id for the principal (idempotency key)").option("--name <name>", "display name (entity namespaces only, not --type user)").option("--email <email>", "email (user only)").option("--service", "user only: mint a SERVICE principal (default HUMAN)").option("--scope <ns:value>", "entity only: a parent ownership edge (repeatable, \u22642, e.g. org:<id>)", collect, []).option("--metadata <json>", "JSON object of metadata");
27624
+ const identity = program.command("identity").description("Manage principals: users (tenant-wide) and namespaced entities (an entity lives with its namespace) \u2014 reference, never embed.");
27625
+ const create = identity.command("create").description("Create (idempotent by externalId) a principal.").addOption(dimensionOption()).requiredOption("--external-id <id>", "your stable id for the principal (idempotency key)").option("--name <name>", "display name (entity namespaces only, not --type user)").option("--email <email>", "email (user only)").option("--service", "user only: mint a SERVICE principal (default HUMAN)").option("--scope <ns:value>", "entity only: a parent ownership edge (repeatable, \u22642, e.g. org:<id>)", collect, []).option("--metadata <json>", "JSON object of metadata").option("--tenant <tenant>", "tenant to target: 'test' or 'live' (default live)");
27512
27626
  addCommonAuthOptions(create).action(
27513
27627
  async (flags) => {
27628
+ const tenant = parseTenantFlag(flags.tenant);
27629
+ if (tenant === TENANT_FLAG_INVALID) return;
27514
27630
  process.exitCode = await runIdentityCreate({
27515
27631
  ...toCommonOptions(flags),
27632
+ tenant,
27516
27633
  type: flags.type,
27517
27634
  externalId: flags.externalId,
27518
27635
  name: flags.name,
@@ -27523,11 +27640,14 @@ function registerIdentityCommands(program, deps) {
27523
27640
  }, deps);
27524
27641
  }
27525
27642
  );
27526
- const list = identity.command("list").description("List principals of a dimension (optionally filtered).").addOption(dimensionOption()).option("--external-id <id>", "filter to a stable id").option("--limit <n>", "page size (1\u2013100)", (v) => Number.parseInt(v, 10));
27643
+ const list = identity.command("list").description("List principals of a dimension (optionally filtered).").addOption(dimensionOption()).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 target: 'test' or 'live' (default live)");
27527
27644
  addCommonAuthOptions(list).action(
27528
27645
  async (flags) => {
27646
+ const tenant = parseTenantFlag(flags.tenant);
27647
+ if (tenant === TENANT_FLAG_INVALID) return;
27529
27648
  process.exitCode = await runIdentityList({
27530
27649
  ...toCommonOptions(flags),
27650
+ tenant,
27531
27651
  type: flags.type,
27532
27652
  externalId: flags.externalId,
27533
27653
  limit: flags.limit
@@ -27547,20 +27667,30 @@ function registerIdentityCommands(program, deps) {
27547
27667
  );
27548
27668
  }
27549
27669
  );
27550
- 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)");
27670
+ 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)").option("--tenant <tenant>", "tenant to target: 'test' or 'live' (default live)");
27551
27671
  addCommonAuthOptions(get).action(async (flags) => {
27552
- process.exitCode = await runIdentityGet({ ...toCommonOptions(flags), type: flags.type, id: flags.id }, deps);
27672
+ const tenant = parseTenantFlag(flags.tenant);
27673
+ if (tenant === TENANT_FLAG_INVALID) return;
27674
+ process.exitCode = await runIdentityGet({ ...toCommonOptions(flags), tenant, type: flags.type, id: flags.id }, deps);
27553
27675
  });
27554
- const del = identity.command("delete").description("Delete a principal by its Vectros id.").addOption(dimensionOption()).requiredOption("--id <vectrosId>", "the Vectros-assigned id");
27676
+ const del = identity.command("delete").description("Delete a principal by its Vectros id.").addOption(dimensionOption()).requiredOption("--id <vectrosId>", "the Vectros-assigned id").option("--tenant <tenant>", "tenant to target: 'test' or 'live' (default live)");
27555
27677
  addAuthOptions(del).action(async (flags) => {
27556
- process.exitCode = await runIdentityDelete({ ...toCommonOptions(flags), type: flags.type, id: flags.id }, deps);
27678
+ const tenant = parseTenantFlag(flags.tenant);
27679
+ if (tenant === TENANT_FLAG_INVALID) return;
27680
+ process.exitCode = await runIdentityDelete(
27681
+ { ...toCommonOptions(flags), tenant, type: flags.type, id: flags.id },
27682
+ deps
27683
+ );
27557
27684
  });
27558
- const invite = identity.command("invite").description("Onboard a person into a context by email (POST /v1/users/invite) \u2014 no root key required.").requiredOption("--context <contextId>", "the app context the invitee will operate in").requiredOption("--role <roleId>", "an existing Role in that context; the invitee inherits its scopes").requiredOption("--email <email>", "the invitee's email address").option("--first-name <name>", "personalizes the invitation email greeting").option("--accept-url <url>", "your accept-invite landing page (required unless --no-send-email)").option("--no-send-email", "skip platform delivery; the response carries the raw inviteToken/acceptLink instead");
27685
+ const invite = identity.command("invite").description("Onboard a person into a context by email (POST /v1/users/invite) \u2014 no root key required.").requiredOption("--context <contextId>", "the app context the invitee will operate in").requiredOption("--role <roleId>", "an existing Role in that context; the invitee inherits its scopes").requiredOption("--email <email>", "the invitee's email address").option("--first-name <name>", "personalizes the invitation email greeting").option("--accept-url <url>", "your accept-invite landing page (required unless --no-send-email)").option("--no-send-email", "skip platform delivery; the response carries the raw inviteToken/acceptLink instead").option("--tenant <tenant>", "tenant to target: 'test' or 'live' (default live)");
27559
27686
  addCommonAuthOptions(invite).action(
27560
27687
  async (flags) => {
27688
+ const tenant = parseTenantFlag(flags.tenant);
27689
+ if (tenant === TENANT_FLAG_INVALID) return;
27561
27690
  process.exitCode = await runIdentityInvite(
27562
27691
  {
27563
27692
  ...toCommonOptions(flags),
27693
+ tenant,
27564
27694
  context: flags.context,
27565
27695
  role: flags.role,
27566
27696
  email: flags.email,