@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.js CHANGED
@@ -4103,7 +4103,7 @@ var AuthClient = class {
4103
4103
  return handleNonStatusCodeError(_response.error, _response.rawResponse, "GET", "/v1/admin/keys/scoped");
4104
4104
  }
4105
4105
  /**
4106
- * 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.
4106
+ * 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.
4107
4107
  *
4108
4108
  * @param {Vectros.CreateScopedKeyRequest} request
4109
4109
  * @param {AuthClient.RequestOptions} requestOptions - Request-specific configuration.
@@ -5599,7 +5599,7 @@ var AuthClient = class {
5599
5599
  return handleNonStatusCodeError(_response.error, _response.rawResponse, "GET", "/v1/usage");
5600
5600
  }
5601
5601
  /**
5602
- * 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.
5602
+ * 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.
5603
5603
  *
5604
5604
  * @param {Vectros.GetIssuerRequest} request
5605
5605
  * @param {AuthClient.RequestOptions} requestOptions - Request-specific configuration.
@@ -5656,6 +5656,73 @@ var AuthClient = class {
5656
5656
  }
5657
5657
  return handleNonStatusCodeError(_response.error, _response.rawResponse, "GET", "/v1/auth/issuers/{issuerId}");
5658
5658
  }
5659
+ /**
5660
+ * 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.
5661
+ *
5662
+ * @param {Vectros.IssuerUpdateRequest} request
5663
+ * @param {AuthClient.RequestOptions} requestOptions - Request-specific configuration.
5664
+ *
5665
+ * @throws {@link Vectros.BadRequestError}
5666
+ * @throws {@link Vectros.ForbiddenError}
5667
+ * @throws {@link Vectros.NotFoundError}
5668
+ * @throws {@link Vectros.TooManyRequestsError}
5669
+ *
5670
+ * @example
5671
+ * await client.auth.updateIssuer({
5672
+ * issuerId: "auth0-prod"
5673
+ * })
5674
+ */
5675
+ updateIssuer(request, requestOptions) {
5676
+ return HttpResponsePromise.fromPromise(this.__updateIssuer(request, requestOptions));
5677
+ }
5678
+ async __updateIssuer(request, requestOptions) {
5679
+ const { issuerId, ..._body } = request;
5680
+ const _authRequest = await this._options.authProvider.getAuthRequest();
5681
+ const _headers = mergeHeaders(
5682
+ _authRequest.headers,
5683
+ this._options?.headers,
5684
+ requestOptions?.headers
5685
+ );
5686
+ const _response = await fetcher({
5687
+ url: url_exports.join(
5688
+ await Supplier.get(this._options.baseUrl) ?? await Supplier.get(this._options.environment),
5689
+ `v1/auth/issuers/${url_exports.encodePathParam(issuerId)}`
5690
+ ),
5691
+ method: "PUT",
5692
+ headers: _headers,
5693
+ contentType: "application/json",
5694
+ queryString: url_exports.queryBuilder().mergeAdditional(requestOptions?.queryParams).build(),
5695
+ requestType: "json",
5696
+ body: _body,
5697
+ timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1e3,
5698
+ maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries,
5699
+ abortSignal: requestOptions?.abortSignal,
5700
+ fetchFn: this._options?.fetch,
5701
+ logging: this._options.logging
5702
+ });
5703
+ if (_response.ok) {
5704
+ return { data: _response.body, rawResponse: _response.rawResponse };
5705
+ }
5706
+ if (_response.error.reason === "status-code") {
5707
+ switch (_response.error.statusCode) {
5708
+ case 400:
5709
+ throw new BadRequestError(_response.error.body, _response.rawResponse);
5710
+ case 403:
5711
+ throw new ForbiddenError(_response.error.body, _response.rawResponse);
5712
+ case 404:
5713
+ throw new NotFoundError(_response.error.body, _response.rawResponse);
5714
+ case 429:
5715
+ throw new TooManyRequestsError(_response.error.body, _response.rawResponse);
5716
+ default:
5717
+ throw new VectrosError({
5718
+ statusCode: _response.error.statusCode,
5719
+ body: _response.error.body,
5720
+ rawResponse: _response.rawResponse
5721
+ });
5722
+ }
5723
+ }
5724
+ return handleNonStatusCodeError(_response.error, _response.rawResponse, "PUT", "/v1/auth/issuers/{issuerId}");
5725
+ }
5659
5726
  /**
5660
5727
  * 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.
5661
5728
  *
@@ -5726,7 +5793,7 @@ var AuthClient = class {
5726
5793
  );
5727
5794
  }
5728
5795
  /**
5729
- * 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.
5796
+ * 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.
5730
5797
  *
5731
5798
  * @param {Vectros.ListIssuersRequest} request
5732
5799
  * @param {AuthClient.RequestOptions} requestOptions - Request-specific configuration.
@@ -5783,7 +5850,7 @@ var AuthClient = class {
5783
5850
  return handleNonStatusCodeError(_response.error, _response.rawResponse, "GET", "/v1/auth/issuers");
5784
5851
  }
5785
5852
  /**
5786
- * 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`.
5853
+ * 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`.
5787
5854
  *
5788
5855
  * @param {Vectros.IssuerRequest} request
5789
5856
  * @param {AuthClient.RequestOptions} requestOptions - Request-specific configuration.
@@ -5968,6 +6035,7 @@ var AuthClient = class {
5968
6035
  *
5969
6036
  * @throws {@link Vectros.BadRequestError}
5970
6037
  * @throws {@link Vectros.ForbiddenError}
6038
+ * @throws {@link Vectros.NotFoundError}
5971
6039
  * @throws {@link Vectros.TooManyRequestsError}
5972
6040
  *
5973
6041
  * @example
@@ -6021,6 +6089,8 @@ var AuthClient = class {
6021
6089
  throw new BadRequestError(_response.error.body, _response.rawResponse);
6022
6090
  case 403:
6023
6091
  throw new ForbiddenError(_response.error.body, _response.rawResponse);
6092
+ case 404:
6093
+ throw new NotFoundError(_response.error.body, _response.rawResponse);
6024
6094
  case 429:
6025
6095
  throw new TooManyRequestsError(_response.error.body, _response.rawResponse);
6026
6096
  default:
@@ -6034,7 +6104,7 @@ var AuthClient = class {
6034
6104
  return handleNonStatusCodeError(_response.error, _response.rawResponse, "POST", "/v1/auth/token");
6035
6105
  }
6036
6106
  /**
6037
- * 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.
6107
+ * Invite a new member to one of your app contexts by email, OR grant an existing member access to an additional app context by inviting their same email again. Idempotent on the combination of context and email: re-inviting the same email into the SAME context rotates the token and resends the invitation rather than creating a duplicate — this requires the `users:r` and `users:u` scopes in addition to `users:c`, because resending rotates a credential on an existing invitation and invalidates any link already sent. Without them the collision returns 409 instead, with no invitation details and no change to the outstanding invitation. Inviting the SAME email into a DIFFERENT app context in this tenant, where that email already resolves to an existing member: if that member is active AND already has (or, once accepted, will have) a credential that works for the new context's own identity provider, this immediately grants them access to the new context (no email is sent — there is nothing to accept, `emailSent` is false) — this additionally requires the `users:r` scope (no `users:u`, since nothing is mutated), because the response names the existing member's userId, a fact about them your credential could not otherwise learn through this endpoint. If that active member's ONLY existing credential is for a DIFFERENT identity provider than the one the new context uses, a normal, independent invitation is created instead (its own new member id, a real token/accept link) — attaching them silently would leave no way for them to ever actually sign in to that context. If the existing member's original invitation is still pending, this attaches the new context's access to that same outstanding invitation and rotates its token (`users:r`+`users:u`, same as an ordinary resend — both the disclosure and the credential rotation apply here). A SUSPENDED member's email does not get new-context access this way — reactivate them explicitly first. Returns HTTP 201 in every one of those cases. Returns 409 if that email already belongs to an active or suspended member of THIS specific app context, already has a PENDING invitation for THIS specific app context, or resolves to an existing member elsewhere in the tenant and your token lacks the additional scope the grant/attach requires (`users:r`, or `users:r`+`users:u` for the still-pending case). An email that already has an identity in your OTHER tenant (test vs. live) is not a collision either — it creates an additional, independent membership in this tenant for that same identity. When `sendEmail` is false, the response includes the raw token and a ready-to-use accept link so you can deliver the invitation through your own email provider. Requires the `users:c` scope — or, for a credential scoped to a single app context, the `member-lifecycle` capability plus the matching `profiles:c`/`profiles:r`/`profiles:u` grant(s) in that context, as an equally sufficient alternative to every `users:c`/`users:r`/`users:u` requirement in this description.
6038
6108
  *
6039
6109
  * @param {Vectros.CreateInviteRequest} request
6040
6110
  * @param {AuthClient.RequestOptions} requestOptions - Request-specific configuration.
@@ -6105,7 +6175,7 @@ var AuthClient = class {
6105
6175
  return handleNonStatusCodeError(_response.error, _response.rawResponse, "POST", "/v1/users/invite");
6106
6176
  }
6107
6177
  /**
6108
- * Resend an outstanding invitation, identified by its email and app context. Rotates the invitation token and extends its expiry, then (when `sendEmail` is true) re-delivers the email. Rotating the token invalidates any previously issued link for this invitation, so only the newest link works. The invitee's pending permissions are left unchanged. Because this rotates a credential on an existing invitation, it requires the `users:c`, `users:r` and `users:u` scopes.
6178
+ * Resend an outstanding invitation, identified by its email and app context. Rotates the invitation token and extends its expiry, then (when `sendEmail` is true) re-delivers the email. Rotating the token invalidates any previously issued link for this invitation, so only the newest link works. The invitee's pending permissions are left unchanged. Because this rotates a credential on an existing invitation, it requires the `users:c`, `users:r` and `users:u` scopes — or, for a credential scoped to a single app context, the `member-lifecycle` capability plus the matching `profiles:c`/`profiles:r`/`profiles:u` grant(s) in that context.
6109
6179
  *
6110
6180
  * @param {Vectros.CreateInviteRequest} request
6111
6181
  * @param {AuthClient.RequestOptions} requestOptions - Request-specific configuration.
@@ -6172,6 +6242,83 @@ var AuthClient = class {
6172
6242
  }
6173
6243
  return handleNonStatusCodeError(_response.error, _response.rawResponse, "POST", "/v1/users/invite/resend");
6174
6244
  }
6245
+ /**
6246
+ * 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).
6247
+ *
6248
+ * **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.
6249
+ *
6250
+ * **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.
6251
+ *
6252
+ * **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.
6253
+ *
6254
+ * 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.
6255
+ *
6256
+ * @param {Record<string, unknown>} request
6257
+ * @param {AuthClient.RequestOptions} requestOptions - Request-specific configuration.
6258
+ *
6259
+ * @throws {@link Vectros.BadRequestError}
6260
+ * @throws {@link Vectros.UnauthorizedError}
6261
+ * @throws {@link Vectros.ForbiddenError}
6262
+ * @throws {@link Vectros.ConflictError}
6263
+ * @throws {@link Vectros.TooManyRequestsError}
6264
+ *
6265
+ * @example
6266
+ * await client.auth.assumeToken({
6267
+ * "scope:org": "orgB"
6268
+ * })
6269
+ */
6270
+ assumeToken(request, requestOptions) {
6271
+ return HttpResponsePromise.fromPromise(this.__assumeToken(request, requestOptions));
6272
+ }
6273
+ async __assumeToken(request, requestOptions) {
6274
+ const _authRequest = await this._options.authProvider.getAuthRequest();
6275
+ const _headers = mergeHeaders(
6276
+ _authRequest.headers,
6277
+ this._options?.headers,
6278
+ requestOptions?.headers
6279
+ );
6280
+ const _response = await fetcher({
6281
+ url: url_exports.join(
6282
+ await Supplier.get(this._options.baseUrl) ?? await Supplier.get(this._options.environment),
6283
+ "v1/auth/token/assume"
6284
+ ),
6285
+ method: "POST",
6286
+ headers: _headers,
6287
+ contentType: "application/json",
6288
+ queryString: url_exports.queryBuilder().mergeAdditional(requestOptions?.queryParams).build(),
6289
+ requestType: "json",
6290
+ body: request,
6291
+ timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1e3,
6292
+ maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries,
6293
+ abortSignal: requestOptions?.abortSignal,
6294
+ fetchFn: this._options?.fetch,
6295
+ logging: this._options.logging
6296
+ });
6297
+ if (_response.ok) {
6298
+ return { data: _response.body, rawResponse: _response.rawResponse };
6299
+ }
6300
+ if (_response.error.reason === "status-code") {
6301
+ switch (_response.error.statusCode) {
6302
+ case 400:
6303
+ throw new BadRequestError(_response.error.body, _response.rawResponse);
6304
+ case 401:
6305
+ throw new UnauthorizedError(_response.error.body, _response.rawResponse);
6306
+ case 403:
6307
+ throw new ForbiddenError(_response.error.body, _response.rawResponse);
6308
+ case 409:
6309
+ throw new ConflictError(_response.error.body, _response.rawResponse);
6310
+ case 429:
6311
+ throw new TooManyRequestsError(_response.error.body, _response.rawResponse);
6312
+ default:
6313
+ throw new VectrosError({
6314
+ statusCode: _response.error.statusCode,
6315
+ body: _response.error.body,
6316
+ rawResponse: _response.rawResponse
6317
+ });
6318
+ }
6319
+ }
6320
+ return handleNonStatusCodeError(_response.error, _response.rawResponse, "POST", "/v1/auth/token/assume");
6321
+ }
6175
6322
  /**
6176
6323
  * 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.
6177
6324
  *
@@ -6249,9 +6396,9 @@ var ComplianceClient = class {
6249
6396
  * @param {Vectros.ErasureRequest} request
6250
6397
  * @param {ComplianceClient.RequestOptions} requestOptions - Request-specific configuration.
6251
6398
  *
6399
+ * @throws {@link Vectros.BadRequestError}
6252
6400
  * @throws {@link Vectros.ForbiddenError}
6253
6401
  * @throws {@link Vectros.TooManyRequestsError}
6254
- * @throws {@link Vectros.NotImplementedError}
6255
6402
  *
6256
6403
  * @example
6257
6404
  * await client.compliance.createErasureRequest({
@@ -6290,12 +6437,12 @@ var ComplianceClient = class {
6290
6437
  }
6291
6438
  if (_response.error.reason === "status-code") {
6292
6439
  switch (_response.error.statusCode) {
6440
+ case 400:
6441
+ throw new BadRequestError(_response.error.body, _response.rawResponse);
6293
6442
  case 403:
6294
6443
  throw new ForbiddenError(_response.error.body, _response.rawResponse);
6295
6444
  case 429:
6296
6445
  throw new TooManyRequestsError(_response.error.body, _response.rawResponse);
6297
- case 501:
6298
- throw new NotImplementedError(_response.error.body, _response.rawResponse);
6299
6446
  default:
6300
6447
  throw new VectrosError({
6301
6448
  statusCode: _response.error.statusCode,
@@ -6314,7 +6461,6 @@ var ComplianceClient = class {
6314
6461
  *
6315
6462
  * @throws {@link Vectros.ForbiddenError}
6316
6463
  * @throws {@link Vectros.NotFoundError}
6317
- * @throws {@link Vectros.NotImplementedError}
6318
6464
  *
6319
6465
  * @example
6320
6466
  * await client.compliance.getErasureRequest({
@@ -6355,8 +6501,6 @@ var ComplianceClient = class {
6355
6501
  throw new ForbiddenError(_response.error.body, _response.rawResponse);
6356
6502
  case 404:
6357
6503
  throw new NotFoundError(_response.error.body, _response.rawResponse);
6358
- case 501:
6359
- throw new NotImplementedError(_response.error.body, _response.rawResponse);
6360
6504
  default:
6361
6505
  throw new VectrosError({
6362
6506
  statusCode: _response.error.statusCode,
@@ -6904,7 +7048,21 @@ var DocumentsClient = class {
6904
7048
  return HttpResponsePromise.fromPromise(this.__lookupDocuments(request, requestOptions));
6905
7049
  }
6906
7050
  async __lookupDocuments(request, requestOptions) {
6907
- const { type: type_, field, value, from: from_, to, prefix, startFrom, limit, order, userId, scope } = request;
7051
+ const {
7052
+ type: type_,
7053
+ field,
7054
+ value,
7055
+ from: from_,
7056
+ to,
7057
+ prefix,
7058
+ sortFrom,
7059
+ sortTo,
7060
+ startFrom,
7061
+ limit,
7062
+ order,
7063
+ userId,
7064
+ scope
7065
+ } = request;
6908
7066
  const _queryParams = {
6909
7067
  type: type_,
6910
7068
  field,
@@ -6912,6 +7070,8 @@ var DocumentsClient = class {
6912
7070
  from: from_,
6913
7071
  to,
6914
7072
  prefix,
7073
+ sortFrom,
7074
+ sortTo,
6915
7075
  startFrom,
6916
7076
  limit,
6917
7077
  order: order != null ? order : void 0,
@@ -8196,7 +8356,7 @@ var IdentityClient = class {
8196
8356
  );
8197
8357
  }
8198
8358
  /**
8199
- * Retrieves a single scope-namespace registration by name. The reserved built-ins `org` and `client` are always resolvable.
8359
+ * Retrieves a single scope-namespace registration by name.
8200
8360
  *
8201
8361
  * @param {Vectros.GetNamespaceRequest} request
8202
8362
  * @param {IdentityClient.RequestOptions} requestOptions - Request-specific configuration.
@@ -8389,7 +8549,7 @@ var IdentityClient = class {
8389
8549
  return handleNonStatusCodeError(_response.error, _response.rawResponse, "DELETE", "/v1/namespaces/{namespace}");
8390
8550
  }
8391
8551
  /**
8392
- * 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.
8552
+ * Returns the scope namespaces registered in your account. Returns a `{data, nextCursor}` envelope.
8393
8553
  *
8394
8554
  * @param {Vectros.ListNamespacesRequest} request
8395
8555
  * @param {IdentityClient.RequestOptions} requestOptions - Request-specific configuration.
@@ -8440,7 +8600,7 @@ var IdentityClient = class {
8440
8600
  return handleNonStatusCodeError(_response.error, _response.rawResponse, "GET", "/v1/namespaces");
8441
8601
  }
8442
8602
  /**
8443
- * 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.
8603
+ * 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.
8444
8604
  *
8445
8605
  * @param {Vectros.RegisterNamespaceRequest} request
8446
8606
  * @param {IdentityClient.RequestOptions} requestOptions - Request-specific configuration.
@@ -8510,7 +8670,7 @@ var IdentityClient = class {
8510
8670
  return handleNonStatusCodeError(_response.error, _response.rawResponse, "POST", "/v1/namespaces");
8511
8671
  }
8512
8672
  /**
8513
- * 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.
8673
+ * 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.
8514
8674
  *
8515
8675
  * @param {Vectros.ListUsersRequest} request
8516
8676
  * @param {IdentityClient.RequestOptions} requestOptions - Request-specific configuration.
@@ -8575,7 +8735,7 @@ var IdentityClient = class {
8575
8735
  return handleNonStatusCodeError(_response.error, _response.rawResponse, "GET", "/v1/users");
8576
8736
  }
8577
8737
  /**
8578
- * 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.
8738
+ * 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`.
8579
8739
  *
8580
8740
  * @param {Vectros.CreateUserRequest} request
8581
8741
  * @param {IdentityClient.RequestOptions} requestOptions - Request-specific configuration.
@@ -8699,7 +8859,7 @@ var IdentityClient = class {
8699
8859
  return handleNonStatusCodeError(_response.error, _response.rawResponse, "GET", "/v1/users/{id}");
8700
8860
  }
8701
8861
  /**
8702
- * 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.
8862
+ * 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.
8703
8863
  *
8704
8864
  * @param {Vectros.UpdateUserRequest} request
8705
8865
  * @param {IdentityClient.RequestOptions} requestOptions - Request-specific configuration.
@@ -8766,7 +8926,7 @@ var IdentityClient = class {
8766
8926
  return handleNonStatusCodeError(_response.error, _response.rawResponse, "PUT", "/v1/users/{id}");
8767
8927
  }
8768
8928
  /**
8769
- * 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.
8929
+ * 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.
8770
8930
  *
8771
8931
  * @param {Vectros.DeleteUserRequest} request
8772
8932
  * @param {IdentityClient.RequestOptions} requestOptions - Request-specific configuration.
@@ -8827,7 +8987,7 @@ var IdentityClient = class {
8827
8987
  return handleNonStatusCodeError(_response.error, _response.rawResponse, "DELETE", "/v1/users/{id}");
8828
8988
  }
8829
8989
  /**
8830
- * 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.
8990
+ * 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.
8831
8991
  *
8832
8992
  * @param {Vectros.IdentityLookupRequest} request
8833
8993
  * @param {IdentityClient.RequestOptions} requestOptions - Request-specific configuration.
@@ -9270,14 +9430,14 @@ var RecordsClient = class {
9270
9430
  this._options = normalizeClientOptionsWithAuth(options);
9271
9431
  }
9272
9432
  /**
9273
- * 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.
9433
+ * 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).
9274
9434
  *
9275
9435
  * @param {Vectros.BatchGetRequest} request
9276
9436
  * @param {RecordsClient.RequestOptions} requestOptions - Request-specific configuration.
9277
9437
  *
9438
+ * @throws {@link Vectros.BadRequestError}
9278
9439
  * @throws {@link Vectros.ForbiddenError}
9279
9440
  * @throws {@link Vectros.TooManyRequestsError}
9280
- * @throws {@link Vectros.NotImplementedError}
9281
9441
  *
9282
9442
  * @example
9283
9443
  * await client.records.batchGetRecords()
@@ -9314,12 +9474,12 @@ var RecordsClient = class {
9314
9474
  }
9315
9475
  if (_response.error.reason === "status-code") {
9316
9476
  switch (_response.error.statusCode) {
9477
+ case 400:
9478
+ throw new BadRequestError(_response.error.body, _response.rawResponse);
9317
9479
  case 403:
9318
9480
  throw new ForbiddenError(_response.error.body, _response.rawResponse);
9319
9481
  case 429:
9320
9482
  throw new TooManyRequestsError(_response.error.body, _response.rawResponse);
9321
- case 501:
9322
- throw new NotImplementedError(_response.error.body, _response.rawResponse);
9323
9483
  default:
9324
9484
  throw new VectrosError({
9325
9485
  statusCode: _response.error.statusCode,
@@ -10133,7 +10293,7 @@ var SchemasClient = class {
10133
10293
  this._options = normalizeClientOptionsWithAuth(options);
10134
10294
  }
10135
10295
  /**
10136
- * 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.
10296
+ * 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.
10137
10297
  *
10138
10298
  * @param {Vectros.ListSchemasRequest} request
10139
10299
  * @param {SchemasClient.RequestOptions} requestOptions - Request-specific configuration.
@@ -10421,6 +10581,7 @@ var SchemasClient = class {
10421
10581
  * @param {Vectros.DeleteSchemaRequest} request
10422
10582
  * @param {SchemasClient.RequestOptions} requestOptions - Request-specific configuration.
10423
10583
  *
10584
+ * @throws {@link Vectros.ForbiddenError}
10424
10585
  * @throws {@link Vectros.NotFoundError}
10425
10586
  * @throws {@link Vectros.ConflictError}
10426
10587
  * @throws {@link Vectros.TooManyRequestsError}
@@ -10460,6 +10621,8 @@ var SchemasClient = class {
10460
10621
  }
10461
10622
  if (_response.error.reason === "status-code") {
10462
10623
  switch (_response.error.statusCode) {
10624
+ case 403:
10625
+ throw new ForbiddenError(_response.error.body, _response.rawResponse);
10463
10626
  case 404:
10464
10627
  throw new NotFoundError(_response.error.body, _response.rawResponse);
10465
10628
  case 409:
@@ -10880,7 +11043,21 @@ var BlueprintSchemaSchema = import_zod.z.object({
10880
11043
  // (it becomes that name's shared base). Must point directly at the base (one
10881
11044
  // hop); immutable once set. Mirrors the `basedOn` field on the platform's
10882
11045
  // schema-create request.
10883
- basedOn: import_zod.z.string().min(1).optional()
11046
+ basedOn: import_zod.z.string().min(1).optional(),
11047
+ // ADVISORY ONLY — not sent to the platform, never runtime-enforced. The
11048
+ // ownership dimensions this schema's records are typically scoped by
11049
+ // (e.g. ['org', 'client']), declared so the CLI's blueprint lint can warn
11050
+ // when a role clause grants r/u/d on this type but its `dataScope` names
11051
+ // only SOME of these dims — a schema author's easiest way to leave a
11052
+ // dimension unintentionally unconstrained, since reads (unlike creates)
11053
+ // never require full-dimension `dataScope` coverage — this helps an
11054
+ // author NOTICE that read/write asymmetry, not change it; the asymmetry
11055
+ // itself is a ratified, deliberate platform behavior.
11056
+ // Same grammar as a `dataScope` key with the `scope:` prefix stripped:
11057
+ // a bare namespace name, or 'userId' for the principal dimension.
11058
+ // Structural-only here (validated below by `lintExpectedScopeDims`); the
11059
+ // cross-referencing lint against role clauses lives in `packages/cli`.
11060
+ expectedScopeDims: import_zod.z.array(import_zod.z.string().min(1)).max(MAX_SCOPE_DIMENSIONS).optional()
10884
11061
  }).strict().superRefine((schema, ctx) => {
10885
11062
  const hasComposite = (schema.lookupFields ?? []).some(
10886
11063
  (lf) => typeof lf === "object" && lf.fieldNames !== void 0
@@ -10928,7 +11105,26 @@ var GrantedCapabilitiesSchema = import_zod.z.array(import_zod.z.string().min(1))
10928
11105
  var BlueprintAccessProfileSchema = import_zod.z.object({
10929
11106
  // Validated structurally here; the SCOPE GATE (in @vectros-ai/cli) is
10930
11107
  // what enforces the data-plane-only security boundary.
10931
- allowedActions: import_zod.z.array(import_zod.z.string().min(1)).min(1),
11108
+ //
11109
+ // Optional now: mutually exclusive with `roleIds` below (the
11110
+ // `.superRefine` on this schema enforces the XOR, mirroring the platform's
11111
+ // own access-profile scopes/role-composition XOR check). Exactly one of
11112
+ // the two authors this profile's grant.
11113
+ allowedActions: import_zod.z.array(import_zod.z.string().min(1)).min(1).optional(),
11114
+ // Additive role composition: this profile's effective grant is
11115
+ // each named role's own clauses, concatenated in the order listed, instead of
11116
+ // an inline clause authored here. Every id must name a role this SAME
11117
+ // blueprint declares in `roles` (`lintAccessProfileRoleIds` below — the
11118
+ // authoring-time mirror of the platform's same-`(tenantId, contextId)`
11119
+ // check, trivially satisfied here since a blueprint's own `roles` are
11120
+ // always created in its own `contextId`); duplicates are rejected outright
11121
+ // rather than silently deduplicated. Mutually exclusive
11122
+ // with `allowedActions`/`dataScope`/`capabilities` — a role-referencing
11123
+ // profile has no inline clause of its own to carry a dataScope or
11124
+ // capability grant; author those on the referenced Role instead, where
11125
+ // every profile referencing it picks them up uniformly (mirrors how
11126
+ // `assumable` already works for a `roleId`-referencing profile).
11127
+ roleIds: import_zod.z.array(import_zod.z.string().min(1)).min(1).optional(),
10932
11128
  // Optional ownership binding: { userId: [...], "scope:org": [...], "scope:client": [...] }.
10933
11129
  // A `null` element in a value list is the documented NULL SENTINEL — it
10934
11130
  // grants access to TENANT-LEVEL (owner-less) records IN ADDITION to the
@@ -10976,13 +11172,54 @@ var BlueprintAccessProfileSchema = import_zod.z.object({
10976
11172
  // a person is bound to. The gate should weigh the two separately rather
10977
11173
  // than reuse one policy for both.
10978
11174
  capabilities: GrantedCapabilitiesSchema
10979
- }).strict();
11175
+ }).strict().superRefine((ap, ctx) => {
11176
+ const hasInline = ap.allowedActions !== void 0;
11177
+ const hasRoleIds = ap.roleIds !== void 0;
11178
+ if (hasInline === hasRoleIds) {
11179
+ ctx.addIssue({
11180
+ code: import_zod.z.ZodIssueCode.custom,
11181
+ path: ["roleIds"],
11182
+ 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)"
11183
+ });
11184
+ return;
11185
+ }
11186
+ if (hasRoleIds) {
11187
+ if (ap.dataScope !== void 0) {
11188
+ ctx.addIssue({
11189
+ code: import_zod.z.ZodIssueCode.custom,
11190
+ path: ["dataScope"],
11191
+ 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"
11192
+ });
11193
+ }
11194
+ if (ap.capabilities !== void 0) {
11195
+ ctx.addIssue({
11196
+ code: import_zod.z.ZodIssueCode.custom,
11197
+ path: ["capabilities"],
11198
+ 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"
11199
+ });
11200
+ }
11201
+ }
11202
+ });
10980
11203
  var BlueprintRoleClauseSchema = import_zod.z.object({
10981
11204
  allowedActions: import_zod.z.array(import_zod.z.string().min(1)).min(1),
10982
11205
  dataScope: import_zod.z.record(import_zod.z.array(import_zod.z.union([import_zod.z.string().min(1), import_zod.z.null()]))).optional(),
11206
+ dataScopeRef: import_zod.z.string().min(1).optional(),
10983
11207
  capabilities: GrantedCapabilitiesSchema
10984
- }).strict();
11208
+ }).strict().superRefine((c, ctx) => {
11209
+ if (c.dataScope !== void 0 && c.dataScopeRef !== void 0) {
11210
+ ctx.addIssue({
11211
+ code: import_zod.z.ZodIssueCode.custom,
11212
+ path: ["dataScopeRef"],
11213
+ 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"
11214
+ });
11215
+ }
11216
+ });
11217
+ var BlueprintFragmentsSchema = import_zod.z.record(
11218
+ import_zod.z.record(import_zod.z.array(import_zod.z.union([import_zod.z.string().min(1), import_zod.z.null()])))
11219
+ );
10985
11220
  var BlueprintRolesSchema = import_zod.z.record(import_zod.z.array(BlueprintRoleClauseSchema).min(1));
11221
+ var BlueprintRoleAssumableSchema = import_zod.z.record(import_zod.z.array(import_zod.z.string().min(1)).min(1));
11222
+ var BlueprintRoleAssumableMapSchema = import_zod.z.record(BlueprintRoleAssumableSchema);
10986
11223
  var IDENTITY_NAMESPACE_RE = /^[a-z][a-z0-9_-]{1,31}$/;
10987
11224
  var FORBIDDEN_IDENTITY_NAMESPACES = /* @__PURE__ */ new Set(["record", "document", "entity", "user"]);
