@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.js CHANGED
@@ -1614,7 +1614,7 @@ var require_proper_lockfile = __commonJS({
1614
1614
  // src/cli-bootstrap.ts
1615
1615
  var readline2 = __toESM(require("readline/promises"));
1616
1616
 
1617
- // node_modules/@vectros-ai/sdk/dist/index.mjs
1617
+ // ../../node_modules/@vectros-ai/sdk/dist/index.mjs
1618
1618
  var __defProp2 = Object.defineProperty;
1619
1619
  var __export = (target, all) => {
1620
1620
  for (var name in all)
@@ -4044,7 +4044,7 @@ var AuthClient = class {
4044
4044
  return handleNonStatusCodeError(_response.error, _response.rawResponse, "GET", "/v1/auth/jwks");
4045
4045
  }
4046
4046
  /**
4047
- * 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.
4047
+ * 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.
4048
4048
  *
4049
4049
  * @param {Vectros.GetAccessLogRequest} request
4050
4050
  * @param {AuthClient.RequestOptions} requestOptions - Request-specific configuration.
@@ -5749,7 +5749,7 @@ var AuthClient = class {
5749
5749
  return handleNonStatusCodeError(_response.error, _response.rawResponse, "GET", "/v1/auth/issuers/{issuerId}");
5750
5750
  }
5751
5751
  /**
5752
- * 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.
5752
+ * 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.
5753
5753
  *
5754
5754
  * @param {Vectros.IssuerUpdateRequest} request
5755
5755
  * @param {AuthClient.RequestOptions} requestOptions - Request-specific configuration.
@@ -6559,7 +6559,7 @@ var ComplianceClient = class {
6559
6559
  this._options = normalizeClientOptionsWithAuth(options);
6560
6560
  }
6561
6561
  /**
6562
- * 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.
6562
+ * 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.
6563
6563
  *
6564
6564
  * @param {Vectros.ErasureRequest} request
6565
6565
  * @param {ComplianceClient.RequestOptions} requestOptions - Request-specific configuration.
@@ -6622,7 +6622,7 @@ var ComplianceClient = class {
6622
6622
  return handleNonStatusCodeError(_response.error, _response.rawResponse, "POST", "/v1/erasure-requests");
6623
6623
  }
6624
6624
  /**
6625
- * 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.
6625
+ * 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.
6626
6626
  *
6627
6627
  * @param {Vectros.GetErasureRequestRequest} request
6628
6628
  * @param {ComplianceClient.RequestOptions} requestOptions - Request-specific configuration.
@@ -9441,6 +9441,8 @@ var InferenceClient = class {
9441
9441
  throw new PaymentRequiredError(_response.error.body, _response.rawResponse);
9442
9442
  case 403:
9443
9443
  throw new ForbiddenError(_response.error.body, _response.rawResponse);
9444
+ case 413:
9445
+ throw new ContentTooLargeError(_response.error.body, _response.rawResponse);
9444
9446
  case 429:
9445
9447
  throw new TooManyRequestsError(_response.error.body, _response.rawResponse);
9446
9448
  default:
@@ -9580,6 +9582,8 @@ var InferenceClient = class {
9580
9582
  throw new PaymentRequiredError(_response.error.body, _response.rawResponse);
9581
9583
  case 403:
9582
9584
  throw new ForbiddenError(_response.error.body, _response.rawResponse);
9585
+ case 413:
9586
+ throw new ContentTooLargeError(_response.error.body, _response.rawResponse);
9583
9587
  case 429:
9584
9588
  throw new TooManyRequestsError(_response.error.body, _response.rawResponse);
9585
9589
  case 500:
@@ -11877,7 +11881,7 @@ var RenderHintsSchema = import_zod.z.object({
11877
11881
  helpText: import_zod.z.string().optional(),
11878
11882
  // Marks this field as the record's headline (display) field — the linked
11879
11883
  // primary column in the records list + the title on the detail view. At most
11880
- // one per schema (the platform takes the first by order). Format passthrough →
11884
+ // one per schema (the platform stores the hint and does not enforce one; the reference UI uses the first by order). Format passthrough →
11881
11885
  // SchemaRequest.renderHints[fieldId].displayField.
11882
11886
  displayField: import_zod.z.boolean().optional()
11883
11887
  }).strict();
@@ -13536,7 +13540,8 @@ var BlueprintSchema = import_zod.z.object({
13536
13540
  * alongside app-context/service-principal creation and issuers, BEFORE
13537
13541
  * schemas/seed (an `entityBacked` namespace's entities, and an
13538
13542
  * `allowedSurfaces: ['entity']` schema, both need the registration to
13539
- * already exist). `org`/`client` need no entry — they exist already.
13543
+ * already exist). `org`/`client` are registered the same way as any other
13544
+ * namespace: signup does not register them, so declare an entry for each you use.
13540
13545
  */
13541
13546
  namespaces: import_zod.z.array(BlueprintNamespaceSchema).optional(),
13542
13547
  /**
@@ -16500,6 +16505,25 @@ async function clearSession(baseDir) {
16500
16505
  return false;
16501
16506
  }
16502
16507
  }
16508
+ var TokenEndpointError = class extends Error {
16509
+ status;
16510
+ /** The OAuth `error` field of a JSON error body (e.g. `invalid_grant`), when there is one. */
16511
+ oauthError;
16512
+ constructor(status, text) {
16513
+ super(`token endpoint HTTP ${status}${text ? ` \u2014 ${stripControlCharacters(text, 500)}` : ""}`);
16514
+ this.name = "TokenEndpointError";
16515
+ this.status = status;
16516
+ this.oauthError = oauthErrorOf(text);
16517
+ }
16518
+ };
16519
+ function oauthErrorOf(text) {
16520
+ try {
16521
+ const parsed = JSON.parse(text);
16522
+ return typeof parsed.error === "string" ? parsed.error : void 0;
16523
+ } catch {
16524
+ return void 0;
16525
+ }
16526
+ }
16503
16527
  async function postToken(config, body, fetchImpl) {
16504
16528
  const res = await fetchImpl(tokenEndpoint(config), {
16505
16529
  method: "POST",
@@ -16507,9 +16531,7 @@ async function postToken(config, body, fetchImpl) {
16507
16531
  body: body.toString()
16508
16532
  });
16509
16533
  const text = await res.text().catch(() => "");
16510
- if (!res.ok) {
16511
- throw new Error(`token endpoint HTTP ${res.status}${text ? ` \u2014 ${stripControlCharacters(text, 500)}` : ""}`);
16512
- }
16534
+ if (!res.ok) throw new TokenEndpointError(res.status, text);
16513
16535
  try {
16514
16536
  return JSON.parse(text);
16515
16537
  } catch {
@@ -16525,31 +16547,54 @@ async function refreshTokens(config, refreshToken, fetchImpl = fetch) {
16525
16547
 
16526
16548
  // src/auth-context.ts
16527
16549
  var COGNITO_TOKEN_ENV = "VECTROS_BOOTSTRAP_TOKEN";
16550
+ function loginCommandFor(env) {
16551
+ return env === "staging" ? "vectros login --env staging" : "vectros login";
16552
+ }
16528
16553
  var MissingCredentialError = class extends Error {
16529
- constructor() {
16554
+ constructor(env) {
16530
16555
  super(
16531
- `No credential found. Set ${COGNITO_TOKEN_ENV} or pass --token (generate a token in the developer portal, or run \`vectros login\` once it ships).`
16556
+ `No credential found. Run \`${loginCommandFor(env)}\` to sign in, or set ${COGNITO_TOKEN_ENV} or pass --token (generate a token in the developer portal).`
16532
16557
  );
16533
16558
  this.name = "MissingCredentialError";
16534
16559
  }
16535
16560
  };
16561
+ var SessionExpiredError = class extends Error {
16562
+ constructor(env) {
16563
+ super(
16564
+ `Your Vectros login session has expired or was revoked. Run \`${loginCommandFor(env)}\` to sign in again.`
16565
+ );
16566
+ this.name = "SessionExpiredError";
16567
+ }
16568
+ };
16536
16569
  function resolveCognitoTokenSync(explicit) {
16537
16570
  if (explicit && explicit.trim()) return explicit.trim();
16538
16571
  const fromEnv = process.env[COGNITO_TOKEN_ENV];
16539
16572
  if (fromEnv && fromEnv.trim()) return fromEnv.trim();
16540
16573
  return void 0;
16541
16574
  }
16542
- async function resolveCognitoToken(env, explicit) {
16575
+ async function resolveCognitoToken(env, explicit, deps = { readSession, refreshTokens, saveSession }) {
16543
16576
  const sync = resolveCognitoTokenSync(explicit);
16544
16577
  if (sync) return sync;
16545
- const session = await readSession();
16578
+ const session = await deps.readSession();
16546
16579
  if (session && session.env === env) {
16547
16580
  const config = resolveCognitoConfig(env);
16548
- const tokens = await refreshTokens(config, session.refreshToken);
16581
+ let tokens;
16582
+ try {
16583
+ tokens = await deps.refreshTokens(config, session.refreshToken);
16584
+ } catch (err) {
16585
+ if (err instanceof TokenEndpointError && err.oauthError === "invalid_grant") throw new SessionExpiredError(env);
16586
+ throw err;
16587
+ }
16588
+ if (tokens.refresh_token && tokens.refresh_token !== session.refreshToken) {
16589
+ const stored = await deps.readSession().catch(() => void 0);
16590
+ if (stored?.refreshToken === session.refreshToken) {
16591
+ await deps.saveSession({ ...session, refreshToken: tokens.refresh_token }).catch(() => void 0);
16592
+ }
16593
+ }
16549
16594
  const bearer = tokens.id_token ?? tokens.access_token;
16550
16595
  if (bearer) return bearer;
16551
16596
  }
16552
- throw new MissingCredentialError();
16597
+ throw new MissingCredentialError(env);
16553
16598
  }
16554
16599
  async function resolveAuthContext(opts) {
16555
16600
  const baseUrl = envToBaseUrl(opts.env, opts.baseUrl ?? process.env.VECTROS_API_BASE_URL);
@@ -17061,6 +17106,7 @@ function keyNameFor(pack, machineId) {
17061
17106
  }
17062
17107
 
17063
17108
  // src/identity-resolver.ts
17109
+ var IDENTITIES_TO_ENSURE_HEADING = " Identities to ensure (idempotent by external id; a non-user entity lives with its namespace):";
17064
17110
  function userPayload(decl) {
17065
17111
  const meta = decl.metadata;
17066
17112
  if (decl.displayName === void 0) return meta;
@@ -17201,7 +17247,7 @@ async function withAuth(opts, deps, body) {
17201
17247
  log
17202
17248
  });
17203
17249
  } catch (err) {
17204
- if (err instanceof MissingCredentialError || err instanceof InvalidBaseUrlError || err instanceof InvalidCognitoRegionError) {
17250
+ if (err instanceof MissingCredentialError || err instanceof SessionExpiredError || err instanceof InvalidBaseUrlError || err instanceof InvalidCognitoRegionError) {
17205
17251
  process.stderr.write(` \u2716 ${err.message}
17206
17252
  `);
17207
17253
  return 2;
@@ -17796,15 +17842,6 @@ async function provisionContext(client, input) {
17796
17842
  const { blueprint, allowTenantWideNamespaces = false, readExistingKey, confirmExistingPrincipal, log } = input;
17797
17843
  assertBlueprintGates(blueprint);
17798
17844
  assertTenantWideNamespacesAllowed(blueprint, allowTenantWideNamespaces);
17799
- const ctxResp = await client.auth.createAppContext({
17800
- body: {
17801
- contextId: blueprint.contextId,
17802
- name: contextNameOf(blueprint),
17803
- ...blueprint.identityProjectionClaims === void 0 ? {} : { identityProjectionClaims: blueprint.identityProjectionClaims }
17804
- }
17805
- });
17806
- const contextAction = actionOf(ctxResp.created);
17807
- log.debug({ component: "loader", contextId: blueprint.contextId, contextAction }, "app-context ensured");
17808
17845
  const user = await client.identity.createUser({
17809
17846
  body: {
17810
17847
  externalId: blueprint.servicePrincipal.externalId,
@@ -17845,6 +17882,15 @@ async function provisionContext(client, input) {
17845
17882
  "existing-principal collision confirmed (or unwired) \u2014 proceeding with any further write to this principal"
17846
17883
  );
17847
17884
  }
17885
+ const ctxResp = await client.auth.createAppContext({
17886
+ body: {
17887
+ contextId: blueprint.contextId,
17888
+ name: contextNameOf(blueprint),
17889
+ ...blueprint.identityProjectionClaims === void 0 ? {} : { identityProjectionClaims: blueprint.identityProjectionClaims }
17890
+ }
17891
+ });
17892
+ const contextAction = actionOf(ctxResp.created);
17893
+ log.debug({ component: "loader", contextId: blueprint.contextId, contextAction }, "app-context ensured");
17848
17894
  const needsContextPinnedBootstrap = (blueprint.namespaces ?? []).length > 0;
17849
17895
  const ctxBootstrapClient = needsContextPinnedBootstrap ? input.remintBootstrapForContext ? await input.remintBootstrapForContext(blueprint.contextId) : client : void 0;
17850
17896
  if (ctxBootstrapClient && input.remintBootstrapForContext) {
@@ -18353,14 +18399,14 @@ function renderPlanDataScope(ds) {
18353
18399
  function buildPlan(blueprint, opts = {}) {
18354
18400
  const steps = [];
18355
18401
  const projection = blueprint.identityProjectionClaims?.length ? `, identity projection: [${blueprint.identityProjectionClaims.join(", ")}]` : blueprint.identityProjectionClaims ? ", identity projection: disabled (empty list)" : "";
18356
- steps.push({
18357
- kind: "context",
18358
- summary: `context '${blueprint.contextId}' (${contextNameOf(blueprint)}${projection})`
18359
- });
18360
18402
  steps.push({
18361
18403
  kind: "principal",
18362
18404
  summary: `principal '${blueprint.servicePrincipal.externalId}' (SERVICE)`
18363
18405
  });
18406
+ steps.push({
18407
+ kind: "context",
18408
+ summary: `context '${blueprint.contextId}' (${contextNameOf(blueprint)}${projection})`
18409
+ });
18364
18410
  for (const ns of blueprint.namespaces ?? []) {
18365
18411
  const ownerNote = ns.tenantWide ? "TENANT-WIDE \u2014 visible to every context" : `owned by '${blueprint.contextId}'`;
18366
18412
  const entityNote = ns.entityBacked ? ", entity-backed" : "";
@@ -18915,9 +18961,8 @@ var OsKeychainSecretStore = class {
18915
18961
  * enumeration, and `deleteCredential` on a nonexistent ref returns the same
18916
18962
  * meaningless `false` as Linux. So the `.some(c => c.account === ref)` comparison
18917
18963
  * is sound on both measured platforms. Still unmeasured: a CI lane that runs this
18918
- * against the real Windows store on every MR — the dev-machine probe is a point
18919
- * measurement, not a regression gate. (Tracked internally; this file ships to
18920
- * public npm, so no tracker number here.)
18964
+ * against the real Windows store on every change — the dev-machine probe is a point
18965
+ * measurement, not a regression gate.
18921
18966
  *
18922
18967
  * ⚠️ THIS METHOD'S FAIL-CLOSED GUARANTEE IS AN UPSTREAM BEHAVIOURAL CONTRACT, NOT
18923
18968
  * SOMETHING THIS CODE ENFORCES. `@napi-rs/keyring` is pinned to an EXACT version
@@ -19073,7 +19118,7 @@ var CryptoFileSecretStore = class {
19073
19118
  async setSecret(ref, secret) {
19074
19119
  const key = await this.encryptionKey();
19075
19120
  const iv = (0, import_node_crypto3.randomBytes)(IV_LEN);
19076
- const cipher = (0, import_node_crypto3.createCipheriv)(ALGO, key, iv);
19121
+ const cipher = (0, import_node_crypto3.createCipheriv)(ALGO, key, iv, { authTagLength: TAG_LEN });
19077
19122
  const enc = Buffer.concat([cipher.update(secret, "utf8"), cipher.final()]);
19078
19123
  const tag = cipher.getAuthTag();
19079
19124
  const blob = Buffer.concat([iv, tag, enc]);
@@ -19096,7 +19141,7 @@ var CryptoFileSecretStore = class {
19096
19141
  const iv = blob.subarray(0, IV_LEN);
19097
19142
  const tag = blob.subarray(IV_LEN, IV_LEN + TAG_LEN);
19098
19143
  const enc = blob.subarray(IV_LEN + TAG_LEN);
19099
- const decipher = (0, import_node_crypto3.createDecipheriv)(ALGO, key, iv);
19144
+ const decipher = (0, import_node_crypto3.createDecipheriv)(ALGO, key, iv, { authTagLength: TAG_LEN });
19100
19145
  decipher.setAuthTag(tag);
19101
19146
  return Buffer.concat([decipher.update(enc), decipher.final()]).toString("utf8");
19102
19147
  } catch {
@@ -19296,7 +19341,7 @@ async function withIndexLock(dir, fn, timings = LOCK_OPTS) {
19296
19341
  // judged: two waiters can both condemn one crash remnant, and the second's
19297
19342
  // already-decided rmdir destroys the first's FRESH lock. Nothing freezes.
19298
19343
  //
19299
- // MEASURED against this exact config (probes recorded in the MR):
19344
+ // MEASURED against this exact config (probe results below):
19300
19345
  // real OS processes, no injection : 0/25 and 1/25 (two independent runs)
19301
19346
  // in-process, 5 waiters : ~3% per round
19302
19347
  // rival's rmdir lagged one turn : 19/25
@@ -21129,7 +21174,7 @@ async function resolveBridgeCognitoToken(args, prompt, isTty, resolveCognito = r
21129
21174
  }
21130
21175
  if (!isTty) {
21131
21176
  throw new Error(
21132
- "No bridge token: set VECTROS_BOOTSTRAP_TOKEN, pass --token, or run `vectros login` first (required in non-interactive mode)."
21177
+ `No bridge token: set VECTROS_BOOTSTRAP_TOKEN, pass --token, or run \`${loginCommandFor(args.env)}\` first (required in non-interactive mode).`
21133
21178
  );
21134
21179
  }
21135
21180
  process.stdout.write(
@@ -21238,7 +21283,9 @@ ${renderPlan(blueprint, planSteps)}
21238
21283
  `);
21239
21284
  const declaredIdentities = Object.entries(blueprint.identities ?? {});
21240
21285
  if (declaredIdentities.length > 0) {
21241
- process.stdout.write("\n Identities to ensure (tenant-wide, idempotent by external id):\n");
21286
+ process.stdout.write(`
21287
+ ${IDENTITIES_TO_ENSURE_HEADING}
21288
+ `);
21242
21289
  for (const [name, decl] of declaredIdentities) {
21243
21290
  process.stdout.write(` ~ ${name} (${decl.kind}) \u2014 external id '${decl.externalId}'
21244
21291
  `);
@@ -21469,6 +21516,12 @@ ${codeAddHint}`);
21469
21516
  }
21470
21517
  return finishApply();
21471
21518
  } catch (err) {
21519
+ if (err instanceof SessionExpiredError) {
21520
+ process.stderr.write(`
21521
+ \u2716 ${err.message}
21522
+ `);
21523
+ return 2;
21524
+ }
21472
21525
  if (err instanceof LostKeyError) {
21473
21526
  process.stderr.write(`
21474
21527
  \u2716 ${err.message}
@@ -21840,7 +21893,9 @@ ${renderPlan(blueprint, buildPlan(blueprint, { noSeed, noKey: true }), { prune:
21840
21893
  process.stdout.write(applySeedNotice(blueprint, args.seed));
21841
21894
  const declaredIdentities = Object.entries(blueprint.identities ?? {});
21842
21895
  if (declaredIdentities.length > 0) {
21843
- process.stdout.write("\n Identities to ensure (tenant-wide, idempotent by external id):\n");
21896
+ process.stdout.write(`
21897
+ ${IDENTITIES_TO_ENSURE_HEADING}
21898
+ `);
21844
21899
  for (const [name, decl] of declaredIdentities) {
21845
21900
  process.stdout.write(` ~ ${name} (${decl.kind}) \u2014 external id '${decl.externalId}'
21846
21901
  `);
@@ -21973,7 +22028,7 @@ function reportApplyError(err) {
21973
22028
  If this is expected (a re-apply of a blueprint you manage), re-run with --confirm-existing-principal
21974
22029
  (with --yes in a pipeline, where it is standing consent), or confirm at the prompt.
21975
22030
  If you don't recognize this identity, investigate the blueprint's servicePrincipal.externalId first.
21976
- Only the app-context and the principal lookup had run; no grant or anything else was changed.
22031
+ Only the principal lookup had run; no context, grant or anything else was created or changed.
21977
22032
  `
21978
22033
  );
21979
22034
  return 3;
@@ -22326,10 +22381,14 @@ schemas:
22326
22381
  accessProfile:
22327
22382
  # SCOPE GATE: data-plane only (records/schemas/search/documents/folders/inference/entities),
22328
22383
  # PLUS profiles:r/u/d (bounded by the platform to redistributing access you already have,
22329
- # never granting more) and profiles:c:self (create a profile for yourself only \u2014 the
22330
- # platform resolves "self" from the caller, not from anything you declare here). A bare
22331
- # profiles:c, or profiles:c:usr_<id> naming another principal, is HARD-REJECTED \u2014 by
22332
- # design, no override. Same for keys/users/admin/namespaces/*.
22384
+ # never granting more), profiles:c:self (create a profile for yourself only \u2014 the
22385
+ # platform resolves "self" from the caller, not from anything you declare here) and
22386
+ # scripts:x:<script-name> (run one named script that this blueprint ships; there is no bare
22387
+ # or wildcard form).
22388
+ # A bare profiles:c is accepted ONLY when this same clause also declares the
22389
+ # member-lifecycle capability (see "capabilities" below); without it, and always for
22390
+ # profiles:c:usr_<id> naming another principal, it is REJECTED \u2014 no override. Every other
22391
+ # control-plane resource (keys/users/admin/namespaces/triggers/...) and "*" is REJECTED the same way.
22333
22392
  # Ops: c|r|u|d, combinable (records:cru). Prefer the narrowest set (least privilege).
22334
22393
  allowedActions: [records:r, records:c, records:u, search:r, schemas:r]
22335
22394
  # dataScope binds the key to specific owners. Include null to ALSO see
@@ -22345,8 +22404,10 @@ accessProfile:
22345
22404
  # above) unless every owner genuinely sharing every record is what you want.
22346
22405
  dataScope: {}
22347
22406
  # capabilities is a SEPARATE gated dimension from allowedActions \u2014 named platform
22348
- # effects a resource:ops verb can't express. Only 'member-lifecycle' is admitted
22349
- # today; every other name is HARD-REJECTED, same posture as the scope gate above.
22407
+ # effects a resource:ops verb can't express. Only 'member-lifecycle' and
22408
+ # 'delegate-principal-stamp' are admitted today; every other name is REJECTED, same
22409
+ # posture as the scope gate above. 'member-lifecycle' is also what lets this clause
22410
+ # carry a bare profiles:c.
22350
22411
  # capabilities: [member-lifecycle]
22351
22412
 
22352
22413
  servicePrincipal:
@@ -22433,8 +22494,10 @@ Usage:
22433
22494
 
22434
22495
  <file|name> is a path to your own blueprint (YAML .yaml/.yml or JSON .json) OR
22435
22496
  the name of a bundled blueprint. The same scope gate that protects bundled
22436
- blueprints applies to file blueprints \u2014 a forked or hand-written blueprint can
22437
- never request control-plane access.
22497
+ blueprints applies to file blueprints: beyond data-plane access, a forked or
22498
+ hand-written blueprint can request only a short allowlist of narrow exceptions
22499
+ (profiles:r/u/d, profiles:c:self, a bare profiles:c beside member-lifecycle,
22500
+ scripts:x:<script-name>); every other control-plane scope is rejected.
22438
22501
 
22439
22502
  A blueprint may declare a top-level 'inputs:' block and reference its values
22440
22503
  with \${{ inputs.<name> }} (and the built-ins \${{ vectros.context }} /
@@ -22546,7 +22609,9 @@ ${renderPlan(blueprint, steps)}
22546
22609
  `);
22547
22610
  const declaredIdentities = Object.entries(blueprint.identities ?? {});
22548
22611
  if (declaredIdentities.length > 0) {
22549
- process.stdout.write("\n Identities to ensure (tenant-wide, idempotent by external id):\n");
22612
+ process.stdout.write(`
22613
+ ${IDENTITIES_TO_ENSURE_HEADING}
22614
+ `);
22550
22615
  for (const [name, decl] of declaredIdentities) {
22551
22616
  process.stdout.write(` ~ ${name} (${decl.kind}) \u2014 external id '${decl.externalId}'
22552
22617
  `);
@@ -22680,6 +22745,89 @@ ${BLUEPRINT_HELP}
22680
22745
  // src/program.ts
22681
22746
  var import_commander = require("commander");
22682
22747
 
22748
+ // src/dev-api.ts
22749
+ var DeveloperApiError = class extends Error {
22750
+ statusCode;
22751
+ /** The platform's error body, when it answered with one (its message, request id and error code). */
22752
+ body;
22753
+ constructor(message, statusCode, body) {
22754
+ super(message);
22755
+ this.name = "DeveloperApiError";
22756
+ this.statusCode = statusCode;
22757
+ this.body = body;
22758
+ }
22759
+ };
22760
+ async function fetchDeveloperUsersPage(opts) {
22761
+ const { cognitoToken, apiBaseUrl, tenant, startFrom, limit, fetchImpl = fetch } = opts;
22762
+ const base = apiBaseUrl.replace(/\/$/, "");
22763
+ let url = `${base}/developer/users?tenant=${encodeURIComponent(tenant)}`;
22764
+ if (startFrom) url += `&startFrom=${encodeURIComponent(startFrom)}`;
22765
+ if (limit !== void 0) url += `&limit=${encodeURIComponent(String(limit))}`;
22766
+ let res;
22767
+ try {
22768
+ res = await fetchImpl(url, {
22769
+ method: "GET",
22770
+ headers: { Authorization: `Bearer ${cognitoToken}` }
22771
+ });
22772
+ } catch (err) {
22773
+ throw new DeveloperApiError(
22774
+ `Request to /developer/users failed: ${err instanceof Error ? err.message : String(err)}`
22775
+ );
22776
+ }
22777
+ if (!res.ok) {
22778
+ const body = await res.text().catch(() => "");
22779
+ const hint = res.status === 403 ? " (this command requires an account owner login \u2014 sign in as the owner and retry)" : "";
22780
+ throw new DeveloperApiError(
22781
+ `GET /developer/users failed: HTTP ${res.status}${hint}${body ? ` \u2014 ${body}` : ""}`,
22782
+ res.status
22783
+ );
22784
+ }
22785
+ let parsed;
22786
+ try {
22787
+ parsed = await res.json();
22788
+ } catch {
22789
+ throw new DeveloperApiError("GET /developer/users returned a non-JSON body");
22790
+ }
22791
+ const data = Array.isArray(parsed.data) ? parsed.data : [];
22792
+ const nextCursor = typeof parsed.nextCursor === "string" ? parsed.nextCursor : null;
22793
+ return { data, nextCursor };
22794
+ }
22795
+ async function fetchDeveloperMe(opts) {
22796
+ const { cognitoToken, apiBaseUrl, tenant, fetchImpl = fetch } = opts;
22797
+ const url = `${apiBaseUrl.replace(/\/$/, "")}/developer/me?tenant=${encodeURIComponent(tenant)}`;
22798
+ let res;
22799
+ try {
22800
+ res = await fetchImpl(url, { method: "GET", headers: { Authorization: `Bearer ${cognitoToken}` } });
22801
+ } catch (err) {
22802
+ throw new DeveloperApiError(
22803
+ `Request to /developer/me failed: ${err instanceof Error ? err.message : String(err)}`
22804
+ );
22805
+ }
22806
+ if (res.status === 404) return null;
22807
+ if (!res.ok) {
22808
+ const failure = await devRouteError(res, "GET /developer/me");
22809
+ throw new DeveloperApiError(
22810
+ `GET /developer/me failed: HTTP ${res.status}`,
22811
+ res.status,
22812
+ failure.body
22813
+ );
22814
+ }
22815
+ let parsed;
22816
+ try {
22817
+ parsed = await res.json();
22818
+ } catch {
22819
+ throw new DeveloperApiError("GET /developer/me returned a non-JSON body");
22820
+ }
22821
+ if (typeof parsed.principalId !== "string" || typeof parsed.userId !== "string") {
22822
+ throw new DeveloperApiError("GET /developer/me returned no principal");
22823
+ }
22824
+ return {
22825
+ principalId: parsed.principalId,
22826
+ userId: parsed.userId,
22827
+ tenantId: typeof parsed.tenantId === "string" ? parsed.tenantId : void 0
22828
+ };
22829
+ }
22830
+
22683
22831
  // src/self-principal.ts
22684
22832
  var PRINCIPAL_PREFIX_USER = "usr_";
22685
22833
  function emailFromJwt(token) {
@@ -22708,6 +22856,37 @@ async function resolveSelfPrincipal(client, cognitoToken) {
22708
22856
  if (!match?.id) return null;
22709
22857
  return { principalId: `${PRINCIPAL_PREFIX_USER}${match.id}`, userId: match.id, email };
22710
22858
  }
22859
+ async function resolveSelfPrincipalForTenant(auth, fetchImpl) {
22860
+ const tenant = auth.tenant ?? "live";
22861
+ let me;
22862
+ try {
22863
+ me = await fetchDeveloperMe({
22864
+ cognitoToken: auth.cognitoToken,
22865
+ apiBaseUrl: auth.baseUrl,
22866
+ tenant,
22867
+ ...fetchImpl ? { fetchImpl } : {}
22868
+ });
22869
+ } catch (err) {
22870
+ if (err instanceof DeveloperApiError && err.statusCode !== void 0) {
22871
+ if (err.body?.errorCode === "SESSION_REFRESH_REQUIRED") throw new SessionExpiredError(auth.env ?? "production");
22872
+ throw err;
22873
+ }
22874
+ if (tenant === "test") throw err;
22875
+ return resolveSelfPrincipal(auth.client, auth.cognitoToken);
22876
+ }
22877
+ if (me === null) return null;
22878
+ const forThisTenant = Boolean(auth.tenantId && me.tenantId && me.tenantId === auth.tenantId);
22879
+ if (forThisTenant && me.principalId.startsWith(PRINCIPAL_PREFIX_USER)) {
22880
+ return { principalId: me.principalId, userId: me.userId, email: emailFromJwt(auth.cognitoToken) ?? "" };
22881
+ }
22882
+ return tenant === "live" ? resolveSelfPrincipal(auth.client, auth.cognitoToken) : null;
22883
+ }
22884
+ function selfPrincipalUnresolvedMessage(tenant, flag) {
22885
+ if (tenant === "test") {
22886
+ 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.`;
22887
+ }
22888
+ 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\`).`;
22889
+ }
22711
22890
 
22712
22891
  // src/commands/whoami.ts
22713
22892
  async function runWhoami(options, deps = {}) {
@@ -22715,7 +22894,10 @@ async function runWhoami(options, deps = {}) {
22715
22894
  const { identity, baseUrl } = auth;
22716
22895
  let self2 = null;
22717
22896
  try {
22718
- self2 = await resolveSelfPrincipal(auth.client, auth.cognitoToken);
22897
+ self2 = await resolveSelfPrincipalForTenant(
22898
+ { ...auth, client: auth.client },
22899
+ deps.fetchImpl
22900
+ );
22719
22901
  } catch {
22720
22902
  self2 = null;
22721
22903
  }
@@ -23121,51 +23303,6 @@ ${list}
23121
23303
  });
23122
23304
  }
23123
23305
 
23124
- // src/dev-api.ts
23125
- var DeveloperApiError = class extends Error {
23126
- statusCode;
23127
- constructor(message, statusCode) {
23128
- super(message);
23129
- this.name = "DeveloperApiError";
23130
- this.statusCode = statusCode;
23131
- }
23132
- };
23133
- async function fetchDeveloperUsersPage(opts) {
23134
- const { cognitoToken, apiBaseUrl, tenant, startFrom, limit, fetchImpl = fetch } = opts;
23135
- const base = apiBaseUrl.replace(/\/$/, "");
23136
- let url = `${base}/developer/users?tenant=${encodeURIComponent(tenant)}`;
23137
- if (startFrom) url += `&startFrom=${encodeURIComponent(startFrom)}`;
23138
- if (limit !== void 0) url += `&limit=${encodeURIComponent(String(limit))}`;
23139
- let res;
23140
- try {
23141
- res = await fetchImpl(url, {
23142
- method: "GET",
23143
- headers: { Authorization: `Bearer ${cognitoToken}` }
23144
- });
23145
- } catch (err) {
23146
- throw new DeveloperApiError(
23147
- `Request to /developer/users failed: ${err instanceof Error ? err.message : String(err)}`
23148
- );
23149
- }
23150
- if (!res.ok) {
23151
- const body = await res.text().catch(() => "");
23152
- const hint = res.status === 403 ? " (this command requires an account owner login \u2014 sign in as the owner and retry)" : "";
23153
- throw new DeveloperApiError(
23154
- `GET /developer/users failed: HTTP ${res.status}${hint}${body ? ` \u2014 ${body}` : ""}`,
23155
- res.status
23156
- );
23157
- }
23158
- let parsed;
23159
- try {
23160
- parsed = await res.json();
23161
- } catch {
23162
- throw new DeveloperApiError("GET /developer/users returned a non-JSON body");
23163
- }
23164
- const data = Array.isArray(parsed.data) ? parsed.data : [];
23165
- const nextCursor = typeof parsed.nextCursor === "string" ? parsed.nextCursor : null;
23166
- return { data, nextCursor };
23167
- }
23168
-
23169
23306
  // src/commands/identity.ts
23170
23307
  function isUserDimension(type) {
23171
23308
  return type === "user";
@@ -23461,14 +23598,13 @@ function assertPrincipalShape(principal) {
23461
23598
  }
23462
23599
  }
23463
23600
  var SELF_PRINCIPAL_ALIAS = "me";
23464
- async function resolvePrincipalArg(raw, auth) {
23601
+ async function resolvePrincipalArg(raw, auth, deps) {
23465
23602
  if (raw !== SELF_PRINCIPAL_ALIAS) return raw;
23466
- const self2 = await resolveSelfPrincipal(auth.client, auth.cognitoToken);
23467
- if (!self2) {
23468
- throw new Error(
23469
- "Could not resolve your own user id from your login. Pass --principal usr_<id> explicitly (find the id with `vectros identity list --type user`)."
23470
- );
23471
- }
23603
+ const self2 = await resolveSelfPrincipalForTenant(
23604
+ { ...auth, client: auth.client },
23605
+ deps.fetchImpl
23606
+ );
23607
+ if (!self2) throw new Error(selfPrincipalUnresolvedMessage(auth.tenant, "--principal"));
23472
23608
  return self2.principalId;
23473
23609
  }
23474
23610
  function roleSummary(p) {
@@ -23521,7 +23657,7 @@ async function runAccessGrant(opts, deps = {}) {
23521
23657
  return withAuth(opts, deps, async (auth) => {
23522
23658
  const client = auth.client;
23523
23659
  try {
23524
- const principal = await resolvePrincipalArg(opts.principal, auth);
23660
+ const principal = await resolvePrincipalArg(opts.principal, auth, deps);
23525
23661
  const scopes = hasActions ? [{ allowed_actions: splitCsv(opts.actions) }] : void 0;
23526
23662
  const profile = await client.auth.createAccessProfile({
23527
23663
  contextId: opts.context,
@@ -23531,8 +23667,10 @@ async function runAccessGrant(opts, deps = {}) {
23531
23667
  // Always send the plural `roleIds`, even for a single
23532
23668
  // `--role`: it is a strict superset of the deprecated singular
23533
23669
  // `roleId` (equivalent to `roleIds: ["<value>"]`), and the platform
23534
- // reads it uniformly regardless of how many roles compose.
23535
- roleIds: opts.role,
23670
+ // reads it uniformly regardless of how many roles compose. With
23671
+ // `--actions` there is no role: omit `roleIds` (the platform refuses an
23672
+ // empty list next to inline `scopes`).
23673
+ roleIds: hasRole ? opts.role : void 0,
23536
23674
  scopes,
23537
23675
  identityOverrides
23538
23676
  }
@@ -23569,7 +23707,7 @@ async function runAccessRevoke(opts, deps = {}) {
23569
23707
  return withAuth(opts, deps, async (auth) => {
23570
23708
  const client = auth.client;
23571
23709
  try {
23572
- const principal = await resolvePrincipalArg(opts.principal, auth);
23710
+ const principal = await resolvePrincipalArg(opts.principal, auth, deps);
23573
23711
  await client.auth.deleteAccessProfile({ contextId: opts.context, principalId: principal });
23574
23712
  process.stdout.write(` \u2714 Revoked ${principal} from '${opts.context}'
23575
23713
  `);
@@ -23598,7 +23736,7 @@ async function runAccessList(opts, deps = {}) {
23598
23736
  return withAuth(opts, deps, async (auth) => {
23599
23737
  const client = auth.client;
23600
23738
  try {
23601
- const principal = opts.principal ? await resolvePrincipalArg(opts.principal, auth) : void 0;
23739
+ const principal = opts.principal ? await resolvePrincipalArg(opts.principal, auth, deps) : void 0;
23602
23740
  const rows = opts.context ? await drainPages(
23603
23741
  (startFrom) => client.auth.listAccessProfiles({ contextId: opts.context, startFrom, limit: PAGE_LIMIT })
23604
23742
  ) : await drainPages(
@@ -23638,7 +23776,7 @@ async function runAccessGet(opts, deps = {}) {
23638
23776
  return withAuth(opts, deps, async (auth) => {
23639
23777
  const client = auth.client;
23640
23778
  try {
23641
- const principal = await resolvePrincipalArg(opts.principal, auth);
23779
+ const principal = await resolvePrincipalArg(opts.principal, auth, deps);
23642
23780
  const p = await client.auth.getAccessProfile({ contextId: opts.context, principalId: principal });
23643
23781
  if (opts.json) {
23644
23782
  process.stdout.write(`${formatJson(p)}
@@ -23860,11 +23998,13 @@ async function runAccessExplain(opts, deps = {}) {
23860
23998
  try {
23861
23999
  let principal = opts.principal;
23862
24000
  if (principal === SELF_PRINCIPAL_ALIAS2) {
23863
- const self2 = await resolveSelfPrincipal(auth.client, auth.cognitoToken);
24001
+ const self2 = await resolveSelfPrincipalForTenant(
24002
+ { ...auth, client: auth.client },
24003
+ deps.fetchImpl
24004
+ );
23864
24005
  if (!self2) {
23865
- process.stderr.write(
23866
- " \u2716 Could not resolve your own user id from your login. Pass --principal usr_<id> explicitly.\n"
23867
- );
24006
+ process.stderr.write(` \u2716 ${selfPrincipalUnresolvedMessage(auth.tenant, "--principal")}
24007
+ `);
23868
24008
  return 1;
23869
24009
  }
23870
24010
  principal = self2.principalId;
@@ -24243,10 +24383,17 @@ async function runContextDestroy(opts, deps = {}) {
24243
24383
  `);
24244
24384
  return 0;
24245
24385
  }
24386
+ const tenantFlag = auth.tenant === "test" ? " --tenant test" : "";
24246
24387
  process.stdout.write(
24247
24388
  `
24248
24389
  \u2714 teardown of '${contextId}' started \u2014 the context and its contents are being
24249
24390
  deleted asynchronously (it reports status 'purging' until the drain completes)
24391
+
24392
+ Not removed: a service principal a blueprint bootstrap created for this context (the
24393
+ externalId in the blueprint's servicePrincipal) belongs to the whole tenant, so it stays. To clean it up
24394
+ when it exists and nothing else uses it:
24395
+ vectros identity list-all${tenantFlag}
24396
+ vectros identity delete --type user --id <id>${tenantFlag}
24250
24397
  `
24251
24398
  );
24252
24399
  return 0;
@@ -24785,7 +24932,7 @@ ${renderIssuer(issuer)}
24785
24932
  var import_node_fs5 = require("fs");
24786
24933
  var readline6 = __toESM(require("readline/promises"));
24787
24934
  var PROVIDER_TYPES = ["ANTHROPIC_NATIVE", "OPENAI_COMPATIBLE"];
24788
- var BILLING_MODES = ["PLATFORM_BILLED", "CUSTOMER_DIRECT"];
24935
+ var BILLING_MODES = ["CUSTOMER_DIRECT"];
24789
24936
  var STATUSES = ["ACTIVE", "DISABLED"];
24790
24937
  var MAX_SECRET_BYTES = 8 * 1024;
24791
24938
  async function readBoundedSecret(stream) {
@@ -24864,12 +25011,17 @@ function renderProviderConfig(c) {
24864
25011
  ["defaultProviderModelId", c.defaultProviderModelId],
24865
25012
  ["waiverType", c.waiverType],
24866
25013
  ["billingMode", c.billingMode],
24867
- ["platformTokenFactor", c.platformTokenFactor],
24868
- ["platformOutputWeight", c.platformOutputWeight],
24869
25014
  ["status", c.status],
24870
25015
  ["createdAt", c.createdAt]
24871
25016
  ]);
24872
25017
  }
25018
+ function billingModeError(value) {
25019
+ if (value === void 0 || BILLING_MODES.includes(value)) return void 0;
25020
+ if (value === "PLATFORM_BILLED") {
25021
+ return "--billing-mode PLATFORM_BILLED is retired: a provider you configure bills you directly, so CUSTOMER_DIRECT is the only billing mode.";
25022
+ }
25023
+ return invalidEnum("--billing-mode", value, BILLING_MODES);
25024
+ }
24873
25025
  function invalidEnum(flag, value, allowed) {
24874
25026
  return `${flag} must be one of ${allowed.join(", ")} (got '${value}').`;
24875
25027
  }
@@ -24880,8 +25032,9 @@ async function runProviderConfigCreate(opts, deps = {}) {
24880
25032
  `);
24881
25033
  return 2;
24882
25034
  }
24883
- if (!BILLING_MODES.includes(opts.billingMode)) {
24884
- process.stderr.write(` \u2716 ${invalidEnum("--billing-mode", opts.billingMode, BILLING_MODES)}
25035
+ const billingModeProblem = billingModeError(opts.billingMode);
25036
+ if (billingModeProblem !== void 0) {
25037
+ process.stderr.write(` \u2716 ${billingModeProblem}
24885
25038
  `);
24886
25039
  return 2;
24887
25040
  }
@@ -24890,19 +25043,6 @@ async function runProviderConfigCreate(opts, deps = {}) {
24890
25043
  `);
24891
25044
  return 2;
24892
25045
  }
24893
- if (opts.billingMode === "PLATFORM_BILLED") {
24894
- if (opts.platformTokenFactor === void 0 || opts.platformOutputWeight === void 0) {
24895
- process.stderr.write(
24896
- " \u2716 --platform-token-factor and --platform-output-weight are required when --billing-mode PLATFORM_BILLED.\n"
24897
- );
24898
- return 2;
24899
- }
24900
- } else if (opts.platformTokenFactor !== void 0 || opts.platformOutputWeight !== void 0) {
24901
- process.stderr.write(
24902
- " \u2716 --platform-token-factor/--platform-output-weight only apply with --billing-mode PLATFORM_BILLED.\n"
24903
- );
24904
- return 2;
24905
- }
24906
25046
  let apiKeySecret;
24907
25047
  try {
24908
25048
  apiKeySecret = await resolveApiKeySecret(opts.apiKeySecretFile, deps, "required");
@@ -24927,9 +25067,7 @@ async function runProviderConfigCreate(opts, deps = {}) {
24927
25067
  defaultProviderModelId: opts.defaultProviderModelId,
24928
25068
  apiKeySecret,
24929
25069
  waiverType: opts.waiverType,
24930
- billingMode: opts.billingMode,
24931
- platformTokenFactor: opts.platformTokenFactor,
24932
- platformOutputWeight: opts.platformOutputWeight,
25070
+ billingMode: opts.billingMode ?? "CUSTOMER_DIRECT",
24933
25071
  status: opts.status
24934
25072
  })
24935
25073
  });
@@ -25010,8 +25148,9 @@ async function runProviderConfigUpdate(opts, deps = {}) {
25010
25148
  `);
25011
25149
  return 2;
25012
25150
  }
25013
- if (opts.billingMode !== void 0 && !BILLING_MODES.includes(opts.billingMode)) {
25014
- process.stderr.write(` \u2716 ${invalidEnum("--billing-mode", opts.billingMode, BILLING_MODES)}
25151
+ const billingModeProblem = billingModeError(opts.billingMode);
25152
+ if (billingModeProblem !== void 0) {
25153
+ process.stderr.write(` \u2716 ${billingModeProblem}
25015
25154
  `);
25016
25155
  return 2;
25017
25156
  }
@@ -25044,8 +25183,6 @@ async function runProviderConfigUpdate(opts, deps = {}) {
25044
25183
  apiKeySecret,
25045
25184
  waiverType: opts.waiverType,
25046
25185
  billingMode: opts.billingMode,
25047
- platformTokenFactor: opts.platformTokenFactor,
25048
- platformOutputWeight: opts.platformOutputWeight,
25049
25186
  status: opts.status
25050
25187
  })
25051
25188
  });
@@ -26622,15 +26759,14 @@ async function runBlueprintTest(fileOrName, opts, deps = {}) {
26622
26759
 
26623
26760
  // src/commands/join.ts
26624
26761
  var SELF_ALIAS = "me";
26625
- async function resolvePrincipal(as, auth) {
26762
+ async function resolvePrincipal(as, auth, deps) {
26626
26763
  const raw = as ?? SELF_ALIAS;
26627
26764
  if (raw === SELF_ALIAS) {
26628
- const self2 = await resolveSelfPrincipal(auth.client, auth.cognitoToken);
26629
- if (!self2) {
26630
- throw new Error(
26631
- "Could not resolve your own user id from your login. Pass --as usr_<id> explicitly (find it with `vectros identity list --type user`)."
26632
- );
26633
- }
26765
+ const self2 = await resolveSelfPrincipalForTenant(
26766
+ { ...auth, client: auth.client },
26767
+ deps.fetchImpl
26768
+ );
26769
+ if (!self2) throw new Error(selfPrincipalUnresolvedMessage(auth.tenant, "--as"));
26634
26770
  return self2.principalId;
26635
26771
  }
26636
26772
  if (!raw.startsWith("usr_")) {
@@ -26700,7 +26836,7 @@ async function runJoin(opts, deps = {}) {
26700
26836
  const client = auth.client;
26701
26837
  try {
26702
26838
  const tenantId = requireTenantId(auth);
26703
- const principalId = await resolvePrincipal(opts.as, auth);
26839
+ const principalId = await resolvePrincipal(opts.as, auth, deps);
26704
26840
  const userId = normalizePrincipalUserId(principalId);
26705
26841
  const scopes = hasActions ? [{ allowed_actions: splitCsv(opts.actions) }] : void 0;
26706
26842
  await client.auth.createAccessProfile({
@@ -26711,7 +26847,9 @@ async function runJoin(opts, deps = {}) {
26711
26847
  // Always send the plural `roleIds`, even for a single
26712
26848
  // `--role`: a strict superset of the deprecated singular `roleId`,
26713
26849
  // read uniformly by the platform regardless of how many compose.
26714
- roleIds: opts.role,
26850
+ // With `--actions` there is no role: omit `roleIds` (the platform
26851
+ // refuses an empty list next to inline `scopes`).
26852
+ roleIds: hasRole ? opts.role : void 0,
26715
26853
  scopes,
26716
26854
  identityOverrides
26717
26855
  }
@@ -26798,9 +26936,9 @@ function emitJoined(opts) {
26798
26936
 
26799
26937
  // src/build-info.ts
26800
26938
  var BUILD_INFO = {
26801
- cli: true ? "0.24.0" : "dev",
26802
- sdk: true ? "0.46.0" : "dev",
26803
- blueprints: true ? "0.20.1" : "dev"
26939
+ cli: true ? "0.25.0" : "dev",
26940
+ sdk: true ? "0.47.0" : "dev",
26941
+ blueprints: true ? "0.20.2" : "dev"
26804
26942
  };
26805
26943
  function formatBuildInfo() {
26806
26944
  return `vectros ${BUILD_INFO.cli} (sdk ${BUILD_INFO.sdk}, blueprints ${BUILD_INFO.blueprints})`;
@@ -26837,18 +26975,6 @@ function parseTenantFlag(raw) {
26837
26975
  }
26838
26976
  return raw;
26839
26977
  }
26840
- var NUMBER_FLAG_INVALID = /* @__PURE__ */ Symbol("parseOptionalNumberFlag:invalid");
26841
- function parseOptionalNumberFlag(flag, raw) {
26842
- if (raw === void 0) return void 0;
26843
- const n = Number(raw);
26844
- if (!Number.isFinite(n)) {
26845
- process.stderr.write(` \u2716 ${flag} must be a number (got '${raw}')
26846
- `);
26847
- process.exitCode = 2;
26848
- return NUMBER_FLAG_INVALID;
26849
- }
26850
- return n;
26851
- }
26852
26978
  function toCommonOptions(flags) {
26853
26979
  return {
26854
26980
  env: flags.env,
@@ -27020,7 +27146,7 @@ function registerContextCommands(program, deps) {
27020
27146
  );
27021
27147
  });
27022
27148
  const destroy = context.command("destroy").description(
27023
- "Permanently destroy an app context and ALL of its data (irreversible; asks for confirmation)."
27149
+ "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'."
27024
27150
  ).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)");
27025
27151
  addCommonAuthOptions(destroy).action(
27026
27152
  async (contextId, flags) => {
@@ -27175,15 +27301,11 @@ function registerIssuersCommands(program, deps) {
27175
27301
  }
27176
27302
  function registerProviderConfigCommands(program, deps) {
27177
27303
  const providerConfig = program.command("provider-config").description("Manage BYOM (bring-your-own-model) tenant-level provider configs.");
27178
- 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)");
27304
+ 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)");
27179
27305
  addCommonAuthOptions(create).action(
27180
27306
  async (flags) => {
27181
27307
  const tenant = parseTenantFlag(flags.tenant);
27182
27308
  if (tenant === TENANT_FLAG_INVALID) return;
27183
- const platformTokenFactor = parseOptionalNumberFlag("--platform-token-factor", flags.platformTokenFactor);
27184
- if (platformTokenFactor === NUMBER_FLAG_INVALID) return;
27185
- const platformOutputWeight = parseOptionalNumberFlag("--platform-output-weight", flags.platformOutputWeight);
27186
- if (platformOutputWeight === NUMBER_FLAG_INVALID) return;
27187
27309
  process.exitCode = await runProviderConfigCreate(
27188
27310
  {
27189
27311
  ...toCommonOptions(flags),
@@ -27195,8 +27317,6 @@ function registerProviderConfigCommands(program, deps) {
27195
27317
  apiKeySecretFile: flags.apiKeySecretFile,
27196
27318
  waiverType: flags.waiverType,
27197
27319
  billingMode: flags.billingMode,
27198
- platformTokenFactor,
27199
- platformOutputWeight,
27200
27320
  status: flags.status,
27201
27321
  tenant
27202
27322
  },
@@ -27214,13 +27334,9 @@ function registerProviderConfigCommands(program, deps) {
27214
27334
  addCommonAuthOptions(get).action(async (id, flags) => {
27215
27335
  process.exitCode = await runProviderConfigGet({ ...toCommonOptions(flags), id }, deps);
27216
27336
  });
27217
- 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");
27337
+ 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");
27218
27338
  addCommonAuthOptions(update).action(
27219
27339
  async (id, flags) => {
27220
- const platformTokenFactor = parseOptionalNumberFlag("--platform-token-factor", flags.platformTokenFactor);
27221
- if (platformTokenFactor === NUMBER_FLAG_INVALID) return;
27222
- const platformOutputWeight = parseOptionalNumberFlag("--platform-output-weight", flags.platformOutputWeight);
27223
- if (platformOutputWeight === NUMBER_FLAG_INVALID) return;
27224
27340
  process.exitCode = await runProviderConfigUpdate(
27225
27341
  {
27226
27342
  ...toCommonOptions(flags),
@@ -27233,8 +27349,6 @@ function registerProviderConfigCommands(program, deps) {
27233
27349
  apiKeySecretFile: flags.apiKeySecretFile,
27234
27350
  waiverType: flags.waiverType,
27235
27351
  billingMode: flags.billingMode,
27236
- platformTokenFactor,
27237
- platformOutputWeight,
27238
27352
  status: flags.status
27239
27353
  },
27240
27354
  deps
@@ -27502,12 +27616,15 @@ function dimensionOption() {
27502
27616
  ).makeOptionMandatory();
27503
27617
  }
27504
27618
  function registerIdentityCommands(program, deps) {
27505
- const identity = program.command("identity").description("Manage tenant-wide principals (users + namespaced entities) \u2014 reference, never embed.");
27506
- 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");
27619
+ const identity = program.command("identity").description("Manage principals: users (tenant-wide) and namespaced entities (an entity lives with its namespace) \u2014 reference, never embed.");
27620
+ 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)");
27507
27621
  addCommonAuthOptions(create).action(
27508
27622
  async (flags) => {
27623
+ const tenant = parseTenantFlag(flags.tenant);
27624
+ if (tenant === TENANT_FLAG_INVALID) return;
27509
27625
  process.exitCode = await runIdentityCreate({
27510
27626
  ...toCommonOptions(flags),
27627
+ tenant,
27511
27628
  type: flags.type,
27512
27629
  externalId: flags.externalId,
27513
27630
  name: flags.name,
@@ -27518,11 +27635,14 @@ function registerIdentityCommands(program, deps) {
27518
27635
  }, deps);
27519
27636
  }
27520
27637
  );
27521
- 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));
27638
+ 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)");
27522
27639
  addCommonAuthOptions(list).action(
27523
27640
  async (flags) => {
27641
+ const tenant = parseTenantFlag(flags.tenant);
27642
+ if (tenant === TENANT_FLAG_INVALID) return;
27524
27643
  process.exitCode = await runIdentityList({
27525
27644
  ...toCommonOptions(flags),
27645
+ tenant,
27526
27646
  type: flags.type,
27527
27647
  externalId: flags.externalId,
27528
27648
  limit: flags.limit
@@ -27542,20 +27662,30 @@ function registerIdentityCommands(program, deps) {
27542
27662
  );
27543
27663
  }
27544
27664
  );
27545
- 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)");
27665
+ 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)");
27546
27666
  addCommonAuthOptions(get).action(async (flags) => {
27547
- process.exitCode = await runIdentityGet({ ...toCommonOptions(flags), type: flags.type, id: flags.id }, deps);
27667
+ const tenant = parseTenantFlag(flags.tenant);
27668
+ if (tenant === TENANT_FLAG_INVALID) return;
27669
+ process.exitCode = await runIdentityGet({ ...toCommonOptions(flags), tenant, type: flags.type, id: flags.id }, deps);
27548
27670
  });
27549
- const del = identity.command("delete").description("Delete a principal by its Vectros id.").addOption(dimensionOption()).requiredOption("--id <vectrosId>", "the Vectros-assigned id");
27671
+ 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)");
27550
27672
  addAuthOptions(del).action(async (flags) => {
27551
- process.exitCode = await runIdentityDelete({ ...toCommonOptions(flags), type: flags.type, id: flags.id }, deps);
27673
+ const tenant = parseTenantFlag(flags.tenant);
27674
+ if (tenant === TENANT_FLAG_INVALID) return;
27675
+ process.exitCode = await runIdentityDelete(
27676
+ { ...toCommonOptions(flags), tenant, type: flags.type, id: flags.id },
27677
+ deps
27678
+ );
27552
27679
  });
27553
- 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");
27680
+ 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)");
27554
27681
  addCommonAuthOptions(invite).action(
27555
27682
  async (flags) => {
27683
+ const tenant = parseTenantFlag(flags.tenant);
27684
+ if (tenant === TENANT_FLAG_INVALID) return;
27556
27685
  process.exitCode = await runIdentityInvite(
27557
27686
  {
27558
27687
  ...toCommonOptions(flags),
27688
+ tenant,
27559
27689
  context: flags.context,
27560
27690
  role: flags.role,
27561
27691
  email: flags.email,