@vectros-ai/cli 0.16.0 → 0.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.mjs CHANGED
@@ -4108,7 +4108,7 @@ var AuthClient = class {
4108
4108
  return handleNonStatusCodeError(_response.error, _response.rawResponse, "GET", "/v1/admin/keys/scoped");
4109
4109
  }
4110
4110
  /**
4111
- * Creates a scoped API key (an `ssk_*` secret) that inherits its permissions from an existing access profile in your account. The call is idempotent on the combination of tenant, context, user, and key name: re-issuing the same request returns the existing key WITHOUT re-disclosing its raw secret. The raw key is returned ONLY in this response — store it securely, as it cannot be retrieved again. Requires the `keys:c` scope. If you use a scoped credential, `keys:c` alone is not sufficient: because the minted key is durably bound to the profile you name, the profile's effective scopes may not exceed your own, and you may only mint against a profile whose `identityOverrides` values your own identity holds. A root API key (`sk_`) is exempt from both bounds.
4111
+ * Creates a scoped API key (an `ssk_*` secret) that inherits its permissions from an existing access profile in your account. The call is idempotent on the combination of tenant, context, user, and key name: re-issuing the same request returns the existing key WITHOUT re-disclosing its raw secret. The raw key is returned ONLY in this response — store it securely, as it cannot be retrieved again. Requires the `keys:c` scope. If you use a scoped credential, `keys:c` alone is not sufficient: because the minted key is durably bound to the profile you name, the profile's effective scopes may not exceed your own, and you may only mint against a profile whose `identityOverrides` values your own identity holds. Minting a key bound to your OWN principal needs nothing further; minting one bound to a DIFFERENT principal additionally requires the `delegate-mint` capability (`granted_capabilities`) on your credential — without it the request is refused. A root API key (`sk_`) is exempt from all three bounds.
4112
4112
  *
4113
4113
  * @param {Vectros.CreateScopedKeyRequest} request
4114
4114
  * @param {AuthClient.RequestOptions} requestOptions - Request-specific configuration.
@@ -5604,7 +5604,7 @@ var AuthClient = class {
5604
5604
  return handleNonStatusCodeError(_response.error, _response.rawResponse, "GET", "/v1/usage");
5605
5605
  }
5606
5606
  /**
5607
- * Retrieves a single registered issuer by issuerId. Requires a root API key or the bootstrap's provisioning capability. A credential confined to one app context sees only an issuer registered in that context; naming one registered in another context returns 404, identically to a nonexistent issuerId. A root API key sees every context.
5607
+ * Retrieves a single registered issuer by issuerId. Requires a root API key or the bootstrap's provisioning capability. A credential confined to one app context sees only an issuer registered in that context; naming one registered in another context returns 404, identically to a nonexistent issuerId. A root API key sees every context. An ordinary bootstrap credential that didn't specify a context resolves to the `default` app context specifically, not every context — so this call returns 404 for an issuer registered under any other context unless you re-minted the bootstrap token pinned to that context.
5608
5608
  *
5609
5609
  * @param {Vectros.GetIssuerRequest} request
5610
5610
  * @param {AuthClient.RequestOptions} requestOptions - Request-specific configuration.
@@ -5661,6 +5661,73 @@ var AuthClient = class {
5661
5661
  }
5662
5662
  return handleNonStatusCodeError(_response.error, _response.rawResponse, "GET", "/v1/auth/issuers/{issuerId}");
5663
5663
  }
5664
+ /**
5665
+ * Updates the mutable fields of a registered issuer: `subClaim`, `emailClaim`, `status` (`active`/`suspended` — 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. 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.
5666
+ *
5667
+ * @param {Vectros.IssuerUpdateRequest} request
5668
+ * @param {AuthClient.RequestOptions} requestOptions - Request-specific configuration.
5669
+ *
5670
+ * @throws {@link Vectros.BadRequestError}
5671
+ * @throws {@link Vectros.ForbiddenError}
5672
+ * @throws {@link Vectros.NotFoundError}
5673
+ * @throws {@link Vectros.TooManyRequestsError}
5674
+ *
5675
+ * @example
5676
+ * await client.auth.updateIssuer({
5677
+ * issuerId: "auth0-prod"
5678
+ * })
5679
+ */
5680
+ updateIssuer(request, requestOptions) {
5681
+ return HttpResponsePromise.fromPromise(this.__updateIssuer(request, requestOptions));
5682
+ }
5683
+ async __updateIssuer(request, requestOptions) {
5684
+ const { issuerId, ..._body } = request;
5685
+ const _authRequest = await this._options.authProvider.getAuthRequest();
5686
+ const _headers = mergeHeaders(
5687
+ _authRequest.headers,
5688
+ this._options?.headers,
5689
+ requestOptions?.headers
5690
+ );
5691
+ const _response = await fetcher({
5692
+ url: url_exports.join(
5693
+ await Supplier.get(this._options.baseUrl) ?? await Supplier.get(this._options.environment),
5694
+ `v1/auth/issuers/${url_exports.encodePathParam(issuerId)}`
5695
+ ),
5696
+ method: "PUT",
5697
+ headers: _headers,
5698
+ contentType: "application/json",
5699
+ queryString: url_exports.queryBuilder().mergeAdditional(requestOptions?.queryParams).build(),
5700
+ requestType: "json",
5701
+ body: _body,
5702
+ timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1e3,
5703
+ maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries,
5704
+ abortSignal: requestOptions?.abortSignal,
5705
+ fetchFn: this._options?.fetch,
5706
+ logging: this._options.logging
5707
+ });
5708
+ if (_response.ok) {
5709
+ return { data: _response.body, rawResponse: _response.rawResponse };
5710
+ }
5711
+ if (_response.error.reason === "status-code") {
5712
+ switch (_response.error.statusCode) {
5713
+ case 400:
5714
+ throw new BadRequestError(_response.error.body, _response.rawResponse);
5715
+ case 403:
5716
+ throw new ForbiddenError(_response.error.body, _response.rawResponse);
5717
+ case 404:
5718
+ throw new NotFoundError(_response.error.body, _response.rawResponse);
5719
+ case 429:
5720
+ throw new TooManyRequestsError(_response.error.body, _response.rawResponse);
5721
+ default:
5722
+ throw new VectrosError({
5723
+ statusCode: _response.error.statusCode,
5724
+ body: _response.error.body,
5725
+ rawResponse: _response.rawResponse
5726
+ });
5727
+ }
5728
+ }
5729
+ return handleNonStatusCodeError(_response.error, _response.rawResponse, "PUT", "/v1/auth/issuers/{issuerId}");
5730
+ }
5664
5731
  /**
5665
5732
  * Deregisters a trusted third-party IdP issuer. Requires a root API key or the bootstrap's provisioning capability. A credential confined to one app context may only deregister an issuer registered in that context; naming one registered in another context returns 404, identically to a nonexistent issuerId. A root API key may deregister any issuer. Refused if any user account was ever created or matched via this issuer (by a prior self-signup or accepted invite, through `POST /v1/auth/token/exchange`) — that access cannot be silently orphaned. Deactivate the affected users first if you intend to cut off their access, or register a replacement issuer before removing this one. An issuer that has never been used for an exchange (no bound users yet) can always be deregistered.
5666
5733
  *
@@ -5731,7 +5798,7 @@ var AuthClient = class {
5731
5798
  );
5732
5799
  }
5733
5800
  /**
5734
- * Returns the issuers registered in your tenant. Requires a root API key or the bootstrap's provisioning capability. A credential confined to one app context sees only the issuers registered in that context; a root API key sees every context. Returns a `{data, nextCursor}` envelope.
5801
+ * Returns the issuers registered in your tenant. Requires a root API key or the bootstrap's provisioning capability. A credential confined to one app context sees only the issuers registered in that context; a root API key sees every context. An ordinary bootstrap credential that didn't specify a context resolves to the `default` app context specifically, not every context — so this call returns an empty page for a tenant whose issuers are all registered under a different context unless you re-minted the bootstrap token pinned to that context. Returns a `{data, nextCursor}` envelope.
5735
5802
  *
5736
5803
  * @param {Vectros.ListIssuersRequest} request
5737
5804
  * @param {AuthClient.RequestOptions} requestOptions - Request-specific configuration.
@@ -5788,7 +5855,7 @@ var AuthClient = class {
5788
5855
  return handleNonStatusCodeError(_response.error, _response.rawResponse, "GET", "/v1/auth/issuers");
5789
5856
  }
5790
5857
  /**
5791
- * Registers a trusted third-party IdP issuer that BYO-IdP token exchange (`POST /v1/auth/token/exchange`) may accept a `subject_token` from. Requires a root API key or the CLI bootstrap's provisioning capability — never an ordinary partner-grantable scope. Idempotent by `issuerId` within your tenant; the `(issuer, audience)` pair must not already be registered by a different issuerId/tenant. An app context may have at most one active issuer — deregister the existing one first if you need to replace it. One issuer MAY serve several contexts today, each via its own registration row with a distinct `audience`.
5858
+ * Registers a trusted third-party IdP issuer that BYO-IdP token exchange (`POST /v1/auth/token/exchange`) may accept a `subject_token` from. Requires a root API key or the CLI bootstrap's provisioning capability — never an ordinary partner-grantable scope. A credential authorized only via the provisioning capability may register only against the app context it is bound to; naming a different one returns 403. A root API key is unaffected and may register against any of its contexts. Idempotent by `issuerId` within your tenant; the `(issuer, audience)` pair must not already be registered by a different issuerId/tenant. If `issuerId` collides with a registration owned by a different app context than the one you're confined to, the request fails with 400 rather than returning that context's configuration. An app context may have at most one active issuer — deregister the existing one first if you need to replace it. One issuer MAY serve several contexts today, each via its own registration row with a distinct `audience`.
5792
5859
  *
5793
5860
  * @param {Vectros.IssuerRequest} request
5794
5861
  * @param {AuthClient.RequestOptions} requestOptions - Request-specific configuration.
@@ -5973,6 +6040,7 @@ var AuthClient = class {
5973
6040
  *
5974
6041
  * @throws {@link Vectros.BadRequestError}
5975
6042
  * @throws {@link Vectros.ForbiddenError}
6043
+ * @throws {@link Vectros.NotFoundError}
5976
6044
  * @throws {@link Vectros.TooManyRequestsError}
5977
6045
  *
5978
6046
  * @example
@@ -6026,6 +6094,8 @@ var AuthClient = class {
6026
6094
  throw new BadRequestError(_response.error.body, _response.rawResponse);
6027
6095
  case 403:
6028
6096
  throw new ForbiddenError(_response.error.body, _response.rawResponse);
6097
+ case 404:
6098
+ throw new NotFoundError(_response.error.body, _response.rawResponse);
6029
6099
  case 429:
6030
6100
  throw new TooManyRequestsError(_response.error.body, _response.rawResponse);
6031
6101
  default:
@@ -6039,7 +6109,7 @@ var AuthClient = class {
6039
6109
  return handleNonStatusCodeError(_response.error, _response.rawResponse, "POST", "/v1/auth/token");
6040
6110
  }
6041
6111
  /**
6042
- * Invite a new member to one of your app contexts by email. Creates a pending user with a pre-resolved access profile (their permissions on accept) and signs an invitation token. This call is idempotent on the combination of context and email: re-inviting the same email in the same context rotates the token and resends the invitation rather than creating a duplicate — this requires the `users:r` and `users:u` scopes in addition to `users:c`, because resending rotates a credential on an existing invitation and invalidates any link already sent. Without them the collision returns 409 instead, with no invitation details and no change to the outstanding invitation. Returns HTTP 201 on a new invite or a successful resend. Returns 409 if that email already belongs to an active or suspended member of the app context, or already has an identity elsewhere in your account (an email can currently belong to only one tenant per account, i.e. your test and live environments cannot share an email). When `sendEmail` is false, the response includes the raw token and a ready-to-use accept link so you can deliver the invitation through your own email provider. Requires the `users:c` scope.
6112
+ * Invite a new member to one of your app contexts by email, OR grant an existing member access to an additional app context by inviting their same email again. Idempotent on the combination of context and email: re-inviting the same email into the SAME context rotates the token and resends the invitation rather than creating a duplicate — this requires the `users:r` and `users:u` scopes in addition to `users:c`, because resending rotates a credential on an existing invitation and invalidates any link already sent. Without them the collision returns 409 instead, with no invitation details and no change to the outstanding invitation. Inviting the SAME email into a DIFFERENT app context in this tenant, where that email already resolves to an existing member: if that member is active AND already has (or, once accepted, will have) a credential that works for the new context's own identity provider, this immediately grants them access to the new context (no email is sent — there is nothing to accept, `emailSent` is false) — this additionally requires the `users:r` scope (no `users:u`, since nothing is mutated), because the response names the existing member's userId, a fact about them your credential could not otherwise learn through this endpoint. If that active member's ONLY existing credential is for a DIFFERENT identity provider than the one the new context uses, a normal, independent invitation is created instead (its own new member id, a real token/accept link) — attaching them silently would leave no way for them to ever actually sign in to that context. If the existing member's original invitation is still pending, this attaches the new context's access to that same outstanding invitation and rotates its token (`users:r`+`users:u`, same as an ordinary resend — both the disclosure and the credential rotation apply here). A SUSPENDED member's email does not get new-context access this way — reactivate them explicitly first. Returns HTTP 201 in every one of those cases. Returns 409 if that email already belongs to an active or suspended member of THIS specific app context, already has a PENDING invitation for THIS specific app context, or resolves to an existing member elsewhere in the tenant and your token lacks the additional scope the grant/attach requires (`users:r`, or `users:r`+`users:u` for the still-pending case). An email that already has an identity in your OTHER tenant (test vs. live) is not a collision either — it creates an additional, independent membership in this tenant for that same identity. When `sendEmail` is false, the response includes the raw token and a ready-to-use accept link so you can deliver the invitation through your own email provider. Requires the `users:c` scope — or, for a credential scoped to a single app context, the `member-lifecycle` capability plus the matching `profiles:c`/`profiles:r`/`profiles:u` grant(s) in that context, as an equally sufficient alternative to every `users:c`/`users:r`/`users:u` requirement in this description.
6043
6113
  *
6044
6114
  * @param {Vectros.CreateInviteRequest} request
6045
6115
  * @param {AuthClient.RequestOptions} requestOptions - Request-specific configuration.
@@ -6110,7 +6180,7 @@ var AuthClient = class {
6110
6180
  return handleNonStatusCodeError(_response.error, _response.rawResponse, "POST", "/v1/users/invite");
6111
6181
  }
6112
6182
  /**
6113
- * Resend an outstanding invitation, identified by its email and app context. Rotates the invitation token and extends its expiry, then (when `sendEmail` is true) re-delivers the email. Rotating the token invalidates any previously issued link for this invitation, so only the newest link works. The invitee's pending permissions are left unchanged. Because this rotates a credential on an existing invitation, it requires the `users:c`, `users:r` and `users:u` scopes.
6183
+ * Resend an outstanding invitation, identified by its email and app context. Rotates the invitation token and extends its expiry, then (when `sendEmail` is true) re-delivers the email. Rotating the token invalidates any previously issued link for this invitation, so only the newest link works. The invitee's pending permissions are left unchanged. Because this rotates a credential on an existing invitation, it requires the `users:c`, `users:r` and `users:u` scopes — or, for a credential scoped to a single app context, the `member-lifecycle` capability plus the matching `profiles:c`/`profiles:r`/`profiles:u` grant(s) in that context.
6114
6184
  *
6115
6185
  * @param {Vectros.CreateInviteRequest} request
6116
6186
  * @param {AuthClient.RequestOptions} requestOptions - Request-specific configuration.
@@ -6177,6 +6247,83 @@ var AuthClient = class {
6177
6247
  }
6178
6248
  return handleNonStatusCodeError(_response.error, _response.rawResponse, "POST", "/v1/users/invite/resend");
6179
6249
  }
6250
+ /**
6251
+ * Re-mints the presented `st_*` scoped token with one or more `identity.<namespace>` values changed — for a caller whose ROLE explicitly grants assuming those values (an invited hr-admin, a multi-org case-handler) and needs to change which value new writes place records under. The request body names one or more namespaces in canonical `scope:<namespace>` form, e.g. `{"scope:org": "orgB"}` — each value must be a plain literal, never a `${{ ... }}` placeholder. When you name MORE THAN ONE namespace, a single one of your roles must grant all of them together: the combination is never assembled from two different roles, because no role author would have vouched for it. `st_*`-only — a root API key or `ssk_*` scoped API key gets 403; neither needs this (root already has full authority, and an `ssk_*`'s identity shape is not what this resolves against).
6252
+ *
6253
+ * **Only an original token may assume.** A token produced BY this endpoint cannot assume again (403) — every assume starts from the token you exchanged for, so the identity you end up with is always one a single role explicitly granted rather than a combination reached by chaining calls. Keep your original token if you need to switch more than once, or exchange for a new one.
6254
+ *
6255
+ * **Entitlement is checked LIVE, against your roles as they are right now** — not against a copy frozen into your token when it was minted. The requested value must be explicitly granted by a role's `assumable` field for that namespace: a POINT check against the one value requested, and a deliberately separate, explicitly-authored question from what the role's `data_scope` permits reading or writing. Holding broad `data_scope` reach in a namespace does NOT by itself grant assuming any value in it.
6256
+ *
6257
+ * **What is preserved, and what is not.** Every clause of your token that does not reference a requested namespace is preserved verbatim, as are all other claims (`partner_user_id`, `context_id`, mint attribution). Clauses that DO reference a requested namespace are kept only if they come from a role that authorized the new value. A role that does not authorize it loses all of its clauses touching that namespace — including any scoped to the value you already held. Assume into a value one role grants and you keep that role's reach, not the reach of roles that never vouched for it.
6258
+ *
6259
+ * The re-minted token's `exp` is IDENTICAL to the presented token's — this call can never extend a session's life. A fresh, independently-revocable `jti` is stamped on every call, and (except when the presented token predates jti support and has none to chain from) the token also carries a `root_jti` revocation-lineage claim so revoking the token you started from closes every value ever assumed from it. Uses the ordinary Vectros `{"message":...}` error shape, not the OAuth envelope `POST /v1/auth/token/exchange` uses — this endpoint's caller is always Vectros-SDK code already holding a bearer token, never generic OAuth tooling.
6260
+ *
6261
+ * @param {Record<string, unknown>} request
6262
+ * @param {AuthClient.RequestOptions} requestOptions - Request-specific configuration.
6263
+ *
6264
+ * @throws {@link Vectros.BadRequestError}
6265
+ * @throws {@link Vectros.UnauthorizedError}
6266
+ * @throws {@link Vectros.ForbiddenError}
6267
+ * @throws {@link Vectros.ConflictError}
6268
+ * @throws {@link Vectros.TooManyRequestsError}
6269
+ *
6270
+ * @example
6271
+ * await client.auth.assumeToken({
6272
+ * "scope:org": "orgB"
6273
+ * })
6274
+ */
6275
+ assumeToken(request, requestOptions) {
6276
+ return HttpResponsePromise.fromPromise(this.__assumeToken(request, requestOptions));
6277
+ }
6278
+ async __assumeToken(request, requestOptions) {
6279
+ const _authRequest = await this._options.authProvider.getAuthRequest();
6280
+ const _headers = mergeHeaders(
6281
+ _authRequest.headers,
6282
+ this._options?.headers,
6283
+ requestOptions?.headers
6284
+ );
6285
+ const _response = await fetcher({
6286
+ url: url_exports.join(
6287
+ await Supplier.get(this._options.baseUrl) ?? await Supplier.get(this._options.environment),
6288
+ "v1/auth/token/assume"
6289
+ ),
6290
+ method: "POST",
6291
+ headers: _headers,
6292
+ contentType: "application/json",
6293
+ queryString: url_exports.queryBuilder().mergeAdditional(requestOptions?.queryParams).build(),
6294
+ requestType: "json",
6295
+ body: request,
6296
+ timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1e3,
6297
+ maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries,
6298
+ abortSignal: requestOptions?.abortSignal,
6299
+ fetchFn: this._options?.fetch,
6300
+ logging: this._options.logging
6301
+ });
6302
+ if (_response.ok) {
6303
+ return { data: _response.body, rawResponse: _response.rawResponse };
6304
+ }
6305
+ if (_response.error.reason === "status-code") {
6306
+ switch (_response.error.statusCode) {
6307
+ case 400:
6308
+ throw new BadRequestError(_response.error.body, _response.rawResponse);
6309
+ case 401:
6310
+ throw new UnauthorizedError(_response.error.body, _response.rawResponse);
6311
+ case 403:
6312
+ throw new ForbiddenError(_response.error.body, _response.rawResponse);
6313
+ case 409:
6314
+ throw new ConflictError(_response.error.body, _response.rawResponse);
6315
+ case 429:
6316
+ throw new TooManyRequestsError(_response.error.body, _response.rawResponse);
6317
+ default:
6318
+ throw new VectrosError({
6319
+ statusCode: _response.error.statusCode,
6320
+ body: _response.error.body,
6321
+ rawResponse: _response.rawResponse
6322
+ });
6323
+ }
6324
+ }
6325
+ return handleNonStatusCodeError(_response.error, _response.rawResponse, "POST", "/v1/auth/token/assume");
6326
+ }
6180
6327
  /**
6181
6328
  * RFC 8693 OAuth 2.0 Token Exchange. Trades a JWT issued by a third-party identity provider you've registered (`POST /v1/auth/issuers`) for a Vectros `st_*` scoped bearer token — no Vectros credential required to call this endpoint. The exchanged token's scope is resolved entirely server-side from the matched user's access profile; this endpoint accepts no caller-supplied scope, resource, or audience parameter (RFC 8693 §2.1's `resource`/`audience`/`scope` are not used in v1 — the registered `(issuer, audience)` pair alone pins the target tenant and app context). On a first-time login (no existing Vectros identity for this subject), two opt-in binding paths exist: `invite_token` (a `PENDING` sub-user invitation), and — if the registration declares one or more self-signup policies — `signup_type` (a brand-new user is created and bound to the policy's configured role). If `invite_token` is present at all, it is the ONLY path tried — a failed invite never falls through to self-signup. Neither field is required for a subject with an existing identity. If your issuer is registered against more than one app context (each via its own audience), `context_id` selects which one to target; omit it when your token's `aud` claim matches only one registered context — the common case, unaffected by this field. Uses the OAuth-standard error envelope (`{"error":..., "error_description":...}`, RFC 6749 §5.2), NOT this API's usual `{"message":...}` shape — its client is generic OAuth tooling, not the Vectros SDK.
6182
6329
  *
@@ -6254,9 +6401,9 @@ var ComplianceClient = class {
6254
6401
  * @param {Vectros.ErasureRequest} request
6255
6402
  * @param {ComplianceClient.RequestOptions} requestOptions - Request-specific configuration.
6256
6403
  *
6404
+ * @throws {@link Vectros.BadRequestError}
6257
6405
  * @throws {@link Vectros.ForbiddenError}
6258
6406
  * @throws {@link Vectros.TooManyRequestsError}
6259
- * @throws {@link Vectros.NotImplementedError}
6260
6407
  *
6261
6408
  * @example
6262
6409
  * await client.compliance.createErasureRequest({
@@ -6295,12 +6442,12 @@ var ComplianceClient = class {
6295
6442
  }
6296
6443
  if (_response.error.reason === "status-code") {
6297
6444
  switch (_response.error.statusCode) {
6445
+ case 400:
6446
+ throw new BadRequestError(_response.error.body, _response.rawResponse);
6298
6447
  case 403:
6299
6448
  throw new ForbiddenError(_response.error.body, _response.rawResponse);
6300
6449
  case 429:
6301
6450
  throw new TooManyRequestsError(_response.error.body, _response.rawResponse);
6302
- case 501:
6303
- throw new NotImplementedError(_response.error.body, _response.rawResponse);
6304
6451
  default:
6305
6452
  throw new VectrosError({
6306
6453
  statusCode: _response.error.statusCode,
@@ -6319,7 +6466,6 @@ var ComplianceClient = class {
6319
6466
  *
6320
6467
  * @throws {@link Vectros.ForbiddenError}
6321
6468
  * @throws {@link Vectros.NotFoundError}
6322
- * @throws {@link Vectros.NotImplementedError}
6323
6469
  *
6324
6470
  * @example
6325
6471
  * await client.compliance.getErasureRequest({
@@ -6360,8 +6506,6 @@ var ComplianceClient = class {
6360
6506
  throw new ForbiddenError(_response.error.body, _response.rawResponse);
6361
6507
  case 404:
6362
6508
  throw new NotFoundError(_response.error.body, _response.rawResponse);
6363
- case 501:
6364
- throw new NotImplementedError(_response.error.body, _response.rawResponse);
6365
6509
  default:
6366
6510
  throw new VectrosError({
6367
6511
  statusCode: _response.error.statusCode,
@@ -6909,7 +7053,21 @@ var DocumentsClient = class {
6909
7053
  return HttpResponsePromise.fromPromise(this.__lookupDocuments(request, requestOptions));
6910
7054
  }
6911
7055
  async __lookupDocuments(request, requestOptions) {
6912
- const { type: type_, field, value, from: from_, to, prefix, startFrom, limit, order, userId, scope } = request;
7056
+ const {
7057
+ type: type_,
7058
+ field,
7059
+ value,
7060
+ from: from_,
7061
+ to,
7062
+ prefix,
7063
+ sortFrom,
7064
+ sortTo,
7065
+ startFrom,
7066
+ limit,
7067
+ order,
7068
+ userId,
7069
+ scope
7070
+ } = request;
6913
7071
  const _queryParams = {
6914
7072
  type: type_,
6915
7073
  field,
@@ -6917,6 +7075,8 @@ var DocumentsClient = class {
6917
7075
  from: from_,
6918
7076
  to,
6919
7077
  prefix,
7078
+ sortFrom,
7079
+ sortTo,
6920
7080
  startFrom,
6921
7081
  limit,
6922
7082
  order: order != null ? order : void 0,
@@ -8201,7 +8361,7 @@ var IdentityClient = class {
8201
8361
  );
8202
8362
  }
8203
8363
  /**
8204
- * Retrieves a single scope-namespace registration by name. The reserved built-ins `org` and `client` are always resolvable.
8364
+ * Retrieves a single scope-namespace registration by name.
8205
8365
  *
8206
8366
  * @param {Vectros.GetNamespaceRequest} request
8207
8367
  * @param {IdentityClient.RequestOptions} requestOptions - Request-specific configuration.
@@ -8394,7 +8554,7 @@ var IdentityClient = class {
8394
8554
  return handleNonStatusCodeError(_response.error, _response.rawResponse, "DELETE", "/v1/namespaces/{namespace}");
8395
8555
  }
8396
8556
  /**
8397
- * Returns the scope namespaces in your account, including `org` and `client`, which are provisioned for you when the account is created. Returns a `{data, nextCursor}` envelope.
8557
+ * Returns the scope namespaces registered in your account. Returns a `{data, nextCursor}` envelope.
8398
8558
  *
8399
8559
  * @param {Vectros.ListNamespacesRequest} request
8400
8560
  * @param {IdentityClient.RequestOptions} requestOptions - Request-specific configuration.
@@ -8445,7 +8605,7 @@ var IdentityClient = class {
8445
8605
  return handleNonStatusCodeError(_response.error, _response.rawResponse, "GET", "/v1/namespaces");
8446
8606
  }
8447
8607
  /**
8448
- * Registers a new scope namespace and declares whether its values resolve to identity entities (`entityBacked`). Also requires `specificityRank`, an explicit, account-unique position in the specificity order used to break recordType schema-resolution ties. Requires a root API key or the CLI bootstrap's provisioning capability — never an ordinary partner-grantable scope. `org` and `client` are provisioned when your account is created, so registering either returns the already-registered error.
8608
+ * Registers a new scope namespace and declares whether its values resolve to identity entities (`entityBacked`). Also requires `specificityRank`, an explicit, account-unique position in the specificity order used to break recordType schema-resolution ties. Requires a root API key or one of the CLI bootstrap's two provisioning capabilities — never an ordinary partner-grantable scope. A bootstrap credential's registration is confined to its own app context UNLESS it additionally holds the tenant-wide namespace-provisioning capability and `contextId` is omitted, in which case it may register a TENANT-WIDE namespace (visible to every context). `org` and `client` are reserved names, registered the same way as any other namespace.
8449
8609
  *
8450
8610
  * @param {Vectros.RegisterNamespaceRequest} request
8451
8611
  * @param {IdentityClient.RequestOptions} requestOptions - Request-specific configuration.
@@ -8515,7 +8675,7 @@ var IdentityClient = class {
8515
8675
  return handleNonStatusCodeError(_response.error, _response.rawResponse, "POST", "/v1/namespaces");
8516
8676
  }
8517
8677
  /**
8518
- * Returns a paginated list of the users in your account. Pass `externalId` to look up a single user by your own identifier. To filter on schema-declared lookup fields, supply `type` and `field` together with one lookup mode: `value` (exact match), `from`+`to` (range), or `prefix`. Requires the `users:r` scope.
8678
+ * Returns a paginated list of the users in your account. Pass `externalId` to look up a single user by your own identifier. To filter on schema-declared lookup fields, supply `type` and `field` together with one lookup mode: `value` (exact match), `from`+`to` (range), or `prefix`. Requires the `users:r` scope. A context-confined credential only sees users who hold an access profile in the credential's own app context — others are silently absent from the page, not an error.
8519
8679
  *
8520
8680
  * @param {Vectros.ListUsersRequest} request
8521
8681
  * @param {IdentityClient.RequestOptions} requestOptions - Request-specific configuration.
@@ -8580,7 +8740,7 @@ var IdentityClient = class {
8580
8740
  return handleNonStatusCodeError(_response.error, _response.rawResponse, "GET", "/v1/users");
8581
8741
  }
8582
8742
  /**
8583
- * Creates a user identity in your account. The operation is idempotent on `externalId`: if a user with the same `externalId` already exists, the existing record is returned instead of creating a duplicate. The response's `created` field (and the HTTP status — 201 when created, 200 when an existing user was returned) tells the two apart. To overwrite an existing user's mutable fields (email, status, payload, schema binding) instead of returning it unchanged, set `?upsert=true` (this also requires the `users:u` scope). Requires the `users:c` scope to create. Being returned the existing user on a collision is a read of that user's data and additionally requires the `users:r` scope — a credential holding `users:c` alone receives a `400` ("already exists") on collision instead of the user.
8743
+ * Creates a user identity in your account. The operation is idempotent on `externalId`: if a user with the same `externalId` already exists, the existing record is returned instead of creating a duplicate. The response's `created` field (and the HTTP status — 201 when created, 200 when an existing user was returned) tells the two apart. To overwrite an existing user's mutable fields (email, status, payload, schema binding) instead of returning it unchanged, set `?upsert=true` (this also requires the `users:u` scope). Requires the `users:c` scope to create. Being returned the existing user on a collision is a read of that user's data and additionally requires the `users:r` scope — a credential holding `users:c` alone receives a `400` ("already exists") on collision instead of the user. A context-confined credential additionally receives the collision echo (or, with `?upsert=true`, is allowed to overwrite) only when the colliding user holds an access profile in the credential's own app context — a cross-context collision otherwise gets the same uniform `400` instead of the other context's user data. One narrow exception on the plain (non-upsert) echo only: the CLI's no-code bootstrap credential can be echoed a colliding user across app contexts, needed for a blueprint to idempotently re-apply against a service principal it provisioned in a non-default context; this does not extend to `?upsert=true`.
8584
8744
  *
8585
8745
  * @param {Vectros.CreateUserRequest} request
8586
8746
  * @param {IdentityClient.RequestOptions} requestOptions - Request-specific configuration.
@@ -8704,7 +8864,7 @@ var IdentityClient = class {
8704
8864
  return handleNonStatusCodeError(_response.error, _response.rawResponse, "GET", "/v1/users/{id}");
8705
8865
  }
8706
8866
  /**
8707
- * Updates mutable fields on an existing user (such as email, status, payload, or schema binding). The `type` field is immutable after creation, and `email` cannot be changed while an invitation to that user is still outstanding — revoke the invitation, or invite the new address instead. This endpoint also activates an invited user: a PUT that moves a PENDING user to ACTIVE and carries `inviteToken`, `externalSubject`, and `emailVerifiedAttestation=true` completes the invitation. Requires the `users:u` scope.
8867
+ * Updates mutable fields on an existing user (such as email, status, payload, or schema binding). The `type` field is immutable after creation, and `email` cannot be changed while an invitation to that user is still outstanding — revoke the invitation, or invite the new address instead. This endpoint also activates an invited user: a PUT that moves a PENDING user to ACTIVE and carries `inviteToken`, `externalSubject`, and `emailVerifiedAttestation=true` completes the invitation. **`externalSubject` set this way is NOT an authentication binding and does not enable sign-in anywhere** — not the Vectros DevPortal/Admin App/web apps, and not `/v1/auth/token/exchange` either; both are granted only by the invitee authenticating themselves. If the invitee needs dev-portal/web-app access, send them the invitation link and let them accept it directly. If the invitee needs to sign in via token exchange, have them redeem `inviteToken` themselves at `/v1/auth/token/exchange` with their own credential — Vectros verifies it there and computes its own value for this field from the verified credential. The value you send here is stored exactly as-is, with no normalization, and does NOT automatically match or deduplicate against what a later token-exchange sign-in computes for the same real-world identity. Call this endpoint directly only if you run your own backend and want Vectros to record which of your own users a given account corresponds to — treat this field as informational for that purpose, not as a way to pre-authorize sign-in. Requires the `users:u` scope.
8708
8868
  *
8709
8869
  * @param {Vectros.UpdateUserRequest} request
8710
8870
  * @param {IdentityClient.RequestOptions} requestOptions - Request-specific configuration.
@@ -8771,7 +8931,7 @@ var IdentityClient = class {
8771
8931
  return handleNonStatusCodeError(_response.error, _response.rawResponse, "PUT", "/v1/users/{id}");
8772
8932
  }
8773
8933
  /**
8774
- * Permanently deletes a user identity. This cannot be undone. If the user is a pending invitation, the associated access profile created for that invitation is also removed. Requires the `users:d` scope. Deleting your account's last OWNER is refused (409) — an account must always retain at least one owner.
8934
+ * Permanently deletes a user identity. This cannot be undone. If the user is a pending invitation, the associated access profile created for that invitation is also removed. Requires the `users:d` scope. Deleting your account's last OWNER is refused (409) — an account must always retain at least one owner. Called by a context-confined credential, deletion is also refused (409) when the user holds access in an app context other than the caller's own — remove the user from the caller's own context first, or use a credential with cross-context reach.
8775
8935
  *
8776
8936
  * @param {Vectros.DeleteUserRequest} request
8777
8937
  * @param {IdentityClient.RequestOptions} requestOptions - Request-specific configuration.
@@ -8832,7 +8992,7 @@ var IdentityClient = class {
8832
8992
  return handleNonStatusCodeError(_response.error, _response.rawResponse, "DELETE", "/v1/users/{id}");
8833
8993
  }
8834
8994
  /**
8835
- * Looks up users by a schema lookup field, with the query criteria carried in the request body rather than the URL. Use this when looking up by a sensitive (blind-indexed) field: the value is blind-indexed server-side and never appears in the URL, request logs, or proxies. The query semantics are identical to the GET /v1/users lookup, which rejects sensitive-field values and directs you here. Returns a page in the `{data, nextCursor}` envelope. Requires the `users:r` scope.
8995
+ * Looks up users by a schema lookup field, with the query criteria carried in the request body rather than the URL. Use this when looking up by a sensitive (blind-indexed) field: the value is blind-indexed server-side and never appears in the URL, request logs, or proxies. The query semantics are identical to the GET /v1/users lookup, which rejects sensitive-field values and directs you here. Returns a page in the `{data, nextCursor}` envelope. Requires the `users:r` scope. A context-confined credential only sees users who hold an access profile in the credential's own app context — others are silently absent from the page, not an error.
8836
8996
  *
8837
8997
  * @param {Vectros.IdentityLookupRequest} request
8838
8998
  * @param {IdentityClient.RequestOptions} requestOptions - Request-specific configuration.
@@ -9275,14 +9435,14 @@ var RecordsClient = class {
9275
9435
  this._options = normalizeClientOptionsWithAuth(options);
9276
9436
  }
9277
9437
  /**
9278
- * Reserved endpoint for fetching multiple records by ID in one call. When available, the response will contain only the records you can see; any IDs that do not exist or are outside your scope are silently omitted (there is no per-ID existence signal), matching the not-found behavior of the single-record GET. It currently returns 501 (not implemented). The documented 200 response schema is the stable shape this endpoint will use once available. Requires the `records:r` scope.
9438
+ * Fetches multiple records by ID in one call (`ids`, maximum 100). The response contains only the records you can access — any id that does not exist, belongs to another account/AppContext, or is outside your token's scope is silently omitted, with no per-id existence signal, matching the not-found behavior of the single-record GET. Payloads are hydrated the same way a by-id GET hydrates them (payloads externalized to object storage are rehydrated for this response). Requires the `records:r` scope (and, for a scoped token, `records:r:<type>` per record type).
9279
9439
  *
9280
9440
  * @param {Vectros.BatchGetRequest} request
9281
9441
  * @param {RecordsClient.RequestOptions} requestOptions - Request-specific configuration.
9282
9442
  *
9443
+ * @throws {@link Vectros.BadRequestError}
9283
9444
  * @throws {@link Vectros.ForbiddenError}
9284
9445
  * @throws {@link Vectros.TooManyRequestsError}
9285
- * @throws {@link Vectros.NotImplementedError}
9286
9446
  *
9287
9447
  * @example
9288
9448
  * await client.records.batchGetRecords()
@@ -9319,12 +9479,12 @@ var RecordsClient = class {
9319
9479
  }
9320
9480
  if (_response.error.reason === "status-code") {
9321
9481
  switch (_response.error.statusCode) {
9482
+ case 400:
9483
+ throw new BadRequestError(_response.error.body, _response.rawResponse);
9322
9484
  case 403:
9323
9485
  throw new ForbiddenError(_response.error.body, _response.rawResponse);
9324
9486
  case 429:
9325
9487
  throw new TooManyRequestsError(_response.error.body, _response.rawResponse);
9326
- case 501:
9327
- throw new NotImplementedError(_response.error.body, _response.rawResponse);
9328
9488
  default:
9329
9489
  throw new VectrosError({
9330
9490
  statusCode: _response.error.statusCode,
@@ -10138,7 +10298,7 @@ var SchemasClient = class {
10138
10298
  this._options = normalizeClientOptionsWithAuth(options);
10139
10299
  }
10140
10300
  /**
10141
- * Returns a paginated list of the record schemas defined in your account. Filter by `userId` or `scope` to scope to an owner, by `surface` to list the types bindable to one surface, or by `recordType` to resolve the single schema for a type directly. Filtering by an identity surface (`user` or `entity`) lists your account-wide identity schemas regardless of the calling context; filtering by record or document lists within the calling context. Requires the `schemas:r` scope.
10301
+ * Returns a paginated list of the record schemas defined in your account. Filter by `userId` or `scope` to scope to an owner, by `surface` to list the types bindable to one surface, or by `recordType` to resolve the single schema for a type directly. Filtering by `surface=user` lists your account-wide identity schemas regardless of the calling context (a `user`-surfaced schema always has one tenant-wide home). Filtering by `surface=entity` reads your own app context's entity schemas AND the tenant-wide home together, newest first, through a single cursor — an entity schema may live in either home depending on where its namespace is placed (see `POST /v1/namespaces`). Filtering by `record` or `document` lists within the calling context only. Requires the `schemas:r` scope.
10142
10302
  *
10143
10303
  * @param {Vectros.ListSchemasRequest} request
10144
10304
  * @param {SchemasClient.RequestOptions} requestOptions - Request-specific configuration.
@@ -10426,6 +10586,7 @@ var SchemasClient = class {
10426
10586
  * @param {Vectros.DeleteSchemaRequest} request
10427
10587
  * @param {SchemasClient.RequestOptions} requestOptions - Request-specific configuration.
10428
10588
  *
10589
+ * @throws {@link Vectros.ForbiddenError}
10429
10590
  * @throws {@link Vectros.NotFoundError}
10430
10591
  * @throws {@link Vectros.ConflictError}
10431
10592
  * @throws {@link Vectros.TooManyRequestsError}
@@ -10465,6 +10626,8 @@ var SchemasClient = class {
10465
10626
  }
10466
10627
  if (_response.error.reason === "status-code") {
10467
10628
  switch (_response.error.statusCode) {
10629
+ case 403:
10630
+ throw new ForbiddenError(_response.error.body, _response.rawResponse);
10468
10631
  case 404:
10469
10632
  throw new NotFoundError(_response.error.body, _response.rawResponse);
10470
10633
  case 409:
@@ -10885,7 +11048,21 @@ var BlueprintSchemaSchema = z.object({
10885
11048
  // (it becomes that name's shared base). Must point directly at the base (one
10886
11049
  // hop); immutable once set. Mirrors the `basedOn` field on the platform's
10887
11050
  // schema-create request.
10888
- basedOn: z.string().min(1).optional()
11051
+ basedOn: z.string().min(1).optional(),
11052
+ // ADVISORY ONLY — not sent to the platform, never runtime-enforced. The
11053
+ // ownership dimensions this schema's records are typically scoped by
11054
+ // (e.g. ['org', 'client']), declared so the CLI's blueprint lint can warn
11055
+ // when a role clause grants r/u/d on this type but its `dataScope` names
11056
+ // only SOME of these dims — a schema author's easiest way to leave a
11057
+ // dimension unintentionally unconstrained, since reads (unlike creates)
11058
+ // never require full-dimension `dataScope` coverage — this helps an
11059
+ // author NOTICE that read/write asymmetry, not change it; the asymmetry
11060
+ // itself is a ratified, deliberate platform behavior.
11061
+ // Same grammar as a `dataScope` key with the `scope:` prefix stripped:
11062
+ // a bare namespace name, or 'userId' for the principal dimension.
11063
+ // Structural-only here (validated below by `lintExpectedScopeDims`); the
11064
+ // cross-referencing lint against role clauses lives in `packages/cli`.
11065
+ expectedScopeDims: z.array(z.string().min(1)).max(MAX_SCOPE_DIMENSIONS).optional()
10889
11066
  }).strict().superRefine((schema, ctx) => {
10890
11067
  const hasComposite = (schema.lookupFields ?? []).some(
10891
11068
  (lf) => typeof lf === "object" && lf.fieldNames !== void 0
@@ -10933,7 +11110,26 @@ var GrantedCapabilitiesSchema = z.array(z.string().min(1)).optional().superRefin
10933
11110
  var BlueprintAccessProfileSchema = z.object({
10934
11111
  // Validated structurally here; the SCOPE GATE (in @vectros-ai/cli) is
10935
11112
  // what enforces the data-plane-only security boundary.
10936
- allowedActions: z.array(z.string().min(1)).min(1),
11113
+ //
11114
+ // Optional now: mutually exclusive with `roleIds` below (the
11115
+ // `.superRefine` on this schema enforces the XOR, mirroring the platform's
11116
+ // own access-profile scopes/role-composition XOR check). Exactly one of
11117
+ // the two authors this profile's grant.
11118
+ allowedActions: z.array(z.string().min(1)).min(1).optional(),
11119
+ // Additive role composition: this profile's effective grant is
11120
+ // each named role's own clauses, concatenated in the order listed, instead of
11121
+ // an inline clause authored here. Every id must name a role this SAME
11122
+ // blueprint declares in `roles` (`lintAccessProfileRoleIds` below — the
11123
+ // authoring-time mirror of the platform's same-`(tenantId, contextId)`
11124
+ // check, trivially satisfied here since a blueprint's own `roles` are
11125
+ // always created in its own `contextId`); duplicates are rejected outright
11126
+ // rather than silently deduplicated. Mutually exclusive
11127
+ // with `allowedActions`/`dataScope`/`capabilities` — a role-referencing
11128
+ // profile has no inline clause of its own to carry a dataScope or
11129
+ // capability grant; author those on the referenced Role instead, where
11130
+ // every profile referencing it picks them up uniformly (mirrors how
11131
+ // `assumable` already works for a `roleId`-referencing profile).
11132
+ roleIds: z.array(z.string().min(1)).min(1).optional(),
10937
11133
  // Optional ownership binding: { userId: [...], "scope:org": [...], "scope:client": [...] }.
10938
11134
  // A `null` element in a value list is the documented NULL SENTINEL — it
10939
11135
  // grants access to TENANT-LEVEL (owner-less) records IN ADDITION to the
@@ -10981,13 +11177,54 @@ var BlueprintAccessProfileSchema = z.object({
10981
11177
  // a person is bound to. The gate should weigh the two separately rather
10982
11178
  // than reuse one policy for both.
10983
11179
  capabilities: GrantedCapabilitiesSchema
10984
- }).strict();
11180
+ }).strict().superRefine((ap, ctx) => {
11181
+ const hasInline = ap.allowedActions !== void 0;
11182
+ const hasRoleIds = ap.roleIds !== void 0;
11183
+ if (hasInline === hasRoleIds) {
11184
+ ctx.addIssue({
11185
+ code: z.ZodIssueCode.custom,
11186
+ path: ["roleIds"],
11187
+ message: hasInline ? "accessProfile carries both 'allowedActions' and 'roleIds' \u2014 provide exactly one: an inline clause (allowedActions, optionally dataScope/capabilities) OR a role composition (roleIds), never both" : "accessProfile carries neither 'allowedActions' nor 'roleIds' \u2014 provide exactly one: an inline clause (allowedActions, optionally dataScope/capabilities) OR a role composition (roleIds)"
11188
+ });
11189
+ return;
11190
+ }
11191
+ if (hasRoleIds) {
11192
+ if (ap.dataScope !== void 0) {
11193
+ ctx.addIssue({
11194
+ code: z.ZodIssueCode.custom,
11195
+ path: ["dataScope"],
11196
+ message: "accessProfile.dataScope is not valid alongside 'roleIds' \u2014 a role-composed profile has no inline clause of its own; author dataScope on the referenced role's clause instead"
11197
+ });
11198
+ }
11199
+ if (ap.capabilities !== void 0) {
11200
+ ctx.addIssue({
11201
+ code: z.ZodIssueCode.custom,
11202
+ path: ["capabilities"],
11203
+ message: "accessProfile.capabilities is not valid alongside 'roleIds' \u2014 a role-composed profile has no inline clause of its own; author capabilities on the referenced role's clause instead"
11204
+ });
11205
+ }
11206
+ }
11207
+ });
10985
11208
  var BlueprintRoleClauseSchema = z.object({
10986
11209
  allowedActions: z.array(z.string().min(1)).min(1),
10987
11210
  dataScope: z.record(z.array(z.union([z.string().min(1), z.null()]))).optional(),
11211
+ dataScopeRef: z.string().min(1).optional(),
10988
11212
  capabilities: GrantedCapabilitiesSchema
10989
- }).strict();
11213
+ }).strict().superRefine((c, ctx) => {
11214
+ if (c.dataScope !== void 0 && c.dataScopeRef !== void 0) {
11215
+ ctx.addIssue({
11216
+ code: z.ZodIssueCode.custom,
11217
+ path: ["dataScopeRef"],
11218
+ message: "a clause may declare 'dataScope' OR 'dataScopeRef', not both \u2014 dataScopeRef is sugar for an inline dataScope fragment, so the two are alternatives for the same slot, not additive"
11219
+ });
11220
+ }
11221
+ });
11222
+ var BlueprintFragmentsSchema = z.record(
11223
+ z.record(z.array(z.union([z.string().min(1), z.null()])))
11224
+ );
10990
11225
  var BlueprintRolesSchema = z.record(z.array(BlueprintRoleClauseSchema).min(1));
11226
+ var BlueprintRoleAssumableSchema = z.record(z.array(z.string().min(1)).min(1));
11227
+ var BlueprintRoleAssumableMapSchema = z.record(BlueprintRoleAssumableSchema);
10991
11228
  var IDENTITY_NAMESPACE_RE = /^[a-z][a-z0-9_-]{1,31}$/;
10992
11229
  var FORBIDDEN_IDENTITY_NAMESPACES = /* @__PURE__ */ new Set(["record", "document", "entity", "user"]);