10988
11225
  var IdentityKindSchema = import_zod.z.string().min(1).superRefine((val, ctx) => {
@@ -11046,6 +11283,14 @@ var BlueprintIssuerSchema = import_zod.z.object({
11046
11283
  subClaim: import_zod.z.string().min(1).optional(),
11047
11284
  /** The claim carrying the subject's email (first-login invite matching). Platform defaults to 'email' when omitted. */
11048
11285
  emailClaim: import_zod.z.string().min(1).optional(),
11286
+ /**
11287
+ * The IdP's OIDC userinfo endpoint. Optional. Presented tokens are access tokens, which under
11288
+ * OIDC don't carry `email` unless the IdP was specifically configured to add it — if
11289
+ * `emailClaim` misses on the presented token, and `userinfoUri` is configured, Vectros falls
11290
+ * back to calling this endpoint (with the presented token as the bearer credential) and reads
11291
+ * `emailClaim` from its JSON response instead. Omit to leave the fallback disabled.
11292
+ */
11293
+ userinfoUri: import_zod.z.string().min(1).optional(),
11049
11294
  /**
11050
11295
  * Opt-in self-service signup: a list of {signupType, roleId} pairs. When a first-time exchange
11051
11296
  * caller presents no invite token but names a signupType matching one of these (or omits it and
@@ -11075,6 +11320,24 @@ var BlueprintNamespaceSchema = import_zod.z.object({
11075
11320
  * free-form strings validated by grammar only.
11076
11321
  */
11077
11322
  entityBacked: import_zod.z.boolean().optional(),
11323
+ /**
11324
+ * Request the platform's TENANT-WIDE registration form instead of this
11325
+ * blueprint's own context — visible to every context in the account, not
11326
+ * just the one this blueprint provisions. `false`/omitted (the default)
11327
+ * is the ordinary, context-owned registration every other namespace here
11328
+ * gets. Mutually exclusive with any future context-targeting field this
11329
+ * schema may grow — there is exactly one way to say "not my own context"
11330
+ * today.
11331
+ *
11332
+ * A blueprint declaring this does NOT get it automatically: the applying
11333
+ * credential must separately hold a platform capability granted only to
11334
+ * an account OWNER, and the CLI refuses to even request it unless
11335
+ * explicitly told to (see `--allow-tenant-wide-namespaces`) — no flag,
11336
+ * no request, regardless of what this field says. Setting this to `true`
11337
+ * states the blueprint's INTENT; it is not, on its own, a grant of
11338
+ * anything.
11339
+ */
11340
+ tenantWide: import_zod.z.boolean().optional(),
11078
11341
  /**
11079
11342
  * This namespace's position in your account's specificity order (used to
11080
11343
  * break a tie when a caller holds two scope dimensions at once). Required
@@ -11258,7 +11521,7 @@ function lintSelfTokenPlacement(value, ctx) {
11258
11521
  ctx.addIssue({
11259
11522
  code: import_zod.z.ZodIssueCode.custom,
11260
11523
  path: path4,
11261
- message: "'${{ self.* }}' / '${{ member.* }}' is a runtime placeholder \u2014 it is only valid inside a roles[].dataScope value"
11524
+ message: "'${{ self.* }}' / '${{ member.* }}' is a runtime placeholder \u2014 it is only valid inside a roles[].dataScope value, a roleAssumable[] value, or a fragments[] value"
11262
11525
  });
11263
11526
  }
11264
11527
  return;
@@ -11269,7 +11532,10 @@ function lintSelfTokenPlacement(value, ctx) {
11269
11532
  }
11270
11533
  if (node && typeof node === "object") {
11271
11534
  for (const [k, v] of Object.entries(node)) {
11272
- const entering = inRoleDataScope || path4.length === 3 && path4[0] === "roles" && k === "dataScope";
11535
+ const inRoleDataScopeClause = path4.length === 3 && path4[0] === "roles" && k === "dataScope";
11536
+ const inRoleAssumable = path4.length === 0 && k === "roleAssumable";
11537
+ const inFragments = path4.length === 0 && k === "fragments";
11538
+ const entering = inRoleDataScope || inRoleDataScopeClause || inRoleAssumable || inFragments;
11273
11539
  walk(v, [...path4, k], entering);
11274
11540
  }
11275
11541
  }
@@ -11440,76 +11706,266 @@ function lintDataScopeKeys(value, ctx) {
11440
11706
  }
11441
11707
  });
11442
11708
  }
11443
- }
11444
- function lintSeedSurfaces(value, ctx) {
11445
- const bp = value;
11446
- if (!Array.isArray(bp.seed)) return;
11447
- const byType = new Map((bp.schemas ?? []).map((s) => [s.typeName, s]));
11448
- bp.seed.forEach((seed, i) => {
11449
- const schema = byType.get(seed.typeName);
11450
- if (!schema) return;
11451
- const allowed = schema.allowedSurfaces ?? ["record"];
11452
- if (!allowed.includes(seed.surface ?? "record")) {
11453
- ctx.addIssue({
11454
- code: import_zod.z.ZodIssueCode.custom,
11455
- path: ["seed", i, "surface"],
11456
- message: `seed '${seed.externalId}' uses surface '${seed.surface ?? "record"}', but schema '${seed.typeName}' allows only [${allowed.join(", ")}]`
11457
- });
11709
+ for (const [name, fragment] of Object.entries(bp.fragments ?? {})) {
11710
+ for (const key of Object.keys(fragment ?? {})) {
11711
+ lintDataScopeKey(key, ["fragments", name, key], ctx);
11458
11712
  }
11459
- });
11713
+ }
11460
11714
  }
11461
- function lintIdentitySurfaces(value, ctx) {
11715
+ function lintExpectedScopeDims(value, ctx) {
11462
11716
  const bp = value;
11463
- if (!Array.isArray(bp.schemas)) return;
11464
- bp.schemas.forEach((schema, i) => {
11465
- if (!schema.allowedSurfaces?.includes("user")) return;
11466
- ctx.addIssue({
11467
- code: import_zod.z.ZodIssueCode.custom,
11468
- path: ["schemas", i, "allowedSurfaces"],
11469
- 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).`
11717
+ (bp.schemas ?? []).forEach((schema, i) => {
11718
+ const dims = schema.expectedScopeDims;
11719
+ if (!dims) return;
11720
+ const seen = /* @__PURE__ */ new Set();
11721
+ dims.forEach((raw, j) => {
11722
+ const dim = String(raw);
11723
+ const path4 = ["schemas", i, "expectedScopeDims", j];
11724
+ if (seen.has(dim)) {
11725
+ ctx.addIssue({
11726
+ code: import_zod.z.ZodIssueCode.custom,
11727
+ path: path4,
11728
+ message: `schema '${schema.typeName}': expectedScopeDims lists '${dim}' more than once.`
11729
+ });
11730
+ }
11731
+ seen.add(dim);
11732
+ if (dim === "userId") return;
11733
+ const nsError = scopeNamespaceGrammarError(dim);
11734
+ if (nsError !== null) {
11735
+ ctx.addIssue({
11736
+ code: import_zod.z.ZodIssueCode.custom,
11737
+ path: path4,
11738
+ message: `schema '${schema.typeName}': expectedScopeDims['${dim}'] ${nsError}`
11739
+ });
11740
+ }
11470
11741
  });
11471
11742
  });
11472
11743
  }
11473
- function lintIssuerContext(value, ctx) {
11744
+ function lintDataScopeRefs(value, ctx) {
11474
11745
  const bp = value;
11475
- if (!Array.isArray(bp.issuers)) return;
11476
- bp.issuers.forEach((issuer, i) => {
11477
- if (issuer.contextId === void 0 || issuer.contextId === bp.contextId) return;
11478
- ctx.addIssue({
11479
- code: import_zod.z.ZodIssueCode.custom,
11480
- path: ["issuers", i, "contextId"],
11481
- 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}'.`
11746
+ const declared = new Set(Object.keys(bp.fragments ?? {}));
11747
+ for (const [roleId, clauses] of Object.entries(bp.roles ?? {})) {
11748
+ clauses.forEach((clause, i) => {
11749
+ if (clause.dataScopeRef === void 0) return;
11750
+ if (!declared.has(clause.dataScopeRef)) {
11751
+ ctx.addIssue({
11752
+ code: import_zod.z.ZodIssueCode.custom,
11753
+ path: ["roles", roleId, i, "dataScopeRef"],
11754
+ message: `dataScopeRef '${clause.dataScopeRef}' is not declared in fragments \u2014 declared fragments: ` + (declared.size ? [...declared].join(", ") : "(none)")
11755
+ });
11756
+ }
11482
11757
  });
11483
- });
11758
+ }
11484
11759
  }
11485
- var BLUEPRINT_FIELD_PHASES = Object.freeze({
11486
- servicePrincipal: "bootstrap",
11487
- issuers: "bootstrap",
11488
- // Registration requires the platform's provisioning:c capability, which
11489
- // only the bootstrap credential carries — same reasoning as issuers.
11490
- namespaces: "bootstrap",
11491
- schemas: "in-context",
11492
- accessProfile: "in-context",
11493
- roles: "in-context",
11494
- seed: "in-context"
11495
- });
11496
- var BlueprintSchema = import_zod.z.object({
11497
- /** Stable blueprint id (the `--blueprint <name>` selector + idempotency key). */
11498
- name: import_zod.z.string().min(1),
11499
- version: import_zod.z.string().min(1),
11500
- description: import_zod.z.string().min(1),
11760
+ var BARE_SELF_PLACEHOLDER_RE = /^\$\{\{\s*self\.[^}]*?\s*\}\}$/;
11761
+ var UNDER_SELF_PLACEHOLDER_RE = /^\$\{\{\s*under\.self\.userId\s*\}\}$/;
11762
+ var UNDER_SELF_SCOPE_PLACEHOLDER_RE = /^\$\{\{\s*under\.self\.scope\.[a-z][a-z0-9_-]{1,31}\s*\}\}$/;
11763
+ var MEMBER_SCOPE_PLACEHOLDER_RE = /^\$\{\{\s*member\.scope\.[a-z][a-z0-9_-]{1,31}(?::[a-z][a-z0-9_-]{1,31})?\s*\}\}$/;
11764
+ var ANY_PLACEHOLDER_RE = /^\$\{\{\s*any\s*\}\}$/;
11765
+ function assumableValueError(publicKey, v) {
11766
+ if (UNDER_SELF_PLACEHOLDER_RE.test(v) || MEMBER_SCOPE_PLACEHOLDER_RE.test(v)) return null;
11767
+ if (UNDER_SELF_SCOPE_PLACEHOLDER_RE.test(v)) {
11768
+ 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)`;
11769
+ }
11770
+ if (ANY_PLACEHOLDER_RE.test(v)) {
11771
+ 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`;
11772
+ }
11773
+ if (BARE_SELF_PLACEHOLDER_RE.test(v)) {
11774
+ 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`;
11775
+ }
11776
+ if (SELF_TOKEN_RE.test(v)) {
11777
+ return `assumable['${publicKey}'] value '${v}' is not a recognized runtime placeholder \u2014 use '\${{ under.self.userId }}' or '\${{ member.scope.<namespace>[:level] }}'`;
11778
+ }
11779
+ if (!SCOPE_VALUE_RE.test(v)) {
11780
+ 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 '-'`;
11781
+ }
11782
+ return null;
11783
+ }
11784
+ function lintAccessProfileRoleIds(value, ctx) {
11785
+ const bp = value;
11786
+ const roleIds = bp.accessProfile?.roleIds;
11787
+ if (!roleIds) return;
11788
+ const declaredRoles = new Set(Object.keys(bp.roles ?? {}));
11789
+ const seen = /* @__PURE__ */ new Set();
11790
+ roleIds.forEach((roleId, i) => {
11791
+ const path4 = ["accessProfile", "roleIds", i];
11792
+ if (!declaredRoles.has(roleId)) {
11793
+ ctx.addIssue({
11794
+ code: import_zod.z.ZodIssueCode.custom,
11795
+ path: path4,
11796
+ 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)")
11797
+ });
11798
+ }
11799
+ if (seen.has(roleId)) {
11800
+ ctx.addIssue({
11801
+ code: import_zod.z.ZodIssueCode.custom,
11802
+ path: path4,
11803
+ message: `accessProfile.roleIds names '${roleId}' more than once \u2014 remove the duplicate`
11804
+ });
11805
+ }
11806
+ seen.add(roleId);
11807
+ });
11808
+ }
11809
+ function lintRoleAssumable(value, ctx) {
11810
+ const bp = value;
11811
+ const declaredRoles = new Set(Object.keys(bp.roles ?? {}));
11812
+ for (const [roleId, assumable] of Object.entries(bp.roleAssumable ?? {})) {
11813
+ if (!declaredRoles.has(roleId)) {
11814
+ ctx.addIssue({
11815
+ code: import_zod.z.ZodIssueCode.custom,
11816
+ path: ["roleAssumable", roleId],
11817
+ message: `roleAssumable names '${roleId}', which is not a role this blueprint declares in roles \u2014 ` + (declaredRoles.size ? `declared roles: ${[...declaredRoles].join(", ")}` : "declared roles: (none)")
11818
+ });
11819
+ continue;
11820
+ }
11821
+ for (const [key, values] of Object.entries(assumable)) {
11822
+ const path4 = ["roleAssumable", roleId, key];
11823
+ if (key === "userId") {
11824
+ ctx.addIssue({
11825
+ code: import_zod.z.ZodIssueCode.custom,
11826
+ path: path4,
11827
+ 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"
11828
+ });
11829
+ continue;
11830
+ }
11831
+ const ns = key.startsWith("scope:") ? key.slice("scope:".length) : null;
11832
+ const nsError = ns === null ? null : scopeNamespaceGrammarError(ns);
11833
+ if (ns === null || nsError !== null) {
11834
+ ctx.addIssue({
11835
+ code: import_zod.z.ZodIssueCode.custom,
11836
+ path: path4,
11837
+ 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}`
11838
+ });
11839
+ continue;
11840
+ }
11841
+ if (!Array.isArray(values)) continue;
11842
+ values.forEach((v, i) => {
11843
+ if (typeof v !== "string") return;
11844
+ const err = assumableValueError(key, v);
11845
+ if (err) ctx.addIssue({ code: import_zod.z.ZodIssueCode.custom, path: [...path4, i], message: err });
11846
+ });
11847
+ }
11848
+ }
11849
+ }
11850
+ function expandDataScopeRefs(bp) {
11851
+ if (!bp.roles) return bp;
11852
+ const fragments = bp.fragments ?? {};
11853
+ let changedAnyRole = false;
11854
+ const roles = {};
11855
+ for (const [roleId, clauses] of Object.entries(bp.roles)) {
11856
+ let changedThisRole = false;
11857
+ const expandedClauses = clauses.map((clause) => {
11858
+ if (clause.dataScopeRef === void 0) return clause;
11859
+ changedThisRole = true;
11860
+ const { dataScopeRef, ...rest } = clause;
11861
+ if (!(dataScopeRef in fragments)) {
11862
+ throw new Error(
11863
+ `expandDataScopeRefs: dataScopeRef '${dataScopeRef}' does not resolve against fragments \u2014 this should have been caught by lintDataScopeRefs before parseBlueprint reaches this function`
11864
+ );
11865
+ }
11866
+ return { ...rest, dataScope: fragments[dataScopeRef] };
11867
+ });
11868
+ roles[roleId] = changedThisRole ? expandedClauses : clauses;
11869
+ changedAnyRole = changedAnyRole || changedThisRole;
11870
+ }
11871
+ return changedAnyRole ? { ...bp, roles } : bp;
11872
+ }
11873
+ function lintSeedSurfaces(value, ctx) {
11874
+ const bp = value;
11875
+ if (!Array.isArray(bp.seed)) return;
11876
+ const byType = new Map((bp.schemas ?? []).map((s) => [s.typeName, s]));
11877
+ bp.seed.forEach((seed, i) => {
11878
+ const schema = byType.get(seed.typeName);
11879
+ if (!schema) return;
11880
+ const allowed = schema.allowedSurfaces ?? ["record"];
11881
+ if (!allowed.includes(seed.surface ?? "record")) {
11882
+ ctx.addIssue({
11883
+ code: import_zod.z.ZodIssueCode.custom,
11884
+ path: ["seed", i, "surface"],
11885
+ message: `seed '${seed.externalId}' uses surface '${seed.surface ?? "record"}', but schema '${seed.typeName}' allows only [${allowed.join(", ")}]`
11886
+ });
11887
+ }
11888
+ });
11889
+ }
11890
+ function lintIdentitySurfaces(value, ctx) {
11891
+ const bp = value;
11892
+ if (!Array.isArray(bp.schemas)) return;
11893
+ bp.schemas.forEach((schema, i) => {
11894
+ if (!schema.allowedSurfaces?.includes("user")) return;
11895
+ ctx.addIssue({
11896
+ code: import_zod.z.ZodIssueCode.custom,
11897
+ path: ["schemas", i, "allowedSurfaces"],
11898
+ 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).`
11899
+ });
11900
+ });
11901
+ }
11902
+ function lintIssuerContext(value, ctx) {
11903
+ const bp = value;
11904
+ if (!Array.isArray(bp.issuers)) return;
11905
+ bp.issuers.forEach((issuer, i) => {
11906
+ if (issuer.contextId === void 0 || issuer.contextId === bp.contextId) return;
11907
+ ctx.addIssue({
11908
+ code: import_zod.z.ZodIssueCode.custom,
11909
+ path: ["issuers", i, "contextId"],
11910
+ 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}'.`
11911
+ });
11912
+ });
11913
+ }
11914
+ var BLUEPRINT_FIELD_PHASES = Object.freeze({
11915
+ servicePrincipal: "bootstrap",
11916
+ issuers: "bootstrap",
11917
+ // Registration requires the platform's provisioning:c capability, which
11918
+ // only the bootstrap credential carries — same reasoning as issuers.
11919
+ namespaces: "bootstrap",
11920
+ schemas: "in-context",
11921
+ accessProfile: "in-context",
11922
+ roles: "in-context",
11923
+ seed: "in-context"
11924
+ });
11925
+ var BlueprintSchema = import_zod.z.object({
11926
+ /** Stable blueprint id (the `--blueprint <name>` selector + idempotency key). */
11927
+ name: import_zod.z.string().min(1),
11928
+ version: import_zod.z.string().min(1),
11929
+ description: import_zod.z.string().min(1),
11501
11930
  /** The app-context the profile + scoped key bind to (e.g. "mcp"). */
11502
11931
  contextId: import_zod.z.string().regex(CONTEXT_ID_RE, {
11503
11932
  message: "contextId must be 3-31 chars, start with a lowercase letter, then lowercase letters/digits/dashes (e.g. 'mcp')"
11504
11933
  }),
11505
11934
  /** Human-readable app-context name; defaults to `MCP — <name>` (see {@link contextNameOf}) when absent. */
11506
11935
  contextName: import_zod.z.string().min(1).optional(),
11936
+ /**
11937
+ * The deploying organization's own display name — distinct from {@link contextName}.
11938
+ * `contextName` is the blueprint author's fixed identity for the app (the same for every
11939
+ * deployer); `companyName` is meant to vary per install, typically templated from a
11940
+ * deployer-supplied `${{ inputs.x }}` value rather than hardcoded. Used for branding on
11941
+ * platform-sent correspondence (e.g. invite emails) alongside `contextName` — not a
11942
+ * replacement for it. See {@link companyNameOf}.
11943
+ */
11944
+ companyName: import_zod.z.string().min(1).optional(),
11507
11945
  schemas: import_zod.z.array(BlueprintSchemaSchema).default([]),
11508
11946
  accessProfile: BlueprintAccessProfileSchema,
11509
11947
  servicePrincipal: BlueprintServicePrincipalSchema,
11510
11948
  seed: import_zod.z.array(BlueprintSeedRecordSchema).optional(),
11511
11949
  /** Optional multi-clause roles, bound to principals via `access grant --role`. */
11512
11950
  roles: BlueprintRolesSchema.optional(),
11951
+ /**
11952
+ * Optional roleId → `POST /v1/auth/token/assume`
11953
+ * entitlement grant, mirroring the platform's role create/update request
11954
+ * and response shape for this field. Kept separate from `roles` rather
11955
+ * than folded into a role entry — see {@link BlueprintRoleAssumableMapSchema}'s
11956
+ * own doc comment for why. Every key must name a role this blueprint
11957
+ * declares in `roles` (`lintRoleAssumable`).
11958
+ */
11959
+ roleAssumable: BlueprintRoleAssumableMapSchema.optional(),
11960
+ /**
11961
+ * Optional named `dataScope` fragments, each
11962
+ * referenceable from a role clause via `dataScopeRef: <name>` instead of
11963
+ * repeating the same map inline. Purely local authoring sugar: not a
11964
+ * provisioned resource (absent from `BLUEPRINT_FIELD_PHASES`), and never
11965
+ * survives `parseBlueprint` as a live reference — see
11966
+ * {@link expandDataScopeRefs}.
11967
+ */
11968
+ fragments: BlueprintFragmentsSchema.optional(),
11513
11969
  /** Optional principals ensured-exist at apply; referenced via ${{ identities.* }}. */
11514
11970
  identities: IdentitiesDeclSchema.optional(),
11515
11971
  /**
@@ -11534,6 +11990,10 @@ var BlueprintSchema = import_zod.z.object({
11534
11990
  lintIdentityOverrides(bp, ctx);
11535
11991
  lintNamespacedScopeArrays(bp, ctx);
11536
11992
  lintDataScopeKeys(bp, ctx);
11993
+ lintExpectedScopeDims(bp, ctx);
11994
+ lintDataScopeRefs(bp, ctx);
11995
+ lintRoleAssumable(bp, ctx);
11996
+ lintAccessProfileRoleIds(bp, ctx);
11537
11997
  lintSeedSurfaces(bp, ctx);
11538
11998
  lintIdentitySurfaces(bp, ctx);
11539
11999
  lintIssuerContext(bp, ctx);
@@ -11573,7 +12033,7 @@ function parseBlueprint(input) {
11573
12033
  throw new BlueprintValidationError(`Malformed blueprint:
11574
12034
  ${renderIssues(issues)}`, issues);
11575
12035
  }
11576
- return result.data;
12036
+ return expandDataScopeRefs(result.data);
11577
12037
  }
11578
12038
  function contextNameOf(blueprint) {
11579
12039
  return blueprint.contextName ?? `MCP \u2014 ${blueprint.name}`;
@@ -11693,6 +12153,7 @@ var WHOLE_TOKEN_RE = /^\$\{\{\s*([A-Za-z_]\w*)\.([A-Za-z_]\w*)\s*\}\}$/;
11693
12153
  var TOKEN_SCAN_RE = /\$\$\{\{|\$\{\{([\s\S]*?)\}\}|\$\{\{/g;
11694
12154
  var INNER_REF_RE = /^([A-Za-z_]\w*)\.([A-Za-z_]\w*)$/;
11695
12155
  var DEFERRED_NAMESPACES = /* @__PURE__ */ new Set(["self", "identities", "under", "member"]);
12156
+ var DEFERRED_BARE_TOKENS = /* @__PURE__ */ new Set(["any"]);
11696
12157
  var DEFERRED_NS_ALTERNATION = [...DEFERRED_NAMESPACES].join("|");
11697
12158
  var DEFERRED_INNER_RE = new RegExp(
11698
12159
  `^(?:${DEFERRED_NS_ALTERNATION})(?:\\.[A-Za-z_]\\w*)+(?::[A-Za-z_]\\w*)?$`
@@ -11754,7 +12215,7 @@ function interpolate(str, ctx, path4) {
11754
12215
  });
11755
12216
  } else {
11756
12217
  const inner = (m[1] ?? "").trim();
11757
- if (DEFERRED_INNER_RE.test(inner)) {
12218
+ if (DEFERRED_INNER_RE.test(inner) || DEFERRED_BARE_TOKENS.has(inner)) {
11758
12219
  out += m[0];
11759
12220
  last = TOKEN_SCAN_RE.lastIndex;
11760
12221
  continue;
@@ -13712,6 +14173,7 @@ var ENV_BASE_URLS = {
13712
14173
  // `--base-url` or VECTROS_API_BASE_URL; verify at smoke time.
13713
14174
  staging: "https://api.staging.vectros.ai"
13714
14175
  };
14176
+ var AUTH_FLAG_NAMES = ["--env", "--base-url", "--token", "--use-scoped-token"];
13715
14177
  function envToBaseUrl(env, override) {
13716
14178
  return validateBaseUrl(override ?? ENV_BASE_URLS[env]);
13717
14179
  }
@@ -14052,7 +14514,12 @@ async function resolveAuthContext(opts) {
14052
14514
  const baseUrl = envToBaseUrl(opts.env, opts.baseUrl ?? process.env.VECTROS_API_BASE_URL);
14053
14515
  const cognitoToken = await resolveCognitoToken(opts.env, opts.token);
14054
14516
  const tenant = opts.tenant ?? "live";
14055
- const bridge = opts.provisioning ? await mintBridgeToken({ cognitoToken, apiBaseUrl: baseUrl, tenant }) : await mintContextToken({ cognitoToken, apiBaseUrl: baseUrl, contextId: opts.context ?? "default", tenant });
14517
+ const bridge = opts.provisioning ? await mintBridgeToken({
14518
+ cognitoToken,
14519
+ apiBaseUrl: baseUrl,
14520
+ tenant,
14521
+ ...opts.context ? { contextId: opts.context } : {}
14522
+ }) : await mintContextToken({ cognitoToken, apiBaseUrl: baseUrl, contextId: opts.context ?? "default", tenant });
14056
14523
  const client = new VectrosClient({ token: bridge.token, environment: baseUrl });
14057
14524
  const identity = await resolveIdentity({ log: opts.log, apiKey: bridge.token, environment: baseUrl });
14058
14525
  return {
@@ -14194,6 +14661,9 @@ var SELF_QUALIFIED_OPS = /* @__PURE__ */ new Map([
14194
14661
  var UNQUALIFIABLE_WITH_ANY_TAIL = /* @__PURE__ */ new Map([
14195
14662
  ["profiles", /* @__PURE__ */ new Set(["r"])]
14196
14663
  ]);
14664
+ var CAPABILITY_UNLOCKED_OPS = /* @__PURE__ */ new Map([
14665
+ ["profiles", /* @__PURE__ */ new Map([["c", "member-lifecycle"]])]
14666
+ ]);
14197
14667
  var VALID_OPS = /* @__PURE__ */ new Set(["c", "r", "u", "d"]);
14198
14668
  var DATA_PLANE_SET = new Set(DATA_PLANE_ALLOWLIST);
14199
14669
  var CONTROL_PLANE_SET = new Set(KNOWN_CONTROL_PLANE_RESOURCES);
@@ -14210,7 +14680,7 @@ ${summary}`
14210
14680
  this.rejected = rejected;
14211
14681
  }
14212
14682
  };
14213
- var GRANTED_CAPABILITIES_ALLOWLIST = ["member-lifecycle"];
14683
+ var GRANTED_CAPABILITIES_ALLOWLIST = ["member-lifecycle", "delegate-principal-stamp"];
14214
14684
  var GRANTED_CAPABILITIES_SET = new Set(GRANTED_CAPABILITIES_ALLOWLIST);
14215
14685
  var CAPABILITIES_HINT = GRANTED_CAPABILITIES_ALLOWLIST.join("/");
14216
14686
  var CapabilityGateError = class extends Error {
@@ -14263,7 +14733,7 @@ function firstInvalidOp(ops) {
14263
14733
  }
14264
14734
  return void 0;
14265
14735
  }
14266
- function evaluateScopeToken(action) {
14736
+ function evaluateScopeToken(action, capabilities) {
14267
14737
  const token = action.trim();
14268
14738
  if (token.length === 0) {
14269
14739
  return { action, reason: "empty scope token" };
@@ -14314,9 +14784,12 @@ function evaluateScopeToken(action) {
14314
14784
  if (admittedOps?.has(ch)) continue;
14315
14785
  if (selfOnlyOps?.has(ch)) {
14316
14786
  if (isSelfQualified) continue;
14787
+ const unlockCapability = CAPABILITY_UNLOCKED_OPS.get(resource)?.get(ch);
14788
+ if (unlockCapability && qualifier === void 0 && capabilities?.includes(unlockCapability)) continue;
14789
+ const capabilityHintApplies = unlockCapability && !capabilities?.includes(unlockCapability);
14317
14790
  return {
14318
14791
  action,
14319
- 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`
14792
+ 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`
14320
14793
  };
14321
14794
  }
14322
14795
  return {
@@ -14349,11 +14822,11 @@ function evaluateScopeToken(action) {
14349
14822
  }
14350
14823
  return null;
14351
14824
  }
14352
- function evaluateScopeGate(allowedActions) {
14825
+ function evaluateScopeGate(allowedActions, capabilities) {
14353
14826
  const accepted = [];
14354
14827
  const rejected = [];
14355
14828
  for (const action of allowedActions) {
14356
- const rejection = evaluateScopeToken(action);
14829
+ const rejection = evaluateScopeToken(action, capabilities);
14357
14830
  if (rejection) {
14358
14831
  rejected.push(rejection);
14359
14832
  } else {
@@ -14362,22 +14835,34 @@ function evaluateScopeGate(allowedActions) {
14362
14835
  }
14363
14836
  return { ok: rejected.length === 0, accepted, rejected };
14364
14837
  }
14365
- function assertScopeGate(allowedActions) {
14366
- const result = evaluateScopeGate(allowedActions);
14838
+ function assertScopeGate(allowedActions, capabilities) {
14839
+ const result = evaluateScopeGate(allowedActions, capabilities);
14367
14840
  if (!result.ok) {
14368
14841
  throw new ScopeGateError(result.rejected);
14369
14842
  }
14370
14843
  }
14371
14844
  function assertBlueprintGates(blueprint) {
14372
- assertScopeGate(blueprint.accessProfile.allowedActions);
14373
- assertCapabilityGate(blueprint.accessProfile.capabilities);
14845
+ if (blueprint.accessProfile.allowedActions !== void 0) {
14846
+ assertScopeGate(blueprint.accessProfile.allowedActions, blueprint.accessProfile.capabilities);
14847
+ assertCapabilityGate(blueprint.accessProfile.capabilities);
14848
+ }
14374
14849
  for (const clauses of Object.values(blueprint.roles ?? {})) {
14375
14850
  for (const clause of clauses) {
14376
- assertScopeGate(clause.allowedActions);
14851
+ assertScopeGate(clause.allowedActions, clause.capabilities);
14377
14852
  assertCapabilityGate(clause.capabilities);
14378
14853
  }
14379
14854
  }
14380
14855
  }
14856
+ function effectiveAccessProfileActions(blueprint) {
14857
+ if (blueprint.accessProfile.allowedActions !== void 0) {
14858
+ return [...blueprint.accessProfile.allowedActions];
14859
+ }
14860
+ const out = [];
14861
+ for (const roleId of blueprint.accessProfile.roleIds ?? []) {
14862
+ for (const clause of blueprint.roles?.[roleId] ?? []) out.push(...clause.allowedActions);
14863
+ }
14864
+ return out;
14865
+ }
14381
14866
 
14382
14867
  // src/machine-id.ts
14383
14868
  var import_node_fs = require("fs");
@@ -14472,7 +14957,8 @@ function makeIdentityResolver(client, log) {
14472
14957
  return id;
14473
14958
  };
14474
14959
  }
14475
- async function resolveApplyTimeIdentities(blueprint, client, log) {
14960
+ async function resolveApplyTimeIdentities(blueprint, provisioned, bootstrapClient, log) {
14961
+ const client = provisioned.ctxBootstrapClient ?? bootstrapClient;
14476
14962
  const resolver = makeIdentityResolver(client, log);
14477
14963
  const resolved = await resolveBlueprintIdentities(blueprint, resolver);
14478
14964
  return parseBlueprint(resolved);
@@ -14482,7 +14968,8 @@ async function resolveApplyTimeIdentities(blueprint, client, log) {
14482
14968
  function actionOf(created) {
14483
14969
  return created ? "created" : "reconciled";
14484
14970
  }
14485
- async function registerNamespaceIdempotently(client, contextIdForRequest, decl) {
14971
+ async function registerNamespaceIdempotently(client, blueprintContextId, decl) {
14972
+ const contextIdForRequest = decl.tenantWide ? void 0 : blueprintContextId;
14486
14973
  const body = {
14487
14974
  namespace: decl.namespace,
14488
14975
  entityBacked: decl.entityBacked,
@@ -14498,6 +14985,7 @@ async function registerNamespaceIdempotently(client, contextIdForRequest, decl)
14498
14985
  } catch (err) {
14499
14986
  const info = extractServerError(err);
14500
14987
  if (info.statusCode !== 400) throw err;
14988
+ if (decl.tenantWide) throw err;
14501
14989
  let existing;
14502
14990
  try {
14503
14991
  existing = await client.getNamespace({ namespace: decl.namespace, contextId: contextIdForRequest });
@@ -14662,20 +15150,24 @@ function assertNoUnresolvedIdentities(blueprint) {
14662
15150
  );
14663
15151
  }
14664
15152
  }
14665
- async function runLoader(client, input) {
14666
- const {
14667
- blueprint,
14668
- tenantId,
14669
- machineId,
14670
- rotate = false,
14671
- skipSeed = false,
14672
- readExistingKey,
14673
- persistMintedKey,
14674
- confirmExistingPrincipal,
14675
- log
14676
- } = input;
15153
+ var TenantWideNamespacesNotAllowedError = class extends Error {
15154
+ constructor(message) {
15155
+ super(message);
15156
+ this.name = "TenantWideNamespacesNotAllowedError";
15157
+ }
15158
+ };
15159
+ function assertTenantWideNamespacesAllowed(blueprint, allow) {
15160
+ if (allow) return;
15161
+ const names = (blueprint.namespaces ?? []).filter((ns) => ns.tenantWide === true).map((ns) => ns.namespace);
15162
+ if (names.length === 0) return;
15163
+ throw new TenantWideNamespacesNotAllowedError(
15164
+ `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).`
15165
+ );
15166
+ }
15167
+ async function provisionContext(client, input) {
15168
+ const { blueprint, allowTenantWideNamespaces = false, readExistingKey, confirmExistingPrincipal, log } = input;
14677
15169
  assertBlueprintGates(blueprint);
14678
- assertNoUnresolvedIdentities(blueprint);
15170
+ assertTenantWideNamespacesAllowed(blueprint, allowTenantWideNamespaces);
14679
15171
  const ctxResp = await client.auth.createAppContext({ body: { contextId: blueprint.contextId, name: contextNameOf(blueprint) } });
14680
15172
  const contextAction = actionOf(ctxResp.created);
14681
15173
  log.debug({ component: "loader", contextId: blueprint.contextId, contextAction }, "app-context ensured");
@@ -14714,10 +15206,10 @@ async function runLoader(client, input) {
14714
15206
  "existing-principal collision confirmed (or unwired) \u2014 proceeding with any further write to this principal"
14715
15207
  );
14716
15208
  }
14717
- const needsContextPinnedBootstrap = (blueprint.issuers ?? []).length > 0 || (blueprint.namespaces ?? []).length > 0;
15209
+ const needsContextPinnedBootstrap = (blueprint.namespaces ?? []).length > 0;
14718
15210
  const ctxBootstrapClient = needsContextPinnedBootstrap ? input.remintBootstrapForContext ? await input.remintBootstrapForContext(blueprint.contextId) : client : void 0;
14719
15211
  if (ctxBootstrapClient && input.remintBootstrapForContext) {
14720
- log.debug({ component: "loader", contextId: blueprint.contextId }, "re-minted context-pinned bootstrap client");
15212
+ log.debug({ component: "loader", contextId: blueprint.contextId }, "re-minted context-pinned bootstrap client (namespaces)");
14721
15213
  }
14722
15214
  const namespaceActions = [];
14723
15215
  for (const ns of blueprint.namespaces ?? []) {
@@ -14725,8 +15217,35 @@ async function runLoader(client, input) {
14725
15217
  namespaceActions.push({ namespace: ns.namespace, action });
14726
15218
  log.debug({ component: "loader", namespace: ns.namespace, contextId: blueprint.contextId, action }, "namespace ensured");
14727
15219
  }
15220
+ return { contextAction, servicePrincipalUserId: userId, userAction, namespaceActions, ctxBootstrapClient };
15221
+ }
15222
+ async function runLoader(client, input) {
15223
+ const {
15224
+ blueprint,
15225
+ tenantId,
15226
+ machineId,
15227
+ rotate = false,
15228
+ skipSeed = false,
15229
+ seedIndexPoll,
15230
+ provisioned,
15231
+ readExistingKey,
15232
+ persistMintedKey,
15233
+ log
15234
+ } = input;
15235
+ assertNoUnresolvedIdentities(blueprint);
15236
+ const { contextAction, servicePrincipalUserId: userId, userAction, namespaceActions } = provisioned;
15237
+ assertBlueprintGates(blueprint);
15238
+ const needsContextPinnedBootstrapForIssuers = (blueprint.issuers ?? []).length > 0;
15239
+ const ctxBootstrapClient = provisioned.ctxBootstrapClient ?? (needsContextPinnedBootstrapForIssuers ? input.remintBootstrapForContext ? await input.remintBootstrapForContext(blueprint.contextId) : client : void 0);
15240
+ if (ctxBootstrapClient && !provisioned.ctxBootstrapClient && input.remintBootstrapForContext) {
15241
+ log.debug({ component: "loader", contextId: blueprint.contextId }, "re-minted context-pinned bootstrap client (issuers)");
15242
+ }
14728
15243
  const issuerActions = [];
14729
15244
  for (const issuer of blueprint.issuers ?? []) {
15245
+ const selfSignupPolicies = issuer.selfSignupPolicies?.map((p) => ({
15246
+ signup_type: p.signupType,
15247
+ role_id: p.roleId
15248
+ }));
14730
15249
  const issuerResp = await ctxBootstrapClient.auth.registerIssuer({
14731
15250
  issuerId: issuer.issuerId,
14732
15251
  issuer: issuer.issuer,
@@ -14735,18 +15254,26 @@ async function runLoader(client, input) {
14735
15254
  contextId: issuer.contextId,
14736
15255
  subClaim: issuer.subClaim,
14737
15256
  emailClaim: issuer.emailClaim,
14738
- selfSignupPolicies: issuer.selfSignupPolicies?.map((p) => ({
14739
- signup_type: p.signupType,
14740
- role_id: p.roleId
14741
- }))
15257
+ userinfoUri: issuer.userinfoUri,
15258
+ selfSignupPolicies
14742
15259
  });
14743
15260
  const action = actionOf(issuerResp.created);
14744
15261
  issuerActions.push({ issuerId: issuer.issuerId, action });
14745
15262
  if (action === "reconciled") {
14746
- log.warn(
14747
- { component: "loader", issuerId: issuer.issuerId },
14748
- "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"
14749
- );
15263
+ await ctxBootstrapClient.auth.updateIssuer({
15264
+ issuerId: issuer.issuerId,
15265
+ // Trust-anchor/routing-pin fields — echoed back, never actually
15266
+ // changed here (the platform rejects a differing value).
15267
+ issuer: issuer.issuer,
15268
+ jwksUri: issuer.jwksUri,
15269
+ audience: issuer.audience,
15270
+ contextId: issuer.contextId,
15271
+ subClaim: issuer.subClaim,
15272
+ emailClaim: issuer.emailClaim,
15273
+ userinfoUri: issuer.userinfoUri,
15274
+ selfSignupPolicies
15275
+ });
15276
+ log.debug({ component: "loader", issuerId: issuer.issuerId }, "issuer already registered \u2014 reconciled safe fields to the declared values");
14750
15277
  } else {
14751
15278
  log.debug({ component: "loader", issuerId: issuer.issuerId, action }, "issuer registered");
14752
15279
  }
@@ -14786,10 +15313,35 @@ async function runLoader(client, input) {
14786
15313
  schemaActions.push({ typeName: s.typeName, action: actionOf(resp.created) });
14787
15314
  log.debug({ component: "loader", typeName: s.typeName, schemaId: resp.id, action: actionOf(resp.created) }, "schema ensured (upsert)");
14788
15315
  }
14789
- const profileBody = {
15316
+ const roleIds = [];
15317
+ const roleActions = [];
15318
+ for (const [roleId, clauses] of Object.entries(blueprint.roles ?? {})) {
15319
+ const roleResp = await ctxClient.auth.createRole({
15320
+ contextId: blueprint.contextId,
15321
+ upsert: true,
15322
+ body: {
15323
+ roleId,
15324
+ // Blueprint roles carry no display name → default it to the roleId.
15325
+ name: roleId,
15326
+ scopes: toRoleScopeClauses(clauses),
15327
+ assumable: blueprint.roleAssumable?.[roleId]
15328
+ }
15329
+ });
15330
+ roleIds.push(roleId);
15331
+ roleActions.push({ roleId, action: actionOf(roleResp.created) });
15332
+ log.debug({ component: "loader", roleId, clauses: clauses.length, action: actionOf(roleResp.created) }, "role ensured (upsert)");
15333
+ }
15334
+ const profileBody = blueprint.accessProfile.roleIds ? {
15335
+ principalId: `usr_${userId}`,
15336
+ roleIds: blueprint.accessProfile.roleIds,
15337
+ identityOverrides: blueprint.accessProfile.identityOverrides
15338
+ } : {
14790
15339
  principalId: `usr_${userId}`,
14791
15340
  scopes: [
14792
15341
  {
15342
+ // Non-null: this is the `else` branch of the roleIds XOR above —
15343
+ // `@vectros-ai/blueprints`' BlueprintAccessProfileSchema guarantees
15344
+ // allowedActions is set whenever roleIds is not.
14793
15345
  allowed_actions: blueprint.accessProfile.allowedActions,
14794
15346
  // Pass dataScope straight through to the wire. The blueprint type is
14795
15347
  // Record<string, (string|null)[]> — a `null` element is the NULL
@@ -14823,22 +15375,6 @@ async function runLoader(client, input) {
14823
15375
  body: profileBody
14824
15376
  });
14825
15377
  log.debug({ component: "loader", principalId: `usr_${userId}`, accessProfileId: profile.id, profileAction }, "access profile ensured + reconciled to declared scopes");
14826
- const roleIds = [];
14827
- const roleActions = [];
14828
- for (const [roleId, clauses] of Object.entries(blueprint.roles ?? {})) {
14829
- const roleResp = await ctxClient.auth.createRole({
14830
- contextId: blueprint.contextId,
14831
- body: {
14832
- roleId,
14833
- // Blueprint roles carry no display name → default it to the roleId.
14834
- name: roleId,
14835
- scopes: toRoleScopeClauses(clauses)
14836
- }
14837
- });
14838
- roleIds.push(roleId);
14839
- roleActions.push({ roleId, action: actionOf(roleResp.created) });
14840
- log.debug({ component: "loader", roleId, clauses: clauses.length, action: actionOf(roleResp.created) }, "role ensured");
14841
- }
14842
15378
  const keyName = keyNameFor(blueprint.name, machineId);
14843
15379
  let keyAction = "created";
14844
15380
  let revokedKeyId;
@@ -14875,6 +15411,7 @@ async function runLoader(client, input) {
14875
15411
  const seededRecordIds = [];
14876
15412
  const seededDocumentIds = [];
14877
15413
  const seedIndexFailures = [];
15414
+ const pendingIndexSeeds = [];
14878
15415
  const noteIfIndexFailed = (created, rec, surface) => {
14879
15416
  if (created.indexStatus !== "FAILED") return;
14880
15417
  seedIndexFailures.push({
@@ -14909,6 +15446,9 @@ async function runLoader(client, input) {
14909
15446
  }
14910
15447
  });
14911
15448
  if (created.id) seededDocumentIds.push(created.id);
15449
+ if (created.id && created.indexStatus === "PENDING_INDEX") {
15450
+ pendingIndexSeeds.push({ id: created.id, externalId: rec.externalId, typeName: rec.typeName, surface: "document" });
15451
+ }
14912
15452
  noteIfIndexFailed(created, rec, "document");
14913
15453
  } else {
14914
15454
  const created = await ctxClient.records.createRecord({
@@ -14921,10 +15461,30 @@ async function runLoader(client, input) {
14921
15461
  }
14922
15462
  });
14923
15463
  if (created.id) seededRecordIds.push(created.id);
15464
+ if (created.id && created.indexStatus === "PENDING_INDEX") {
15465
+ pendingIndexSeeds.push({ id: created.id, externalId: rec.externalId, typeName: rec.typeName, surface: "record" });
15466
+ }
14924
15467
  noteIfIndexFailed(created, rec, "record");
14925
15468
  }
14926
15469
  seededCount++;
14927
15470
  }
15471
+ const pollAttempts = seedIndexPoll?.attempts ?? 3;
15472
+ const pollDelayMs = seedIndexPoll?.delayMs ?? 500;
15473
+ if (pollAttempts > 0 && pendingIndexSeeds.length > 0) {
15474
+ await Promise.all(
15475
+ pendingIndexSeeds.map(async (seed) => {
15476
+ for (let attempt = 0; attempt < pollAttempts; attempt++) {
15477
+ await new Promise((r) => setTimeout(r, pollDelayMs));
15478
+ const readBack = seed.surface === "document" ? await ctxClient.documents.getDocument({ id: seed.id }) : await ctxClient.records.getRecord({ id: seed.id });
15479
+ if (readBack.indexStatus === "FAILED") {
15480
+ noteIfIndexFailed(readBack, seed, seed.surface);
15481
+ return;
15482
+ }
15483
+ if (readBack.indexStatus !== "PENDING_INDEX") return;
15484
+ }
15485
+ })
15486
+ );
15487
+ }
14928
15488
  return {
14929
15489
  contextId: blueprint.contextId,
14930
15490
  servicePrincipalUserId: userId,
@@ -14949,6 +15509,11 @@ async function runLoader(client, input) {
14949
15509
  seedIndexFailures
14950
15510
  };
14951
15511
  }
15512
+ function renderPlanDataScope(ds) {
15513
+ const entries = Object.entries(ds ?? {});
15514
+ if (entries.length === 0) return "(none \u2014 unconstrained)";
15515
+ return entries.map(([dim, values]) => `${dim}=[${values.map((v) => v === null ? "null" : v).join(", ")}]`).join(", ");
15516
+ }
14952
15517
  function buildPlan(blueprint, opts = {}) {
14953
15518
  const steps = [];
14954
15519
  steps.push({ kind: "context", summary: `context '${blueprint.contextId}' (${contextNameOf(blueprint)})` });
@@ -14957,7 +15522,7 @@ function buildPlan(blueprint, opts = {}) {
14957
15522
  summary: `principal '${blueprint.servicePrincipal.externalId}' (SERVICE)`
14958
15523
  });
14959
15524
  for (const ns of blueprint.namespaces ?? []) {
14960
- const ownerNote = `owned by '${blueprint.contextId}'`;
15525
+ const ownerNote = ns.tenantWide ? "TENANT-WIDE \u2014 visible to every context" : `owned by '${blueprint.contextId}'`;
14961
15526
  const entityNote = ns.entityBacked ? ", entity-backed" : "";
14962
15527
  const membershipNote = ns.membershipRecordType ? `, membership: ${ns.membershipRecordType}.${ns.membershipTargetField}` + (ns.membershipLevelField ? ` (levels: ${ns.membershipLevels?.join("/")})` : "") : "";
14963
15528
  steps.push({
@@ -14985,20 +15550,40 @@ function buildPlan(blueprint, opts = {}) {
14985
15550
  summary: `schema '${s.typeName}' (${s.fields.length} field${s.fields.length === 1 ? "" : "s"}, ${indexNote})${lookupNote}${surfaceNote}${sensitiveNote}`
14986
15551
  });
14987
15552
  }
14988
- const actions = blueprint.accessProfile.allowedActions;
14989
- const scopeNote = blueprint.accessProfile.dataScope && Object.keys(blueprint.accessProfile.dataScope).length ? ` dataScope: ${Object.keys(blueprint.accessProfile.dataScope).join(", ")}` : "";
14990
- const capabilityNote = blueprint.accessProfile.capabilities?.length ? ` capabilities: [${blueprint.accessProfile.capabilities.join(", ")}]` : "";
14991
- steps.push({
14992
- kind: "profile",
14993
- summary: `profile scopes [${actions.join(", ")}] (data-plane \u2713)${scopeNote}${capabilityNote}`
14994
- });
14995
15553
  for (const [roleId, clauses] of Object.entries(blueprint.roles ?? {})) {
14996
15554
  const actionSet = [...new Set(clauses.flatMap((c) => c.allowedActions))];
14997
15555
  const capSet = [...new Set(clauses.flatMap((c) => c.capabilities ?? []))];
14998
15556
  const roleCapabilityNote = capSet.length ? `, capabilities [${capSet.join(", ")}]` : "";
15557
+ const clauseLines = clauses.map((c, i) => {
15558
+ const capNote = c.capabilities?.length ? `, capabilities [${c.capabilities.join(", ")}]` : "";
15559
+ return ` clause ${i + 1}: actions [${c.allowedActions.join(", ")}], dataScope: ${renderPlanDataScope(c.dataScope)}${capNote}`;
15560
+ });
15561
+ const assumable = blueprint.roleAssumable?.[roleId];
15562
+ const assumableLine = assumable ? `
15563
+ assumable: ${renderPlanDataScope(assumable)}` : "";
14999
15564
  steps.push({
15000
15565
  kind: "role",
15001
- summary: `role '${roleId}' (${clauses.length} clause${clauses.length === 1 ? "" : "s"}, actions [${actionSet.join(", ")}]${roleCapabilityNote})`
15566
+ summary: `role '${roleId}' (${clauses.length} clause${clauses.length === 1 ? "" : "s"}, actions [${actionSet.join(", ")}]${roleCapabilityNote})
15567
+ ` + clauseLines.join("\n") + assumableLine
15568
+ });
15569
+ }
15570
+ const roleIdsNote = blueprint.accessProfile.roleIds;
15571
+ if (roleIdsNote) {
15572
+ steps.push({
15573
+ kind: "profile",
15574
+ summary: `profile via role(s) [${roleIdsNote.join(", ")}] (composed \u2014 each role's own clauses, concatenated in order)`
15575
+ });
15576
+ } else {
15577
+ const actions = blueprint.accessProfile.allowedActions ?? [];
15578
+ const scopeNote = blueprint.accessProfile.dataScope && Object.keys(blueprint.accessProfile.dataScope).length ? ` dataScope: ${Object.keys(blueprint.accessProfile.dataScope).join(", ")}` : "";
15579
+ const capabilityNote = blueprint.accessProfile.capabilities?.length ? ` capabilities: [${blueprint.accessProfile.capabilities.join(", ")}]` : "";
15580
+ const allDataPlane = !actions.some(
15581
+ (a) => KNOWN_CONTROL_PLANE_RESOURCES.includes(a.split(":", 1)[0])
15582
+ );
15583
+ const planeNote = allDataPlane ? " (data-plane \u2713)" : " (includes a control-plane exception)";
15584
+ steps.push({
15585
+ kind: "profile",
15586
+ summary: `profile scopes [${actions.join(", ")}]${planeNote}${scopeNote}${capabilityNote}`
15002
15587
  });
15003
15588
  }
15004
15589
  steps.push({ kind: "key", summary: `key ${blueprint.name}-<machine> (mint 1 scoped ssk_*)` });
@@ -15021,8 +15606,10 @@ function renderPlan(blueprint, steps) {
15021
15606
  ];
15022
15607
  if (steps.some((s) => s.kind === "issuer")) {
15023
15608
  legendLines.push(
15024
- ` Exception: issuers cannot be updated once registered \u2014 an existing`,
15025
- ` issuer is left unchanged, not reconciled to match.`
15609
+ ` Exception: an issuer's trust-anchor fields (issuer, jwksUri, audience,`,
15610
+ ` contextId) are immutable once registered \u2014 only subClaim/emailClaim/`,
15611
+ ` userinfoUri/selfSignupPolicies actually reconcile on a re-apply (status is not`,
15612
+ ` authorable in a blueprint at all).`
15026
15613
  );
15027
15614
  }
15028
15615
  if (steps.some((s) => s.kind === "namespace")) {
@@ -15031,6 +15618,12 @@ function renderPlan(blueprint, steps) {
15031
15618
  ` an existing registration converges silently, but one that disagrees FAILS`,
15032
15619
  ` the apply rather than reconciling it.`
15033
15620
  );
15621
+ if (steps.some((s) => s.kind === "namespace" && /TENANT-WIDE/.test(s.summary))) {
15622
+ legendLines.push(
15623
+ ` Stricter still for a TENANT-WIDE namespace: it never converges, even on an`,
15624
+ ` exact match \u2014 any existing registration of that name fails the apply.`
15625
+ );
15626
+ }
15034
15627
  }
15035
15628
  const legend = legendLines.join("\n");
15036
15629
  const lines = steps.map((s) => ` + ${s.summary}`);
@@ -15071,6 +15664,11 @@ function buildClaudeCodeAddCommand(entry, serverKey = "vectros") {
15071
15664
  const envFlags = Object.entries(entry.env ?? {}).map(([k, v]) => `-e ${k}=${v}`);
15072
15665
  return ["claude", "mcp", "add", serverKey, ...envFlags, "--", entry.command, ...entry.args].join(" ");
15073
15666
  }
15667
+ function buildRestartHint(blueprintName, isCode) {
15668
+ const target = isCode ? "Reopen Claude Code in this project" : "Restart your MCP client";
15669
+ return ` ${target}, then ask Claude about your '${blueprintName}' data.
15670
+ `;
15671
+ }
15074
15672
  function mergeServerEntry(existing, entry, serverKey = "vectros") {
15075
15673
  const base = existing ? { ...existing } : {};
15076
15674
  const servers = { ...base.mcpServers ?? {} };
@@ -15410,11 +16008,19 @@ var CryptoFileSecretStore = class {
15410
16008
  * sub-millisecond in practice; the budget is generous for a loaded machine)
15411
16009
  * before deciding it is a crash remnant. A SHORT-but-nonempty winner is never
15412
16010
  * a write in flight — the 16 salt bytes land in one write — so it is refused
15413
- * immediately, exactly like the top-of-function read's twin check.
16011
+ * immediately, exactly like the top-of-function read's twin check. A winner
16012
+ * that goes MISSING mid-poll (ENOENT) restarts creation from the top rather
16013
+ * than surfacing the raw ENOENT — see the catch below.
15414
16014
  */
15415
16015
  async adoptSalt(saltPath) {
15416
16016
  for (let attempt = 0; ; attempt++) {
15417
- const winner = await import_node_fs3.promises.readFile(saltPath);
16017
+ let winner;
16018
+ try {
16019
+ winner = await import_node_fs3.promises.readFile(saltPath);
16020
+ } catch (err) {
16021
+ if (err.code === "ENOENT") return this.readOrCreateSalt();
16022
+ throw err;
16023
+ }
15418
16024
  if (winner.length >= 16) return winner;
15419
16025
  if (winner.length > 0) throw corruptSaltError(saltPath, winner.length);
15420
16026
  if (attempt >= 24) break;
@@ -17094,6 +17700,7 @@ function parseBootstrapArgs(argv) {
17094
17700
  useScopedToken: false,
17095
17701
  blankSlate: false,
17096
17702
  noSeed: false,
17703
+ allowTenantWideNamespaces: false,
17097
17704
  client: "desktop",
17098
17705
  tenant: "live",
17099
17706
  help: false
@@ -17155,6 +17762,9 @@ function parseBootstrapArgs(argv) {
17155
17762
  case "--no-seed":
17156
17763
  args.noSeed = true;
17157
17764
  break;
17765
+ case "--allow-tenant-wide-namespaces":
17766
+ args.allowTenantWideNamespaces = true;
17767
+ break;
17158
17768
  case "--tenant": {
17159
17769
  const v = argv[++i];
17160
17770
  if (v !== "test" && v !== "live") {
@@ -17236,15 +17846,6 @@ function reconcileTally(items, noun) {
17236
17846
  if (reconciled) seg.push(`${reconciled} reconciled`);
17237
17847
  return `${items.length} ${noun}${items.length === 1 ? "" : "s"} (${seg.join(", ")})`;
17238
17848
  }
17239
- function issuerTally(items) {
17240
- if (items.length === 0) return null;
17241
- const created = items.filter((i) => i.action === "created").length;
17242
- const unchanged = items.length - created;
17243
- const seg = [];
17244
- if (created) seg.push(`${created} created`);
17245
- if (unchanged) seg.push(`${unchanged} already registered \u2014 unchanged (issuers cannot be updated once registered)`);
17246
- return `${items.length} issuer${items.length === 1 ? "" : "s"} (${seg.join(", ")})`;
17247
- }
17248
17849
  function namespaceTally(items) {
17249
17850
  if (items.length === 0) return null;
17250
17851
  const created = items.filter((i) => i.action === "created").length;
@@ -17258,7 +17859,9 @@ function applyOutcomeSummary(result) {
17258
17859
  const parts = [
17259
17860
  reconcileTally(result.schemaActions, "schema"),
17260
17861
  reconcileTally(result.roleActions, "role"),
17261
- issuerTally(result.issuerActions),
17862
+ // The platform's issuer partial-update endpoint makes an issuer's
17863
+ // 'reconciled' mean the same "updated to match" as every other kind here.
17864
+ reconcileTally(result.issuerActions, "issuer"),
17262
17865
  namespaceTally(result.namespaceActions),
17263
17866
  `profile ${result.profileAction}`
17264
17867
  ].filter((p) => p !== null);
@@ -17291,6 +17894,14 @@ Options:
17291
17894
  sample seed records \u2014 the right choice for a production context
17292
17895
  (a blueprint's seeds are illustrative, not real data). Unlike
17293
17896
  --blank, the schemas are still created.
17897
+ --allow-tenant-wide-namespaces
17898
+ Required to apply a blueprint that declares a namespace with
17899
+ tenantWide: true \u2014 visible to every context in the account, not
17900
+ just this one. Without it, bootstrap refuses before provisioning
17901
+ anything, naming the namespace(s). Only an account OWNER's
17902
+ credential can actually complete the registration either way \u2014
17903
+ this flag doesn't change who's allowed, only whether this run
17904
+ tries. Never needed for a blueprint with no tenantWide namespace.
17294
17905
  --tenant test|live Which tenant the minted key scopes to (default: live).
17295
17906
  'test' provisions into your isolated test tenant \u2014 a
17296
17907
  dry-run before pointing real data at the same blueprint.
@@ -17473,11 +18084,16 @@ ${BOOTSTRAP_HELP}
17473
18084
  }
17474
18085
  throw err;
17475
18086
  }
17476
- const gate = evaluateScopeGate(blueprint.accessProfile.allowedActions);
17477
- process.stdout.write(`
18087
+ const roleNote = blueprint.accessProfile.roleIds ? ` via role(s) [${blueprint.accessProfile.roleIds.join(", ")}]` : "";
18088
+ const acceptedActions = effectiveAccessProfileActions(blueprint);
18089
+ const allDataPlane = !acceptedActions.some(
18090
+ (a) => KNOWN_CONTROL_PLANE_RESOURCES.includes(a.split(":", 1)[0])
18091
+ );
18092
+ process.stdout.write(
18093
+ `
17478
18094
  Blueprint: ${blueprint.name}
17479
- \u2714 Scope gate: [${gate.accepted.join(", ")}] \u2014 all data-plane \u2713
17480
- `);
18095
+ \u2714 Scope gate: [${acceptedActions.join(", ")}]${roleNote}` + (allDataPlane ? " \u2014 all data-plane \u2713\n" : " \u2014 includes a narrow, allowlisted control-plane exception (see above)\n")
18096
+ );
17481
18097
  if (blueprint.accessProfile.capabilities?.length) {
17482
18098
  const capGate = evaluateCapabilityGate(blueprint.accessProfile.capabilities);
17483
18099
  process.stdout.write(` \u2714 Capability gate: [${capGate.accepted.join(", ")}] \u2014 granted \u2713
@@ -17533,6 +18149,7 @@ ${renderPlan(blueprint, planSteps)}
17533
18149
  process.stdout.write(` \u2714 Bootstrap token minted (5-min TTL, ${args.tenant} tenant)
17534
18150
  `);
17535
18151
  const pingIdentity = deps.resolveIdentity ?? resolveIdentity;
18152
+ const provision = deps.provisionContext ?? provisionContext;
17536
18153
  const resolveIdentities = deps.resolveApplyTimeIdentities ?? resolveApplyTimeIdentities;
17537
18154
  const loader = deps.runLoader ?? runLoader;
17538
18155
  const client = new VectrosClient({ token: bridge.token, environment: baseUrl });
@@ -17543,11 +18160,6 @@ ${renderPlan(blueprint, planSteps)}
17543
18160
  "Could not resolve tenantId from /v1/ping \u2014 the extended ping (tenant_id) must be deployed for bootstrap."
17544
18161
  );
17545
18162
  }
17546
- const resolvedBlueprint = await resolveIdentities(
17547
- blueprint,
17548
- client,
17549
- log
17550
- );
17551
18163
  let keyring;
17552
18164
  let keyringOpenError;
17553
18165
  try {
@@ -17622,6 +18234,23 @@ ${renderPlan(blueprint, planSteps)}
17622
18234
  );
17623
18235
  }
17624
18236
  };
18237
+ const remintBootstrapForContext = buildRemintBootstrapForContext({
18238
+ cognitoToken,
18239
+ apiBaseUrl: baseUrl,
18240
+ tenant: args.tenant,
18241
+ useScopedToken: args.useScopedToken,
18242
+ mintBridge
18243
+ });
18244
+ const confirmExistingPrincipal = buildConfirmExistingPrincipal(args, promptCtx.prompt, isTty);
18245
+ const provisioned = await provision(client, {
18246
+ blueprint,
18247
+ allowTenantWideNamespaces: args.allowTenantWideNamespaces,
18248
+ remintBootstrapForContext,
18249
+ readExistingKey,
18250
+ confirmExistingPrincipal,
18251
+ log
18252
+ });
18253
+ const resolvedBlueprint = await resolveIdentities(blueprint, provisioned, client, log);
17625
18254
  const machineId = await resolveMachineId();
17626
18255
  const result = await loader(client, {
17627
18256
  blueprint: resolvedBlueprint,
@@ -17631,20 +18260,14 @@ ${renderPlan(blueprint, planSteps)}
17631
18260
  skipSeed: noSeed,
17632
18261
  readExistingKey,
17633
18262
  persistMintedKey,
17634
- remintBootstrapForContext: buildRemintBootstrapForContext({
17635
- cognitoToken,
17636
- apiBaseUrl: baseUrl,
17637
- tenant: args.tenant,
17638
- useScopedToken: args.useScopedToken,
17639
- mintBridge
17640
- }),
18263
+ provisioned,
18264
+ remintBootstrapForContext,
17641
18265
  remintForContext: buildRemintForContext({
17642
18266
  cognitoToken,
17643
18267
  apiBaseUrl: baseUrl,
17644
18268
  tenant: args.tenant,
17645
18269
  mintContext
17646
18270
  }),
17647
- confirmExistingPrincipal: buildConfirmExistingPrincipal(args, promptCtx.prompt, isTty),
17648
18271
  log
17649
18272
  });
17650
18273
  process.stdout.write(
@@ -17700,10 +18323,13 @@ ${codeAddHint}`);
17700
18323
  }
17701
18324
  try {
17702
18325
  const merged = await safeMergeConfigFile({ configPath, entry });
17703
- const restartHint = isCode ? ' Reopen Claude Code in this project, then try: "list my tasks" / "add a task to \u2026"\n' : ' Restart your MCP client, then try: "list my tasks" / "add a task to \u2026"\n';
18326
+ const restartHint = buildRestartHint(blueprint.name, isCode);
17704
18327
  process.stdout.write(
17705
18328
  ` \u2714 Config ${merged.action}${merged.backupPath ? ` (backup: ${merged.backupPath})` : ""}
17706
- ` + restartHint
18329
+ ` + restartHint + // The `claude mcp add` line is otherwise only shown on --no-merge / a
18330
+ // declined / a failed merge — surface it here too so a successful
18331
+ // auto-merge doesn't hide the idiomatic install command entirely.
18332
+ codeAddHint
17707
18333
  );
17708
18334
  } catch (err) {
17709
18335
  process.stderr.write(` \u26A0 Could not merge config (${err instanceof Error ? err.message : String(err)}).
@@ -17735,9 +18361,18 @@ ${codeAddHint}`);
17735
18361
  return 3;
17736
18362
  }
17737
18363
  if (err instanceof BlueprintIdentityError || err instanceof UnresolvedIdentitiesError) {
18364
+ process.stderr.write(
18365
+ `
18366
+ \u2716 ${err.message}
18367
+ The app-context, service principal, and any declared namespaces were already provisioned. No schemas, profile, roles, key, or seed records were.
18368
+ `
18369
+ );
18370
+ return 2;
18371
+ }
18372
+ if (err instanceof TenantWideNamespacesNotAllowedError) {
17738
18373
  process.stderr.write(`
17739
18374
  \u2716 ${err.message}
17740
- No context or key was provisioned.
18375
+ Nothing was provisioned.
17741
18376
  `);
17742
18377
  return 2;
17743
18378
  }
@@ -17812,11 +18447,14 @@ function assertIdentityOverrideValues(overrides, flag) {
17812
18447
  }
17813
18448
  }
17814
18449
  function placeholderScopeNamespace(value) {
17815
- const m = /^\$\{\{\s*(?:under\.)?self\.scope\.([^\s}]+)\s*\}\}$/.exec(value);
17816
- return m ? m[1] : null;
18450
+ const self2 = /^\$\{\{\s*(?:under\.)?self\.scope\.([^\s}]+)\s*\}\}$/.exec(value);
18451
+ if (self2) return self2[1];
18452
+ const member = MEMBER_SCOPE_PLACEHOLDER_RE2.exec(value);
18453
+ return member ? member[1] : null;
17817
18454
  }
17818
18455
  var SELF_PLACEHOLDER_RE = /^\$\{\{\s*self\.(?:userId|partnerUserId|scope\.[a-z][a-z0-9_-]{1,31})\s*\}\}$/;
17819
- var UNDER_SELF_PLACEHOLDER_RE = /^\$\{\{\s*under\.self\.(?:userId|partnerUserId|scope\.[a-z][a-z0-9_-]{1,31})\s*\}\}$/;
18456
+ var UNDER_SELF_PLACEHOLDER_RE2 = /^\$\{\{\s*under\.self\.(?:userId|partnerUserId|scope\.[a-z][a-z0-9_-]{1,31})\s*\}\}$/;
18457
+ var MEMBER_SCOPE_PLACEHOLDER_RE2 = /^\$\{\{\s*member\.scope\.([a-z][a-z0-9_-]{1,31})(?::[a-z][a-z0-9_-]{1,31})?\s*\}\}$/;
17820
18458
 
17821
18459
  // src/lint.ts
17822
18460
  var ENUM_COMPATIBLE_TYPES = /* @__PURE__ */ new Set(["enum", "string", "string[]"]);
@@ -17875,8 +18513,12 @@ function compositeLegFindings(schema, lf, fieldsById, rangeEnabledFieldNames) {
17875
18513
  function lintBlueprint(bp) {
17876
18514
  const findings = [];
17877
18515
  const typeNames = /* @__PURE__ */ new Set();
18516
+ const expectedDimsByTypeName = /* @__PURE__ */ new Map();
17878
18517
  for (const schema of bp.schemas) {
17879
18518
  typeNames.add(schema.typeName);
18519
+ if (schema.expectedScopeDims && schema.expectedScopeDims.length > 0) {
18520
+ expectedDimsByTypeName.set(schema.typeName, schema.expectedScopeDims);
18521
+ }
17880
18522
  const fieldIds = /* @__PURE__ */ new Set();
17881
18523
  const fieldsById = /* @__PURE__ */ new Map();
17882
18524
  for (const f of schema.fields) {
@@ -17954,9 +18596,12 @@ function lintBlueprint(bp) {
17954
18596
  }
17955
18597
  }
17956
18598
  }
17957
- {
18599
+ if (bp.accessProfile.allowedActions !== void 0) {
17958
18600
  const reached = ownerScopedReachWithoutDataScope(bp.accessProfile.allowedActions, ds);
17959
18601
  if (reached.length > 0) findings.push(unconstrainedReachFinding("accessProfile", reached));
18602
+ findings.push(
18603
+ ...partialCoverageFindings("accessProfile", bp.accessProfile.allowedActions, ds, expectedDimsByTypeName)
18604
+ );
17960
18605
  }
17961
18606
  for (const roleId of Object.keys(bp.roles ?? {})) {
17962
18607
  if (!ROLE_ID_RE.test(roleId)) {
@@ -17974,6 +18619,9 @@ function lintBlueprint(bp) {
17974
18619
  if (reached.length > 0) {
17975
18620
  findings.push(unconstrainedReachFinding(`role '${roleId}' clause ${i}`, reached));
17976
18621
  }
18622
+ findings.push(
18623
+ ...partialCoverageFindings(`role '${roleId}' clause ${i}`, clause.allowedActions, cds, expectedDimsByTypeName)
18624
+ );
17977
18625
  if (!cds) return;
17978
18626
  for (const [key, values] of Object.entries(cds)) {
17979
18627
  for (const value of values) {
@@ -18005,7 +18653,12 @@ function lintBlueprint(bp) {
18005
18653
  }
18006
18654
  var ROLE_ID_RE = /^[a-z][a-z0-9-]{2,30}$/;
18007
18655
  var PLACEHOLDER_SHAPED_RE = /^\s*\$\{\{.*\}\}\s*$/s;
18008
- var RECOGNIZED_PLACEHOLDER_RES = [/^\$\{\{\s*any\s*\}\}$/, SELF_PLACEHOLDER_RE, UNDER_SELF_PLACEHOLDER_RE];
18656
+ var RECOGNIZED_PLACEHOLDER_RES = [
18657
+ /^\$\{\{\s*any\s*\}\}$/,
18658
+ SELF_PLACEHOLDER_RE,
18659
+ UNDER_SELF_PLACEHOLDER_RE2,
18660
+ MEMBER_SCOPE_PLACEHOLDER_RE2
18661
+ ];
18009
18662
  var BLUEPRINT_TOKEN_RE = /^\s*\$\{\{\s*(identities|inputs|vectros|ns)\./;
18010
18663
  function placeholderFinding(where, key, value) {
18011
18664
  if (typeof value !== "string" || !PLACEHOLDER_SHAPED_RE.test(value)) return null;
@@ -18023,7 +18676,7 @@ function placeholderFinding(where, key, value) {
18023
18676
  return {
18024
18677
  severity: "error",
18025
18678
  rule: "datascope-unknown-placeholder",
18026
- 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.`
18679
+ 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.`
18027
18680
  };
18028
18681
  }
18029
18682
  var OWNER_SCOPED_RESOURCES = /* @__PURE__ */ new Set(["records", "documents", "entities"]);
@@ -18045,11 +18698,37 @@ function unconstrainedReachFinding(where, resources) {
18045
18698
  message: `${where}: no dataScope at all, but allowedActions grants r/u/d on ${resources.join("/")} \u2014 this reaches EVERY row of EVERY owner on ${resources.length > 1 ? "those resources" : "that resource"}, not just "your own org" or any other value you may have intended. If that's deliberate (a genuinely shared/tenant-wide resource), you can ignore this; otherwise add a dataScope entry.`
18046
18699
  };
18047
18700
  }
18048
- function lintPasses(findings) {
18049
- return !findings.some((f) => f.severity === "error");
18701
+ function dimToDataScopeKey(dim) {
18702
+ return dim === "userId" ? "userId" : `scope:${dim}`;
18050
18703
  }
18051
-
18052
- // src/blueprint-init.ts
18704
+ function partialCoverageFindings(where, allowedActions, dataScope, expectedDimsByTypeName) {
18705
+ const findings = [];
18706
+ const dataScopeKeys = new Set(Object.keys(dataScope ?? {}));
18707
+ for (const action of allowedActions) {
18708
+ const parts = action.split(":");
18709
+ const [resource, ops] = parts;
18710
+ if (resource !== "records" && resource !== "documents" || !ops) continue;
18711
+ const qualifier = parts.length > 2 ? parts.slice(2).join(":") : void 0;
18712
+ if (qualifier === void 0) continue;
18713
+ const dims = expectedDimsByTypeName.get(qualifier);
18714
+ if (!dims || dims.length === 0) continue;
18715
+ if (!(ops === "*" || [...ops].some((op) => op === "r" || op === "u" || op === "d"))) continue;
18716
+ const covered = dims.filter((d) => dataScopeKeys.has(dimToDataScopeKey(d)));
18717
+ if (covered.length === 0 || covered.length === dims.length) continue;
18718
+ const missing = dims.filter((d) => !covered.includes(d));
18719
+ findings.push({
18720
+ severity: "warning",
18721
+ rule: "partial-datascope-dimension-coverage",
18722
+ message: `${where}: ${action} covers dataScope dim${covered.length > 1 ? "s" : ""} [${covered.join(", ")}] of '${qualifier}'s declared expectedScopeDims [${dims.join(", ")}] \u2014 the ${missing.join("/")} dimension${missing.length > 1 ? "s are" : " is"} UNCONSTRAINED for this grant (every value reaches, not just the one(s) you may have intended). If that's deliberate, you can ignore this; otherwise add a dataScope entry for the missing dimension(s).`
18723
+ });
18724
+ }
18725
+ return findings;
18726
+ }
18727
+ function lintPasses(findings) {
18728
+ return !findings.some((f) => f.severity === "error");
18729
+ }
18730
+
18731
+ // src/blueprint-init.ts
18053
18732
  var import_yaml2 = require("yaml");
18054
18733
  function scaffoldPath(name) {
18055
18734
  return `./${name}.blueprint.yaml`;
@@ -18136,6 +18815,10 @@ function buildScaffold(name, from) {
18136
18815
  }
18137
18816
 
18138
18817
  // src/cli-blueprint.ts
18818
+ function noEffectOnBlueprint(flag, sub) {
18819
+ const cmd = sub ? `blueprint ${sub}` : "blueprint";
18820
+ return `'${flag}' has no effect on '${cmd}' \u2014 blueprint validate/plan/list/init are creds-free and never call the API, so there's no environment/credential to select. Did you mean 'vectros bootstrap'?`;
18821
+ }
18139
18822
  function parseBlueprintArgs(argv) {
18140
18823
  const args = { help: false, force: false, set: [] };
18141
18824
  const positionals = [];
@@ -18162,6 +18845,9 @@ function parseBlueprintArgs(argv) {
18162
18845
  args.values = argv[++i];
18163
18846
  break;
18164
18847
  default:
18848
+ if (AUTH_FLAG_NAMES.includes(a)) {
18849
+ throw new Error(noEffectOnBlueprint(a, positionals[0]));
18850
+ }
18165
18851
  if (a.startsWith("-")) throw new Error(`Unknown blueprint flag: ${a}`);
18166
18852
  positionals.push(a);
18167
18853
  }
@@ -18269,11 +18955,11 @@ async function runValidate(args) {
18269
18955
  }
18270
18956
  const blueprint = await resolveAndValidate(args.file, "validate", supplied);
18271
18957
  if (!blueprint) return 2;
18272
- const gate = evaluateScopeGate(blueprint.accessProfile.allowedActions);
18273
18958
  const capGate = evaluateCapabilityGate(blueprint.accessProfile.capabilities);
18274
18959
  const warnCount = lintBlueprint(blueprint).length;
18960
+ const roleNote = blueprint.accessProfile.roleIds ? ` via role(s) [${blueprint.accessProfile.roleIds.join(", ")}]` : "";
18275
18961
  process.stdout.write(
18276
- ` \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"
18962
+ ` \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"
18277
18963
  );
18278
18964
  return 0;
18279
18965
  }
@@ -18954,6 +19640,51 @@ ${list}
18954
19640
  });
18955
19641
  }
18956
19642
 
19643
+ // src/dev-api.ts
19644
+ var DeveloperApiError = class extends Error {
19645
+ statusCode;
19646
+ constructor(message, statusCode) {
19647
+ super(message);
19648
+ this.name = "DeveloperApiError";
19649
+ this.statusCode = statusCode;
19650
+ }
19651
+ };
19652
+ async function fetchDeveloperUsersPage(opts) {
19653
+ const { cognitoToken, apiBaseUrl, tenant, startFrom, limit, fetchImpl = fetch } = opts;
19654
+ const base = apiBaseUrl.replace(/\/$/, "");
19655
+ let url = `${base}/developer/users?tenant=${encodeURIComponent(tenant)}`;
19656
+ if (startFrom) url += `&startFrom=${encodeURIComponent(startFrom)}`;
19657
+ if (limit !== void 0) url += `&limit=${encodeURIComponent(String(limit))}`;
19658
+ let res;
19659
+ try {
19660
+ res = await fetchImpl(url, {
19661
+ method: "GET",
19662
+ headers: { Authorization: `Bearer ${cognitoToken}` }
19663
+ });
19664
+ } catch (err) {
19665
+ throw new DeveloperApiError(
19666
+ `Request to /developer/users failed: ${err instanceof Error ? err.message : String(err)}`
19667
+ );
19668
+ }
19669
+ if (!res.ok) {
19670
+ const body = await res.text().catch(() => "");
19671
+ const hint = res.status === 403 ? " (this command requires an account owner login \u2014 sign in as the owner and retry)" : "";
19672
+ throw new DeveloperApiError(
19673
+ `GET /developer/users failed: HTTP ${res.status}${hint}${body ? ` \u2014 ${body}` : ""}`,
19674
+ res.status
19675
+ );
19676
+ }
19677
+ let parsed;
19678
+ try {
19679
+ parsed = await res.json();
19680
+ } catch {
19681
+ throw new DeveloperApiError("GET /developer/users returned a non-JSON body");
19682
+ }
19683
+ const data = Array.isArray(parsed.data) ? parsed.data : [];
19684
+ const nextCursor = typeof parsed.nextCursor === "string" ? parsed.nextCursor : null;
19685
+ return { data, nextCursor };
19686
+ }
19687
+
18957
19688
  // src/commands/identity.ts
18958
19689
  function isUserDimension(type) {
18959
19690
  return type === "user";
@@ -19108,6 +19839,57 @@ async function runIdentityList(opts, deps = {}) {
19108
19839
  }
19109
19840
  });
19110
19841
  }
19842
+ function toIdentityView(row) {
19843
+ const str = (v) => typeof v === "string" ? v : void 0;
19844
+ return {
19845
+ id: str(row.id),
19846
+ externalId: str(row.externalId),
19847
+ email: str(row.email),
19848
+ status: str(row.status),
19849
+ type: str(row.type),
19850
+ createdAt: str(row.createdAt)
19851
+ };
19852
+ }
19853
+ async function runIdentityListAll(opts, deps = {}) {
19854
+ const fetchImpl = deps.fetchImpl ?? fetch;
19855
+ return withAuth({ ...opts, provisioning: true }, deps, async (auth) => {
19856
+ try {
19857
+ const tenant = opts.tenant ?? "live";
19858
+ const fetchPage = async (startFrom) => {
19859
+ const page = await fetchDeveloperUsersPage({
19860
+ cognitoToken: auth.cognitoToken,
19861
+ apiBaseUrl: auth.baseUrl,
19862
+ tenant,
19863
+ startFrom,
19864
+ limit: opts.limit,
19865
+ fetchImpl
19866
+ });
19867
+ return { data: page.data.map(toIdentityView), nextCursor: page.nextCursor };
19868
+ };
19869
+ let rows = opts.limit !== void 0 ? (await fetchPage()).data ?? [] : await drainPages(fetchPage);
19870
+ if (opts.externalId) rows = rows.filter((r) => r.externalId === opts.externalId);
19871
+ if (opts.json) {
19872
+ process.stdout.write(`${formatJson(rows)}
19873
+ `);
19874
+ return 0;
19875
+ }
19876
+ process.stdout.write(
19877
+ "\n" + formatTable(rows, [
19878
+ { header: "id", get: (r) => r.id },
19879
+ { header: "external id", get: (r) => r.externalId },
19880
+ { header: "email", get: (r) => r.email },
19881
+ { header: "kind", get: (r) => r.type },
19882
+ { header: "status", get: (r) => r.status }
19883
+ ]) + "\n"
19884
+ );
19885
+ return 0;
19886
+ } catch (err) {
19887
+ process.stderr.write(` \u2716 identity list-all failed: ${formatError(err)}
19888
+ `);
19889
+ return 1;
19890
+ }
19891
+ });
19892
+ }
19111
19893
  async function runIdentityGet(opts, deps = {}) {
19112
19894
  return withAuth(opts, deps, async (auth) => {
19113
19895
  const client = auth.client;
@@ -19144,6 +19926,52 @@ async function runIdentityDelete(opts, deps = {}) {
19144
19926
  }
19145
19927
  });
19146
19928
  }
19929
+ function renderInvite(v) {
19930
+ return formatKeyValues([
19931
+ ["userId", v.userId],
19932
+ ["inviteExpiresAt", v.inviteExpiresAt],
19933
+ ["emailSent", v.emailSent === void 0 ? void 0 : String(v.emailSent)],
19934
+ ["inviteToken", v.inviteToken],
19935
+ ["acceptLink", v.acceptLink]
19936
+ ]);
19937
+ }
19938
+ async function runIdentityInvite(opts, deps = {}) {
19939
+ const sendEmail = !opts.noSendEmail;
19940
+ if (sendEmail && !opts.acceptUrl) {
19941
+ process.stderr.write(
19942
+ " \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"
19943
+ );
19944
+ return 2;
19945
+ }
19946
+ return withAuth(opts, deps, async (auth) => {
19947
+ const client = auth.client;
19948
+ try {
19949
+ const invite = await client.auth.createInvite({
19950
+ email: opts.email,
19951
+ contextId: opts.context,
19952
+ accessProfile: { roleId: opts.role },
19953
+ firstName: opts.firstName,
19954
+ acceptUrl: opts.acceptUrl,
19955
+ sendEmail
19956
+ });
19957
+ if (opts.json) {
19958
+ process.stdout.write(`${formatJson(invite)}
19959
+ `);
19960
+ return 0;
19961
+ }
19962
+ process.stdout.write(
19963
+ `
19964
+ \u2714 Invited '${opts.email}' to '${opts.context}' as '${opts.role}'
19965
+ ${renderInvite(invite)}
19966
+ `
19967
+ );
19968
+ return 0;
19969
+ } catch (err) {
19970
+ process.stderr.write(formatCliError(err, { action: "identity invite failed" }));
19971
+ return 1;
19972
+ }
19973
+ });
19974
+ }
19147
19975
 
19148
19976
  // src/commands/access.ts
19149
19977
  function assertPrincipalShape(principal) {
@@ -19162,20 +19990,24 @@ async function resolvePrincipalArg(raw, auth) {
19162
19990
  }
19163
19991
  return self2.principalId;
19164
19992
  }
19993
+ function roleSummary(p) {
19994
+ if (p.roleIds?.length) return p.roleIds.join(", ");
19995
+ return p.roleId;
19996
+ }
19165
19997
  function renderProfile(p) {
19166
19998
  return formatKeyValues([
19167
19999
  ["context", p.contextId],
19168
20000
  ["principal", p.principalId],
19169
- ["role", p.roleId],
20001
+ ["role", roleSummary(p)],
19170
20002
  ["scopes", p.scopes?.length ? `${p.scopes.length} clause(s)` : void 0],
19171
20003
  ["status", p.status]
19172
20004
  ]);
19173
20005
  }
19174
20006
  async function runAccessGrant(opts, deps = {}) {
19175
- const hasRole = Boolean(opts.role);
20007
+ const hasRole = Boolean(opts.role?.length);
19176
20008
  const hasActions = Boolean(opts.actions);
19177
20009
  if (hasRole === hasActions) {
19178
- process.stderr.write(" \u2716 Provide exactly one of --role <roleId> or --actions <csv>.\n");
20010
+ process.stderr.write(" \u2716 Provide exactly one of --role <roleId> (repeatable) or --actions <csv>.\n");
19179
20011
  return 2;
19180
20012
  }
19181
20013
  if (hasActions && splitCsv(opts.actions).length === 0) {
@@ -19215,7 +20047,11 @@ async function runAccessGrant(opts, deps = {}) {
19215
20047
  upsert: true,
19216
20048
  body: {
19217
20049
  principalId: principal,
19218
- roleId: opts.role,
20050
+ // Always send the plural `roleIds`, even for a single
20051
+ // `--role`: it is a strict superset of the deprecated singular
20052
+ // `roleId` (equivalent to `roleIds: ["<value>"]`), and the platform
20053
+ // reads it uniformly regardless of how many roles compose.
20054
+ roleIds: opts.role,
19219
20055
  scopes,
19220
20056
  identityOverrides
19221
20057
  }
@@ -19227,7 +20063,7 @@ async function runAccessGrant(opts, deps = {}) {
19227
20063
  }
19228
20064
  process.stdout.write(
19229
20065
  `
19230
- \u2714 Granted ${principal} in '${opts.context}'` + (hasRole ? ` via role '${opts.role}'` : ` with ${splitCsv(opts.actions).length} action(s)`) + `
20066
+ \u2714 Granted ${principal} in '${opts.context}'` + (hasRole ? ` via role(s) [${opts.role.join(", ")}]` : ` with ${splitCsv(opts.actions).length} action(s)`) + `
19231
20067
  ${renderProfile(profile)}
19232
20068
  `
19233
20069
  );
@@ -19296,7 +20132,7 @@ async function runAccessList(opts, deps = {}) {
19296
20132
  "\n" + formatTable(rows, [
19297
20133
  { header: "context", get: (r) => r.contextId },
19298
20134
  { header: "principal", get: (r) => r.principalId },
19299
- { header: "role", get: (r) => r.roleId ?? (r.scopes?.length ? "(inline)" : void 0) },
20135
+ { header: "role", get: (r) => roleSummary(r) ?? (r.scopes?.length ? "(inline)" : void 0) },
19300
20136
  { header: "status", get: (r) => r.status }
19301
20137
  ]) + "\n"
19302
20138
  );
@@ -19344,6 +20180,7 @@ ${renderProfile(p)}
19344
20180
  var SELF = /* @__PURE__ */ Symbol("self");
19345
20181
  var ANY = /* @__PURE__ */ Symbol("any");
19346
20182
  var UNDER_SELF = /* @__PURE__ */ Symbol("under.self");
20183
+ var MEMBER = /* @__PURE__ */ Symbol("member.scope");
19347
20184
  function canonicalDim(key) {
19348
20185
  if (key === "userId" || key === "partnerUserId") return "user";
19349
20186
  return key;
@@ -19373,7 +20210,8 @@ function normalizeClauses(raw) {
19373
20210
  if (val === null) return null;
19374
20211
  const s = String(val);
19375
20212
  if (ANY_TOKEN_RE.test(s)) return ANY;
19376
- if (UNDER_SELF_PLACEHOLDER_RE.test(s)) return UNDER_SELF;
20213
+ if (UNDER_SELF_PLACEHOLDER_RE2.test(s)) return UNDER_SELF;
20214
+ if (MEMBER_SCOPE_PLACEHOLDER_RE2.test(s)) return MEMBER;
19377
20215
  return SELF_PLACEHOLDER_RE.test(s) ? SELF : s;
19378
20216
  });
19379
20217
  if (!isRecognizedOwnershipKey(k)) unrecognizedDims.push(k);
@@ -19428,7 +20266,7 @@ function buildArchetypes(clauses) {
19428
20266
  const set = scopeDims.get(dim) ?? /* @__PURE__ */ new Set();
19429
20267
  set.add(SELF);
19430
20268
  for (const v of values) {
19431
- if (v === null || v === ANY || v === UNDER_SELF) continue;
20269
+ if (v === null || v === ANY || v === UNDER_SELF || v === MEMBER) continue;
19432
20270
  set.add(v);
19433
20271
  }
19434
20272
  scopeDims.set(dim, set);
@@ -19455,6 +20293,7 @@ function listAdmits(allowed, recordValue) {
19455
20293
  if (a === SELF) return recordValue === SELF;
19456
20294
  if (a === ANY) return recordValue !== void 0;
19457
20295
  if (a === UNDER_SELF) return false;
20296
+ if (a === MEMBER) return false;
19458
20297
  return recordValue === a;
19459
20298
  });
19460
20299
  }
@@ -19480,7 +20319,7 @@ function undecidableDims(clauses) {
19480
20319
  const dims = /* @__PURE__ */ new Set();
19481
20320
  for (const c of clauses) {
19482
20321
  for (const [dim, values] of Object.entries(c.dataScope)) {
19483
- if (values.includes(UNDER_SELF)) dims.add(dim);
20322
+ if (values.includes(UNDER_SELF) || values.includes(MEMBER)) dims.add(dim);
19484
20323
  }
19485
20324
  }
19486
20325
  return [...dims];
@@ -19513,6 +20352,7 @@ function renderDataScope(ds) {
19513
20352
  if (v === SELF) return "self";
19514
20353
  if (v === ANY) return "any";
19515
20354
  if (v === UNDER_SELF) return "under-self";
20355
+ if (v === MEMBER) return "member";
19516
20356
  if (v === null) return "null";
19517
20357
  return v;
19518
20358
  });
@@ -19544,14 +20384,25 @@ async function runAccessExplain(opts, deps = {}) {
19544
20384
  const profile = await client.auth.getAccessProfile({ contextId: opts.context, principalId: principal });
19545
20385
  let rawClauses = profile.scopes ?? [];
19546
20386
  let via = "inline scopes";
19547
- if (profile.roleId) {
19548
- const role = await client.auth.getRole({ contextId: opts.context, roleId: profile.roleId });
19549
- rawClauses = role.scopes ?? [];
19550
- via = `role '${profile.roleId}'`;
20387
+ const roleIds = profile.roleIds ?? (profile.roleId ? [profile.roleId] : []);
20388
+ if (roleIds.length > 0) {
20389
+ const roleClauses = [];
20390
+ for (const roleId of roleIds) {
20391
+ const role = await client.auth.getRole({ contextId: opts.context, roleId });
20392
+ roleClauses.push(...role.scopes ?? []);
20393
+ }
20394
+ rawClauses = roleClauses;
20395
+ via = roleIds.length === 1 ? `role '${roleIds[0]}'` : `role(s) [${roleIds.join(", ")}]`;
19551
20396
  }
19552
20397
  const clauses = normalizeClauses(rawClauses);
19553
20398
  const rows = explainClauses(clauses);
19554
20399
  const undecidable = undecidableDims(clauses);
20400
+ const hasUnderSelfUndecidable = clauses.some(
20401
+ (c) => Object.values(c.dataScope).some((values) => values.includes(UNDER_SELF))
20402
+ );
20403
+ const hasMemberUndecidable = clauses.some(
20404
+ (c) => Object.values(c.dataScope).some((values) => values.includes(MEMBER))
20405
+ );
19555
20406
  if (opts.json) {
19556
20407
  process.stdout.write(
19557
20408
  `${formatJson({
@@ -19608,10 +20459,7 @@ async function runAccessExplain(opts, deps = {}) {
19608
20459
  ]) + (undecidable.length > 0 ? `
19609
20460
 
19610
20461
  \u26A0 UNDECIDED offline: ${undecidable.join(", ")}
19611
- A clause matching on \`\${{ under.self.* }}\` needs the parent graph, which only the server has,
19612
- so rows granted through it are shown as NO above. Read those cells as "cannot tell here",
19613
- NOT as a denial \u2014 the matrix under-reports this clause.
19614
- ` : "") + '\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'
20462
+ ` + (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'
19615
20463
  );
19616
20464
  return 0;
19617
20465
  } catch (err) {
@@ -19629,7 +20477,8 @@ function renderRole(r) {
19629
20477
  ["name", r.name],
19630
20478
  ["context", r.contextId],
19631
20479
  ["description", r.description],
19632
- ["scopes", r.scopes?.length ? `${r.scopes.length} clause(s)` : void 0]
20480
+ ["scopes", r.scopes?.length ? `${r.scopes.length} clause(s)` : void 0],
20481
+ ["assumable", r.assumable && Object.keys(r.assumable).length ? formatJson(r.assumable) : void 0]
19633
20482
  ]);
19634
20483
  }
19635
20484
  async function runRoleCreate(opts, deps = {}) {
@@ -19644,6 +20493,16 @@ async function runRoleCreate(opts, deps = {}) {
19644
20493
  dataScope = parseJsonObjectFlag("--data-scope", opts.dataScope);
19645
20494
  } catch (err) {
19646
20495
  process.stderr.write(` \u2716 ${formatError(err)}
20496
+ `);
20497
+ return 2;
20498
+ }
20499
+ }
20500
+ let assumable;
20501
+ if (opts.assumable !== void 0) {
20502
+ try {
20503
+ assumable = parseJsonObjectFlag("--assumable", opts.assumable);
20504
+ } catch (err) {
20505
+ process.stderr.write(` \u2716 ${formatError(err)}
19647
20506
  `);
19648
20507
  return 2;
19649
20508
  }
@@ -19661,7 +20520,12 @@ async function runRoleCreate(opts, deps = {}) {
19661
20520
  // stays byte-identical for unscoped roles.
19662
20521
  scopes: [
19663
20522
  dataScope ? { allowed_actions: actions, data_scope: dataScope } : { allowed_actions: actions }
19664
- ]
20523
+ ],
20524
+ // Included only when supplied — omitting it (rather than sending an
20525
+ // empty object) keeps "no assume grant" and "explicitly empty grant"
20526
+ // indistinguishable on the wire, matching the platform's own
20527
+ // "omit for the safe default" contract.
20528
+ assumable
19665
20529
  }
19666
20530
  });
19667
20531
  if (opts.json) {
@@ -19900,6 +20764,390 @@ ${renderContext(ctx)}
19900
20764
  });
19901
20765
  }
19902
20766
 
20767
+ // src/commands/issuers.ts
20768
+ var readline4 = __toESM(require("readline/promises"));
20769
+ function parseSelfSignupPolicies(raw) {
20770
+ if (!raw || raw.length === 0) return void 0;
20771
+ return raw.map((entry) => {
20772
+ const sep = entry.indexOf(":");
20773
+ if (sep <= 0 || sep === entry.length - 1) {
20774
+ throw new Error(
20775
+ `--self-signup-policy must be '<signup_type>:<roleId>' (e.g. 'default:member'); got '${entry}'.`
20776
+ );
20777
+ }
20778
+ return { signup_type: entry.slice(0, sep), role_id: entry.slice(sep + 1) };
20779
+ });
20780
+ }
20781
+ function renderSelfSignup(policies) {
20782
+ if (!policies || policies.length === 0) return "\u2014";
20783
+ return policies.map((p) => `${p.signup_type} \u2192 ${p.role_id}`).join(", ");
20784
+ }
20785
+ function renderIssuer(i) {
20786
+ return formatKeyValues([
20787
+ ["issuerId", i.issuerId],
20788
+ ["issuer", i.issuer],
20789
+ ["jwksUri", i.jwksUri],
20790
+ ["audience", i.audience],
20791
+ ["contextId", i.contextId],
20792
+ ["subClaim", i.subClaim],
20793
+ ["emailClaim", i.emailClaim],
20794
+ ["userinfoUri", i.userinfoUri],
20795
+ ["status", i.status],
20796
+ ["createdAt", i.createdAt],
20797
+ ["selfSignup", renderSelfSignup(i.selfSignupPolicies)]
20798
+ ]);
20799
+ }
20800
+ async function runIssuersList(opts, deps = {}) {
20801
+ return withAuth({ ...opts, provisioning: true, context: opts.context }, deps, async (auth) => {
20802
+ const client = auth.client;
20803
+ try {
20804
+ const rows = await drainPages(
20805
+ (startFrom) => client.auth.listIssuers({ limit: PAGE_LIMIT, ...startFrom ? { startFrom } : {} })
20806
+ );
20807
+ if (opts.json) {
20808
+ process.stdout.write(`${formatJson(rows)}
20809
+ `);
20810
+ return 0;
20811
+ }
20812
+ process.stdout.write(
20813
+ "\n" + formatTable(rows, [
20814
+ { header: "issuerId", get: (i) => i.issuerId },
20815
+ { header: "issuer", get: (i) => i.issuer },
20816
+ { header: "audience", get: (i) => i.audience },
20817
+ { header: "status", get: (i) => i.status },
20818
+ { header: "self-signup", get: (i) => renderSelfSignup(i.selfSignupPolicies) }
20819
+ ]) + "\n"
20820
+ );
20821
+ return 0;
20822
+ } catch (err) {
20823
+ process.stderr.write(formatCliError(err, { action: "issuers list failed" }));
20824
+ return 1;
20825
+ }
20826
+ });
20827
+ }
20828
+ async function runIssuersGet(opts, deps = {}) {
20829
+ return withAuth({ ...opts, provisioning: true, context: opts.context }, deps, async (auth) => {
20830
+ const client = auth.client;
20831
+ try {
20832
+ const issuer = await client.auth.getIssuer({ issuerId: opts.issuerId });
20833
+ if (opts.json) {
20834
+ process.stdout.write(`${formatJson(issuer)}
20835
+ `);
20836
+ return 0;
20837
+ }
20838
+ process.stdout.write(`
20839
+ ${renderIssuer(issuer)}
20840
+ `);
20841
+ return 0;
20842
+ } catch (err) {
20843
+ process.stderr.write(formatCliError(err, { action: "issuers get failed" }));
20844
+ return 1;
20845
+ }
20846
+ });
20847
+ }
20848
+ async function runIssuersRegister(opts, deps = {}) {
20849
+ let selfSignupPolicies;
20850
+ try {
20851
+ selfSignupPolicies = parseSelfSignupPolicies(opts.selfSignupPolicy);
20852
+ } catch (err) {
20853
+ process.stderr.write(` \u2716 ${formatError(err)}
20854
+ `);
20855
+ return 2;
20856
+ }
20857
+ return withAuth({ ...opts, provisioning: true, context: opts.context }, deps, async (auth) => {
20858
+ const client = auth.client;
20859
+ try {
20860
+ const issuer = await client.auth.registerIssuer({
20861
+ issuerId: opts.issuerId,
20862
+ issuer: opts.issuer,
20863
+ jwksUri: opts.jwksUri,
20864
+ audience: opts.audience,
20865
+ contextId: opts.context,
20866
+ subClaim: opts.subClaim,
20867
+ emailClaim: opts.emailClaim,
20868
+ userinfoUri: opts.userinfoUri,
20869
+ selfSignupPolicies
20870
+ });
20871
+ if (opts.json) {
20872
+ process.stdout.write(`${formatJson(issuer)}
20873
+ `);
20874
+ return 0;
20875
+ }
20876
+ const verb = issuer.created === false ? "already registered \u2014 returned unchanged" : "registered";
20877
+ process.stdout.write(`
20878
+ \u2714 issuer '${opts.issuerId}' ${verb}
20879
+ ${renderIssuer(issuer)}
20880
+ `);
20881
+ return 0;
20882
+ } catch (err) {
20883
+ process.stderr.write(formatCliError(err, { action: "issuers register failed" }));
20884
+ return 1;
20885
+ }
20886
+ });
20887
+ }
20888
+ async function runIssuersUpdate(opts, deps = {}) {
20889
+ let selfSignupPolicies;
20890
+ try {
20891
+ selfSignupPolicies = parseSelfSignupPolicies(opts.selfSignupPolicy);
20892
+ } catch (err) {
20893
+ process.stderr.write(` \u2716 ${formatError(err)}
20894
+ `);
20895
+ return 2;
20896
+ }
20897
+ if (opts.clearSelfSignupPolicies && selfSignupPolicies && selfSignupPolicies.length > 0) {
20898
+ process.stderr.write(
20899
+ " \u2716 --clear-self-signup-policies and --self-signup-policy are mutually exclusive.\n"
20900
+ );
20901
+ return 2;
20902
+ }
20903
+ return withAuth({ ...opts, provisioning: true, context: opts.context }, deps, async (auth) => {
20904
+ const client = auth.client;
20905
+ try {
20906
+ const issuer = await client.auth.updateIssuer({
20907
+ issuerId: opts.issuerId,
20908
+ subClaim: opts.subClaim,
20909
+ emailClaim: opts.emailClaim,
20910
+ userinfoUri: opts.userinfoUri,
20911
+ status: opts.status,
20912
+ selfSignupPolicies: opts.clearSelfSignupPolicies ? [] : selfSignupPolicies
20913
+ });
20914
+ if (opts.json) {
20915
+ process.stdout.write(`${formatJson(issuer)}
20916
+ `);
20917
+ return 0;
20918
+ }
20919
+ process.stdout.write(`
20920
+ \u2714 issuer '${opts.issuerId}' updated
20921
+ ${renderIssuer(issuer)}
20922
+ `);
20923
+ return 0;
20924
+ } catch (err) {
20925
+ process.stderr.write(formatCliError(err, { action: "issuers update failed" }));
20926
+ return 1;
20927
+ }
20928
+ });
20929
+ }
20930
+ async function runIssuersDelete(opts, deps = {}) {
20931
+ const { issuerId, context } = opts;
20932
+ if (!opts.force) {
20933
+ const isTty = deps.isTty ?? Boolean(process.stdin.isTTY);
20934
+ if (!isTty) {
20935
+ process.stderr.write(
20936
+ ` \u2716 issuers delete is destructive and needs an interactive confirmation.
20937
+ Re-run with --force to delete '${issuerId}' without a prompt.
20938
+ `
20939
+ );
20940
+ return 2;
20941
+ }
20942
+ const promptCtx = deps.createPrompt ? deps.createPrompt() : (() => {
20943
+ const rl = readline4.createInterface({ input: process.stdin, output: process.stdout });
20944
+ return { prompt: (q) => rl.question(q), close: () => rl.close() };
20945
+ })();
20946
+ try {
20947
+ process.stdout.write(
20948
+ `
20949
+ \u26A0 Deregistering issuer '${issuerId}' from context '${context}' removes it as a token-exchange
20950
+ trust anchor. The platform refuses this while any user was ever created or matched
20951
+ through it \u2014 deactivate those users or register a replacement issuer first if you
20952
+ intend to cut off access.
20953
+
20954
+ `
20955
+ );
20956
+ const typed = await promptCtx.prompt(` Type the issuer id ('${issuerId}') to confirm: `);
20957
+ if (typed.trim() !== issuerId) {
20958
+ process.stderr.write(` \u2716 Confirmation did not match \u2014 nothing deleted.
20959
+ `);
20960
+ return 1;
20961
+ }
20962
+ } finally {
20963
+ promptCtx.close();
20964
+ }
20965
+ }
20966
+ return withAuth({ ...opts, provisioning: true, context }, deps, async (auth) => {
20967
+ const client = auth.client;
20968
+ try {
20969
+ await client.auth.deleteIssuer({ issuerId });
20970
+ if (opts.json) {
20971
+ process.stdout.write(`${formatJson({ issuerId, deleted: true })}
20972
+ `);
20973
+ return 0;
20974
+ }
20975
+ process.stdout.write(`
20976
+ \u2714 issuer '${issuerId}' deregistered from '${context}'
20977
+ `);
20978
+ return 0;
20979
+ } catch (err) {
20980
+ process.stderr.write(formatCliError(err, { action: "issuers delete failed" }));
20981
+ return 1;
20982
+ }
20983
+ });
20984
+ }
20985
+
20986
+ // src/commands/namespace.ts
20987
+ function renderNamespace(n) {
20988
+ return formatKeyValues([
20989
+ ["namespace", n.namespace],
20990
+ ["contextId", n.contextId ?? "(tenant-wide)"],
20991
+ ["entityBacked", n.entityBacked === void 0 ? void 0 : String(n.entityBacked)],
20992
+ ["reserved", n.reserved === void 0 ? void 0 : String(n.reserved)],
20993
+ ["specificityRank", n.specificityRank === void 0 ? void 0 : String(n.specificityRank)],
20994
+ ["defaultSchemaId", n.defaultSchemaId],
20995
+ ["createdAt", n.createdAt]
20996
+ ]);
20997
+ }
20998
+ async function runNamespaceList(opts, deps = {}) {
20999
+ return withAuth({ ...opts, context: opts.context }, deps, async (auth) => {
21000
+ const client = auth.client;
21001
+ try {
21002
+ const rows = await drainPages(
21003
+ (startFrom) => client.identity.listNamespaces({
21004
+ limit: PAGE_LIMIT,
21005
+ ...opts.context ? { contextId: opts.context } : {},
21006
+ ...startFrom ? { startFrom } : {}
21007
+ })
21008
+ );
21009
+ if (opts.json) {
21010
+ process.stdout.write(`${formatJson(rows)}
21011
+ `);
21012
+ return 0;
21013
+ }
21014
+ process.stdout.write(
21015
+ "\n" + formatTable(rows, [
21016
+ { header: "namespace", get: (n) => n.namespace },
21017
+ { header: "contextId", get: (n) => n.contextId ?? "(tenant-wide)" },
21018
+ {
21019
+ header: "entityBacked",
21020
+ get: (n) => n.entityBacked === void 0 ? void 0 : String(n.entityBacked)
21021
+ },
21022
+ {
21023
+ header: "specificityRank",
21024
+ get: (n) => n.specificityRank === void 0 ? void 0 : String(n.specificityRank)
21025
+ }
21026
+ ]) + "\n"
21027
+ );
21028
+ return 0;
21029
+ } catch (err) {
21030
+ process.stderr.write(formatCliError(err, { action: "namespace list failed" }));
21031
+ return 1;
21032
+ }
21033
+ });
21034
+ }
21035
+ async function runNamespaceGet(opts, deps = {}) {
21036
+ return withAuth({ ...opts, context: opts.context }, deps, async (auth) => {
21037
+ const client = auth.client;
21038
+ try {
21039
+ const ns = await client.identity.getNamespace({
21040
+ namespace: opts.namespace,
21041
+ ...opts.context ? { contextId: opts.context } : {}
21042
+ });
21043
+ if (opts.json) {
21044
+ process.stdout.write(`${formatJson(ns)}
21045
+ `);
21046
+ return 0;
21047
+ }
21048
+ process.stdout.write(`
21049
+ ${renderNamespace(ns)}
21050
+ `);
21051
+ return 0;
21052
+ } catch (err) {
21053
+ process.stderr.write(formatCliError(err, { action: "namespace get failed" }));
21054
+ return 1;
21055
+ }
21056
+ });
21057
+ }
21058
+ async function runNamespaceRegister(opts, deps = {}) {
21059
+ return withAuth({ ...opts, provisioning: true, context: opts.context }, deps, async (auth) => {
21060
+ const client = auth.client;
21061
+ try {
21062
+ const ns = await client.identity.registerNamespace({
21063
+ contextId: opts.context,
21064
+ body: {
21065
+ namespace: opts.namespace,
21066
+ specificityRank: opts.specificityRank,
21067
+ entityBacked: opts.entityBacked,
21068
+ defaultSchemaId: opts.defaultSchemaId
21069
+ }
21070
+ });
21071
+ if (opts.json) {
21072
+ process.stdout.write(`${formatJson(ns)}
21073
+ `);
21074
+ return 0;
21075
+ }
21076
+ process.stdout.write(`
21077
+ \u2714 namespace '${opts.namespace}' registered
21078
+ ${renderNamespace(ns)}
21079
+ `);
21080
+ return 0;
21081
+ } catch (err) {
21082
+ process.stderr.write(formatCliError(err, { action: "namespace register failed" }));
21083
+ return 1;
21084
+ }
21085
+ });
21086
+ }
21087
+
21088
+ // src/commands/assume.ts
21089
+ function parseAssumeScopes(raw) {
21090
+ const out = {};
21091
+ for (const entry of raw) {
21092
+ const sep = entry.indexOf(":");
21093
+ if (sep <= 0 || sep === entry.length - 1) {
21094
+ throw new Error(`--scope must be '<namespace>:<value>' (e.g. 'org:orgB'); got '${entry}'.`);
21095
+ }
21096
+ const ns = entry.slice(0, sep);
21097
+ const value = entry.slice(sep + 1);
21098
+ const valueError = valueGrammarError(value);
21099
+ if (valueError) throw new Error(`--scope value ${valueError}`);
21100
+ const key = `scope:${ns}`;
21101
+ if (Object.prototype.hasOwnProperty.call(out, key)) {
21102
+ throw new Error(`--scope names namespace '${ns}' more than once \u2014 pass one value per namespace.`);
21103
+ }
21104
+ out[key] = value;
21105
+ }
21106
+ return out;
21107
+ }
21108
+ function renderAssumeToken(t) {
21109
+ return formatKeyValues([
21110
+ ["access_token", t.access_token],
21111
+ ["token_type", t.token_type],
21112
+ ["expires_in", t.expires_in === void 0 ? void 0 : `${t.expires_in}s`]
21113
+ ]);
21114
+ }
21115
+ async function runAssume(opts, deps = {}) {
21116
+ if (!opts.scope || opts.scope.length === 0) {
21117
+ process.stderr.write(" \u2716 --scope must be given at least once (e.g. --scope org:orgB).\n");
21118
+ return 2;
21119
+ }
21120
+ let scopes;
21121
+ try {
21122
+ scopes = parseAssumeScopes(opts.scope);
21123
+ } catch (err) {
21124
+ process.stderr.write(` \u2716 ${formatError(err)}
21125
+ `);
21126
+ return 2;
21127
+ }
21128
+ return withAuth({ ...opts, context: opts.context }, deps, async (auth) => {
21129
+ const client = auth.client;
21130
+ try {
21131
+ const result = await client.auth.assumeToken(scopes);
21132
+ if (opts.json) {
21133
+ process.stdout.write(`${formatJson(result)}
21134
+ `);
21135
+ return 0;
21136
+ }
21137
+ process.stdout.write(
21138
+ `
21139
+ \u2714 assumed ${Object.keys(scopes).join(", ")}
21140
+ ${renderAssumeToken(result)}
21141
+ `
21142
+ );
21143
+ return 0;
21144
+ } catch (err) {
21145
+ process.stderr.write(formatCliError(err, { action: "assume failed" }));
21146
+ return 1;
21147
+ }
21148
+ });
21149
+ }
21150
+
19903
21151
  // src/commands/login.ts
19904
21152
  var import_node_http = require("http");
19905
21153
  var import_open = __toESM(require("open"));
@@ -20121,7 +21369,7 @@ async function runAssertions(client, bootstrapClient, deps, blueprint, tenantId,
20121
21369
  const assertions = [];
20122
21370
  try {
20123
21371
  const ping = await deps.pingWithKey(result.rawKey);
20124
- const wantActions = blueprint.accessProfile.allowedActions;
21372
+ const wantActions = effectiveAccessProfileActions(blueprint);
20125
21373
  const got = ping.allowedActions ?? [];
20126
21374
  const missing = wantActions.filter((a) => !got.includes(a));
20127
21375
  const ok = ping.principalType === "scoped_key" && ping.tenantId === tenantId && missing.length === 0;
@@ -20355,15 +21603,22 @@ async function runBlueprintHarness(client, input, deps) {
20355
21603
  const readExistingKey = async () => void 0;
20356
21604
  const persistMintedKey = async () => {
20357
21605
  };
21606
+ const provisioned = await provisionContext(client, {
21607
+ blueprint: input.blueprint,
21608
+ remintBootstrapForContext,
21609
+ readExistingKey,
21610
+ log
21611
+ });
20358
21612
  const blueprint = await resolveApplyTimeIdentities(
20359
21613
  input.blueprint,
21614
+ provisioned,
20360
21615
  client,
20361
21616
  log
20362
21617
  );
20363
21618
  const remint = remintForContext ?? (async () => client);
20364
21619
  const remintBootstrap = remintBootstrapForContext ?? (async () => client);
20365
21620
  const hasIssuers = (blueprint.issuers ?? []).length > 0;
20366
- const hasNamespaces = (blueprint.namespaces ?? []).length > 0;
21621
+ const hasNamespaces = provisioned.namespaceActions.length > 0;
20367
21622
  let contextSnapshot;
20368
21623
  if (remintForContext) {
20369
21624
  try {
@@ -20407,6 +21662,7 @@ async function runBlueprintHarness(client, input, deps) {
20407
21662
  readExistingKey,
20408
21663
  persistMintedKey,
20409
21664
  log,
21665
+ provisioned,
20410
21666
  remintForContext,
20411
21667
  remintBootstrapForContext
20412
21668
  });
@@ -20625,12 +21881,13 @@ async function issueOrRotateKey(client, tenantId, context, userId, keyName) {
20625
21881
  return { key: rotated, revokedKeyId: existing?.keyId };
20626
21882
  }
20627
21883
  async function runJoin(opts, deps = {}) {
20628
- const hasRole = Boolean(opts.role);
21884
+ const hasRole = Boolean(opts.role?.length);
20629
21885
  const hasActions = Boolean(opts.actions);
20630
21886
  if (hasRole === hasActions) {
20631
- process.stderr.write(" \u2716 Provide exactly one of --role <roleId> or --actions <csv>.\n");
21887
+ process.stderr.write(" \u2716 Provide exactly one of --role <roleId> (repeatable) or --actions <csv>.\n");
20632
21888
  return 2;
20633
21889
  }
21890
+ const roleDisplay = opts.role?.join(", ");
20634
21891
  if (hasActions && splitCsv(opts.actions).length === 0) {
20635
21892
  process.stderr.write(" \u2716 --actions needs at least one action (an empty/blank list grants nothing).\n");
20636
21893
  return 2;
@@ -20678,7 +21935,10 @@ async function runJoin(opts, deps = {}) {
20678
21935
  upsert: true,
20679
21936
  body: {
20680
21937
  principalId,
20681
- roleId: opts.role,
21938
+ // Always send the plural `roleIds`, even for a single
21939
+ // `--role`: a strict superset of the deprecated singular `roleId`,
21940
+ // read uniformly by the platform regardless of how many compose.
21941
+ roleIds: opts.role,
20682
21942
  scopes,
20683
21943
  identityOverrides
20684
21944
  }
@@ -20697,7 +21957,7 @@ async function runJoin(opts, deps = {}) {
20697
21957
  alias,
20698
21958
  context,
20699
21959
  principalId,
20700
- role: opts.role,
21960
+ role: roleDisplay,
20701
21961
  keyId: existingEntry.keyId,
20702
21962
  reused: true,
20703
21963
  tenant,
@@ -20718,7 +21978,7 @@ async function runJoin(opts, deps = {}) {
20718
21978
  const willActivate = await determineActivateDecision(keyring, { tenant, explicitActivate: opts.activate });
20719
21979
  const { entry } = await storeMintedKey(
20720
21980
  keyring,
20721
- { alias, context, principalId, role: opts.role, keyId: key.keyId, env: auth.env },
21981
+ { alias, context, principalId, role: roleDisplay, keyId: key.keyId, env: auth.env },
20722
21982
  key.rawKey,
20723
21983
  { activate: willActivate, revokedKeyId }
20724
21984
  );
@@ -20726,7 +21986,7 @@ async function runJoin(opts, deps = {}) {
20726
21986
  alias,
20727
21987
  context,
20728
21988
  principalId,
20729
- role: opts.role,
21989
+ role: roleDisplay,
20730
21990
  keyId: entry.keyId,
20731
21991
  reused: false,
20732
21992
  tenant,
@@ -20765,9 +22025,9 @@ function emitJoined(opts) {
20765
22025
 
20766
22026
  // src/build-info.ts
20767
22027
  var BUILD_INFO = {
20768
- cli: true ? "0.16.0" : "dev",
20769
- sdk: true ? "0.40.0" : "dev",
20770
- blueprints: true ? "0.13.0" : "dev"
22028
+ cli: true ? "0.20.0" : "dev",
22029
+ sdk: true ? "0.42.0" : "dev",
22030
+ blueprints: true ? "0.17.0" : "dev"
20771
22031
  };
20772
22032
  function formatBuildInfo() {
20773
22033
  return `vectros ${BUILD_INFO.cli} (sdk ${BUILD_INFO.sdk}, blueprints ${BUILD_INFO.blueprints})`;
@@ -20847,12 +22107,25 @@ function buildProgram(deps = {}) {
20847
22107
  registerAccessCommands(program, deps);
20848
22108
  registerRoleCommands(program, deps);
20849
22109
  registerContextCommands(program, deps);
22110
+ registerIssuersCommands(program, deps);
22111
+ registerNamespaceCommands(program, deps);
22112
+ registerAssumeCommand(program, deps);
20850
22113
  registerJoinCommand(program, deps);
20851
22114
  registerKeyringCommands(program);
20852
22115
  return program;
20853
22116
  }
22117
+ function registerAssumeCommand(program, deps) {
22118
+ const assume = program.command("assume").description("Re-mint your own verb token with one or more identity.<namespace> values switched (needs a role's assumable grant).").requiredOption("--context <c>", "app context to operate in").option("--scope <ns:value>", "namespace value to assume, repeatable (e.g. org:orgB)", collect, []);
22119
+ addCommonAuthOptions(assume).action(
22120
+ async (flags) => {
22121
+ process.exit(
22122
+ await runAssume({ ...toCommonOptions(flags), context: flags.context, scope: flags.scope ?? [] }, deps)
22123
+ );
22124
+ }
22125
+ );
22126
+ }
20854
22127
  function registerJoinCommand(program, deps) {
20855
- 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(
22128
+ const join3 = program.command("join").description("Enroll a principal in a context (grant + key) and keep the key in the keyring.").argument("<context>", "the app context to join").option("--role <roleId>", "reference a Role to grant (repeatable \u2014 2+ compose additively; XOR --actions)", collect, []).option("--actions <csv>", "inline single-clause actions to grant, comma-separated (XOR --role)").option("--as <principal>", "who to enroll: 'me' (default) or an explicit 'usr_<id>'", "me").option("--scope <ns:value>", "identity override: a scope the member acts under (repeatable, e.g. org:<id>)", collect, []).option("--alias <name>", "keyring entry name (defaults to the context id)").option("--tenant <tenant>", "target tenant: 'test' or 'live' (default live)").option(
20856
22129
  "--activate",
20857
22130
  "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)"
20858
22131
  ).option("--no-activate", "store the key without activating it, regardless of tenant");
@@ -20986,11 +22259,145 @@ function registerContextCommands(program, deps) {
20986
22259
  }
20987
22260
  );
20988
22261
  }
22262
+ function registerIssuersCommands(program, deps) {
22263
+ const issuers = program.command("issuers").description("Manage registered third-party IdP issuers (token-exchange trust anchors).");
22264
+ const list = issuers.command("list").description("List issuers registered in one app context.").requiredOption("--context <c>", "app context the issuer(s) are registered under").option("--tenant <tenant>", "tenant to read: 'test' or 'live' (default live)");
22265
+ addCommonAuthOptions(list).action(
22266
+ async (flags) => {
22267
+ const tenant = parseTenantFlag(flags.tenant);
22268
+ process.exit(
22269
+ await runIssuersList({ ...toCommonOptions(flags), context: flags.context, tenant }, deps)
22270
+ );
22271
+ }
22272
+ );
22273
+ 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)");
22274
+ addCommonAuthOptions(get).action(
22275
+ async (issuerId, flags) => {
22276
+ const tenant = parseTenantFlag(flags.tenant);
22277
+ process.exit(
22278
+ await runIssuersGet({ ...toCommonOptions(flags), context: flags.context, issuerId, tenant }, deps)
22279
+ );
22280
+ }
22281
+ );
22282
+ const register = issuers.command("register").description("Register (idempotent by issuerId) a trusted third-party IdP issuer.").requiredOption("--context <c>", "app context an exchanged token targets").requiredOption("--issuer-id <id>", "short slug for this issuer (3-31 chars; lowercase letter + lowercase/digit/dash)").requiredOption("--issuer <url>", "the IdP's iss claim value").requiredOption("--jwks-uri <url>", "the IdP's remote JWKS endpoint").requiredOption("--audience <aud>", "the required aud claim value (globally unique with --issuer)").option("--sub-claim <claim>", "claim carrying the subject identifier (default 'sub')").option("--email-claim <claim>", "claim carrying the subject's email (default 'email')").option("--userinfo-uri <url>", "OIDC userinfo endpoint, used as an email-resolution fallback").option(
22283
+ "--self-signup-policy <type:roleId>",
22284
+ "opt-in self-service signup entry, repeatable (e.g. default:member) \u2014 no entry may target an elevated role",
22285
+ collect,
22286
+ []
22287
+ ).option("--tenant <tenant>", "tenant the context lives in: 'test' or 'live' (default live)");
22288
+ addCommonAuthOptions(register).action(
22289
+ async (flags) => {
22290
+ const tenant = parseTenantFlag(flags.tenant);
22291
+ process.exit(
22292
+ await runIssuersRegister(
22293
+ {
22294
+ ...toCommonOptions(flags),
22295
+ context: flags.context,
22296
+ issuerId: flags.issuerId,
22297
+ issuer: flags.issuer,
22298
+ jwksUri: flags.jwksUri,
22299
+ audience: flags.audience,
22300
+ subClaim: flags.subClaim,
22301
+ emailClaim: flags.emailClaim,
22302
+ userinfoUri: flags.userinfoUri,
22303
+ selfSignupPolicy: flags.selfSignupPolicy,
22304
+ tenant
22305
+ },
22306
+ deps
22307
+ )
22308
+ );
22309
+ }
22310
+ );
22311
+ const update = issuers.command("update").description("Patch the safe, mutable fields of a registered issuer (trust-anchor fields are immutable).").argument("<issuerId>").requiredOption("--context <c>", "app context the issuer is registered under").option("--sub-claim <claim>", "update the subject-identifier claim").option("--email-claim <claim>", "update the subject-email claim").option("--userinfo-uri <url>", "update the OIDC userinfo fallback endpoint").option("--status <status>", "'active' or 'suspended' \u2014 a suspended issuer's tokens are rejected at exchange time").option(
22312
+ "--self-signup-policy <type:roleId>",
22313
+ "replace self-signup policies, repeatable (e.g. default:member) \u2014 omit entirely to leave unchanged",
22314
+ collect,
22315
+ []
22316
+ ).option("--clear-self-signup-policies", "disable self-signup entirely (sends an empty list; XOR --self-signup-policy)").option("--tenant <tenant>", "tenant the context lives in: 'test' or 'live' (default live)");
22317
+ addCommonAuthOptions(update).action(
22318
+ async (issuerId, flags) => {
22319
+ const tenant = parseTenantFlag(flags.tenant);
22320
+ process.exit(
22321
+ await runIssuersUpdate(
22322
+ {
22323
+ ...toCommonOptions(flags),
22324
+ context: flags.context,
22325
+ issuerId,
22326
+ subClaim: flags.subClaim,
22327
+ emailClaim: flags.emailClaim,
22328
+ userinfoUri: flags.userinfoUri,
22329
+ status: flags.status,
22330
+ selfSignupPolicy: flags.selfSignupPolicy,
22331
+ clearSelfSignupPolicies: Boolean(flags.clearSelfSignupPolicies),
22332
+ tenant
22333
+ },
22334
+ deps
22335
+ )
22336
+ );
22337
+ }
22338
+ );
22339
+ const del = issuers.command("delete").description("Deregister a trusted third-party IdP issuer (refused while any user is bound through it).").argument("<issuerId>").requiredOption("--context <c>", "app context the issuer is registered under").option("--force", "skip the interactive confirmation (required when stdin is not a TTY)").option("--tenant <tenant>", "tenant the context lives in: 'test' or 'live' (default live)");
22340
+ addCommonAuthOptions(del).action(
22341
+ async (issuerId, flags) => {
22342
+ const tenant = parseTenantFlag(flags.tenant);
22343
+ process.exit(
22344
+ await runIssuersDelete(
22345
+ { ...toCommonOptions(flags), context: flags.context, issuerId, force: Boolean(flags.force), tenant },
22346
+ deps
22347
+ )
22348
+ );
22349
+ }
22350
+ );
22351
+ }
22352
+ function registerNamespaceCommands(program, deps) {
22353
+ const namespace = program.command("namespace").description("Manage scope-namespace registrations (whether scope:<namespace> values resolve to identity entities).");
22354
+ const register = namespace.command("register").description("Register a new scope namespace (NOT idempotent \u2014 a repeat name 400s).").argument("<name>", "namespace name: 2-32 chars, a lowercase letter first, then lowercase/digit/dash/underscore").requiredOption("--context <c>", "app context to own this registration").requiredOption("--specificity-rank <n>", "position in the account specificity order (integer 0-1000000, unique)").option("--entity-backed", "values must resolve to an identity entity (create via `identity create --type <name>`)").option("--default-schema-id <id>", "default record schema bound to entities in this namespace");
22355
+ addCommonAuthOptions(register).action(
22356
+ async (name, flags) => {
22357
+ const specificityRank = Number(flags.specificityRank);
22358
+ if (!Number.isInteger(specificityRank) || specificityRank < 0 || specificityRank > 1e6) {
22359
+ process.stderr.write(
22360
+ ` \u2716 --specificity-rank must be an integer between 0 and 1000000 (got '${flags.specificityRank}').
22361
+ `
22362
+ );
22363
+ process.exit(2);
22364
+ }
22365
+ process.exit(
22366
+ await runNamespaceRegister(
22367
+ {
22368
+ ...toCommonOptions(flags),
22369
+ namespace: name,
22370
+ context: flags.context,
22371
+ specificityRank,
22372
+ entityBacked: Boolean(flags.entityBacked),
22373
+ defaultSchemaId: flags.defaultSchemaId
22374
+ },
22375
+ deps
22376
+ )
22377
+ );
22378
+ }
22379
+ );
22380
+ const list = namespace.command("list").description("List scope-namespace registrations \u2014 tenant-wide, or one context\u2019s own.").option("--context <c>", "list one app context\u2019s own registrations instead of the tenant-wide set");
22381
+ addCommonAuthOptions(list).action(async (flags) => {
22382
+ process.exit(await runNamespaceList({ ...toCommonOptions(flags), context: flags.context }, deps));
22383
+ });
22384
+ const get = namespace.command("get").description("Show one namespace registration \u2014 tenant-wide, or one context\u2019s own.").argument("<name>").option("--context <c>", "resolve for one app context\u2019s own registration instead of the tenant-wide one");
22385
+ addCommonAuthOptions(get).action(
22386
+ async (name, flags) => {
22387
+ process.exit(
22388
+ await runNamespaceGet({ ...toCommonOptions(flags), namespace: name, context: flags.context }, deps)
22389
+ );
22390
+ }
22391
+ );
22392
+ }
20989
22393
  function registerRoleCommands(program, deps) {
20990
22394
  const role = program.command("role").description("Manage context-scoped roles (reusable scope rules; bind via `access grant --role`).");
20991
22395
  const create = role.command("create").description("Create (idempotent by roleId) a single-clause role from --actions.").requiredOption("--context <c>", "app context").requiredOption("--role-id <id>", "role id (3-31 chars; lowercase letter + lowercase/digit/dash)").requiredOption("--name <name>", "human-readable name").requiredOption("--actions <csv>", "allowed actions, comma-separated (e.g. records:cru,search:r)").option(
20992
22396
  "--data-scope <json>",
20993
22397
  `confine the clause to owned data \u2014 JSON map of dimension \u2192 allowed values, e.g. '{"userId":["\${{ self.userId }}"]}' or '{"scope:org":["<orgId>", null]}'`
22398
+ ).option(
22399
+ "--assumable <json>",
22400
+ `role-level \`vectros assume\` grant \u2014 JSON map of scope:<namespace> \u2192 allowed values (e.g. '{"scope:org":["orgB"]}'); independent of --data-scope, never data_scope's grammar (userId cannot be named)`
20994
22401
  ).option("--description <text>", "free-text description");
20995
22402
  addCommonAuthOptions(create).action(
20996
22403
  async (flags) => {
@@ -21002,6 +22409,7 @@ function registerRoleCommands(program, deps) {
21002
22409
  name: flags.name,
21003
22410
  actions: flags.actions,
21004
22411
  dataScope: flags.dataScope,
22412
+ assumable: flags.assumable,
21005
22413
  description: flags.description
21006
22414
  }, deps)
21007
22415
  );
@@ -21022,7 +22430,7 @@ function registerRoleCommands(program, deps) {
21022
22430
  }
21023
22431
  function registerAccessCommands(program, deps) {
21024
22432
  const access2 = program.command("access").description("Manage access bindings: principal \u2194 context \u2194 role/scopes.");
21025
- 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)");
22433
+ 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)");
21026
22434
  addCommonAuthOptions(grant).action(
21027
22435
  async (flags) => {
21028
22436
  process.exit(
@@ -21095,6 +22503,20 @@ function registerIdentityCommands(program, deps) {
21095
22503
  );
21096
22504
  }
21097
22505
  );
22506
+ const listAll = identity.command("list-all").description(
22507
+ "List every user in the tenant (owner login required), bypassing the app-context confinement 'identity list' is subject to \u2014 for recovering a user created but never granted access anywhere, which 'identity list' can never show."
22508
+ ).option("--external-id <id>", "filter to a stable id").option("--limit <n>", "page size (1\u2013100)", (v) => Number.parseInt(v, 10)).option("--tenant <tenant>", "tenant to list: 'test' or 'live' (default live)");
22509
+ addCommonAuthOptions(listAll).action(
22510
+ async (flags) => {
22511
+ const tenant = parseTenantFlag(flags.tenant);
22512
+ process.exit(
22513
+ await runIdentityListAll(
22514
+ { ...toCommonOptions(flags), externalId: flags.externalId, limit: flags.limit, tenant },
22515
+ deps
22516
+ )
22517
+ );
22518
+ }
22519
+ );
21098
22520
  const get = identity.command("get").description("Show one principal by its Vectros id.").addOption(dimensionOption()).requiredOption("--id <vectrosId>", "the Vectros-assigned id (from create/list)");
21099
22521
  addCommonAuthOptions(get).action(async (flags) => {
21100
22522
  process.exit(await runIdentityGet({ ...toCommonOptions(flags), type: flags.type, id: flags.id }, deps));
@@ -21103,6 +22525,25 @@ function registerIdentityCommands(program, deps) {
21103
22525
  addAuthOptions(del).action(async (flags) => {
21104
22526
  process.exit(await runIdentityDelete({ ...toCommonOptions(flags), type: flags.type, id: flags.id }, deps));
21105
22527
  });
22528
+ 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");
22529
+ addCommonAuthOptions(invite).action(
22530
+ async (flags) => {
22531
+ process.exit(
22532
+ await runIdentityInvite(
22533
+ {
22534
+ ...toCommonOptions(flags),
22535
+ context: flags.context,
22536
+ role: flags.role,
22537
+ email: flags.email,
22538
+ firstName: flags.firstName,
22539
+ acceptUrl: flags.acceptUrl,
22540
+ noSendEmail: flags.sendEmail === false
22541
+ },
22542
+ deps
22543
+ )
22544
+ );
22545
+ }
22546
+ );
21106
22547
  }
21107
22548
  function addKeyDestinationOptions(cmd) {
21108
22549
  return cmd.addOption(new import_commander.Option("--format <fmt>", "how to emit the secret").choices(KEY_FORMATS).default("human")).option(