10993
11230
  var IdentityKindSchema = z.string().min(1).superRefine((val, ctx) => {
@@ -11051,6 +11288,14 @@ var BlueprintIssuerSchema = z.object({
11051
11288
  subClaim: z.string().min(1).optional(),
11052
11289
  /** The claim carrying the subject's email (first-login invite matching). Platform defaults to 'email' when omitted. */
11053
11290
  emailClaim: z.string().min(1).optional(),
11291
+ /**
11292
+ * The IdP's OIDC userinfo endpoint. Optional. Presented tokens are access tokens, which under
11293
+ * OIDC don't carry `email` unless the IdP was specifically configured to add it — if
11294
+ * `emailClaim` misses on the presented token, and `userinfoUri` is configured, Vectros falls
11295
+ * back to calling this endpoint (with the presented token as the bearer credential) and reads
11296
+ * `emailClaim` from its JSON response instead. Omit to leave the fallback disabled.
11297
+ */
11298
+ userinfoUri: z.string().min(1).optional(),
11054
11299
  /**
11055
11300
  * Opt-in self-service signup: a list of {signupType, roleId} pairs. When a first-time exchange
11056
11301
  * caller presents no invite token but names a signupType matching one of these (or omits it and
@@ -11080,6 +11325,24 @@ var BlueprintNamespaceSchema = z.object({
11080
11325
  * free-form strings validated by grammar only.
11081
11326
  */
11082
11327
  entityBacked: z.boolean().optional(),
11328
+ /**
11329
+ * Request the platform's TENANT-WIDE registration form instead of this
11330
+ * blueprint's own context — visible to every context in the account, not
11331
+ * just the one this blueprint provisions. `false`/omitted (the default)
11332
+ * is the ordinary, context-owned registration every other namespace here
11333
+ * gets. Mutually exclusive with any future context-targeting field this
11334
+ * schema may grow — there is exactly one way to say "not my own context"
11335
+ * today.
11336
+ *
11337
+ * A blueprint declaring this does NOT get it automatically: the applying
11338
+ * credential must separately hold a platform capability granted only to
11339
+ * an account OWNER, and the CLI refuses to even request it unless
11340
+ * explicitly told to (see `--allow-tenant-wide-namespaces`) — no flag,
11341
+ * no request, regardless of what this field says. Setting this to `true`
11342
+ * states the blueprint's INTENT; it is not, on its own, a grant of
11343
+ * anything.
11344
+ */
11345
+ tenantWide: z.boolean().optional(),
11083
11346
  /**
11084
11347
  * This namespace's position in your account's specificity order (used to
11085
11348
  * break a tie when a caller holds two scope dimensions at once). Required
@@ -11263,7 +11526,7 @@ function lintSelfTokenPlacement(value, ctx) {
11263
11526
  ctx.addIssue({
11264
11527
  code: z.ZodIssueCode.custom,
11265
11528
  path: path4,
11266
- message: "'${{ self.* }}' / '${{ member.* }}' is a runtime placeholder \u2014 it is only valid inside a roles[].dataScope value"
11529
+ message: "'${{ self.* }}' / '${{ member.* }}' is a runtime placeholder \u2014 it is only valid inside a roles[].dataScope value, a roleAssumable[] value, or a fragments[] value"
11267
11530
  });
11268
11531
  }
11269
11532
  return;
@@ -11274,7 +11537,10 @@ function lintSelfTokenPlacement(value, ctx) {
11274
11537
  }
11275
11538
  if (node && typeof node === "object") {
11276
11539
  for (const [k, v] of Object.entries(node)) {
11277
- const entering = inRoleDataScope || path4.length === 3 && path4[0] === "roles" && k === "dataScope";
11540
+ const inRoleDataScopeClause = path4.length === 3 && path4[0] === "roles" && k === "dataScope";
11541
+ const inRoleAssumable = path4.length === 0 && k === "roleAssumable";
11542
+ const inFragments = path4.length === 0 && k === "fragments";
11543
+ const entering = inRoleDataScope || inRoleDataScopeClause || inRoleAssumable || inFragments;
11278
11544
  walk(v, [...path4, k], entering);
11279
11545
  }
11280
11546
  }
@@ -11445,76 +11711,266 @@ function lintDataScopeKeys(value, ctx) {
11445
11711
  }
11446
11712
  });
11447
11713
  }
11448
- }
11449
- function lintSeedSurfaces(value, ctx) {
11450
- const bp = value;
11451
- if (!Array.isArray(bp.seed)) return;
11452
- const byType = new Map((bp.schemas ?? []).map((s) => [s.typeName, s]));
11453
- bp.seed.forEach((seed, i) => {
11454
- const schema = byType.get(seed.typeName);
11455
- if (!schema) return;
11456
- const allowed = schema.allowedSurfaces ?? ["record"];
11457
- if (!allowed.includes(seed.surface ?? "record")) {
11458
- ctx.addIssue({
11459
- code: z.ZodIssueCode.custom,
11460
- path: ["seed", i, "surface"],
11461
- message: `seed '${seed.externalId}' uses surface '${seed.surface ?? "record"}', but schema '${seed.typeName}' allows only [${allowed.join(", ")}]`
11462
- });
11714
+ for (const [name, fragment] of Object.entries(bp.fragments ?? {})) {
11715
+ for (const key of Object.keys(fragment ?? {})) {
11716
+ lintDataScopeKey(key, ["fragments", name, key], ctx);
11463
11717
  }
11464
- });
11718
+ }
11465
11719
  }
11466
- function lintIdentitySurfaces(value, ctx) {
11720
+ function lintExpectedScopeDims(value, ctx) {
11467
11721
  const bp = value;
11468
- if (!Array.isArray(bp.schemas)) return;
11469
- bp.schemas.forEach((schema, i) => {
11470
- if (!schema.allowedSurfaces?.includes("user")) return;
11471
- ctx.addIssue({
11472
- code: z.ZodIssueCode.custom,
11473
- path: ["schemas", i, "allowedSurfaces"],
11474
- message: `schema '${schema.typeName}': allowedSurfaces cannot include 'user' \u2014 a user-surfaced schema is account-global and requires a root API key, which no blueprint credential ever holds, so this would fail at apply with a 403. Model the data as a 'record' scoped to the user, or as an 'entity' schema (which a blueprint CAN provision, in its own app context).`
11722
+ (bp.schemas ?? []).forEach((schema, i) => {
11723
+ const dims = schema.expectedScopeDims;
11724
+ if (!dims) return;
11725
+ const seen = /* @__PURE__ */ new Set();
11726
+ dims.forEach((raw, j) => {
11727
+ const dim = String(raw);
11728
+ const path4 = ["schemas", i, "expectedScopeDims", j];
11729
+ if (seen.has(dim)) {
11730
+ ctx.addIssue({
11731
+ code: z.ZodIssueCode.custom,
11732
+ path: path4,
11733
+ message: `schema '${schema.typeName}': expectedScopeDims lists '${dim}' more than once.`
11734
+ });
11735
+ }
11736
+ seen.add(dim);
11737
+ if (dim === "userId") return;
11738
+ const nsError = scopeNamespaceGrammarError(dim);
11739
+ if (nsError !== null) {
11740
+ ctx.addIssue({
11741
+ code: z.ZodIssueCode.custom,
11742
+ path: path4,
11743
+ message: `schema '${schema.typeName}': expectedScopeDims['${dim}'] ${nsError}`
11744
+ });
11745
+ }
11475
11746
  });
11476
11747
  });
11477
11748
  }
11478
- function lintIssuerContext(value, ctx) {
11749
+ function lintDataScopeRefs(value, ctx) {
11479
11750
  const bp = value;
11480
- if (!Array.isArray(bp.issuers)) return;
11481
- bp.issuers.forEach((issuer, i) => {
11482
- if (issuer.contextId === void 0 || issuer.contextId === bp.contextId) return;
11483
- ctx.addIssue({
11484
- code: z.ZodIssueCode.custom,
11485
- path: ["issuers", i, "contextId"],
11486
- message: `issuer '${issuer.issuerId}' targets app context '${issuer.contextId}', but this blueprint provisions '${bp.contextId}'. An issuer must target the blueprint's own context \u2014 everything else a blueprint creates does, and an issuer that points elsewhere attaches an identity provider to an app context the blueprint never mentions. Set it to '${bp.contextId}', or move this issuer into a blueprint for '${issuer.contextId}'.`
11751
+ const declared = new Set(Object.keys(bp.fragments ?? {}));
11752
+ for (const [roleId, clauses] of Object.entries(bp.roles ?? {})) {
11753
+ clauses.forEach((clause, i) => {
11754
+ if (clause.dataScopeRef === void 0) return;
11755
+ if (!declared.has(clause.dataScopeRef)) {
11756
+ ctx.addIssue({
11757
+ code: z.ZodIssueCode.custom,
11758
+ path: ["roles", roleId, i, "dataScopeRef"],
11759
+ message: `dataScopeRef '${clause.dataScopeRef}' is not declared in fragments \u2014 declared fragments: ` + (declared.size ? [...declared].join(", ") : "(none)")
11760
+ });
11761
+ }
11487
11762
  });
11488
- });
11763
+ }
11489
11764
  }
11490
- var BLUEPRINT_FIELD_PHASES = Object.freeze({
11491
- servicePrincipal: "bootstrap",
11492
- issuers: "bootstrap",
11493
- // Registration requires the platform's provisioning:c capability, which
11494
- // only the bootstrap credential carries — same reasoning as issuers.
11495
- namespaces: "bootstrap",
11496
- schemas: "in-context",
11497
- accessProfile: "in-context",
11498
- roles: "in-context",
11499
- seed: "in-context"
11500
- });
11501
- var BlueprintSchema = z.object({
11502
- /** Stable blueprint id (the `--blueprint <name>` selector + idempotency key). */
11503
- name: z.string().min(1),
11504
- version: z.string().min(1),
11505
- description: z.string().min(1),
11765
+ var BARE_SELF_PLACEHOLDER_RE = /^\$\{\{\s*self\.[^}]*?\s*\}\}$/;
11766
+ var UNDER_SELF_PLACEHOLDER_RE = /^\$\{\{\s*under\.self\.userId\s*\}\}$/;
11767
+ var UNDER_SELF_SCOPE_PLACEHOLDER_RE = /^\$\{\{\s*under\.self\.scope\.[a-z][a-z0-9_-]{1,31}\s*\}\}$/;
11768
+ var MEMBER_SCOPE_PLACEHOLDER_RE = /^\$\{\{\s*member\.scope\.[a-z][a-z0-9_-]{1,31}(?::[a-z][a-z0-9_-]{1,31})?\s*\}\}$/;
11769
+ var ANY_PLACEHOLDER_RE = /^\$\{\{\s*any\s*\}\}$/;
11770
+ function assumableValueError(publicKey, v) {
11771
+ if (UNDER_SELF_PLACEHOLDER_RE.test(v) || MEMBER_SCOPE_PLACEHOLDER_RE.test(v)) return null;
11772
+ if (UNDER_SELF_SCOPE_PLACEHOLDER_RE.test(v)) {
11773
+ return `assumable['${publicKey}'] value '${v}' resolves against your own current value for that namespace, which an assume can itself change \u2014 so what this grant admits would depend on what was last assumed. Use '\${{ under.self.userId }}', '\${{ member.scope.<namespace>[:level] }}', or a plain literal, all of which mean the same thing before and after an assume. ('\${{ under.self.scope.<namespace> }}' remains valid in dataScope, where it is re-derived per write)`;
11774
+ }
11775
+ if (ANY_PLACEHOLDER_RE.test(v)) {
11776
+ return `assumable['${publicKey}'] value '${v}' is not a concrete value and cannot be authored as an assumable grant \u2014 name the specific value(s) holders of this role may become`;
11777
+ }
11778
+ if (BARE_SELF_PLACEHOLDER_RE.test(v)) {
11779
+ return `assumable['${publicKey}'] value '${v}' is the bare self placeholder, which is tautological here \u2014 it can only ever equal whatever identity currently is, never a distinct value to become. Use '\${{ under.self.userId }}', '\${{ member.scope.<namespace>[:level] }}', or a plain literal instead`;
11780
+ }
11781
+ if (SELF_TOKEN_RE.test(v)) {
11782
+ return `assumable['${publicKey}'] value '${v}' is not a recognized runtime placeholder \u2014 use '\${{ under.self.userId }}' or '\${{ member.scope.<namespace>[:level] }}'`;
11783
+ }
11784
+ if (!SCOPE_VALUE_RE.test(v)) {
11785
+ return `assumable['${publicKey}'] value '${v}' is not a valid scope value \u2014 1-128 characters, starting with a letter or digit and continuing with letters, digits, '_' or '-'`;
11786
+ }
11787
+ return null;
11788
+ }
11789
+ function lintAccessProfileRoleIds(value, ctx) {
11790
+ const bp = value;
11791
+ const roleIds = bp.accessProfile?.roleIds;
11792
+ if (!roleIds) return;
11793
+ const declaredRoles = new Set(Object.keys(bp.roles ?? {}));
11794
+ const seen = /* @__PURE__ */ new Set();
11795
+ roleIds.forEach((roleId, i) => {
11796
+ const path4 = ["accessProfile", "roleIds", i];
11797
+ if (!declaredRoles.has(roleId)) {
11798
+ ctx.addIssue({
11799
+ code: z.ZodIssueCode.custom,
11800
+ path: path4,
11801
+ message: `accessProfile.roleIds names '${roleId}', which is not a role this blueprint declares in roles \u2014 ` + (declaredRoles.size ? `declared roles: ${[...declaredRoles].join(", ")}` : "declared roles: (none)")
11802
+ });
11803
+ }
11804
+ if (seen.has(roleId)) {
11805
+ ctx.addIssue({
11806
+ code: z.ZodIssueCode.custom,
11807
+ path: path4,
11808
+ message: `accessProfile.roleIds names '${roleId}' more than once \u2014 remove the duplicate`
11809
+ });
11810
+ }
11811
+ seen.add(roleId);
11812
+ });
11813
+ }
11814
+ function lintRoleAssumable(value, ctx) {
11815
+ const bp = value;
11816
+ const declaredRoles = new Set(Object.keys(bp.roles ?? {}));
11817
+ for (const [roleId, assumable] of Object.entries(bp.roleAssumable ?? {})) {
11818
+ if (!declaredRoles.has(roleId)) {
11819
+ ctx.addIssue({
11820
+ code: z.ZodIssueCode.custom,
11821
+ path: ["roleAssumable", roleId],
11822
+ message: `roleAssumable names '${roleId}', which is not a role this blueprint declares in roles \u2014 ` + (declaredRoles.size ? `declared roles: ${[...declaredRoles].join(", ")}` : "declared roles: (none)")
11823
+ });
11824
+ continue;
11825
+ }
11826
+ for (const [key, values] of Object.entries(assumable)) {
11827
+ const path4 = ["roleAssumable", roleId, key];
11828
+ if (key === "userId") {
11829
+ ctx.addIssue({
11830
+ code: z.ZodIssueCode.custom,
11831
+ path: path4,
11832
+ message: "assumable cannot name the principal ('userId') \u2014 the principal is never assumable via /assume, and a grant for it would save but could never be exercised"
11833
+ });
11834
+ continue;
11835
+ }
11836
+ const ns = key.startsWith("scope:") ? key.slice("scope:".length) : null;
11837
+ const nsError = ns === null ? null : scopeNamespaceGrammarError(ns);
11838
+ if (ns === null || nsError !== null) {
11839
+ ctx.addIssue({
11840
+ code: z.ZodIssueCode.custom,
11841
+ path: path4,
11842
+ message: ns === null ? `'${key}' is not allowed \u2014 assumable keys must be a namespaced 'scope:<namespace>' key (the principal cannot be named)` : `assumable['${key}'] ${nsError}`
11843
+ });
11844
+ continue;
11845
+ }
11846
+ if (!Array.isArray(values)) continue;
11847
+ values.forEach((v, i) => {
11848
+ if (typeof v !== "string") return;
11849
+ const err = assumableValueError(key, v);
11850
+ if (err) ctx.addIssue({ code: z.ZodIssueCode.custom, path: [...path4, i], message: err });
11851
+ });
11852
+ }
11853
+ }
11854
+ }
11855
+ function expandDataScopeRefs(bp) {
11856
+ if (!bp.roles) return bp;
11857
+ const fragments = bp.fragments ?? {};
11858
+ let changedAnyRole = false;
11859
+ const roles = {};
11860
+ for (const [roleId, clauses] of Object.entries(bp.roles)) {
11861
+ let changedThisRole = false;
11862
+ const expandedClauses = clauses.map((clause) => {
11863
+ if (clause.dataScopeRef === void 0) return clause;
11864
+ changedThisRole = true;
11865
+ const { dataScopeRef, ...rest } = clause;
11866
+ if (!(dataScopeRef in fragments)) {
11867
+ throw new Error(
11868
+ `expandDataScopeRefs: dataScopeRef '${dataScopeRef}' does not resolve against fragments \u2014 this should have been caught by lintDataScopeRefs before parseBlueprint reaches this function`
11869
+ );
11870
+ }
11871
+ return { ...rest, dataScope: fragments[dataScopeRef] };
11872
+ });
11873
+ roles[roleId] = changedThisRole ? expandedClauses : clauses;
11874
+ changedAnyRole = changedAnyRole || changedThisRole;
11875
+ }
11876
+ return changedAnyRole ? { ...bp, roles } : bp;
11877
+ }
11878
+ function lintSeedSurfaces(value, ctx) {
11879
+ const bp = value;
11880
+ if (!Array.isArray(bp.seed)) return;
11881
+ const byType = new Map((bp.schemas ?? []).map((s) => [s.typeName, s]));
11882
+ bp.seed.forEach((seed, i) => {
11883
+ const schema = byType.get(seed.typeName);
11884
+ if (!schema) return;
11885
+ const allowed = schema.allowedSurfaces ?? ["record"];
11886
+ if (!allowed.includes(seed.surface ?? "record")) {
11887
+ ctx.addIssue({
11888
+ code: z.ZodIssueCode.custom,
11889
+ path: ["seed", i, "surface"],
11890
+ message: `seed '${seed.externalId}' uses surface '${seed.surface ?? "record"}', but schema '${seed.typeName}' allows only [${allowed.join(", ")}]`
11891
+ });
11892
+ }
11893
+ });
11894
+ }
11895
+ function lintIdentitySurfaces(value, ctx) {
11896
+ const bp = value;
11897
+ if (!Array.isArray(bp.schemas)) return;
11898
+ bp.schemas.forEach((schema, i) => {
11899
+ if (!schema.allowedSurfaces?.includes("user")) return;
11900
+ ctx.addIssue({
11901
+ code: z.ZodIssueCode.custom,
11902
+ path: ["schemas", i, "allowedSurfaces"],
11903
+ message: `schema '${schema.typeName}': allowedSurfaces cannot include 'user' \u2014 a user-surfaced schema is account-global and requires a root API key, which no blueprint credential ever holds, so this would fail at apply with a 403. Model the data as a 'record' scoped to the user, or as an 'entity' schema (which a blueprint CAN provision, in its own app context).`
11904
+ });
11905
+ });
11906
+ }
11907
+ function lintIssuerContext(value, ctx) {
11908
+ const bp = value;
11909
+ if (!Array.isArray(bp.issuers)) return;
11910
+ bp.issuers.forEach((issuer, i) => {
11911
+ if (issuer.contextId === void 0 || issuer.contextId === bp.contextId) return;
11912
+ ctx.addIssue({
11913
+ code: z.ZodIssueCode.custom,
11914
+ path: ["issuers", i, "contextId"],
11915
+ message: `issuer '${issuer.issuerId}' targets app context '${issuer.contextId}', but this blueprint provisions '${bp.contextId}'. An issuer must target the blueprint's own context \u2014 everything else a blueprint creates does, and an issuer that points elsewhere attaches an identity provider to an app context the blueprint never mentions. Set it to '${bp.contextId}', or move this issuer into a blueprint for '${issuer.contextId}'.`
11916
+ });
11917
+ });
11918
+ }
11919
+ var BLUEPRINT_FIELD_PHASES = Object.freeze({
11920
+ servicePrincipal: "bootstrap",
11921
+ issuers: "bootstrap",
11922
+ // Registration requires the platform's provisioning:c capability, which
11923
+ // only the bootstrap credential carries — same reasoning as issuers.
11924
+ namespaces: "bootstrap",
11925
+ schemas: "in-context",
11926
+ accessProfile: "in-context",
11927
+ roles: "in-context",
11928
+ seed: "in-context"
11929
+ });
11930
+ var BlueprintSchema = z.object({
11931
+ /** Stable blueprint id (the `--blueprint <name>` selector + idempotency key). */
11932
+ name: z.string().min(1),
11933
+ version: z.string().min(1),
11934
+ description: z.string().min(1),
11506
11935
  /** The app-context the profile + scoped key bind to (e.g. "mcp"). */
11507
11936
  contextId: z.string().regex(CONTEXT_ID_RE, {
11508
11937
  message: "contextId must be 3-31 chars, start with a lowercase letter, then lowercase letters/digits/dashes (e.g. 'mcp')"
11509
11938
  }),
11510
11939
  /** Human-readable app-context name; defaults to `MCP — <name>` (see {@link contextNameOf}) when absent. */
11511
11940
  contextName: z.string().min(1).optional(),
11941
+ /**
11942
+ * The deploying organization's own display name — distinct from {@link contextName}.
11943
+ * `contextName` is the blueprint author's fixed identity for the app (the same for every
11944
+ * deployer); `companyName` is meant to vary per install, typically templated from a
11945
+ * deployer-supplied `${{ inputs.x }}` value rather than hardcoded. Used for branding on
11946
+ * platform-sent correspondence (e.g. invite emails) alongside `contextName` — not a
11947
+ * replacement for it. See {@link companyNameOf}.
11948
+ */
11949
+ companyName: z.string().min(1).optional(),
11512
11950
  schemas: z.array(BlueprintSchemaSchema).default([]),
11513
11951
  accessProfile: BlueprintAccessProfileSchema,
11514
11952
  servicePrincipal: BlueprintServicePrincipalSchema,
11515
11953
  seed: z.array(BlueprintSeedRecordSchema).optional(),
11516
11954
  /** Optional multi-clause roles, bound to principals via `access grant --role`. */
11517
11955
  roles: BlueprintRolesSchema.optional(),
11956
+ /**
11957
+ * Optional roleId → `POST /v1/auth/token/assume`
11958
+ * entitlement grant, mirroring the platform's role create/update request
11959
+ * and response shape for this field. Kept separate from `roles` rather
11960
+ * than folded into a role entry — see {@link BlueprintRoleAssumableMapSchema}'s
11961
+ * own doc comment for why. Every key must name a role this blueprint
11962
+ * declares in `roles` (`lintRoleAssumable`).
11963
+ */
11964
+ roleAssumable: BlueprintRoleAssumableMapSchema.optional(),
11965
+ /**
11966
+ * Optional named `dataScope` fragments, each
11967
+ * referenceable from a role clause via `dataScopeRef: <name>` instead of
11968
+ * repeating the same map inline. Purely local authoring sugar: not a
11969
+ * provisioned resource (absent from `BLUEPRINT_FIELD_PHASES`), and never
11970
+ * survives `parseBlueprint` as a live reference — see
11971
+ * {@link expandDataScopeRefs}.
11972
+ */
11973
+ fragments: BlueprintFragmentsSchema.optional(),
11518
11974
  /** Optional principals ensured-exist at apply; referenced via ${{ identities.* }}. */
11519
11975
  identities: IdentitiesDeclSchema.optional(),
11520
11976
  /**
@@ -11539,6 +11995,10 @@ var BlueprintSchema = z.object({
11539
11995
  lintIdentityOverrides(bp, ctx);
11540
11996
  lintNamespacedScopeArrays(bp, ctx);
11541
11997
  lintDataScopeKeys(bp, ctx);
11998
+ lintExpectedScopeDims(bp, ctx);
11999
+ lintDataScopeRefs(bp, ctx);
12000
+ lintRoleAssumable(bp, ctx);
12001
+ lintAccessProfileRoleIds(bp, ctx);
11542
12002
  lintSeedSurfaces(bp, ctx);
11543
12003
  lintIdentitySurfaces(bp, ctx);
11544
12004
  lintIssuerContext(bp, ctx);
@@ -11578,7 +12038,7 @@ function parseBlueprint(input) {
11578
12038
  throw new BlueprintValidationError(`Malformed blueprint:
11579
12039
  ${renderIssues(issues)}`, issues);
11580
12040
  }
11581
- return result.data;
12041
+ return expandDataScopeRefs(result.data);
11582
12042
  }
11583
12043
  function contextNameOf(blueprint) {
11584
12044
  return blueprint.contextName ?? `MCP \u2014 ${blueprint.name}`;
@@ -11698,6 +12158,7 @@ var WHOLE_TOKEN_RE = /^\$\{\{\s*([A-Za-z_]\w*)\.([A-Za-z_]\w*)\s*\}\}$/;
11698
12158
  var TOKEN_SCAN_RE = /\$\$\{\{|\$\{\{([\s\S]*?)\}\}|\$\{\{/g;
11699
12159
  var INNER_REF_RE = /^([A-Za-z_]\w*)\.([A-Za-z_]\w*)$/;
11700
12160
  var DEFERRED_NAMESPACES = /* @__PURE__ */ new Set(["self", "identities", "under", "member"]);
12161
+ var DEFERRED_BARE_TOKENS = /* @__PURE__ */ new Set(["any"]);
11701
12162
  var DEFERRED_NS_ALTERNATION = [...DEFERRED_NAMESPACES].join("|");
11702
12163
  var DEFERRED_INNER_RE = new RegExp(
11703
12164
  `^(?:${DEFERRED_NS_ALTERNATION})(?:\\.[A-Za-z_]\\w*)+(?::[A-Za-z_]\\w*)?$`
@@ -11759,7 +12220,7 @@ function interpolate(str, ctx, path4) {
11759
12220
  });
11760
12221
  } else {
11761
12222
  const inner = (m[1] ?? "").trim();
11762
- if (DEFERRED_INNER_RE.test(inner)) {
12223
+ if (DEFERRED_INNER_RE.test(inner) || DEFERRED_BARE_TOKENS.has(inner)) {
11763
12224
  out += m[0];
11764
12225
  last = TOKEN_SCAN_RE.lastIndex;
11765
12226
  continue;
@@ -13717,6 +14178,7 @@ var ENV_BASE_URLS = {
13717
14178
  // `--base-url` or VECTROS_API_BASE_URL; verify at smoke time.
13718
14179
  staging: "https://api.staging.vectros.ai"
13719
14180
  };
14181
+ var AUTH_FLAG_NAMES = ["--env", "--base-url", "--token", "--use-scoped-token"];
13720
14182
  function envToBaseUrl(env, override) {
13721
14183
  return validateBaseUrl(override ?? ENV_BASE_URLS[env]);
13722
14184
  }
@@ -14057,7 +14519,12 @@ async function resolveAuthContext(opts) {
14057
14519
  const baseUrl = envToBaseUrl(opts.env, opts.baseUrl ?? process.env.VECTROS_API_BASE_URL);
14058
14520
  const cognitoToken = await resolveCognitoToken(opts.env, opts.token);
14059
14521
  const tenant = opts.tenant ?? "live";
14060
- const bridge = opts.provisioning ? await mintBridgeToken({ cognitoToken, apiBaseUrl: baseUrl, tenant }) : await mintContextToken({ cognitoToken, apiBaseUrl: baseUrl, contextId: opts.context ?? "default", tenant });
14522
+ const bridge = opts.provisioning ? await mintBridgeToken({
14523
+ cognitoToken,
14524
+ apiBaseUrl: baseUrl,
14525
+ tenant,
14526
+ ...opts.context ? { contextId: opts.context } : {}
14527
+ }) : await mintContextToken({ cognitoToken, apiBaseUrl: baseUrl, contextId: opts.context ?? "default", tenant });
14061
14528
  const client = new VectrosClient({ token: bridge.token, environment: baseUrl });
14062
14529
  const identity = await resolveIdentity({ log: opts.log, apiKey: bridge.token, environment: baseUrl });
14063
14530
  return {
@@ -14199,6 +14666,9 @@ var SELF_QUALIFIED_OPS = /* @__PURE__ */ new Map([
14199
14666
  var UNQUALIFIABLE_WITH_ANY_TAIL = /* @__PURE__ */ new Map([
14200
14667
  ["profiles", /* @__PURE__ */ new Set(["r"])]
14201
14668
  ]);
14669
+ var CAPABILITY_UNLOCKED_OPS = /* @__PURE__ */ new Map([
14670
+ ["profiles", /* @__PURE__ */ new Map([["c", "member-lifecycle"]])]
14671
+ ]);
14202
14672
  var VALID_OPS = /* @__PURE__ */ new Set(["c", "r", "u", "d"]);
14203
14673
  var DATA_PLANE_SET = new Set(DATA_PLANE_ALLOWLIST);
14204
14674
  var CONTROL_PLANE_SET = new Set(KNOWN_CONTROL_PLANE_RESOURCES);
@@ -14215,7 +14685,7 @@ ${summary}`
14215
14685
  this.rejected = rejected;
14216
14686
  }
14217
14687
  };
14218
- var GRANTED_CAPABILITIES_ALLOWLIST = ["member-lifecycle"];
14688
+ var GRANTED_CAPABILITIES_ALLOWLIST = ["member-lifecycle", "delegate-principal-stamp"];
14219
14689
  var GRANTED_CAPABILITIES_SET = new Set(GRANTED_CAPABILITIES_ALLOWLIST);
14220
14690
  var CAPABILITIES_HINT = GRANTED_CAPABILITIES_ALLOWLIST.join("/");
14221
14691
  var CapabilityGateError = class extends Error {
@@ -14268,7 +14738,7 @@ function firstInvalidOp(ops) {
14268
14738
  }
14269
14739
  return void 0;
14270
14740
  }
14271
- function evaluateScopeToken(action) {
14741
+ function evaluateScopeToken(action, capabilities) {
14272
14742
  const token = action.trim();
14273
14743
  if (token.length === 0) {
14274
14744
  return { action, reason: "empty scope token" };
@@ -14319,9 +14789,12 @@ function evaluateScopeToken(action) {
14319
14789
  if (admittedOps?.has(ch)) continue;
14320
14790
  if (selfOnlyOps?.has(ch)) {
14321
14791
  if (isSelfQualified) continue;
14792
+ const unlockCapability = CAPABILITY_UNLOCKED_OPS.get(resource)?.get(ch);
14793
+ if (unlockCapability && qualifier === void 0 && capabilities?.includes(unlockCapability)) continue;
14794
+ const capabilityHintApplies = unlockCapability && !capabilities?.includes(unlockCapability);
14322
14795
  return {
14323
14796
  action,
14324
- reason: `requests control-plane scope '${resource}:${ch}' without the required ':self' qualifier. A bare or arbitrarily-qualified '${resource}:${ch}' is outside the narrow allowlist (${admittedHint}) admitted for this resource \u2014 only '${resource}:${ch}:self' is. That operation must otherwise be done deliberately in the developer portal, never via the bootstrap loader`
14797
+ reason: `requests control-plane scope '${resource}:${ch}' without the required ':self' qualifier. A bare or arbitrarily-qualified '${resource}:${ch}' is outside the narrow allowlist (${admittedHint}) admitted for this resource \u2014 only '${resource}:${ch}:self' is` + (capabilityHintApplies ? `, or this SAME clause also declaring 'capabilities: [${unlockCapability}]'` : unlockCapability ? ` (declaring 'capabilities: [${unlockCapability}]' does not help here \u2014 that capability only unlocks the BARE, unqualified form, never a literal-qualified one like this)` : "") + `. That operation must otherwise be done deliberately in the developer portal, never via the bootstrap loader`
14325
14798
  };
14326
14799
  }
14327
14800
  return {
@@ -14354,11 +14827,11 @@ function evaluateScopeToken(action) {
14354
14827
  }
14355
14828
  return null;
14356
14829
  }
14357
- function evaluateScopeGate(allowedActions) {
14830
+ function evaluateScopeGate(allowedActions, capabilities) {
14358
14831
  const accepted = [];
14359
14832
  const rejected = [];
14360
14833
  for (const action of allowedActions) {
14361
- const rejection = evaluateScopeToken(action);
14834
+ const rejection = evaluateScopeToken(action, capabilities);
14362
14835
  if (rejection) {
14363
14836
  rejected.push(rejection);
14364
14837
  } else {
@@ -14367,22 +14840,34 @@ function evaluateScopeGate(allowedActions) {
14367
14840
  }
14368
14841
  return { ok: rejected.length === 0, accepted, rejected };
14369
14842
  }
14370
- function assertScopeGate(allowedActions) {
14371
- const result = evaluateScopeGate(allowedActions);
14843
+ function assertScopeGate(allowedActions, capabilities) {
14844
+ const result = evaluateScopeGate(allowedActions, capabilities);
14372
14845
  if (!result.ok) {
14373
14846
  throw new ScopeGateError(result.rejected);
14374
14847
  }
14375
14848
  }
14376
14849
  function assertBlueprintGates(blueprint) {
14377
- assertScopeGate(blueprint.accessProfile.allowedActions);
14378
- assertCapabilityGate(blueprint.accessProfile.capabilities);
14850
+ if (blueprint.accessProfile.allowedActions !== void 0) {
14851
+ assertScopeGate(blueprint.accessProfile.allowedActions, blueprint.accessProfile.capabilities);
14852
+ assertCapabilityGate(blueprint.accessProfile.capabilities);
14853
+ }
14379
14854
  for (const clauses of Object.values(blueprint.roles ?? {})) {
14380
14855
  for (const clause of clauses) {
14381
- assertScopeGate(clause.allowedActions);
14856
+ assertScopeGate(clause.allowedActions, clause.capabilities);
14382
14857
  assertCapabilityGate(clause.capabilities);
14383
14858
  }
14384
14859
  }
14385
14860
  }
14861
+ function effectiveAccessProfileActions(blueprint) {
14862
+ if (blueprint.accessProfile.allowedActions !== void 0) {
14863
+ return [...blueprint.accessProfile.allowedActions];
14864
+ }
14865
+ const out = [];
14866
+ for (const roleId of blueprint.accessProfile.roleIds ?? []) {
14867
+ for (const clause of blueprint.roles?.[roleId] ?? []) out.push(...clause.allowedActions);
14868
+ }
14869
+ return out;
14870
+ }
14386
14871
 
14387
14872
  // src/machine-id.ts
14388
14873
  import { promises as fs } from "fs";
@@ -14477,7 +14962,8 @@ function makeIdentityResolver(client, log) {
14477
14962
  return id;
14478
14963
  };
14479
14964
  }
14480
- async function resolveApplyTimeIdentities(blueprint, client, log) {
14965
+ async function resolveApplyTimeIdentities(blueprint, provisioned, bootstrapClient, log) {
14966
+ const client = provisioned.ctxBootstrapClient ?? bootstrapClient;
14481
14967
  const resolver = makeIdentityResolver(client, log);
14482
14968
  const resolved = await resolveBlueprintIdentities(blueprint, resolver);
14483
14969
  return parseBlueprint(resolved);
@@ -14487,7 +14973,8 @@ async function resolveApplyTimeIdentities(blueprint, client, log) {
14487
14973
  function actionOf(created) {
14488
14974
  return created ? "created" : "reconciled";
14489
14975
  }
14490
- async function registerNamespaceIdempotently(client, contextIdForRequest, decl) {
14976
+ async function registerNamespaceIdempotently(client, blueprintContextId, decl) {
14977
+ const contextIdForRequest = decl.tenantWide ? void 0 : blueprintContextId;
14491
14978
  const body = {
14492
14979
  namespace: decl.namespace,
14493
14980
  entityBacked: decl.entityBacked,
@@ -14503,6 +14990,7 @@ async function registerNamespaceIdempotently(client, contextIdForRequest, decl)
14503
14990
  } catch (err) {
14504
14991
  const info = extractServerError(err);
14505
14992
  if (info.statusCode !== 400) throw err;
14993
+ if (decl.tenantWide) throw err;
14506
14994
  let existing;
14507
14995
  try {
14508
14996
  existing = await client.getNamespace({ namespace: decl.namespace, contextId: contextIdForRequest });
@@ -14667,20 +15155,24 @@ function assertNoUnresolvedIdentities(blueprint) {
14667
15155
  );
14668
15156
  }
14669
15157
  }
14670
- async function runLoader(client, input) {
14671
- const {
14672
- blueprint,
14673
- tenantId,
14674
- machineId,
14675
- rotate = false,
14676
- skipSeed = false,
14677
- readExistingKey,
14678
- persistMintedKey,
14679
- confirmExistingPrincipal,
14680
- log
14681
- } = input;
15158
+ var TenantWideNamespacesNotAllowedError = class extends Error {
15159
+ constructor(message) {
15160
+ super(message);
15161
+ this.name = "TenantWideNamespacesNotAllowedError";
15162
+ }
15163
+ };
15164
+ function assertTenantWideNamespacesAllowed(blueprint, allow) {
15165
+ if (allow) return;
15166
+ const names = (blueprint.namespaces ?? []).filter((ns) => ns.tenantWide === true).map((ns) => ns.namespace);
15167
+ if (names.length === 0) return;
15168
+ throw new TenantWideNamespacesNotAllowedError(
15169
+ `Blueprint declares namespace${names.length > 1 ? "s" : ""} ${names.map((n) => `'${n}'`).join(", ")} as tenantWide: true \u2014 visible to every context in the account, not just this blueprint's own. Refusing to request that registration without an explicit opt-in. Re-run with --allow-tenant-wide-namespaces once you've confirmed that's actually intended (only an account OWNER's credential can complete it either way \u2014 this flag doesn't change who's allowed, only whether this run tries).`
15170
+ );
15171
+ }
15172
+ async function provisionContext(client, input) {
15173
+ const { blueprint, allowTenantWideNamespaces = false, readExistingKey, confirmExistingPrincipal, log } = input;
14682
15174
  assertBlueprintGates(blueprint);
14683
- assertNoUnresolvedIdentities(blueprint);
15175
+ assertTenantWideNamespacesAllowed(blueprint, allowTenantWideNamespaces);
14684
15176
  const ctxResp = await client.auth.createAppContext({ body: { contextId: blueprint.contextId, name: contextNameOf(blueprint) } });
14685
15177
  const contextAction = actionOf(ctxResp.created);
14686
15178
  log.debug({ component: "loader", contextId: blueprint.contextId, contextAction }, "app-context ensured");
@@ -14719,10 +15211,10 @@ async function runLoader(client, input) {
14719
15211
  "existing-principal collision confirmed (or unwired) \u2014 proceeding with any further write to this principal"
14720
15212
  );
14721
15213
  }
14722
- const needsContextPinnedBootstrap = (blueprint.issuers ?? []).length > 0 || (blueprint.namespaces ?? []).length > 0;
15214
+ const needsContextPinnedBootstrap = (blueprint.namespaces ?? []).length > 0;
14723
15215
  const ctxBootstrapClient = needsContextPinnedBootstrap ? input.remintBootstrapForContext ? await input.remintBootstrapForContext(blueprint.contextId) : client : void 0;
14724
15216
  if (ctxBootstrapClient && input.remintBootstrapForContext) {
14725
- log.debug({ component: "loader", contextId: blueprint.contextId }, "re-minted context-pinned bootstrap client");
15217
+ log.debug({ component: "loader", contextId: blueprint.contextId }, "re-minted context-pinned bootstrap client (namespaces)");
14726
15218
  }
14727
15219
  const namespaceActions = [];
14728
15220
  for (const ns of blueprint.namespaces ?? []) {
@@ -14730,8 +15222,35 @@ async function runLoader(client, input) {
14730
15222
  namespaceActions.push({ namespace: ns.namespace, action });
14731
15223
  log.debug({ component: "loader", namespace: ns.namespace, contextId: blueprint.contextId, action }, "namespace ensured");
14732
15224
  }
15225
+ return { contextAction, servicePrincipalUserId: userId, userAction, namespaceActions, ctxBootstrapClient };
15226
+ }
15227
+ async function runLoader(client, input) {
15228
+ const {
15229
+ blueprint,
15230
+ tenantId,
15231
+ machineId,
15232
+ rotate = false,
15233
+ skipSeed = false,
15234
+ seedIndexPoll,
15235
+ provisioned,
15236
+ readExistingKey,
15237
+ persistMintedKey,
15238
+ log
15239
+ } = input;
15240
+ assertNoUnresolvedIdentities(blueprint);
15241
+ const { contextAction, servicePrincipalUserId: userId, userAction, namespaceActions } = provisioned;
15242
+ assertBlueprintGates(blueprint);
15243
+ const needsContextPinnedBootstrapForIssuers = (blueprint.issuers ?? []).length > 0;
15244
+ const ctxBootstrapClient = provisioned.ctxBootstrapClient ?? (needsContextPinnedBootstrapForIssuers ? input.remintBootstrapForContext ? await input.remintBootstrapForContext(blueprint.contextId) : client : void 0);
15245
+ if (ctxBootstrapClient && !provisioned.ctxBootstrapClient && input.remintBootstrapForContext) {
15246
+ log.debug({ component: "loader", contextId: blueprint.contextId }, "re-minted context-pinned bootstrap client (issuers)");
15247
+ }
14733
15248
  const issuerActions = [];
14734
15249
  for (const issuer of blueprint.issuers ?? []) {
15250
+ const selfSignupPolicies = issuer.selfSignupPolicies?.map((p) => ({
15251
+ signup_type: p.signupType,
15252
+ role_id: p.roleId
15253
+ }));
14735
15254
  const issuerResp = await ctxBootstrapClient.auth.registerIssuer({
14736
15255
  issuerId: issuer.issuerId,
14737
15256
  issuer: issuer.issuer,
@@ -14740,18 +15259,26 @@ async function runLoader(client, input) {
14740
15259
  contextId: issuer.contextId,
14741
15260
  subClaim: issuer.subClaim,
14742
15261
  emailClaim: issuer.emailClaim,
14743
- selfSignupPolicies: issuer.selfSignupPolicies?.map((p) => ({
14744
- signup_type: p.signupType,
14745
- role_id: p.roleId
14746
- }))
15262
+ userinfoUri: issuer.userinfoUri,
15263
+ selfSignupPolicies
14747
15264
  });
14748
15265
  const action = actionOf(issuerResp.created);
14749
15266
  issuerActions.push({ issuerId: issuer.issuerId, action });
14750
15267
  if (action === "reconciled") {
14751
- log.warn(
14752
- { component: "loader", issuerId: issuer.issuerId },
14753
- "issuer already registered \u2014 the platform has no update-issuer endpoint, so any edited fields (jwksUri, audience, subClaim, emailClaim, selfSignupPolicies) on this apply were NOT applied to the existing registration"
14754
- );
15268
+ await ctxBootstrapClient.auth.updateIssuer({
15269
+ issuerId: issuer.issuerId,
15270
+ // Trust-anchor/routing-pin fields — echoed back, never actually
15271
+ // changed here (the platform rejects a differing value).
15272
+ issuer: issuer.issuer,
15273
+ jwksUri: issuer.jwksUri,
15274
+ audience: issuer.audience,
15275
+ contextId: issuer.contextId,
15276
+ subClaim: issuer.subClaim,
15277
+ emailClaim: issuer.emailClaim,
15278
+ userinfoUri: issuer.userinfoUri,
15279
+ selfSignupPolicies
15280
+ });
15281
+ log.debug({ component: "loader", issuerId: issuer.issuerId }, "issuer already registered \u2014 reconciled safe fields to the declared values");
14755
15282
  } else {
14756
15283
  log.debug({ component: "loader", issuerId: issuer.issuerId, action }, "issuer registered");
14757
15284
  }
@@ -14791,10 +15318,35 @@ async function runLoader(client, input) {
14791
15318
  schemaActions.push({ typeName: s.typeName, action: actionOf(resp.created) });
14792
15319
  log.debug({ component: "loader", typeName: s.typeName, schemaId: resp.id, action: actionOf(resp.created) }, "schema ensured (upsert)");
14793
15320
  }
14794
- const profileBody = {
15321
+ const roleIds = [];
15322
+ const roleActions = [];
15323
+ for (const [roleId, clauses] of Object.entries(blueprint.roles ?? {})) {
15324
+ const roleResp = await ctxClient.auth.createRole({
15325
+ contextId: blueprint.contextId,
15326
+ upsert: true,
15327
+ body: {
15328
+ roleId,
15329
+ // Blueprint roles carry no display name → default it to the roleId.
15330
+ name: roleId,
15331
+ scopes: toRoleScopeClauses(clauses),
15332
+ assumable: blueprint.roleAssumable?.[roleId]
15333
+ }
15334
+ });
15335
+ roleIds.push(roleId);
15336
+ roleActions.push({ roleId, action: actionOf(roleResp.created) });
15337
+ log.debug({ component: "loader", roleId, clauses: clauses.length, action: actionOf(roleResp.created) }, "role ensured (upsert)");
15338
+ }
15339
+ const profileBody = blueprint.accessProfile.roleIds ? {
15340
+ principalId: `usr_${userId}`,
15341
+ roleIds: blueprint.accessProfile.roleIds,
15342
+ identityOverrides: blueprint.accessProfile.identityOverrides
15343
+ } : {
14795
15344
  principalId: `usr_${userId}`,
14796
15345
  scopes: [
14797
15346
  {
15347
+ // Non-null: this is the `else` branch of the roleIds XOR above —
15348
+ // `@vectros-ai/blueprints`' BlueprintAccessProfileSchema guarantees
15349
+ // allowedActions is set whenever roleIds is not.
14798
15350
  allowed_actions: blueprint.accessProfile.allowedActions,
14799
15351
  // Pass dataScope straight through to the wire. The blueprint type is
14800
15352
  // Record<string, (string|null)[]> — a `null` element is the NULL
@@ -14828,22 +15380,6 @@ async function runLoader(client, input) {
14828
15380
  body: profileBody
14829
15381
  });
14830
15382
  log.debug({ component: "loader", principalId: `usr_${userId}`, accessProfileId: profile.id, profileAction }, "access profile ensured + reconciled to declared scopes");
14831
- const roleIds = [];
14832
- const roleActions = [];
14833
- for (const [roleId, clauses] of Object.entries(blueprint.roles ?? {})) {
14834
- const roleResp = await ctxClient.auth.createRole({
14835
- contextId: blueprint.contextId,
14836
- body: {
14837
- roleId,
14838
- // Blueprint roles carry no display name → default it to the roleId.
14839
- name: roleId,
14840
- scopes: toRoleScopeClauses(clauses)
14841
- }
14842
- });
14843
- roleIds.push(roleId);
14844
- roleActions.push({ roleId, action: actionOf(roleResp.created) });
14845
- log.debug({ component: "loader", roleId, clauses: clauses.length, action: actionOf(roleResp.created) }, "role ensured");
14846
- }
14847
15383
  const keyName = keyNameFor(blueprint.name, machineId);
14848
15384
  let keyAction = "created";
14849
15385
  let revokedKeyId;
@@ -14880,6 +15416,7 @@ async function runLoader(client, input) {
14880
15416
  const seededRecordIds = [];
14881
15417
  const seededDocumentIds = [];
14882
15418
  const seedIndexFailures = [];
15419
+ const pendingIndexSeeds = [];
14883
15420
  const noteIfIndexFailed = (created, rec, surface) => {
14884
15421
  if (created.indexStatus !== "FAILED") return;
14885
15422
  seedIndexFailures.push({
@@ -14914,6 +15451,9 @@ async function runLoader(client, input) {
14914
15451
  }
14915
15452
  });
14916
15453
  if (created.id) seededDocumentIds.push(created.id);
15454
+ if (created.id && created.indexStatus === "PENDING_INDEX") {
15455
+ pendingIndexSeeds.push({ id: created.id, externalId: rec.externalId, typeName: rec.typeName, surface: "document" });
15456
+ }
14917
15457
  noteIfIndexFailed(created, rec, "document");
14918
15458
  } else {
14919
15459
  const created = await ctxClient.records.createRecord({
@@ -14926,10 +15466,30 @@ async function runLoader(client, input) {
14926
15466
  }
14927
15467
  });
14928
15468
  if (created.id) seededRecordIds.push(created.id);
15469
+ if (created.id && created.indexStatus === "PENDING_INDEX") {
15470
+ pendingIndexSeeds.push({ id: created.id, externalId: rec.externalId, typeName: rec.typeName, surface: "record" });
15471
+ }
14929
15472
  noteIfIndexFailed(created, rec, "record");
14930
15473
  }
14931
15474
  seededCount++;
14932
15475
  }
15476
+ const pollAttempts = seedIndexPoll?.attempts ?? 3;
15477
+ const pollDelayMs = seedIndexPoll?.delayMs ?? 500;
15478
+ if (pollAttempts > 0 && pendingIndexSeeds.length > 0) {
15479
+ await Promise.all(
15480
+ pendingIndexSeeds.map(async (seed) => {
15481
+ for (let attempt = 0; attempt < pollAttempts; attempt++) {
15482
+ await new Promise((r) => setTimeout(r, pollDelayMs));
15483
+ const readBack = seed.surface === "document" ? await ctxClient.documents.getDocument({ id: seed.id }) : await ctxClient.records.getRecord({ id: seed.id });
15484
+ if (readBack.indexStatus === "FAILED") {
15485
+ noteIfIndexFailed(readBack, seed, seed.surface);
15486
+ return;
15487
+ }
15488
+ if (readBack.indexStatus !== "PENDING_INDEX") return;
15489
+ }
15490
+ })
15491
+ );
15492
+ }
14933
15493
  return {
14934
15494
  contextId: blueprint.contextId,
14935
15495
  servicePrincipalUserId: userId,
@@ -14954,6 +15514,11 @@ async function runLoader(client, input) {
14954
15514
  seedIndexFailures
14955
15515
  };
14956
15516
  }
15517
+ function renderPlanDataScope(ds) {
15518
+ const entries = Object.entries(ds ?? {});
15519
+ if (entries.length === 0) return "(none \u2014 unconstrained)";
15520
+ return entries.map(([dim, values]) => `${dim}=[${values.map((v) => v === null ? "null" : v).join(", ")}]`).join(", ");
15521
+ }
14957
15522
  function buildPlan(blueprint, opts = {}) {
14958
15523
  const steps = [];
14959
15524
  steps.push({ kind: "context", summary: `context '${blueprint.contextId}' (${contextNameOf(blueprint)})` });
@@ -14962,7 +15527,7 @@ function buildPlan(blueprint, opts = {}) {
14962
15527
  summary: `principal '${blueprint.servicePrincipal.externalId}' (SERVICE)`
14963
15528
  });
14964
15529
  for (const ns of blueprint.namespaces ?? []) {
14965
- const ownerNote = `owned by '${blueprint.contextId}'`;
15530
+ const ownerNote = ns.tenantWide ? "TENANT-WIDE \u2014 visible to every context" : `owned by '${blueprint.contextId}'`;
14966
15531
  const entityNote = ns.entityBacked ? ", entity-backed" : "";
14967
15532
  const membershipNote = ns.membershipRecordType ? `, membership: ${ns.membershipRecordType}.${ns.membershipTargetField}` + (ns.membershipLevelField ? ` (levels: ${ns.membershipLevels?.join("/")})` : "") : "";
14968
15533
  steps.push({
@@ -14990,20 +15555,40 @@ function buildPlan(blueprint, opts = {}) {
14990
15555
  summary: `schema '${s.typeName}' (${s.fields.length} field${s.fields.length === 1 ? "" : "s"}, ${indexNote})${lookupNote}${surfaceNote}${sensitiveNote}`
14991
15556
  });
14992
15557
  }
14993
- const actions = blueprint.accessProfile.allowedActions;
14994
- const scopeNote = blueprint.accessProfile.dataScope && Object.keys(blueprint.accessProfile.dataScope).length ? ` dataScope: ${Object.keys(blueprint.accessProfile.dataScope).join(", ")}` : "";
14995
- const capabilityNote = blueprint.accessProfile.capabilities?.length ? ` capabilities: [${blueprint.accessProfile.capabilities.join(", ")}]` : "";
14996
- steps.push({
14997
- kind: "profile",
14998
- summary: `profile scopes [${actions.join(", ")}] (data-plane \u2713)${scopeNote}${capabilityNote}`
14999
- });
15000
15558
  for (const [roleId, clauses] of Object.entries(blueprint.roles ?? {})) {
15001
15559
  const actionSet = [...new Set(clauses.flatMap((c) => c.allowedActions))];
15002
15560
  const capSet = [...new Set(clauses.flatMap((c) => c.capabilities ?? []))];
15003
15561
  const roleCapabilityNote = capSet.length ? `, capabilities [${capSet.join(", ")}]` : "";
15562
+ const clauseLines = clauses.map((c, i) => {
15563
+ const capNote = c.capabilities?.length ? `, capabilities [${c.capabilities.join(", ")}]` : "";
15564
+ return ` clause ${i + 1}: actions [${c.allowedActions.join(", ")}], dataScope: ${renderPlanDataScope(c.dataScope)}${capNote}`;
15565
+ });
15566
+ const assumable = blueprint.roleAssumable?.[roleId];
15567
+ const assumableLine = assumable ? `
15568
+ assumable: ${renderPlanDataScope(assumable)}` : "";
15004
15569
  steps.push({
15005
15570
  kind: "role",
15006
- summary: `role '${roleId}' (${clauses.length} clause${clauses.length === 1 ? "" : "s"}, actions [${actionSet.join(", ")}]${roleCapabilityNote})`
15571
+ summary: `role '${roleId}' (${clauses.length} clause${clauses.length === 1 ? "" : "s"}, actions [${actionSet.join(", ")}]${roleCapabilityNote})
15572
+ ` + clauseLines.join("\n") + assumableLine
15573
+ });
15574
+ }
15575
+ const roleIdsNote = blueprint.accessProfile.roleIds;
15576
+ if (roleIdsNote) {
15577
+ steps.push({
15578
+ kind: "profile",
15579
+ summary: `profile via role(s) [${roleIdsNote.join(", ")}] (composed \u2014 each role's own clauses, concatenated in order)`
15580
+ });
15581
+ } else {
15582
+ const actions = blueprint.accessProfile.allowedActions ?? [];
15583
+ const scopeNote = blueprint.accessProfile.dataScope && Object.keys(blueprint.accessProfile.dataScope).length ? ` dataScope: ${Object.keys(blueprint.accessProfile.dataScope).join(", ")}` : "";
15584
+ const capabilityNote = blueprint.accessProfile.capabilities?.length ? ` capabilities: [${blueprint.accessProfile.capabilities.join(", ")}]` : "";
15585
+ const allDataPlane = !actions.some(
15586
+ (a) => KNOWN_CONTROL_PLANE_RESOURCES.includes(a.split(":", 1)[0])
15587
+ );
15588
+ const planeNote = allDataPlane ? " (data-plane \u2713)" : " (includes a control-plane exception)";
15589
+ steps.push({
15590
+ kind: "profile",
15591
+ summary: `profile scopes [${actions.join(", ")}]${planeNote}${scopeNote}${capabilityNote}`
15007
15592
  });
15008
15593
  }
15009
15594
  steps.push({ kind: "key", summary: `key ${blueprint.name}-<machine> (mint 1 scoped ssk_*)` });
@@ -15026,8 +15611,10 @@ function renderPlan(blueprint, steps) {
15026
15611
  ];
15027
15612
  if (steps.some((s) => s.kind === "issuer")) {
15028
15613
  legendLines.push(
15029
- ` Exception: issuers cannot be updated once registered \u2014 an existing`,
15030
- ` issuer is left unchanged, not reconciled to match.`
15614
+ ` Exception: an issuer's trust-anchor fields (issuer, jwksUri, audience,`,
15615
+ ` contextId) are immutable once registered \u2014 only subClaim/emailClaim/`,
15616
+ ` userinfoUri/selfSignupPolicies actually reconcile on a re-apply (status is not`,
15617
+ ` authorable in a blueprint at all).`
15031
15618
  );
15032
15619
  }
15033
15620
  if (steps.some((s) => s.kind === "namespace")) {
@@ -15036,6 +15623,12 @@ function renderPlan(blueprint, steps) {
15036
15623
  ` an existing registration converges silently, but one that disagrees FAILS`,
15037
15624
  ` the apply rather than reconciling it.`
15038
15625
  );
15626
+ if (steps.some((s) => s.kind === "namespace" && /TENANT-WIDE/.test(s.summary))) {
15627
+ legendLines.push(
15628
+ ` Stricter still for a TENANT-WIDE namespace: it never converges, even on an`,
15629
+ ` exact match \u2014 any existing registration of that name fails the apply.`
15630
+ );
15631
+ }
15039
15632
  }
15040
15633
  const legend = legendLines.join("\n");
15041
15634
  const lines = steps.map((s) => ` + ${s.summary}`);
@@ -15076,6 +15669,11 @@ function buildClaudeCodeAddCommand(entry, serverKey = "vectros") {
15076
15669
  const envFlags = Object.entries(entry.env ?? {}).map(([k, v]) => `-e ${k}=${v}`);
15077
15670
  return ["claude", "mcp", "add", serverKey, ...envFlags, "--", entry.command, ...entry.args].join(" ");
15078
15671
  }
15672
+ function buildRestartHint(blueprintName, isCode) {
15673
+ const target = isCode ? "Reopen Claude Code in this project" : "Restart your MCP client";
15674
+ return ` ${target}, then ask Claude about your '${blueprintName}' data.
15675
+ `;
15676
+ }
15079
15677
  function mergeServerEntry(existing, entry, serverKey = "vectros") {
15080
15678
  const base = existing ? { ...existing } : {};
15081
15679
  const servers = { ...base.mcpServers ?? {} };
@@ -15415,11 +16013,19 @@ var CryptoFileSecretStore = class {
15415
16013
  * sub-millisecond in practice; the budget is generous for a loaded machine)
15416
16014
  * before deciding it is a crash remnant. A SHORT-but-nonempty winner is never
15417
16015
  * a write in flight — the 16 salt bytes land in one write — so it is refused
15418
- * immediately, exactly like the top-of-function read's twin check.
16016
+ * immediately, exactly like the top-of-function read's twin check. A winner
16017
+ * that goes MISSING mid-poll (ENOENT) restarts creation from the top rather
16018
+ * than surfacing the raw ENOENT — see the catch below.
15419
16019
  */
15420
16020
  async adoptSalt(saltPath) {
15421
16021
  for (let attempt = 0; ; attempt++) {
15422
- const winner = await fs3.readFile(saltPath);
16022
+ let winner;
16023
+ try {
16024
+ winner = await fs3.readFile(saltPath);
16025
+ } catch (err) {
16026
+ if (err.code === "ENOENT") return this.readOrCreateSalt();
16027
+ throw err;
16028
+ }
15423
16029
  if (winner.length >= 16) return winner;
15424
16030
  if (winner.length > 0) throw corruptSaltError(saltPath, winner.length);
15425
16031
  if (attempt >= 24) break;
@@ -17099,6 +17705,7 @@ function parseBootstrapArgs(argv) {
17099
17705
  useScopedToken: false,
17100
17706
  blankSlate: false,
17101
17707
  noSeed: false,
17708
+ allowTenantWideNamespaces: false,
17102
17709
  client: "desktop",
17103
17710
  tenant: "live",
17104
17711
  help: false
@@ -17160,6 +17767,9 @@ function parseBootstrapArgs(argv) {
17160
17767
  case "--no-seed":
17161
17768
  args.noSeed = true;
17162
17769
  break;
17770
+ case "--allow-tenant-wide-namespaces":
17771
+ args.allowTenantWideNamespaces = true;
17772
+ break;
17163
17773
  case "--tenant": {
17164
17774
  const v = argv[++i];
17165
17775
  if (v !== "test" && v !== "live") {
@@ -17241,15 +17851,6 @@ function reconcileTally(items, noun) {
17241
17851
  if (reconciled) seg.push(`${reconciled} reconciled`);
17242
17852
  return `${items.length} ${noun}${items.length === 1 ? "" : "s"} (${seg.join(", ")})`;
17243
17853
  }
17244
- function issuerTally(items) {
17245
- if (items.length === 0) return null;
17246
- const created = items.filter((i) => i.action === "created").length;
17247
- const unchanged = items.length - created;
17248
- const seg = [];
17249
- if (created) seg.push(`${created} created`);
17250
- if (unchanged) seg.push(`${unchanged} already registered \u2014 unchanged (issuers cannot be updated once registered)`);
17251
- return `${items.length} issuer${items.length === 1 ? "" : "s"} (${seg.join(", ")})`;
17252
- }
17253
17854
  function namespaceTally(items) {
17254
17855
  if (items.length === 0) return null;
17255
17856
  const created = items.filter((i) => i.action === "created").length;
@@ -17263,7 +17864,9 @@ function applyOutcomeSummary(result) {
17263
17864
  const parts = [
17264
17865
  reconcileTally(result.schemaActions, "schema"),
17265
17866
  reconcileTally(result.roleActions, "role"),
17266
- issuerTally(result.issuerActions),
17867
+ // The platform's issuer partial-update endpoint makes an issuer's
17868
+ // 'reconciled' mean the same "updated to match" as every other kind here.
17869
+ reconcileTally(result.issuerActions, "issuer"),
17267
17870
  namespaceTally(result.namespaceActions),
17268
17871
  `profile ${result.profileAction}`
17269
17872
  ].filter((p) => p !== null);
@@ -17296,6 +17899,14 @@ Options:
17296
17899
  sample seed records \u2014 the right choice for a production context
17297
17900
  (a blueprint's seeds are illustrative, not real data). Unlike
17298
17901
  --blank, the schemas are still created.
17902
+ --allow-tenant-wide-namespaces
17903
+ Required to apply a blueprint that declares a namespace with
17904
+ tenantWide: true \u2014 visible to every context in the account, not
17905
+ just this one. Without it, bootstrap refuses before provisioning
17906
+ anything, naming the namespace(s). Only an account OWNER's
17907
+ credential can actually complete the registration either way \u2014
17908
+ this flag doesn't change who's allowed, only whether this run
17909
+ tries. Never needed for a blueprint with no tenantWide namespace.
17299
17910
  --tenant test|live Which tenant the minted key scopes to (default: live).
17300
17911
  'test' provisions into your isolated test tenant \u2014 a
17301
17912
  dry-run before pointing real data at the same blueprint.
@@ -17478,11 +18089,16 @@ ${BOOTSTRAP_HELP}
17478
18089
  }
17479
18090
  throw err;
17480
18091
  }
17481
- const gate = evaluateScopeGate(blueprint.accessProfile.allowedActions);
17482
- process.stdout.write(`
18092
+ const roleNote = blueprint.accessProfile.roleIds ? ` via role(s) [${blueprint.accessProfile.roleIds.join(", ")}]` : "";
18093
+ const acceptedActions = effectiveAccessProfileActions(blueprint);
18094
+ const allDataPlane = !acceptedActions.some(
18095
+ (a) => KNOWN_CONTROL_PLANE_RESOURCES.includes(a.split(":", 1)[0])
18096
+ );
18097
+ process.stdout.write(
18098
+ `
17483
18099
  Blueprint: ${blueprint.name}
17484
- \u2714 Scope gate: [${gate.accepted.join(", ")}] \u2014 all data-plane \u2713
17485
- `);
18100
+ \u2714 Scope gate: [${acceptedActions.join(", ")}]${roleNote}` + (allDataPlane ? " \u2014 all data-plane \u2713\n" : " \u2014 includes a narrow, allowlisted control-plane exception (see above)\n")
18101
+ );
17486
18102
  if (blueprint.accessProfile.capabilities?.length) {
17487
18103
  const capGate = evaluateCapabilityGate(blueprint.accessProfile.capabilities);
17488
18104
  process.stdout.write(` \u2714 Capability gate: [${capGate.accepted.join(", ")}] \u2014 granted \u2713
@@ -17538,6 +18154,7 @@ ${renderPlan(blueprint, planSteps)}
17538
18154
  process.stdout.write(` \u2714 Bootstrap token minted (5-min TTL, ${args.tenant} tenant)
17539
18155
  `);
17540
18156
  const pingIdentity = deps.resolveIdentity ?? resolveIdentity;
18157
+ const provision = deps.provisionContext ?? provisionContext;
17541
18158
  const resolveIdentities = deps.resolveApplyTimeIdentities ?? resolveApplyTimeIdentities;
17542
18159
  const loader = deps.runLoader ?? runLoader;
17543
18160
  const client = new VectrosClient({ token: bridge.token, environment: baseUrl });
@@ -17548,11 +18165,6 @@ ${renderPlan(blueprint, planSteps)}
17548
18165
  "Could not resolve tenantId from /v1/ping \u2014 the extended ping (tenant_id) must be deployed for bootstrap."
17549
18166
  );
17550
18167
  }
17551
- const resolvedBlueprint = await resolveIdentities(
17552
- blueprint,
17553
- client,
17554
- log
17555
- );
17556
18168
  let keyring;
17557
18169
  let keyringOpenError;
17558
18170
  try {
@@ -17627,6 +18239,23 @@ ${renderPlan(blueprint, planSteps)}
17627
18239
  );
17628
18240
  }
17629
18241
  };
18242
+ const remintBootstrapForContext = buildRemintBootstrapForContext({
18243
+ cognitoToken,
18244
+ apiBaseUrl: baseUrl,
18245
+ tenant: args.tenant,
18246
+ useScopedToken: args.useScopedToken,
18247
+ mintBridge
18248
+ });
18249
+ const confirmExistingPrincipal = buildConfirmExistingPrincipal(args, promptCtx.prompt, isTty);
18250
+ const provisioned = await provision(client, {
18251
+ blueprint,
18252
+ allowTenantWideNamespaces: args.allowTenantWideNamespaces,
18253
+ remintBootstrapForContext,
18254
+ readExistingKey,
18255
+ confirmExistingPrincipal,
18256
+ log
18257
+ });
18258
+ const resolvedBlueprint = await resolveIdentities(blueprint, provisioned, client, log);
17630
18259
  const machineId = await resolveMachineId();
17631
18260
  const result = await loader(client, {
17632
18261
  blueprint: resolvedBlueprint,
@@ -17636,20 +18265,14 @@ ${renderPlan(blueprint, planSteps)}
17636
18265
  skipSeed: noSeed,
17637
18266
  readExistingKey,
17638
18267
  persistMintedKey,
17639
- remintBootstrapForContext: buildRemintBootstrapForContext({
17640
- cognitoToken,
17641
- apiBaseUrl: baseUrl,
17642
- tenant: args.tenant,
17643
- useScopedToken: args.useScopedToken,
17644
- mintBridge
17645
- }),
18268
+ provisioned,
18269
+ remintBootstrapForContext,
17646
18270
  remintForContext: buildRemintForContext({
17647
18271
  cognitoToken,
17648
18272
  apiBaseUrl: baseUrl,
17649
18273
  tenant: args.tenant,
17650
18274
  mintContext
17651
18275
  }),
17652
- confirmExistingPrincipal: buildConfirmExistingPrincipal(args, promptCtx.prompt, isTty),
17653
18276
  log
17654
18277
  });
17655
18278
  process.stdout.write(
@@ -17705,10 +18328,13 @@ ${codeAddHint}`);
17705
18328
  }
17706
18329
  try {
17707
18330
  const merged = await safeMergeConfigFile({ configPath, entry });
17708
- const restartHint = isCode ? ' Reopen Claude Code in this project, then try: "list my tasks" / "add a task to \u2026"\n' : ' Restart your MCP client, then try: "list my tasks" / "add a task to \u2026"\n';
18331
+ const restartHint = buildRestartHint(blueprint.name, isCode);
17709
18332
  process.stdout.write(
17710
18333
  ` \u2714 Config ${merged.action}${merged.backupPath ? ` (backup: ${merged.backupPath})` : ""}
17711
- ` + restartHint
18334
+ ` + restartHint + // The `claude mcp add` line is otherwise only shown on --no-merge / a
18335
+ // declined / a failed merge — surface it here too so a successful
18336
+ // auto-merge doesn't hide the idiomatic install command entirely.
18337
+ codeAddHint
17712
18338
  );
17713
18339
  } catch (err) {
17714
18340
  process.stderr.write(` \u26A0 Could not merge config (${err instanceof Error ? err.message : String(err)}).
@@ -17740,9 +18366,18 @@ ${codeAddHint}`);
17740
18366
  return 3;
17741
18367
  }
17742
18368
  if (err instanceof BlueprintIdentityError || err instanceof UnresolvedIdentitiesError) {
18369
+ process.stderr.write(
18370
+ `
18371
+ \u2716 ${err.message}
18372
+ The app-context, service principal, and any declared namespaces were already provisioned. No schemas, profile, roles, key, or seed records were.
18373
+ `
18374
+ );
18375
+ return 2;
18376
+ }
18377
+ if (err instanceof TenantWideNamespacesNotAllowedError) {
17743
18378
  process.stderr.write(`
17744
18379
  \u2716 ${err.message}
17745
- No context or key was provisioned.
18380
+ Nothing was provisioned.
17746
18381
  `);
17747
18382
  return 2;
17748
18383
  }
@@ -17817,11 +18452,14 @@ function assertIdentityOverrideValues(overrides, flag) {
17817
18452
  }
17818
18453
  }
17819
18454
  function placeholderScopeNamespace(value) {
17820
- const m = /^\$\{\{\s*(?:under\.)?self\.scope\.([^\s}]+)\s*\}\}$/.exec(value);
17821
- return m ? m[1] : null;
18455
+ const self2 = /^\$\{\{\s*(?:under\.)?self\.scope\.([^\s}]+)\s*\}\}$/.exec(value);
18456
+ if (self2) return self2[1];
18457
+ const member = MEMBER_SCOPE_PLACEHOLDER_RE2.exec(value);
18458
+ return member ? member[1] : null;
17822
18459
  }
17823
18460
  var SELF_PLACEHOLDER_RE = /^\$\{\{\s*self\.(?:userId|partnerUserId|scope\.[a-z][a-z0-9_-]{1,31})\s*\}\}$/;
17824
- var UNDER_SELF_PLACEHOLDER_RE = /^\$\{\{\s*under\.self\.(?:userId|partnerUserId|scope\.[a-z][a-z0-9_-]{1,31})\s*\}\}$/;
18461
+ var UNDER_SELF_PLACEHOLDER_RE2 = /^\$\{\{\s*under\.self\.(?:userId|partnerUserId|scope\.[a-z][a-z0-9_-]{1,31})\s*\}\}$/;
18462
+ var MEMBER_SCOPE_PLACEHOLDER_RE2 = /^\$\{\{\s*member\.scope\.([a-z][a-z0-9_-]{1,31})(?::[a-z][a-z0-9_-]{1,31})?\s*\}\}$/;
17825
18463
 
17826
18464
  // src/lint.ts
17827
18465
  var ENUM_COMPATIBLE_TYPES = /* @__PURE__ */ new Set(["enum", "string", "string[]"]);
@@ -17880,8 +18518,12 @@ function compositeLegFindings(schema, lf, fieldsById, rangeEnabledFieldNames) {
17880
18518
  function lintBlueprint(bp) {
17881
18519
  const findings = [];
17882
18520
  const typeNames = /* @__PURE__ */ new Set();
18521
+ const expectedDimsByTypeName = /* @__PURE__ */ new Map();
17883
18522
  for (const schema of bp.schemas) {
17884
18523
  typeNames.add(schema.typeName);
18524
+ if (schema.expectedScopeDims && schema.expectedScopeDims.length > 0) {
18525
+ expectedDimsByTypeName.set(schema.typeName, schema.expectedScopeDims);
18526
+ }
17885
18527
  const fieldIds = /* @__PURE__ */ new Set();
17886
18528
  const fieldsById = /* @__PURE__ */ new Map();
17887
18529
  for (const f of schema.fields) {
@@ -17959,9 +18601,12 @@ function lintBlueprint(bp) {
17959
18601
  }
17960
18602
  }
17961
18603
  }
17962
- {
18604
+ if (bp.accessProfile.allowedActions !== void 0) {
17963
18605
  const reached = ownerScopedReachWithoutDataScope(bp.accessProfile.allowedActions, ds);
17964
18606
  if (reached.length > 0) findings.push(unconstrainedReachFinding("accessProfile", reached));
18607
+ findings.push(
18608
+ ...partialCoverageFindings("accessProfile", bp.accessProfile.allowedActions, ds, expectedDimsByTypeName)
18609
+ );
17965
18610
  }
17966
18611
  for (const roleId of Object.keys(bp.roles ?? {})) {
17967
18612
  if (!ROLE_ID_RE.test(roleId)) {
@@ -17979,6 +18624,9 @@ function lintBlueprint(bp) {
17979
18624
  if (reached.length > 0) {
17980
18625
  findings.push(unconstrainedReachFinding(`role '${roleId}' clause ${i}`, reached));
17981
18626
  }
18627
+ findings.push(
18628
+ ...partialCoverageFindings(`role '${roleId}' clause ${i}`, clause.allowedActions, cds, expectedDimsByTypeName)
18629
+ );
17982
18630
  if (!cds) return;
17983
18631
  for (const [key, values] of Object.entries(cds)) {
17984
18632
  for (const value of values) {
@@ -18010,7 +18658,12 @@ function lintBlueprint(bp) {
18010
18658
  }
18011
18659
  var ROLE_ID_RE = /^[a-z][a-z0-9-]{2,30}$/;
18012
18660
  var PLACEHOLDER_SHAPED_RE = /^\s*\$\{\{.*\}\}\s*$/s;
18013
- var RECOGNIZED_PLACEHOLDER_RES = [/^\$\{\{\s*any\s*\}\}$/, SELF_PLACEHOLDER_RE, UNDER_SELF_PLACEHOLDER_RE];
18661
+ var RECOGNIZED_PLACEHOLDER_RES = [
18662
+ /^\$\{\{\s*any\s*\}\}$/,
18663
+ SELF_PLACEHOLDER_RE,
18664
+ UNDER_SELF_PLACEHOLDER_RE2,
18665
+ MEMBER_SCOPE_PLACEHOLDER_RE2
18666
+ ];
18014
18667
  var BLUEPRINT_TOKEN_RE = /^\s*\$\{\{\s*(identities|inputs|vectros|ns)\./;
18015
18668
  function placeholderFinding(where, key, value) {
18016
18669
  if (typeof value !== "string" || !PLACEHOLDER_SHAPED_RE.test(value)) return null;
@@ -18028,7 +18681,7 @@ function placeholderFinding(where, key, value) {
18028
18681
  return {
18029
18682
  severity: "error",
18030
18683
  rule: "datascope-unknown-placeholder",
18031
- message: `${where}: dataScope.${key} value '${value}' is not a placeholder the platform recognizes \u2014 it will be rejected at apply. Valid: \${{ any }}, \${{ self.userId }}, \${{ self.scope.<namespace> }}, \${{ under.self.userId }}, \${{ under.self.scope.<namespace> }}. Spelling is exact and lowercase; use a plain value for a literal owner id.`
18684
+ message: `${where}: dataScope.${key} value '${value}' is not a placeholder the platform recognizes \u2014 it will be rejected at apply. Valid: \${{ any }}, \${{ self.userId }}, \${{ self.scope.<namespace> }}, \${{ under.self.userId }}, \${{ under.self.scope.<namespace> }}, \${{ member.scope.<namespace> }}, \${{ member.scope.<namespace>:<level> }}. Spelling is exact and lowercase; use a plain value for a literal owner id.`
18032
18685
  };
18033
18686
  }
18034
18687
  var OWNER_SCOPED_RESOURCES = /* @__PURE__ */ new Set(["records", "documents", "entities"]);
@@ -18050,11 +18703,37 @@ function unconstrainedReachFinding(where, resources) {
18050
18703
  message: `${where}: no dataScope at all, but allowedActions grants r/u/d on ${resources.join("/")} \u2014 this reaches EVERY row of EVERY owner on ${resources.length > 1 ? "those resources" : "that resource"}, not just "your own org" or any other value you may have intended. If that's deliberate (a genuinely shared/tenant-wide resource), you can ignore this; otherwise add a dataScope entry.`
18051
18704
  };
18052
18705
  }
18053
- function lintPasses(findings) {
18054
- return !findings.some((f) => f.severity === "error");
18706
+ function dimToDataScopeKey(dim) {
18707
+ return dim === "userId" ? "userId" : `scope:${dim}`;
18055
18708
  }
18056
-
18057
- // src/blueprint-init.ts
18709
+ function partialCoverageFindings(where, allowedActions, dataScope, expectedDimsByTypeName) {
18710
+ const findings = [];
18711
+ const dataScopeKeys = new Set(Object.keys(dataScope ?? {}));
18712
+ for (const action of allowedActions) {
18713
+ const parts = action.split(":");
18714
+ const [resource, ops] = parts;
18715
+ if (resource !== "records" && resource !== "documents" || !ops) continue;
18716
+ const qualifier = parts.length > 2 ? parts.slice(2).join(":") : void 0;
18717
+ if (qualifier === void 0) continue;
18718
+ const dims = expectedDimsByTypeName.get(qualifier);
18719
+ if (!dims || dims.length === 0) continue;
18720
+ if (!(ops === "*" || [...ops].some((op) => op === "r" || op === "u" || op === "d"))) continue;
18721
+ const covered = dims.filter((d) => dataScopeKeys.has(dimToDataScopeKey(d)));
18722
+ if (covered.length === 0 || covered.length === dims.length) continue;
18723
+ const missing = dims.filter((d) => !covered.includes(d));
18724
+ findings.push({
18725
+ severity: "warning",
18726
+ rule: "partial-datascope-dimension-coverage",
18727
+ message: `${where}: ${action} covers dataScope dim${covered.length > 1 ? "s" : ""} [${covered.join(", ")}] of '${qualifier}'s declared expectedScopeDims [${dims.join(", ")}] \u2014 the ${missing.join("/")} dimension${missing.length > 1 ? "s are" : " is"} UNCONSTRAINED for this grant (every value reaches, not just the one(s) you may have intended). If that's deliberate, you can ignore this; otherwise add a dataScope entry for the missing dimension(s).`
18728
+ });
18729
+ }
18730
+ return findings;
18731
+ }
18732
+ function lintPasses(findings) {
18733
+ return !findings.some((f) => f.severity === "error");
18734
+ }
18735
+
18736
+ // src/blueprint-init.ts
18058
18737
  import { stringify as yamlStringify } from "yaml";
18059
18738
  function scaffoldPath(name) {
18060
18739
  return `./${name}.blueprint.yaml`;
@@ -18141,6 +18820,10 @@ function buildScaffold(name, from) {
18141
18820
  }
18142
18821
 
18143
18822
  // src/cli-blueprint.ts
18823
+ function noEffectOnBlueprint(flag, sub) {
18824
+ const cmd = sub ? `blueprint ${sub}` : "blueprint";
18825
+ return `'${flag}' has no effect on '${cmd}' \u2014 blueprint validate/plan/list/init are creds-free and never call the API, so there's no environment/credential to select. Did you mean 'vectros bootstrap'?`;
18826
+ }
18144
18827
  function parseBlueprintArgs(argv) {
18145
18828
  const args = { help: false, force: false, set: [] };
18146
18829
  const positionals = [];
@@ -18167,6 +18850,9 @@ function parseBlueprintArgs(argv) {
18167
18850
  args.values = argv[++i];
18168
18851
  break;
18169
18852
  default:
18853
+ if (AUTH_FLAG_NAMES.includes(a)) {
18854
+ throw new Error(noEffectOnBlueprint(a, positionals[0]));
18855
+ }
18170
18856
  if (a.startsWith("-")) throw new Error(`Unknown blueprint flag: ${a}`);
18171
18857
  positionals.push(a);
18172
18858
  }
@@ -18274,11 +18960,11 @@ async function runValidate(args) {
18274
18960
  }
18275
18961
  const blueprint = await resolveAndValidate(args.file, "validate", supplied);
18276
18962
  if (!blueprint) return 2;
18277
- const gate = evaluateScopeGate(blueprint.accessProfile.allowedActions);
18278
18963
  const capGate = evaluateCapabilityGate(blueprint.accessProfile.capabilities);
18279
18964
  const warnCount = lintBlueprint(blueprint).length;
18965
+ const roleNote = blueprint.accessProfile.roleIds ? ` via role(s) [${blueprint.accessProfile.roleIds.join(", ")}]` : "";
18280
18966
  process.stdout.write(
18281
- ` \u2714 '${blueprint.name}' is valid \u2014 structural \u2713, scope gate \u2713 [${gate.accepted.join(", ")}]` + (capGate.accepted.length ? `, capability gate \u2713 [${capGate.accepted.join(", ")}]` : "") + `, lint \u2713` + (warnCount ? ` (${warnCount} warning${warnCount === 1 ? "" : "s"})` : "") + "\n"
18967
+ ` \u2714 '${blueprint.name}' is valid \u2014 structural \u2713, scope gate \u2713 [${effectiveAccessProfileActions(blueprint).join(", ")}]${roleNote}` + (capGate.accepted.length ? `, capability gate \u2713 [${capGate.accepted.join(", ")}]` : "") + `, lint \u2713` + (warnCount ? ` (${warnCount} warning${warnCount === 1 ? "" : "s"})` : "") + "\n"
18282
18968
  );
18283
18969
  return 0;
18284
18970
  }
@@ -18959,6 +19645,51 @@ ${list}
18959
19645
  });
18960
19646
  }
18961
19647
 
19648
+ // src/dev-api.ts
19649
+ var DeveloperApiError = class extends Error {
19650
+ statusCode;
19651
+ constructor(message, statusCode) {
19652
+ super(message);
19653
+ this.name = "DeveloperApiError";
19654
+ this.statusCode = statusCode;
19655
+ }
19656
+ };
19657
+ async function fetchDeveloperUsersPage(opts) {
19658
+ const { cognitoToken, apiBaseUrl, tenant, startFrom, limit, fetchImpl = fetch } = opts;
19659
+ const base = apiBaseUrl.replace(/\/$/, "");
19660
+ let url = `${base}/developer/users?tenant=${encodeURIComponent(tenant)}`;
19661
+ if (startFrom) url += `&startFrom=${encodeURIComponent(startFrom)}`;
19662
+ if (limit !== void 0) url += `&limit=${encodeURIComponent(String(limit))}`;
19663
+ let res;
19664
+ try {
19665
+ res = await fetchImpl(url, {
19666
+ method: "GET",
19667
+ headers: { Authorization: `Bearer ${cognitoToken}` }
19668
+ });
19669
+ } catch (err) {
19670
+ throw new DeveloperApiError(
19671
+ `Request to /developer/users failed: ${err instanceof Error ? err.message : String(err)}`
19672
+ );
19673
+ }
19674
+ if (!res.ok) {
19675
+ const body = await res.text().catch(() => "");
19676
+ const hint = res.status === 403 ? " (this command requires an account owner login \u2014 sign in as the owner and retry)" : "";
19677
+ throw new DeveloperApiError(
19678
+ `GET /developer/users failed: HTTP ${res.status}${hint}${body ? ` \u2014 ${body}` : ""}`,
19679
+ res.status
19680
+ );
19681
+ }
19682
+ let parsed;
19683
+ try {
19684
+ parsed = await res.json();
19685
+ } catch {
19686
+ throw new DeveloperApiError("GET /developer/users returned a non-JSON body");
19687
+ }
19688
+ const data = Array.isArray(parsed.data) ? parsed.data : [];
19689
+ const nextCursor = typeof parsed.nextCursor === "string" ? parsed.nextCursor : null;
19690
+ return { data, nextCursor };
19691
+ }
19692
+
18962
19693
  // src/commands/identity.ts
18963
19694
  function isUserDimension(type) {
18964
19695
  return type === "user";
@@ -19113,6 +19844,57 @@ async function runIdentityList(opts, deps = {}) {
19113
19844
  }
19114
19845
  });
19115
19846
  }
19847
+ function toIdentityView(row) {
19848
+ const str = (v) => typeof v === "string" ? v : void 0;
19849
+ return {
19850
+ id: str(row.id),
19851
+ externalId: str(row.externalId),
19852
+ email: str(row.email),
19853
+ status: str(row.status),
19854
+ type: str(row.type),
19855
+ createdAt: str(row.createdAt)
19856
+ };
19857
+ }
19858
+ async function runIdentityListAll(opts, deps = {}) {
19859
+ const fetchImpl = deps.fetchImpl ?? fetch;
19860
+ return withAuth({ ...opts, provisioning: true }, deps, async (auth) => {
19861
+ try {
19862
+ const tenant = opts.tenant ?? "live";
19863
+ const fetchPage = async (startFrom) => {
19864
+ const page = await fetchDeveloperUsersPage({
19865
+ cognitoToken: auth.cognitoToken,
19866
+ apiBaseUrl: auth.baseUrl,
19867
+ tenant,
19868
+ startFrom,
19869
+ limit: opts.limit,
19870
+ fetchImpl
19871
+ });
19872
+ return { data: page.data.map(toIdentityView), nextCursor: page.nextCursor };
19873
+ };
19874
+ let rows = opts.limit !== void 0 ? (await fetchPage()).data ?? [] : await drainPages(fetchPage);
19875
+ if (opts.externalId) rows = rows.filter((r) => r.externalId === opts.externalId);
19876
+ if (opts.json) {
19877
+ process.stdout.write(`${formatJson(rows)}
19878
+ `);
19879
+ return 0;
19880
+ }
19881
+ process.stdout.write(
19882
+ "\n" + formatTable(rows, [
19883
+ { header: "id", get: (r) => r.id },
19884
+ { header: "external id", get: (r) => r.externalId },
19885
+ { header: "email", get: (r) => r.email },
19886
+ { header: "kind", get: (r) => r.type },
19887
+ { header: "status", get: (r) => r.status }
19888
+ ]) + "\n"
19889
+ );
19890
+ return 0;
19891
+ } catch (err) {
19892
+ process.stderr.write(` \u2716 identity list-all failed: ${formatError(err)}
19893
+ `);
19894
+ return 1;
19895
+ }
19896
+ });
19897
+ }
19116
19898
  async function runIdentityGet(opts, deps = {}) {
19117
19899
  return withAuth(opts, deps, async (auth) => {
19118
19900
  const client = auth.client;
@@ -19149,6 +19931,52 @@ async function runIdentityDelete(opts, deps = {}) {
19149
19931
  }
19150
19932
  });
19151
19933
  }
19934
+ function renderInvite(v) {
19935
+ return formatKeyValues([
19936
+ ["userId", v.userId],
19937
+ ["inviteExpiresAt", v.inviteExpiresAt],
19938
+ ["emailSent", v.emailSent === void 0 ? void 0 : String(v.emailSent)],
19939
+ ["inviteToken", v.inviteToken],
19940
+ ["acceptLink", v.acceptLink]
19941
+ ]);
19942
+ }
19943
+ async function runIdentityInvite(opts, deps = {}) {
19944
+ const sendEmail = !opts.noSendEmail;
19945
+ if (sendEmail && !opts.acceptUrl) {
19946
+ process.stderr.write(
19947
+ " \u2716 --accept-url is required unless --no-send-email is given (the platform delivers the invitation email itself and needs somewhere to point it).\n"
19948
+ );
19949
+ return 2;
19950
+ }
19951
+ return withAuth(opts, deps, async (auth) => {
19952
+ const client = auth.client;
19953
+ try {
19954
+ const invite = await client.auth.createInvite({
19955
+ email: opts.email,
19956
+ contextId: opts.context,
19957
+ accessProfile: { roleId: opts.role },
19958
+ firstName: opts.firstName,
19959
+ acceptUrl: opts.acceptUrl,
19960
+ sendEmail
19961
+ });
19962
+ if (opts.json) {
19963
+ process.stdout.write(`${formatJson(invite)}
19964
+ `);
19965
+ return 0;
19966
+ }
19967
+ process.stdout.write(
19968
+ `
19969
+ \u2714 Invited '${opts.email}' to '${opts.context}' as '${opts.role}'
19970
+ ${renderInvite(invite)}
19971
+ `
19972
+ );
19973
+ return 0;
19974
+ } catch (err) {
19975
+ process.stderr.write(formatCliError(err, { action: "identity invite failed" }));
19976
+ return 1;
19977
+ }
19978
+ });
19979
+ }
19152
19980
 
19153
19981
  // src/commands/access.ts
19154
19982
  function assertPrincipalShape(principal) {
@@ -19167,20 +19995,24 @@ async function resolvePrincipalArg(raw, auth) {
19167
19995
  }
19168
19996
  return self2.principalId;
19169
19997
  }
19998
+ function roleSummary(p) {
19999
+ if (p.roleIds?.length) return p.roleIds.join(", ");
20000
+ return p.roleId;
20001
+ }
19170
20002
  function renderProfile(p) {
19171
20003
  return formatKeyValues([
19172
20004
  ["context", p.contextId],
19173
20005
  ["principal", p.principalId],
19174
- ["role", p.roleId],
20006
+ ["role", roleSummary(p)],
19175
20007
  ["scopes", p.scopes?.length ? `${p.scopes.length} clause(s)` : void 0],
19176
20008
  ["status", p.status]
19177
20009
  ]);
19178
20010
  }
19179
20011
  async function runAccessGrant(opts, deps = {}) {
19180
- const hasRole = Boolean(opts.role);
20012
+ const hasRole = Boolean(opts.role?.length);
19181
20013
  const hasActions = Boolean(opts.actions);
19182
20014
  if (hasRole === hasActions) {
19183
- process.stderr.write(" \u2716 Provide exactly one of --role <roleId> or --actions <csv>.\n");
20015
+ process.stderr.write(" \u2716 Provide exactly one of --role <roleId> (repeatable) or --actions <csv>.\n");
19184
20016
  return 2;
19185
20017
  }
19186
20018
  if (hasActions && splitCsv(opts.actions).length === 0) {
@@ -19220,7 +20052,11 @@ async function runAccessGrant(opts, deps = {}) {
19220
20052
  upsert: true,
19221
20053
  body: {
19222
20054
  principalId: principal,
19223
- roleId: opts.role,
20055
+ // Always send the plural `roleIds`, even for a single
20056
+ // `--role`: it is a strict superset of the deprecated singular
20057
+ // `roleId` (equivalent to `roleIds: ["<value>"]`), and the platform
20058
+ // reads it uniformly regardless of how many roles compose.
20059
+ roleIds: opts.role,
19224
20060
  scopes,
19225
20061
  identityOverrides
19226
20062
  }
@@ -19232,7 +20068,7 @@ async function runAccessGrant(opts, deps = {}) {
19232
20068
  }
19233
20069
  process.stdout.write(
19234
20070
  `
19235
- \u2714 Granted ${principal} in '${opts.context}'` + (hasRole ? ` via role '${opts.role}'` : ` with ${splitCsv(opts.actions).length} action(s)`) + `
20071
+ \u2714 Granted ${principal} in '${opts.context}'` + (hasRole ? ` via role(s) [${opts.role.join(", ")}]` : ` with ${splitCsv(opts.actions).length} action(s)`) + `
19236
20072
  ${renderProfile(profile)}
19237
20073
  `
19238
20074
  );
@@ -19301,7 +20137,7 @@ async function runAccessList(opts, deps = {}) {
19301
20137
  "\n" + formatTable(rows, [
19302
20138
  { header: "context", get: (r) => r.contextId },
19303
20139
  { header: "principal", get: (r) => r.principalId },
19304
- { header: "role", get: (r) => r.roleId ?? (r.scopes?.length ? "(inline)" : void 0) },
20140
+ { header: "role", get: (r) => roleSummary(r) ?? (r.scopes?.length ? "(inline)" : void 0) },
19305
20141
  { header: "status", get: (r) => r.status }
19306
20142
  ]) + "\n"
19307
20143
  );
@@ -19349,6 +20185,7 @@ ${renderProfile(p)}
19349
20185
  var SELF = /* @__PURE__ */ Symbol("self");
19350
20186
  var ANY = /* @__PURE__ */ Symbol("any");
19351
20187
  var UNDER_SELF = /* @__PURE__ */ Symbol("under.self");
20188
+ var MEMBER = /* @__PURE__ */ Symbol("member.scope");
19352
20189
  function canonicalDim(key) {
19353
20190
  if (key === "userId" || key === "partnerUserId") return "user";
19354
20191
  return key;
@@ -19378,7 +20215,8 @@ function normalizeClauses(raw) {
19378
20215
  if (val === null) return null;
19379
20216
  const s = String(val);
19380
20217
  if (ANY_TOKEN_RE.test(s)) return ANY;
19381
- if (UNDER_SELF_PLACEHOLDER_RE.test(s)) return UNDER_SELF;
20218
+ if (UNDER_SELF_PLACEHOLDER_RE2.test(s)) return UNDER_SELF;
20219
+ if (MEMBER_SCOPE_PLACEHOLDER_RE2.test(s)) return MEMBER;
19382
20220
  return SELF_PLACEHOLDER_RE.test(s) ? SELF : s;
19383
20221
  });
19384
20222
  if (!isRecognizedOwnershipKey(k)) unrecognizedDims.push(k);
@@ -19433,7 +20271,7 @@ function buildArchetypes(clauses) {
19433
20271
  const set = scopeDims.get(dim) ?? /* @__PURE__ */ new Set();
19434
20272
  set.add(SELF);
19435
20273
  for (const v of values) {
19436
- if (v === null || v === ANY || v === UNDER_SELF) continue;
20274
+ if (v === null || v === ANY || v === UNDER_SELF || v === MEMBER) continue;
19437
20275
  set.add(v);
19438
20276
  }
19439
20277
  scopeDims.set(dim, set);
@@ -19460,6 +20298,7 @@ function listAdmits(allowed, recordValue) {
19460
20298
  if (a === SELF) return recordValue === SELF;
19461
20299
  if (a === ANY) return recordValue !== void 0;
19462
20300
  if (a === UNDER_SELF) return false;
20301
+ if (a === MEMBER) return false;
19463
20302
  return recordValue === a;
19464
20303
  });
19465
20304
  }
@@ -19485,7 +20324,7 @@ function undecidableDims(clauses) {
19485
20324
  const dims = /* @__PURE__ */ new Set();
19486
20325
  for (const c of clauses) {
19487
20326
  for (const [dim, values] of Object.entries(c.dataScope)) {
19488
- if (values.includes(UNDER_SELF)) dims.add(dim);
20327
+ if (values.includes(UNDER_SELF) || values.includes(MEMBER)) dims.add(dim);
19489
20328
  }
19490
20329
  }
19491
20330
  return [...dims];
@@ -19518,6 +20357,7 @@ function renderDataScope(ds) {
19518
20357
  if (v === SELF) return "self";
19519
20358
  if (v === ANY) return "any";
19520
20359
  if (v === UNDER_SELF) return "under-self";
20360
+ if (v === MEMBER) return "member";
19521
20361
  if (v === null) return "null";
19522
20362
  return v;
19523
20363
  });
@@ -19549,14 +20389,25 @@ async function runAccessExplain(opts, deps = {}) {
19549
20389
  const profile = await client.auth.getAccessProfile({ contextId: opts.context, principalId: principal });
19550
20390
  let rawClauses = profile.scopes ?? [];
19551
20391
  let via = "inline scopes";
19552
- if (profile.roleId) {
19553
- const role = await client.auth.getRole({ contextId: opts.context, roleId: profile.roleId });
19554
- rawClauses = role.scopes ?? [];
19555
- via = `role '${profile.roleId}'`;
20392
+ const roleIds = profile.roleIds ?? (profile.roleId ? [profile.roleId] : []);
20393
+ if (roleIds.length > 0) {
20394
+ const roleClauses = [];
20395
+ for (const roleId of roleIds) {
20396
+ const role = await client.auth.getRole({ contextId: opts.context, roleId });
20397
+ roleClauses.push(...role.scopes ?? []);
20398
+ }
20399
+ rawClauses = roleClauses;
20400
+ via = roleIds.length === 1 ? `role '${roleIds[0]}'` : `role(s) [${roleIds.join(", ")}]`;
19556
20401
  }
19557
20402
  const clauses = normalizeClauses(rawClauses);
19558
20403
  const rows = explainClauses(clauses);
19559
20404
  const undecidable = undecidableDims(clauses);
20405
+ const hasUnderSelfUndecidable = clauses.some(
20406
+ (c) => Object.values(c.dataScope).some((values) => values.includes(UNDER_SELF))
20407
+ );
20408
+ const hasMemberUndecidable = clauses.some(
20409
+ (c) => Object.values(c.dataScope).some((values) => values.includes(MEMBER))
20410
+ );
19560
20411
  if (opts.json) {
19561
20412
  process.stdout.write(
19562
20413
  `${formatJson({
@@ -19613,10 +20464,7 @@ async function runAccessExplain(opts, deps = {}) {
19613
20464
  ]) + (undecidable.length > 0 ? `
19614
20465
 
19615
20466
  \u26A0 UNDECIDED offline: ${undecidable.join(", ")}
19616
- A clause matching on \`\${{ under.self.* }}\` needs the parent graph, which only the server has,
19617
- so rows granted through it are shown as NO above. Read those cells as "cannot tell here",
19618
- NOT as a denial \u2014 the matrix under-reports this clause.
19619
- ` : "") + '\n\n \u26A0 READ ONLY: "granted?"/"actions" model whether a row can be READ, not whether a c/u/d\n verb shown there can actually WRITE that row\'s exact shape \u2014 the platform requires a clause\n to name a dimension to place a value into it, while reads are never narrowed by a dimension a\n clause omits. A create/update verb next to a row is advisory: it names which records exist to\n act on, not that a specific write will succeed.\n\n\n NOTE: this matrix models the OWNERSHIP dimensions only \u2014 "granted?" says a row\'s ownership\n shape matches a clause, and "record types" (above) says which `records:` types that grant\n actually reaches; "all" reaches every type, "\u2014" means no `records:` action grants this row at\n all. Search/RAG visibility is a separate, still-unmodeled narrowing: it is bounded per row by\n the SAME clause\'s typed read grants \u2014 a row shown with `search:r` is searchable only for the\n content types that clause can read.\n Preview only \u2014 computed client-side from the documented matching rules; the platform decides at request time.\n'
20467
+ ` + (hasUnderSelfUndecidable ? " A clause matching on `${{ under.self.* }}` needs the server's parent graph, which this\n preview does not have, so rows granted through it are shown as NO above.\n" : "") + (hasMemberUndecidable ? " A clause matching on `${{ member.scope.<ns>[:<level>] }}` needs a live per-request read of\n this account's membership records, which this preview does not have, so rows granted\n through it are shown as NO above.\n" : "") + ' Read those cells as "cannot tell here", NOT as a denial \u2014 the matrix under-reports\n whichever clause(s) above are responsible.\n' : "") + '\n\n \u26A0 READ ONLY: "granted?"/"actions" model whether a row can be READ, not whether a c/u/d\n verb shown there can actually WRITE that row\'s exact shape \u2014 the platform requires a clause\n to name a dimension to place a value into it, while reads are never narrowed by a dimension a\n clause omits. A create/update verb next to a row is advisory: it names which records exist to\n act on, not that a specific write will succeed.\n\n\n NOTE: this matrix models the OWNERSHIP dimensions only \u2014 "granted?" says a row\'s ownership\n shape matches a clause, and "record types" (above) says which `records:` types that grant\n actually reaches; "all" reaches every type, "\u2014" means no `records:` action grants this row at\n all. Search/RAG visibility is a separate, still-unmodeled narrowing: it is bounded per row by\n the SAME clause\'s typed read grants \u2014 a row shown with `search:r` is searchable only for the\n content types that clause can read.\n Preview only \u2014 computed client-side from the documented matching rules; the platform decides at request time.\n'
19620
20468
  );
19621
20469
  return 0;
19622
20470
  } catch (err) {
@@ -19634,7 +20482,8 @@ function renderRole(r) {
19634
20482
  ["name", r.name],
19635
20483
  ["context", r.contextId],
19636
20484
  ["description", r.description],
19637
- ["scopes", r.scopes?.length ? `${r.scopes.length} clause(s)` : void 0]
20485
+ ["scopes", r.scopes?.length ? `${r.scopes.length} clause(s)` : void 0],
20486
+ ["assumable", r.assumable && Object.keys(r.assumable).length ? formatJson(r.assumable) : void 0]
19638
20487
  ]);
19639
20488
  }
19640
20489
  async function runRoleCreate(opts, deps = {}) {
@@ -19649,6 +20498,16 @@ async function runRoleCreate(opts, deps = {}) {
19649
20498
  dataScope = parseJsonObjectFlag("--data-scope", opts.dataScope);
19650
20499
  } catch (err) {
19651
20500
  process.stderr.write(` \u2716 ${formatError(err)}
20501
+ `);
20502
+ return 2;
20503
+ }
20504
+ }
20505
+ let assumable;
20506
+ if (opts.assumable !== void 0) {
20507
+ try {
20508
+ assumable = parseJsonObjectFlag("--assumable", opts.assumable);
20509
+ } catch (err) {
20510
+ process.stderr.write(` \u2716 ${formatError(err)}
19652
20511
  `);
19653
20512
  return 2;
19654
20513
  }
@@ -19666,7 +20525,12 @@ async function runRoleCreate(opts, deps = {}) {
19666
20525
  // stays byte-identical for unscoped roles.
19667
20526
  scopes: [
19668
20527
  dataScope ? { allowed_actions: actions, data_scope: dataScope } : { allowed_actions: actions }
19669
- ]
20528
+ ],
20529
+ // Included only when supplied — omitting it (rather than sending an
20530
+ // empty object) keeps "no assume grant" and "explicitly empty grant"
20531
+ // indistinguishable on the wire, matching the platform's own
20532
+ // "omit for the safe default" contract.
20533
+ assumable
19670
20534
  }
19671
20535
  });
19672
20536
  if (opts.json) {
@@ -19905,6 +20769,390 @@ ${renderContext(ctx)}
19905
20769
  });
19906
20770
  }
19907
20771
 
20772
+ // src/commands/issuers.ts
20773
+ import * as readline4 from "readline/promises";
20774
+ function parseSelfSignupPolicies(raw) {
20775
+ if (!raw || raw.length === 0) return void 0;
20776
+ return raw.map((entry) => {
20777
+ const sep = entry.indexOf(":");
20778
+ if (sep <= 0 || sep === entry.length - 1) {
20779
+ throw new Error(
20780
+ `--self-signup-policy must be '<signup_type>:<roleId>' (e.g. 'default:member'); got '${entry}'.`
20781
+ );
20782
+ }
20783
+ return { signup_type: entry.slice(0, sep), role_id: entry.slice(sep + 1) };
20784
+ });
20785
+ }
20786
+ function renderSelfSignup(policies) {
20787
+ if (!policies || policies.length === 0) return "\u2014";
20788
+ return policies.map((p) => `${p.signup_type} \u2192 ${p.role_id}`).join(", ");
20789
+ }
20790
+ function renderIssuer(i) {
20791
+ return formatKeyValues([
20792
+ ["issuerId", i.issuerId],
20793
+ ["issuer", i.issuer],
20794
+ ["jwksUri", i.jwksUri],
20795
+ ["audience", i.audience],
20796
+ ["contextId", i.contextId],
20797
+ ["subClaim", i.subClaim],
20798
+ ["emailClaim", i.emailClaim],
20799
+ ["userinfoUri", i.userinfoUri],
20800
+ ["status", i.status],
20801
+ ["createdAt", i.createdAt],
20802
+ ["selfSignup", renderSelfSignup(i.selfSignupPolicies)]
20803
+ ]);
20804
+ }
20805
+ async function runIssuersList(opts, deps = {}) {
20806
+ return withAuth({ ...opts, provisioning: true, context: opts.context }, deps, async (auth) => {
20807
+ const client = auth.client;
20808
+ try {
20809
+ const rows = await drainPages(
20810
+ (startFrom) => client.auth.listIssuers({ limit: PAGE_LIMIT, ...startFrom ? { startFrom } : {} })
20811
+ );
20812
+ if (opts.json) {
20813
+ process.stdout.write(`${formatJson(rows)}
20814
+ `);
20815
+ return 0;
20816
+ }
20817
+ process.stdout.write(
20818
+ "\n" + formatTable(rows, [
20819
+ { header: "issuerId", get: (i) => i.issuerId },
20820
+ { header: "issuer", get: (i) => i.issuer },
20821
+ { header: "audience", get: (i) => i.audience },
20822
+ { header: "status", get: (i) => i.status },
20823
+ { header: "self-signup", get: (i) => renderSelfSignup(i.selfSignupPolicies) }
20824
+ ]) + "\n"
20825
+ );
20826
+ return 0;
20827
+ } catch (err) {
20828
+ process.stderr.write(formatCliError(err, { action: "issuers list failed" }));
20829
+ return 1;
20830
+ }
20831
+ });
20832
+ }
20833
+ async function runIssuersGet(opts, deps = {}) {
20834
+ return withAuth({ ...opts, provisioning: true, context: opts.context }, deps, async (auth) => {
20835
+ const client = auth.client;
20836
+ try {
20837
+ const issuer = await client.auth.getIssuer({ issuerId: opts.issuerId });
20838
+ if (opts.json) {
20839
+ process.stdout.write(`${formatJson(issuer)}
20840
+ `);
20841
+ return 0;
20842
+ }
20843
+ process.stdout.write(`
20844
+ ${renderIssuer(issuer)}
20845
+ `);
20846
+ return 0;
20847
+ } catch (err) {
20848
+ process.stderr.write(formatCliError(err, { action: "issuers get failed" }));
20849
+ return 1;
20850
+ }
20851
+ });
20852
+ }
20853
+ async function runIssuersRegister(opts, deps = {}) {
20854
+ let selfSignupPolicies;
20855
+ try {
20856
+ selfSignupPolicies = parseSelfSignupPolicies(opts.selfSignupPolicy);
20857
+ } catch (err) {
20858
+ process.stderr.write(` \u2716 ${formatError(err)}
20859
+ `);
20860
+ return 2;
20861
+ }
20862
+ return withAuth({ ...opts, provisioning: true, context: opts.context }, deps, async (auth) => {
20863
+ const client = auth.client;
20864
+ try {
20865
+ const issuer = await client.auth.registerIssuer({
20866
+ issuerId: opts.issuerId,
20867
+ issuer: opts.issuer,
20868
+ jwksUri: opts.jwksUri,
20869
+ audience: opts.audience,
20870
+ contextId: opts.context,
20871
+ subClaim: opts.subClaim,
20872
+ emailClaim: opts.emailClaim,
20873
+ userinfoUri: opts.userinfoUri,
20874
+ selfSignupPolicies
20875
+ });
20876
+ if (opts.json) {
20877
+ process.stdout.write(`${formatJson(issuer)}
20878
+ `);
20879
+ return 0;
20880
+ }
20881
+ const verb = issuer.created === false ? "already registered \u2014 returned unchanged" : "registered";
20882
+ process.stdout.write(`
20883
+ \u2714 issuer '${opts.issuerId}' ${verb}
20884
+ ${renderIssuer(issuer)}
20885
+ `);
20886
+ return 0;
20887
+ } catch (err) {
20888
+ process.stderr.write(formatCliError(err, { action: "issuers register failed" }));
20889
+ return 1;
20890
+ }
20891
+ });
20892
+ }
20893
+ async function runIssuersUpdate(opts, deps = {}) {
20894
+ let selfSignupPolicies;
20895
+ try {
20896
+ selfSignupPolicies = parseSelfSignupPolicies(opts.selfSignupPolicy);
20897
+ } catch (err) {
20898
+ process.stderr.write(` \u2716 ${formatError(err)}
20899
+ `);
20900
+ return 2;
20901
+ }
20902
+ if (opts.clearSelfSignupPolicies && selfSignupPolicies && selfSignupPolicies.length > 0) {
20903
+ process.stderr.write(
20904
+ " \u2716 --clear-self-signup-policies and --self-signup-policy are mutually exclusive.\n"
20905
+ );
20906
+ return 2;
20907
+ }
20908
+ return withAuth({ ...opts, provisioning: true, context: opts.context }, deps, async (auth) => {
20909
+ const client = auth.client;
20910
+ try {
20911
+ const issuer = await client.auth.updateIssuer({
20912
+ issuerId: opts.issuerId,
20913
+ subClaim: opts.subClaim,
20914
+ emailClaim: opts.emailClaim,
20915
+ userinfoUri: opts.userinfoUri,
20916
+ status: opts.status,
20917
+ selfSignupPolicies: opts.clearSelfSignupPolicies ? [] : selfSignupPolicies
20918
+ });
20919
+ if (opts.json) {
20920
+ process.stdout.write(`${formatJson(issuer)}
20921
+ `);
20922
+ return 0;
20923
+ }
20924
+ process.stdout.write(`
20925
+ \u2714 issuer '${opts.issuerId}' updated
20926
+ ${renderIssuer(issuer)}
20927
+ `);
20928
+ return 0;
20929
+ } catch (err) {
20930
+ process.stderr.write(formatCliError(err, { action: "issuers update failed" }));
20931
+ return 1;
20932
+ }
20933
+ });
20934
+ }
20935
+ async function runIssuersDelete(opts, deps = {}) {
20936
+ const { issuerId, context } = opts;
20937
+ if (!opts.force) {
20938
+ const isTty = deps.isTty ?? Boolean(process.stdin.isTTY);
20939
+ if (!isTty) {
20940
+ process.stderr.write(
20941
+ ` \u2716 issuers delete is destructive and needs an interactive confirmation.
20942
+ Re-run with --force to delete '${issuerId}' without a prompt.
20943
+ `
20944
+ );
20945
+ return 2;
20946
+ }
20947
+ const promptCtx = deps.createPrompt ? deps.createPrompt() : (() => {
20948
+ const rl = readline4.createInterface({ input: process.stdin, output: process.stdout });
20949
+ return { prompt: (q) => rl.question(q), close: () => rl.close() };
20950
+ })();
20951
+ try {
20952
+ process.stdout.write(
20953
+ `
20954
+ \u26A0 Deregistering issuer '${issuerId}' from context '${context}' removes it as a token-exchange
20955
+ trust anchor. The platform refuses this while any user was ever created or matched
20956
+ through it \u2014 deactivate those users or register a replacement issuer first if you
20957
+ intend to cut off access.
20958
+
20959
+ `
20960
+ );
20961
+ const typed = await promptCtx.prompt(` Type the issuer id ('${issuerId}') to confirm: `);
20962
+ if (typed.trim() !== issuerId) {
20963
+ process.stderr.write(` \u2716 Confirmation did not match \u2014 nothing deleted.
20964
+ `);
20965
+ return 1;
20966
+ }
20967
+ } finally {
20968
+ promptCtx.close();
20969
+ }
20970
+ }
20971
+ return withAuth({ ...opts, provisioning: true, context }, deps, async (auth) => {
20972
+ const client = auth.client;
20973
+ try {
20974
+ await client.auth.deleteIssuer({ issuerId });
20975
+ if (opts.json) {
20976
+ process.stdout.write(`${formatJson({ issuerId, deleted: true })}
20977
+ `);
20978
+ return 0;
20979
+ }
20980
+ process.stdout.write(`
20981
+ \u2714 issuer '${issuerId}' deregistered from '${context}'
20982
+ `);
20983
+ return 0;
20984
+ } catch (err) {
20985
+ process.stderr.write(formatCliError(err, { action: "issuers delete failed" }));
20986
+ return 1;
20987
+ }
20988
+ });
20989
+ }
20990
+
20991
+ // src/commands/namespace.ts
20992
+ function renderNamespace(n) {
20993
+ return formatKeyValues([
20994
+ ["namespace", n.namespace],
20995
+ ["contextId", n.contextId ?? "(tenant-wide)"],
20996
+ ["entityBacked", n.entityBacked === void 0 ? void 0 : String(n.entityBacked)],
20997
+ ["reserved", n.reserved === void 0 ? void 0 : String(n.reserved)],
20998
+ ["specificityRank", n.specificityRank === void 0 ? void 0 : String(n.specificityRank)],
20999
+ ["defaultSchemaId", n.defaultSchemaId],
21000
+ ["createdAt", n.createdAt]
21001
+ ]);
21002
+ }
21003
+ async function runNamespaceList(opts, deps = {}) {
21004
+ return withAuth({ ...opts, context: opts.context }, deps, async (auth) => {
21005
+ const client = auth.client;
21006
+ try {
21007
+ const rows = await drainPages(
21008
+ (startFrom) => client.identity.listNamespaces({
21009
+ limit: PAGE_LIMIT,
21010
+ ...opts.context ? { contextId: opts.context } : {},
21011
+ ...startFrom ? { startFrom } : {}
21012
+ })
21013
+ );
21014
+ if (opts.json) {
21015
+ process.stdout.write(`${formatJson(rows)}
21016
+ `);
21017
+ return 0;
21018
+ }
21019
+ process.stdout.write(
21020
+ "\n" + formatTable(rows, [
21021
+ { header: "namespace", get: (n) => n.namespace },
21022
+ { header: "contextId", get: (n) => n.contextId ?? "(tenant-wide)" },
21023
+ {
21024
+ header: "entityBacked",
21025
+ get: (n) => n.entityBacked === void 0 ? void 0 : String(n.entityBacked)
21026
+ },
21027
+ {
21028
+ header: "specificityRank",
21029
+ get: (n) => n.specificityRank === void 0 ? void 0 : String(n.specificityRank)
21030
+ }
21031
+ ]) + "\n"
21032
+ );
21033
+ return 0;
21034
+ } catch (err) {
21035
+ process.stderr.write(formatCliError(err, { action: "namespace list failed" }));
21036
+ return 1;
21037
+ }
21038
+ });
21039
+ }
21040
+ async function runNamespaceGet(opts, deps = {}) {
21041
+ return withAuth({ ...opts, context: opts.context }, deps, async (auth) => {
21042
+ const client = auth.client;
21043
+ try {
21044
+ const ns = await client.identity.getNamespace({
21045
+ namespace: opts.namespace,
21046
+ ...opts.context ? { contextId: opts.context } : {}
21047
+ });
21048
+ if (opts.json) {
21049
+ process.stdout.write(`${formatJson(ns)}
21050
+ `);
21051
+ return 0;
21052
+ }
21053
+ process.stdout.write(`
21054
+ ${renderNamespace(ns)}
21055
+ `);
21056
+ return 0;
21057
+ } catch (err) {
21058
+ process.stderr.write(formatCliError(err, { action: "namespace get failed" }));
21059
+ return 1;
21060
+ }
21061
+ });
21062
+ }
21063
+ async function runNamespaceRegister(opts, deps = {}) {
21064
+ return withAuth({ ...opts, provisioning: true, context: opts.context }, deps, async (auth) => {
21065
+ const client = auth.client;
21066
+ try {
21067
+ const ns = await client.identity.registerNamespace({
21068
+ contextId: opts.context,
21069
+ body: {
21070
+ namespace: opts.namespace,
21071
+ specificityRank: opts.specificityRank,
21072
+ entityBacked: opts.entityBacked,
21073
+ defaultSchemaId: opts.defaultSchemaId
21074
+ }
21075
+ });
21076
+ if (opts.json) {
21077
+ process.stdout.write(`${formatJson(ns)}
21078
+ `);
21079
+ return 0;
21080
+ }
21081
+ process.stdout.write(`
21082
+ \u2714 namespace '${opts.namespace}' registered
21083
+ ${renderNamespace(ns)}
21084
+ `);
21085
+ return 0;
21086
+ } catch (err) {
21087
+ process.stderr.write(formatCliError(err, { action: "namespace register failed" }));
21088
+ return 1;
21089
+ }
21090
+ });
21091
+ }
21092
+
21093
+ // src/commands/assume.ts
21094
+ function parseAssumeScopes(raw) {
21095
+ const out = {};
21096
+ for (const entry of raw) {
21097
+ const sep = entry.indexOf(":");
21098
+ if (sep <= 0 || sep === entry.length - 1) {
21099
+ throw new Error(`--scope must be '<namespace>:<value>' (e.g. 'org:orgB'); got '${entry}'.`);
21100
+ }
21101
+ const ns = entry.slice(0, sep);
21102
+ const value = entry.slice(sep + 1);
21103
+ const valueError = valueGrammarError(value);
21104
+ if (valueError) throw new Error(`--scope value ${valueError}`);
21105
+ const key = `scope:${ns}`;
21106
+ if (Object.prototype.hasOwnProperty.call(out, key)) {
21107
+ throw new Error(`--scope names namespace '${ns}' more than once \u2014 pass one value per namespace.`);
21108
+ }
21109
+ out[key] = value;
21110
+ }
21111
+ return out;
21112
+ }
21113
+ function renderAssumeToken(t) {
21114
+ return formatKeyValues([
21115
+ ["access_token", t.access_token],
21116
+ ["token_type", t.token_type],
21117
+ ["expires_in", t.expires_in === void 0 ? void 0 : `${t.expires_in}s`]
21118
+ ]);
21119
+ }
21120
+ async function runAssume(opts, deps = {}) {
21121
+ if (!opts.scope || opts.scope.length === 0) {
21122
+ process.stderr.write(" \u2716 --scope must be given at least once (e.g. --scope org:orgB).\n");
21123
+ return 2;
21124
+ }
21125
+ let scopes;
21126
+ try {
21127
+ scopes = parseAssumeScopes(opts.scope);
21128
+ } catch (err) {
21129
+ process.stderr.write(` \u2716 ${formatError(err)}
21130
+ `);
21131
+ return 2;
21132
+ }
21133
+ return withAuth({ ...opts, context: opts.context }, deps, async (auth) => {
21134
+ const client = auth.client;
21135
+ try {
21136
+ const result = await client.auth.assumeToken(scopes);
21137
+ if (opts.json) {
21138
+ process.stdout.write(`${formatJson(result)}
21139
+ `);
21140
+ return 0;
21141
+ }
21142
+ process.stdout.write(
21143
+ `
21144
+ \u2714 assumed ${Object.keys(scopes).join(", ")}
21145
+ ${renderAssumeToken(result)}
21146
+ `
21147
+ );
21148
+ return 0;
21149
+ } catch (err) {
21150
+ process.stderr.write(formatCliError(err, { action: "assume failed" }));
21151
+ return 1;
21152
+ }
21153
+ });
21154
+ }
21155
+
19908
21156
  // src/commands/login.ts
19909
21157
  import { createServer } from "http";
19910
21158
  import open from "open";
@@ -20126,7 +21374,7 @@ async function runAssertions(client, bootstrapClient, deps, blueprint, tenantId,
20126
21374
  const assertions = [];
20127
21375
  try {
20128
21376
  const ping = await deps.pingWithKey(result.rawKey);
20129
- const wantActions = blueprint.accessProfile.allowedActions;
21377
+ const wantActions = effectiveAccessProfileActions(blueprint);
20130
21378
  const got = ping.allowedActions ?? [];
20131
21379
  const missing = wantActions.filter((a) => !got.includes(a));
20132
21380
  const ok = ping.principalType === "scoped_key" && ping.tenantId === tenantId && missing.length === 0;
@@ -20360,15 +21608,22 @@ async function runBlueprintHarness(client, input, deps) {
20360
21608
  const readExistingKey = async () => void 0;
20361
21609
  const persistMintedKey = async () => {
20362
21610
  };
21611
+ const provisioned = await provisionContext(client, {
21612
+ blueprint: input.blueprint,
21613
+ remintBootstrapForContext,
21614
+ readExistingKey,
21615
+ log
21616
+ });
20363
21617
  const blueprint = await resolveApplyTimeIdentities(
20364
21618
  input.blueprint,
21619
+ provisioned,
20365
21620
  client,
20366
21621
  log
20367
21622
  );
20368
21623
  const remint = remintForContext ?? (async () => client);
20369
21624
  const remintBootstrap = remintBootstrapForContext ?? (async () => client);
20370
21625
  const hasIssuers = (blueprint.issuers ?? []).length > 0;
20371
- const hasNamespaces = (blueprint.namespaces ?? []).length > 0;
21626
+ const hasNamespaces = provisioned.namespaceActions.length > 0;
20372
21627
  let contextSnapshot;
20373
21628
  if (remintForContext) {
20374
21629
  try {
@@ -20412,6 +21667,7 @@ async function runBlueprintHarness(client, input, deps) {
20412
21667
  readExistingKey,
20413
21668
  persistMintedKey,
20414
21669
  log,
21670
+ provisioned,
20415
21671
  remintForContext,
20416
21672
  remintBootstrapForContext
20417
21673
  });
@@ -20630,12 +21886,13 @@ async function issueOrRotateKey(client, tenantId, context, userId, keyName) {
20630
21886
  return { key: rotated, revokedKeyId: existing?.keyId };
20631
21887
  }
20632
21888
  async function runJoin(opts, deps = {}) {
20633
- const hasRole = Boolean(opts.role);
21889
+ const hasRole = Boolean(opts.role?.length);
20634
21890
  const hasActions = Boolean(opts.actions);
20635
21891
  if (hasRole === hasActions) {
20636
- process.stderr.write(" \u2716 Provide exactly one of --role <roleId> or --actions <csv>.\n");
21892
+ process.stderr.write(" \u2716 Provide exactly one of --role <roleId> (repeatable) or --actions <csv>.\n");
20637
21893
  return 2;
20638
21894
  }
21895
+ const roleDisplay = opts.role?.join(", ");
20639
21896
  if (hasActions && splitCsv(opts.actions).length === 0) {
20640
21897
  process.stderr.write(" \u2716 --actions needs at least one action (an empty/blank list grants nothing).\n");
20641
21898
  return 2;
@@ -20683,7 +21940,10 @@ async function runJoin(opts, deps = {}) {
20683
21940
  upsert: true,
20684
21941
  body: {
20685
21942
  principalId,
20686
- roleId: opts.role,
21943
+ // Always send the plural `roleIds`, even for a single
21944
+ // `--role`: a strict superset of the deprecated singular `roleId`,
21945
+ // read uniformly by the platform regardless of how many compose.
21946
+ roleIds: opts.role,
20687
21947
  scopes,
20688
21948
  identityOverrides
20689
21949
  }
@@ -20702,7 +21962,7 @@ async function runJoin(opts, deps = {}) {
20702
21962
  alias,
20703
21963
  context,
20704
21964
  principalId,
20705
- role: opts.role,
21965
+ role: roleDisplay,
20706
21966
  keyId: existingEntry.keyId,
20707
21967
  reused: true,
20708
21968
  tenant,
@@ -20723,7 +21983,7 @@ async function runJoin(opts, deps = {}) {
20723
21983
  const willActivate = await determineActivateDecision(keyring, { tenant, explicitActivate: opts.activate });
20724
21984
  const { entry } = await storeMintedKey(
20725
21985
  keyring,
20726
- { alias, context, principalId, role: opts.role, keyId: key.keyId, env: auth.env },
21986
+ { alias, context, principalId, role: roleDisplay, keyId: key.keyId, env: auth.env },
20727
21987
  key.rawKey,
20728
21988
  { activate: willActivate, revokedKeyId }
20729
21989
  );
@@ -20731,7 +21991,7 @@ async function runJoin(opts, deps = {}) {
20731
21991
  alias,
20732
21992
  context,
20733
21993
  principalId,
20734
- role: opts.role,
21994
+ role: roleDisplay,
20735
21995
  keyId: entry.keyId,
20736
21996
  reused: false,
20737
21997
  tenant,
@@ -20770,9 +22030,9 @@ function emitJoined(opts) {
20770
22030
 
20771
22031
  // src/build-info.ts
20772
22032
  var BUILD_INFO = {
20773
- cli: true ? "0.16.0" : "dev",
20774
- sdk: true ? "0.40.0" : "dev",
20775
- blueprints: true ? "0.13.0" : "dev"
22033
+ cli: true ? "0.20.0" : "dev",
22034
+ sdk: true ? "0.42.0" : "dev",
22035
+ blueprints: true ? "0.17.0" : "dev"
20776
22036
  };
20777
22037
  function formatBuildInfo() {
20778
22038
  return `vectros ${BUILD_INFO.cli} (sdk ${BUILD_INFO.sdk}, blueprints ${BUILD_INFO.blueprints})`;
@@ -20852,12 +22112,25 @@ function buildProgram(deps = {}) {
20852
22112
  registerAccessCommands(program, deps);
20853
22113
  registerRoleCommands(program, deps);
20854
22114
  registerContextCommands(program, deps);
22115
+ registerIssuersCommands(program, deps);
22116
+ registerNamespaceCommands(program, deps);
22117
+ registerAssumeCommand(program, deps);
20855
22118
  registerJoinCommand(program, deps);
20856
22119
  registerKeyringCommands(program);
20857
22120
  return program;
20858
22121
  }
22122
+ function registerAssumeCommand(program, deps) {
22123
+ const assume = program.command("assume").description("Re-mint your own verb token with one or more identity.<namespace> values switched (needs a role's assumable grant).").requiredOption("--context <c>", "app context to operate in").option("--scope <ns:value>", "namespace value to assume, repeatable (e.g. org:orgB)", collect, []);
22124
+ addCommonAuthOptions(assume).action(
22125
+ async (flags) => {
22126
+ process.exit(
22127
+ await runAssume({ ...toCommonOptions(flags), context: flags.context, scope: flags.scope ?? [] }, deps)
22128
+ );
22129
+ }
22130
+ );
22131
+ }
20859
22132
  function registerJoinCommand(program, deps) {
20860
- const join3 = program.command("join").description("Enroll a principal in a context (grant + key) and keep the key in the keyring.").argument("<context>", "the app context to join").option("--role <roleId>", "reference a Role to grant (XOR --actions)").option("--actions <csv>", "inline single-clause actions to grant, comma-separated (XOR --role)").option("--as <principal>", "who to enroll: 'me' (default) or an explicit 'usr_<id>'", "me").option("--scope <ns:value>", "identity override: a scope the member acts under (repeatable, e.g. org:<id>)", collect, []).option("--alias <name>", "keyring entry name (defaults to the context id)").option("--tenant <tenant>", "target tenant: 'test' or 'live' (default live)").option(
22133
+ const join3 = program.command("join").description("Enroll a principal in a context (grant + key) and keep the key in the keyring.").argument("<context>", "the app context to join").option("--role <roleId>", "reference a Role to grant (repeatable \u2014 2+ compose additively; XOR --actions)", collect, []).option("--actions <csv>", "inline single-clause actions to grant, comma-separated (XOR --role)").option("--as <principal>", "who to enroll: 'me' (default) or an explicit 'usr_<id>'", "me").option("--scope <ns:value>", "identity override: a scope the member acts under (repeatable, e.g. org:<id>)", collect, []).option("--alias <name>", "keyring entry name (defaults to the context id)").option("--tenant <tenant>", "target tenant: 'test' or 'live' (default live)").option(
20861
22134
  "--activate",
20862
22135
  "force the freshly-stored key to become your active identity, even over a currently-active LIVE credential (the one case --tenant test does NOT do automatically)"
20863
22136
  ).option("--no-activate", "store the key without activating it, regardless of tenant");
@@ -20991,11 +22264,145 @@ function registerContextCommands(program, deps) {
20991
22264
  }
20992
22265
  );
20993
22266
  }
22267
+ function registerIssuersCommands(program, deps) {
22268
+ const issuers = program.command("issuers").description("Manage registered third-party IdP issuers (token-exchange trust anchors).");
22269
+ const list = issuers.command("list").description("List issuers registered in one app context.").requiredOption("--context <c>", "app context the issuer(s) are registered under").option("--tenant <tenant>", "tenant to read: 'test' or 'live' (default live)");
22270
+ addCommonAuthOptions(list).action(
22271
+ async (flags) => {
22272
+ const tenant = parseTenantFlag(flags.tenant);
22273
+ process.exit(
22274
+ await runIssuersList({ ...toCommonOptions(flags), context: flags.context, tenant }, deps)
22275
+ );
22276
+ }
22277
+ );
22278
+ const get = issuers.command("get").description("Show one registered issuer.").argument("<issuerId>").requiredOption("--context <c>", "app context the issuer is registered under").option("--tenant <tenant>", "tenant to read: 'test' or 'live' (default live)");
22279
+ addCommonAuthOptions(get).action(
22280
+ async (issuerId, flags) => {
22281
+ const tenant = parseTenantFlag(flags.tenant);
22282
+ process.exit(
22283
+ await runIssuersGet({ ...toCommonOptions(flags), context: flags.context, issuerId, tenant }, deps)
22284
+ );
22285
+ }
22286
+ );
22287
+ const register = issuers.command("register").description("Register (idempotent by issuerId) a trusted third-party IdP issuer.").requiredOption("--context <c>", "app context an exchanged token targets").requiredOption("--issuer-id <id>", "short slug for this issuer (3-31 chars; lowercase letter + lowercase/digit/dash)").requiredOption("--issuer <url>", "the IdP's iss claim value").requiredOption("--jwks-uri <url>", "the IdP's remote JWKS endpoint").requiredOption("--audience <aud>", "the required aud claim value (globally unique with --issuer)").option("--sub-claim <claim>", "claim carrying the subject identifier (default 'sub')").option("--email-claim <claim>", "claim carrying the subject's email (default 'email')").option("--userinfo-uri <url>", "OIDC userinfo endpoint, used as an email-resolution fallback").option(
22288
+ "--self-signup-policy <type:roleId>",
22289
+ "opt-in self-service signup entry, repeatable (e.g. default:member) \u2014 no entry may target an elevated role",
22290
+ collect,
22291
+ []
22292
+ ).option("--tenant <tenant>", "tenant the context lives in: 'test' or 'live' (default live)");
22293
+ addCommonAuthOptions(register).action(
22294
+ async (flags) => {
22295
+ const tenant = parseTenantFlag(flags.tenant);
22296
+ process.exit(
22297
+ await runIssuersRegister(
22298
+ {
22299
+ ...toCommonOptions(flags),
22300
+ context: flags.context,
22301
+ issuerId: flags.issuerId,
22302
+ issuer: flags.issuer,
22303
+ jwksUri: flags.jwksUri,
22304
+ audience: flags.audience,
22305
+ subClaim: flags.subClaim,
22306
+ emailClaim: flags.emailClaim,
22307
+ userinfoUri: flags.userinfoUri,
22308
+ selfSignupPolicy: flags.selfSignupPolicy,
22309
+ tenant
22310
+ },
22311
+ deps
22312
+ )
22313
+ );
22314
+ }
22315
+ );
22316
+ const update = issuers.command("update").description("Patch the safe, mutable fields of a registered issuer (trust-anchor fields are immutable).").argument("<issuerId>").requiredOption("--context <c>", "app context the issuer is registered under").option("--sub-claim <claim>", "update the subject-identifier claim").option("--email-claim <claim>", "update the subject-email claim").option("--userinfo-uri <url>", "update the OIDC userinfo fallback endpoint").option("--status <status>", "'active' or 'suspended' \u2014 a suspended issuer's tokens are rejected at exchange time").option(
22317
+ "--self-signup-policy <type:roleId>",
22318
+ "replace self-signup policies, repeatable (e.g. default:member) \u2014 omit entirely to leave unchanged",
22319
+ collect,
22320
+ []
22321
+ ).option("--clear-self-signup-policies", "disable self-signup entirely (sends an empty list; XOR --self-signup-policy)").option("--tenant <tenant>", "tenant the context lives in: 'test' or 'live' (default live)");
22322
+ addCommonAuthOptions(update).action(
22323
+ async (issuerId, flags) => {
22324
+ const tenant = parseTenantFlag(flags.tenant);
22325
+ process.exit(
22326
+ await runIssuersUpdate(
22327
+ {
22328
+ ...toCommonOptions(flags),
22329
+ context: flags.context,
22330
+ issuerId,
22331
+ subClaim: flags.subClaim,
22332
+ emailClaim: flags.emailClaim,
22333
+ userinfoUri: flags.userinfoUri,
22334
+ status: flags.status,
22335
+ selfSignupPolicy: flags.selfSignupPolicy,
22336
+ clearSelfSignupPolicies: Boolean(flags.clearSelfSignupPolicies),
22337
+ tenant
22338
+ },
22339
+ deps
22340
+ )
22341
+ );
22342
+ }
22343
+ );
22344
+ const del = issuers.command("delete").description("Deregister a trusted third-party IdP issuer (refused while any user is bound through it).").argument("<issuerId>").requiredOption("--context <c>", "app context the issuer is registered under").option("--force", "skip the interactive confirmation (required when stdin is not a TTY)").option("--tenant <tenant>", "tenant the context lives in: 'test' or 'live' (default live)");
22345
+ addCommonAuthOptions(del).action(
22346
+ async (issuerId, flags) => {
22347
+ const tenant = parseTenantFlag(flags.tenant);
22348
+ process.exit(
22349
+ await runIssuersDelete(
22350
+ { ...toCommonOptions(flags), context: flags.context, issuerId, force: Boolean(flags.force), tenant },
22351
+ deps
22352
+ )
22353
+ );
22354
+ }
22355
+ );
22356
+ }
22357
+ function registerNamespaceCommands(program, deps) {
22358
+ const namespace = program.command("namespace").description("Manage scope-namespace registrations (whether scope:<namespace> values resolve to identity entities).");
22359
+ const register = namespace.command("register").description("Register a new scope namespace (NOT idempotent \u2014 a repeat name 400s).").argument("<name>", "namespace name: 2-32 chars, a lowercase letter first, then lowercase/digit/dash/underscore").requiredOption("--context <c>", "app context to own this registration").requiredOption("--specificity-rank <n>", "position in the account specificity order (integer 0-1000000, unique)").option("--entity-backed", "values must resolve to an identity entity (create via `identity create --type <name>`)").option("--default-schema-id <id>", "default record schema bound to entities in this namespace");
22360
+ addCommonAuthOptions(register).action(
22361
+ async (name, flags) => {
22362
+ const specificityRank = Number(flags.specificityRank);
22363
+ if (!Number.isInteger(specificityRank) || specificityRank < 0 || specificityRank > 1e6) {
22364
+ process.stderr.write(
22365
+ ` \u2716 --specificity-rank must be an integer between 0 and 1000000 (got '${flags.specificityRank}').
22366
+ `
22367
+ );
22368
+ process.exit(2);
22369
+ }
22370
+ process.exit(
22371
+ await runNamespaceRegister(
22372
+ {
22373
+ ...toCommonOptions(flags),
22374
+ namespace: name,
22375
+ context: flags.context,
22376
+ specificityRank,
22377
+ entityBacked: Boolean(flags.entityBacked),
22378
+ defaultSchemaId: flags.defaultSchemaId
22379
+ },
22380
+ deps
22381
+ )
22382
+ );
22383
+ }
22384
+ );
22385
+ const list = namespace.command("list").description("List scope-namespace registrations \u2014 tenant-wide, or one context\u2019s own.").option("--context <c>", "list one app context\u2019s own registrations instead of the tenant-wide set");
22386
+ addCommonAuthOptions(list).action(async (flags) => {
22387
+ process.exit(await runNamespaceList({ ...toCommonOptions(flags), context: flags.context }, deps));
22388
+ });
22389
+ const get = namespace.command("get").description("Show one namespace registration \u2014 tenant-wide, or one context\u2019s own.").argument("<name>").option("--context <c>", "resolve for one app context\u2019s own registration instead of the tenant-wide one");
22390
+ addCommonAuthOptions(get).action(
22391
+ async (name, flags) => {
22392
+ process.exit(
22393
+ await runNamespaceGet({ ...toCommonOptions(flags), namespace: name, context: flags.context }, deps)
22394
+ );
22395
+ }
22396
+ );
22397
+ }
20994
22398
  function registerRoleCommands(program, deps) {
20995
22399
  const role = program.command("role").description("Manage context-scoped roles (reusable scope rules; bind via `access grant --role`).");
20996
22400
  const create = role.command("create").description("Create (idempotent by roleId) a single-clause role from --actions.").requiredOption("--context <c>", "app context").requiredOption("--role-id <id>", "role id (3-31 chars; lowercase letter + lowercase/digit/dash)").requiredOption("--name <name>", "human-readable name").requiredOption("--actions <csv>", "allowed actions, comma-separated (e.g. records:cru,search:r)").option(
20997
22401
  "--data-scope <json>",
20998
22402
  `confine the clause to owned data \u2014 JSON map of dimension \u2192 allowed values, e.g. '{"userId":["\${{ self.userId }}"]}' or '{"scope:org":["<orgId>", null]}'`
22403
+ ).option(
22404
+ "--assumable <json>",
22405
+ `role-level \`vectros assume\` grant \u2014 JSON map of scope:<namespace> \u2192 allowed values (e.g. '{"scope:org":["orgB"]}'); independent of --data-scope, never data_scope's grammar (userId cannot be named)`
20999
22406
  ).option("--description <text>", "free-text description");
21000
22407
  addCommonAuthOptions(create).action(
21001
22408
  async (flags) => {
@@ -21007,6 +22414,7 @@ function registerRoleCommands(program, deps) {
21007
22414
  name: flags.name,
21008
22415
  actions: flags.actions,
21009
22416
  dataScope: flags.dataScope,
22417
+ assumable: flags.assumable,
21010
22418
  description: flags.description
21011
22419
  }, deps)
21012
22420
  );
@@ -21027,7 +22435,7 @@ function registerRoleCommands(program, deps) {
21027
22435
  }
21028
22436
  function registerAccessCommands(program, deps) {
21029
22437
  const access2 = program.command("access").description("Manage access bindings: principal \u2194 context \u2194 role/scopes.");
21030
- const grant = access2.command("grant").description("Bind a principal to a context via a role or inline actions.").requiredOption("--principal <p>", "principal: 'usr_<userId>', 'key_<keyId>', or 'me' (your own user)").requiredOption("--context <c>", "app context").option("--role <roleId>", "reference a Role (XOR --actions)").option("--actions <csv>", "inline single-clause actions, comma-separated (XOR --role)").option("--scope <ns:value>", "identity override: a scope the principal acts under (repeatable, e.g. org:<id>)", collect, []).option("--identity-overrides <json>", "general identity overrides as a JSON object (merged over --scope)");
22438
+ const grant = access2.command("grant").description("Bind a principal to a context via a role or inline actions.").requiredOption("--principal <p>", "principal: 'usr_<userId>', 'key_<keyId>', or 'me' (your own user)").requiredOption("--context <c>", "app context").option("--role <roleId>", "reference a Role (repeatable \u2014 2+ compose additively; XOR --actions)", collect, []).option("--actions <csv>", "inline single-clause actions, comma-separated (XOR --role)").option("--scope <ns:value>", "identity override: a scope the principal acts under (repeatable, e.g. org:<id>)", collect, []).option("--identity-overrides <json>", "general identity overrides as a JSON object (merged over --scope)");
21031
22439
  addCommonAuthOptions(grant).action(
21032
22440
  async (flags) => {
21033
22441
  process.exit(
@@ -21100,6 +22508,20 @@ function registerIdentityCommands(program, deps) {
21100
22508
  );
21101
22509
  }
21102
22510
  );
22511
+ const listAll = identity.command("list-all").description(
22512
+ "List every user in the tenant (owner login required), bypassing the app-context confinement 'identity list' is subject to \u2014 for recovering a user created but never granted access anywhere, which 'identity list' can never show."
22513
+ ).option("--external-id <id>", "filter to a stable id").option("--limit <n>", "page size (1\u2013100)", (v) => Number.parseInt(v, 10)).option("--tenant <tenant>", "tenant to list: 'test' or 'live' (default live)");
22514
+ addCommonAuthOptions(listAll).action(
22515
+ async (flags) => {
22516
+ const tenant = parseTenantFlag(flags.tenant);
22517
+ process.exit(
22518
+ await runIdentityListAll(
22519
+ { ...toCommonOptions(flags), externalId: flags.externalId, limit: flags.limit, tenant },
22520
+ deps
22521
+ )
22522
+ );
22523
+ }
22524
+ );
21103
22525
  const get = identity.command("get").description("Show one principal by its Vectros id.").addOption(dimensionOption()).requiredOption("--id <vectrosId>", "the Vectros-assigned id (from create/list)");
21104
22526
  addCommonAuthOptions(get).action(async (flags) => {
21105
22527
  process.exit(await runIdentityGet({ ...toCommonOptions(flags), type: flags.type, id: flags.id }, deps));
@@ -21108,6 +22530,25 @@ function registerIdentityCommands(program, deps) {
21108
22530
  addAuthOptions(del).action(async (flags) => {
21109
22531
  process.exit(await runIdentityDelete({ ...toCommonOptions(flags), type: flags.type, id: flags.id }, deps));
21110
22532
  });
22533
+ 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");
22534
+ addCommonAuthOptions(invite).action(
22535
+ async (flags) => {
22536
+ process.exit(
22537
+ await runIdentityInvite(
22538
+ {
22539
+ ...toCommonOptions(flags),
22540
+ context: flags.context,
22541
+ role: flags.role,
22542
+ email: flags.email,
22543
+ firstName: flags.firstName,
22544
+ acceptUrl: flags.acceptUrl,
22545
+ noSendEmail: flags.sendEmail === false
22546
+ },
22547
+ deps
22548
+ )
22549
+ );
22550
+ }
22551
+ );
21111
22552
  }
21112
22553
  function addKeyDestinationOptions(cmd) {
21113
22554
  return cmd.addOption(new Option("--format <fmt>", "how to emit the secret").choices(KEY_FORMATS).default("human")).option(