@vectros-ai/cli 0.15.0 → 0.19.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.
@@ -4435,7 +4435,7 @@ var AuthClient = class {
4435
4435
  );
4436
4436
  }
4437
4437
  /**
4438
- * Creates a new access profile under the given app context. This call is idempotent by `principalId`: if a profile with the same `principalId` already exists, the existing profile is returned (with status 200) instead of creating a duplicate. The response's `created` field (and the HTTP status — 201 when created, 200 when an existing profile was returned) tells the two apart. To overwrite an existing profile's `scopes`/`roleId`, `identityOverrides`, and `status` instead of returning it unchanged, set `?upsert=true` (this also requires the `profiles:u` scope, and applies the same `identityOverrides` bounds the update endpoint documents — a scoped credential may not repoint or clear an identity value it does not itself hold). You must provide exactly one of `scopes` (an inline list of scopes) or `roleId` (a reference to a role); supplying both, or neither, is rejected. `identityOverrides` is keyed by ownership namespace in `scope:<namespace>` form — `scope:org` and `scope:client` for the reserved namespaces, or any namespace you have registered — and may name at most two; any other key (including the account identifier or `userId`) is rejected. If you use a scoped credential, the profile's effective scopes may not exceed your own; a root API key (`sk_`) is exempt. Requires the `profiles:c` scope.
4438
+ * Creates a new access profile under the given app context. This call is idempotent by `principalId`: if a profile with the same `principalId` already exists, the existing profile is returned (with status 200) instead of creating a duplicate. The response's `created` field (and the HTTP status — 201 when created, 200 when an existing profile was returned) tells the two apart. To overwrite an existing profile's `scopes`/`roleId`, `identityOverrides`, and `status` instead of returning it unchanged, set `?upsert=true` (this also requires the `profiles:u` scope, and applies the same `identityOverrides` bounds the update endpoint documents — a scoped credential may not repoint or clear an identity value it does not itself hold). The `principalId` must name a principal that already exists: a `usr_` principal must be a live user in your tenant, so create the user before granting it a profile. A `usr_` id that names no such user is rejected, and no profile is created. `key_` principals are not checked this way. You must provide exactly one of `scopes` (an inline list of scopes) or `roleId` (a reference to a role); supplying both, or neither, is rejected. `identityOverrides` is keyed by ownership namespace in `scope:<namespace>` form — `scope:org` and `scope:client` for the reserved namespaces, or any namespace you have registered — and may name at most two; any other key (including the account identifier or `userId`) is rejected. If you use a scoped credential, the profile's effective scopes may not exceed your own; a root API key (`sk_`) is exempt. Requires the `profiles:c` scope.
4439
4439
  *
4440
4440
  * @param {Vectros.CreateAccessProfileRequest} request
4441
4441
  * @param {AuthClient.RequestOptions} requestOptions - Request-specific configuration.
@@ -5536,7 +5536,7 @@ var AuthClient = class {
5536
5536
  );
5537
5537
  }
5538
5538
  /**
5539
- * Returns full usage detail for the requested calendar month, broken down by category (search, documents, and records) with per-category credit estimates and a split between your live and test environments. Defaults to the current month when `year` and `month` are omitted. Requires the `billing:r` scope on scoped tokens; API keys always have access.
5539
+ * Returns full usage detail for the requested calendar month, broken down by category (search, documents, and records) with per-category credit estimates and a split between your live and test environments. Defaults to the current month when `year` and `month` are omitted. Requires the `billing:r` scope on scoped tokens; API keys always have access. **A token confined to a single app context sees only that context's usage**: totals, the environment split, and the `contexts` breakdown narrow to it, and the environment your context is not bound to is omitted (`null`), not zeroed. Only a token with cross-context reach sees your full account-wide totals. Two exceptions to the narrowing, since they have no per-context breakdown to narrow to: `reads.calls.used`/`reads.dataOut.bytes` (metered per account, not per context) read as `0` for a confined token rather than a narrowed figure — the corresponding overage-credit charge fields narrow correctly; and `credits.limit` stays your whole plan's ceiling, so `credits.remaining` may overstate the account's true remaining room.
5540
5540
  *
5541
5541
  * @param {Vectros.GetUsageRequest} request
5542
5542
  * @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.
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.
@@ -5657,13 +5657,81 @@ var AuthClient = class {
5657
5657
  return handleNonStatusCodeError(_response.error, _response.rawResponse, "GET", "/v1/auth/issuers/{issuerId}");
5658
5658
  }
5659
5659
  /**
5660
- * Deregisters a trusted third-party IdP issuer. Requires a root API key or the bootstrap's provisioning capability. Unconditional — unlike some other registry deletes on this API, this does not check for or block on any downstream reference. Any user account already created via this issuer (by a prior self-signup or accepted invite) is NOT deleted or modified, but that user can no longer obtain a NEW token this way once you deregister the issuer — every `POST /v1/auth/token/exchange` call against it will 404, with no distinction between an unknown issuer and one you just deregistered. Register a replacement issuer, or restore this one, before your users' current tokens expire if you want their access to continue uninterrupted.
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
+ }
5726
+ /**
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
  *
5662
5729
  * @param {Vectros.DeleteIssuerRequest} request
5663
5730
  * @param {AuthClient.RequestOptions} requestOptions - Request-specific configuration.
5664
5731
  *
5665
5732
  * @throws {@link Vectros.ForbiddenError}
5666
5733
  * @throws {@link Vectros.NotFoundError}
5734
+ * @throws {@link Vectros.ConflictError}
5667
5735
  * @throws {@link Vectros.TooManyRequestsError}
5668
5736
  *
5669
5737
  * @example
@@ -5705,6 +5773,8 @@ var AuthClient = class {
5705
5773
  throw new ForbiddenError(_response.error.body, _response.rawResponse);
5706
5774
  case 404:
5707
5775
  throw new NotFoundError(_response.error.body, _response.rawResponse);
5776
+ case 409:
5777
+ throw new ConflictError(_response.error.body, _response.rawResponse);
5708
5778
  case 429:
5709
5779
  throw new TooManyRequestsError(_response.error.body, _response.rawResponse);
5710
5780
  default:
@@ -5723,7 +5793,7 @@ var AuthClient = class {
5723
5793
  );
5724
5794
  }
5725
5795
  /**
5726
- * Returns the issuers registered in your tenant. Requires a root API key or the bootstrap's provisioning capability. 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.
5727
5797
  *
5728
5798
  * @param {Vectros.ListIssuersRequest} request
5729
5799
  * @param {AuthClient.RequestOptions} requestOptions - Request-specific configuration.
@@ -5780,7 +5850,7 @@ var AuthClient = class {
5780
5850
  return handleNonStatusCodeError(_response.error, _response.rawResponse, "GET", "/v1/auth/issuers");
5781
5851
  }
5782
5852
  /**
5783
- * 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.
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`.
5784
5854
  *
5785
5855
  * @param {Vectros.IssuerRequest} request
5786
5856
  * @param {AuthClient.RequestOptions} requestOptions - Request-specific configuration.
@@ -5891,7 +5961,7 @@ var AuthClient = class {
5891
5961
  return handleNonStatusCodeError(_response.error, _response.rawResponse, "GET", "/v1/ping");
5892
5962
  }
5893
5963
  /**
5894
- * Returns the access profiles for the given principal across all of your contexts. Use this to answer questions like "which apps does this user have access to?" — for example, to build a member-access summary. Results are confined to your account. Requires the `profiles:r` scope.
5964
+ * Returns the access profiles for the given principal. Looking up your OWN principal — or holding the `context-directory-read` capability — returns the profiles across ALL of your contexts, letting you answer questions like "which apps does this user have access to?". A context-bound credential looking up a DIFFERENT principal instead sees only that principal's profile in your credential's own context (at most one result), never across contexts it has no authority over. Results are always confined to your account. Requires the `profiles:r` scope.
5895
5965
  *
5896
5966
  * @param {Vectros.ListProfilesForPrincipalRequest} request
5897
5967
  * @param {AuthClient.RequestOptions} requestOptions - Request-specific configuration.
@@ -5965,6 +6035,7 @@ var AuthClient = class {
5965
6035
  *
5966
6036
  * @throws {@link Vectros.BadRequestError}
5967
6037
  * @throws {@link Vectros.ForbiddenError}
6038
+ * @throws {@link Vectros.NotFoundError}
5968
6039
  * @throws {@link Vectros.TooManyRequestsError}
5969
6040
  *
5970
6041
  * @example
@@ -6018,6 +6089,8 @@ var AuthClient = class {
6018
6089
  throw new BadRequestError(_response.error.body, _response.rawResponse);
6019
6090
  case 403:
6020
6091
  throw new ForbiddenError(_response.error.body, _response.rawResponse);
6092
+ case 404:
6093
+ throw new NotFoundError(_response.error.body, _response.rawResponse);
6021
6094
  case 429:
6022
6095
  throw new TooManyRequestsError(_response.error.body, _response.rawResponse);
6023
6096
  default:
@@ -6031,7 +6104,7 @@ var AuthClient = class {
6031
6104
  return handleNonStatusCodeError(_response.error, _response.rawResponse, "POST", "/v1/auth/token");
6032
6105
  }
6033
6106
  /**
6034
- * 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.
6035
6108
  *
6036
6109
  * @param {Vectros.CreateInviteRequest} request
6037
6110
  * @param {AuthClient.RequestOptions} requestOptions - Request-specific configuration.
@@ -6170,7 +6243,84 @@ var AuthClient = class {
6170
6243
  return handleNonStatusCodeError(_response.error, _response.rawResponse, "POST", "/v1/users/invite/resend");
6171
6244
  }
6172
6245
  /**
6173
- * 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. 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.
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
+ }
6322
+ /**
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.
6174
6324
  *
6175
6325
  * @param {Vectros.TokenExchangeRequest} request
6176
6326
  * @param {AuthClient.RequestOptions} requestOptions - Request-specific configuration.
@@ -6246,9 +6396,9 @@ var ComplianceClient = class {
6246
6396
  * @param {Vectros.ErasureRequest} request
6247
6397
  * @param {ComplianceClient.RequestOptions} requestOptions - Request-specific configuration.
6248
6398
  *
6399
+ * @throws {@link Vectros.BadRequestError}
6249
6400
  * @throws {@link Vectros.ForbiddenError}
6250
6401
  * @throws {@link Vectros.TooManyRequestsError}
6251
- * @throws {@link Vectros.NotImplementedError}
6252
6402
  *
6253
6403
  * @example
6254
6404
  * await client.compliance.createErasureRequest({
@@ -6287,12 +6437,12 @@ var ComplianceClient = class {
6287
6437
  }
6288
6438
  if (_response.error.reason === "status-code") {
6289
6439
  switch (_response.error.statusCode) {
6440
+ case 400:
6441
+ throw new BadRequestError(_response.error.body, _response.rawResponse);
6290
6442
  case 403:
6291
6443
  throw new ForbiddenError(_response.error.body, _response.rawResponse);
6292
6444
  case 429:
6293
6445
  throw new TooManyRequestsError(_response.error.body, _response.rawResponse);
6294
- case 501:
6295
- throw new NotImplementedError(_response.error.body, _response.rawResponse);
6296
6446
  default:
6297
6447
  throw new VectrosError({
6298
6448
  statusCode: _response.error.statusCode,
@@ -6311,7 +6461,6 @@ var ComplianceClient = class {
6311
6461
  *
6312
6462
  * @throws {@link Vectros.ForbiddenError}
6313
6463
  * @throws {@link Vectros.NotFoundError}
6314
- * @throws {@link Vectros.NotImplementedError}
6315
6464
  *
6316
6465
  * @example
6317
6466
  * await client.compliance.getErasureRequest({
@@ -6352,8 +6501,6 @@ var ComplianceClient = class {
6352
6501
  throw new ForbiddenError(_response.error.body, _response.rawResponse);
6353
6502
  case 404:
6354
6503
  throw new NotFoundError(_response.error.body, _response.rawResponse);
6355
- case 501:
6356
- throw new NotImplementedError(_response.error.body, _response.rawResponse);
6357
6504
  default:
6358
6505
  throw new VectrosError({
6359
6506
  statusCode: _response.error.statusCode,
@@ -7712,6 +7859,7 @@ var IdentityClient = class {
7712
7859
  * @example
7713
7860
  * await client.identity.listEntities({
7714
7861
  * namespace: "team",
7862
+ * contextId: "myapp",
7715
7863
  * startFrom: "b3BhcXVlLWN1cnNvci1mcm9tLXRoZS1wcmV2aW91cy1wYWdl"
7716
7864
  * })
7717
7865
  */
@@ -7721,6 +7869,7 @@ var IdentityClient = class {
7721
7869
  async __listEntities(request, requestOptions) {
7722
7870
  const {
7723
7871
  namespace,
7872
+ contextId,
7724
7873
  userId,
7725
7874
  externalId,
7726
7875
  scope,
@@ -7735,6 +7884,7 @@ var IdentityClient = class {
7735
7884
  limit
7736
7885
  } = request;
7737
7886
  const _queryParams = {
7887
+ contextId,
7738
7888
  userId,
7739
7889
  externalId,
7740
7890
  scope,
@@ -7793,6 +7943,7 @@ var IdentityClient = class {
7793
7943
  * @example
7794
7944
  * await client.identity.createEntity({
7795
7945
  * namespace: "team",
7946
+ * contextId: "myapp",
7796
7947
  * body: {
7797
7948
  * externalId: "team_eng_platform"
7798
7949
  * }
@@ -7802,9 +7953,10 @@ var IdentityClient = class {
7802
7953
  return HttpResponsePromise.fromPromise(this.__createEntity(request, requestOptions));
7803
7954
  }
7804
7955
  async __createEntity(request, requestOptions) {
7805
- const { namespace, upsert, body: _body } = request;
7956
+ const { namespace, upsert, contextId, body: _body } = request;
7806
7957
  const _queryParams = {
7807
- upsert
7958
+ upsert,
7959
+ contextId
7808
7960
  };
7809
7961
  const _authRequest = await this._options.authProvider.getAuthRequest();
7810
7962
  const _headers = mergeHeaders(
@@ -7861,14 +8013,18 @@ var IdentityClient = class {
7861
8013
  * @example
7862
8014
  * await client.identity.getEntity({
7863
8015
  * namespace: "team",
7864
- * id: "6ba7b810-9dad-11d1-80b4-00c04fd430c8"
8016
+ * id: "6ba7b810-9dad-11d1-80b4-00c04fd430c8",
8017
+ * contextId: "myapp"
7865
8018
  * })
7866
8019
  */
7867
8020
  getEntity(request, requestOptions) {
7868
8021
  return HttpResponsePromise.fromPromise(this.__getEntity(request, requestOptions));
7869
8022
  }
7870
8023
  async __getEntity(request, requestOptions) {
7871
- const { namespace, id } = request;
8024
+ const { namespace, id, contextId } = request;
8025
+ const _queryParams = {
8026
+ contextId
8027
+ };
7872
8028
  const _authRequest = await this._options.authProvider.getAuthRequest();
7873
8029
  const _headers = mergeHeaders(
7874
8030
  _authRequest.headers,
@@ -7882,7 +8038,7 @@ var IdentityClient = class {
7882
8038
  ),
7883
8039
  method: "GET",
7884
8040
  headers: _headers,
7885
- queryString: url_exports.queryBuilder().mergeAdditional(requestOptions?.queryParams).build(),
8041
+ queryString: url_exports.queryBuilder().addMany(_queryParams).mergeAdditional(requestOptions?.queryParams).build(),
7886
8042
  timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1e3,
7887
8043
  maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries,
7888
8044
  abortSignal: requestOptions?.abortSignal,
@@ -7919,6 +8075,7 @@ var IdentityClient = class {
7919
8075
  * await client.identity.updateEntity({
7920
8076
  * namespace: "team",
7921
8077
  * id: "id",
8078
+ * contextId: "myapp",
7922
8079
  * body: {
7923
8080
  * externalId: "team_eng_platform"
7924
8081
  * }
@@ -7928,7 +8085,10 @@ var IdentityClient = class {
7928
8085
  return HttpResponsePromise.fromPromise(this.__updateEntity(request, requestOptions));
7929
8086
  }
7930
8087
  async __updateEntity(request, requestOptions) {
7931
- const { namespace, id, body: _body } = request;
8088
+ const { namespace, id, contextId, body: _body } = request;
8089
+ const _queryParams = {
8090
+ contextId
8091
+ };
7932
8092
  const _authRequest = await this._options.authProvider.getAuthRequest();
7933
8093
  const _headers = mergeHeaders(
7934
8094
  _authRequest.headers,
@@ -7943,7 +8103,7 @@ var IdentityClient = class {
7943
8103
  method: "PUT",
7944
8104
  headers: _headers,
7945
8105
  contentType: "application/json",
7946
- queryString: url_exports.queryBuilder().mergeAdditional(requestOptions?.queryParams).build(),
8106
+ queryString: url_exports.queryBuilder().addMany(_queryParams).mergeAdditional(requestOptions?.queryParams).build(),
7947
8107
  requestType: "json",
7948
8108
  body: _body,
7949
8109
  timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1e3,
@@ -7983,14 +8143,18 @@ var IdentityClient = class {
7983
8143
  * @example
7984
8144
  * await client.identity.deleteEntity({
7985
8145
  * namespace: "team",
7986
- * id: "id"
8146
+ * id: "id",
8147
+ * contextId: "myapp"
7987
8148
  * })
7988
8149
  */
7989
8150
  deleteEntity(request, requestOptions) {
7990
8151
  return HttpResponsePromise.fromPromise(this.__deleteEntity(request, requestOptions));
7991
8152
  }
7992
8153
  async __deleteEntity(request, requestOptions) {
7993
- const { namespace, id } = request;
8154
+ const { namespace, id, contextId } = request;
8155
+ const _queryParams = {
8156
+ contextId
8157
+ };
7994
8158
  const _authRequest = await this._options.authProvider.getAuthRequest();
7995
8159
  const _headers = mergeHeaders(
7996
8160
  _authRequest.headers,
@@ -8004,7 +8168,7 @@ var IdentityClient = class {
8004
8168
  ),
8005
8169
  method: "DELETE",
8006
8170
  headers: _headers,
8007
- queryString: url_exports.queryBuilder().mergeAdditional(requestOptions?.queryParams).build(),
8171
+ queryString: url_exports.queryBuilder().addMany(_queryParams).mergeAdditional(requestOptions?.queryParams).build(),
8008
8172
  timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1e3,
8009
8173
  maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries,
8010
8174
  abortSignal: requestOptions?.abortSignal,
@@ -8047,6 +8211,7 @@ var IdentityClient = class {
8047
8211
  * @example
8048
8212
  * await client.identity.lookupEntities({
8049
8213
  * namespace: "team",
8214
+ * contextId: "myapp",
8050
8215
  * body: {
8051
8216
  * type: "person_v1",
8052
8217
  * field: "ssn"
@@ -8057,7 +8222,10 @@ var IdentityClient = class {
8057
8222
  return HttpResponsePromise.fromPromise(this.__lookupEntities(request, requestOptions));
8058
8223
  }
8059
8224
  async __lookupEntities(request, requestOptions) {
8060
- const { namespace, body: _body } = request;
8225
+ const { namespace, contextId, body: _body } = request;
8226
+ const _queryParams = {
8227
+ contextId
8228
+ };
8061
8229
  const _authRequest = await this._options.authProvider.getAuthRequest();
8062
8230
  const _headers = mergeHeaders(
8063
8231
  _authRequest.headers,
@@ -8072,7 +8240,7 @@ var IdentityClient = class {
8072
8240
  method: "POST",
8073
8241
  headers: _headers,
8074
8242
  contentType: "application/json",
8075
- queryString: url_exports.queryBuilder().mergeAdditional(requestOptions?.queryParams).build(),
8243
+ queryString: url_exports.queryBuilder().addMany(_queryParams).mergeAdditional(requestOptions?.queryParams).build(),
8076
8244
  requestType: "json",
8077
8245
  body: _body,
8078
8246
  timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1e3,
@@ -8116,15 +8284,17 @@ var IdentityClient = class {
8116
8284
  * @example
8117
8285
  * await client.identity.getEntityVersions({
8118
8286
  * namespace: "team",
8119
- * id: "id"
8287
+ * id: "id",
8288
+ * contextId: "myapp"
8120
8289
  * })
8121
8290
  */
8122
8291
  getEntityVersions(request, requestOptions) {
8123
8292
  return HttpResponsePromise.fromPromise(this.__getEntityVersions(request, requestOptions));
8124
8293
  }
8125
8294
  async __getEntityVersions(request, requestOptions) {
8126
- const { namespace, id, startFrom } = request;
8295
+ const { namespace, id, contextId, startFrom } = request;
8127
8296
  const _queryParams = {
8297
+ contextId,
8128
8298
  startFrom
8129
8299
  };
8130
8300
  const _authRequest = await this._options.authProvider.getAuthRequest();
@@ -8170,7 +8340,7 @@ var IdentityClient = class {
8170
8340
  );
8171
8341
  }
8172
8342
  /**
8173
- * Retrieves a single scope-namespace registration by name. The reserved built-ins `org` and `client` are always resolvable.
8343
+ * Retrieves a single scope-namespace registration by name.
8174
8344
  *
8175
8345
  * @param {Vectros.GetNamespaceRequest} request
8176
8346
  * @param {IdentityClient.RequestOptions} requestOptions - Request-specific configuration.
@@ -8186,7 +8356,10 @@ var IdentityClient = class {
8186
8356
  return HttpResponsePromise.fromPromise(this.__getNamespace(request, requestOptions));
8187
8357
  }
8188
8358
  async __getNamespace(request, requestOptions) {
8189
- const { namespace } = request;
8359
+ const { namespace, contextId } = request;
8360
+ const _queryParams = {
8361
+ contextId
8362
+ };
8190
8363
  const _authRequest = await this._options.authProvider.getAuthRequest();
8191
8364
  const _headers = mergeHeaders(
8192
8365
  _authRequest.headers,
@@ -8200,7 +8373,7 @@ var IdentityClient = class {
8200
8373
  ),
8201
8374
  method: "GET",
8202
8375
  headers: _headers,
8203
- queryString: url_exports.queryBuilder().mergeAdditional(requestOptions?.queryParams).build(),
8376
+ queryString: url_exports.queryBuilder().addMany(_queryParams).mergeAdditional(requestOptions?.queryParams).build(),
8204
8377
  timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1e3,
8205
8378
  maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries,
8206
8379
  abortSignal: requestOptions?.abortSignal,
@@ -8225,7 +8398,7 @@ var IdentityClient = class {
8225
8398
  return handleNonStatusCodeError(_response.error, _response.rawResponse, "GET", "/v1/namespaces/{namespace}");
8226
8399
  }
8227
8400
  /**
8228
- * Updates the mutable fields (`entityBacked`, `defaultSchemaId`, `specificityRank`) of a registered namespace. The namespace name itself is immutable. Requires a root API key. The reserved built-ins cannot be updated.
8401
+ * Updates the mutable fields (`entityBacked`, `defaultSchemaId`, `specificityRank`) of a registered namespace. The namespace name and its `contextId` (which row is selected) are both immutable. Requires a root API key. `org` and `client` are updatable like any other namespace — there is no reserved-built-in exception.
8229
8402
  *
8230
8403
  * @param {Vectros.UpdateNamespaceRequest} request
8231
8404
  * @param {IdentityClient.RequestOptions} requestOptions - Request-specific configuration.
@@ -8247,7 +8420,10 @@ var IdentityClient = class {
8247
8420
  return HttpResponsePromise.fromPromise(this.__updateNamespace(request, requestOptions));
8248
8421
  }
8249
8422
  async __updateNamespace(request, requestOptions) {
8250
- const { namespace, body: _body } = request;
8423
+ const { namespace, contextId, body: _body } = request;
8424
+ const _queryParams = {
8425
+ contextId
8426
+ };
8251
8427
  const _authRequest = await this._options.authProvider.getAuthRequest();
8252
8428
  const _headers = mergeHeaders(
8253
8429
  _authRequest.headers,
@@ -8262,7 +8438,7 @@ var IdentityClient = class {
8262
8438
  method: "PUT",
8263
8439
  headers: _headers,
8264
8440
  contentType: "application/json",
8265
- queryString: url_exports.queryBuilder().mergeAdditional(requestOptions?.queryParams).build(),
8441
+ queryString: url_exports.queryBuilder().addMany(_queryParams).mergeAdditional(requestOptions?.queryParams).build(),
8266
8442
  requestType: "json",
8267
8443
  body: _body,
8268
8444
  timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1e3,
@@ -8293,7 +8469,7 @@ var IdentityClient = class {
8293
8469
  return handleNonStatusCodeError(_response.error, _response.rawResponse, "PUT", "/v1/namespaces/{namespace}");
8294
8470
  }
8295
8471
  /**
8296
- * Deletes a registered scope namespace. Requires a root API key. The reserved built-ins cannot be deleted. A namespace that still has entities cannot be deleted (409) — delete its entities first; this keeps them reachable by the account-teardown and erasure sweeps.
8472
+ * Deletes a registered scope namespace. Requires a root API key. `org` and `client` are deletable like any other namespace — there is no reserved-built-in exception. A namespace that still has entities cannot be deleted (409) — delete its entities first; this keeps them reachable by the account-teardown and erasure sweeps.
8297
8473
  *
8298
8474
  * @param {Vectros.DeleteNamespaceRequest} request
8299
8475
  * @param {IdentityClient.RequestOptions} requestOptions - Request-specific configuration.
@@ -8311,7 +8487,10 @@ var IdentityClient = class {
8311
8487
  return HttpResponsePromise.fromPromise(this.__deleteNamespace(request, requestOptions));
8312
8488
  }
8313
8489
  async __deleteNamespace(request, requestOptions) {
8314
- const { namespace } = request;
8490
+ const { namespace, contextId } = request;
8491
+ const _queryParams = {
8492
+ contextId
8493
+ };
8315
8494
  const _authRequest = await this._options.authProvider.getAuthRequest();
8316
8495
  const _headers = mergeHeaders(
8317
8496
  _authRequest.headers,
@@ -8325,7 +8504,7 @@ var IdentityClient = class {
8325
8504
  ),
8326
8505
  method: "DELETE",
8327
8506
  headers: _headers,
8328
- queryString: url_exports.queryBuilder().mergeAdditional(requestOptions?.queryParams).build(),
8507
+ queryString: url_exports.queryBuilder().addMany(_queryParams).mergeAdditional(requestOptions?.queryParams).build(),
8329
8508
  timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1e3,
8330
8509
  maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries,
8331
8510
  abortSignal: requestOptions?.abortSignal,
@@ -8354,7 +8533,7 @@ var IdentityClient = class {
8354
8533
  return handleNonStatusCodeError(_response.error, _response.rawResponse, "DELETE", "/v1/namespaces/{namespace}");
8355
8534
  }
8356
8535
  /**
8357
- * Returns the scope namespaces registered in your account, with the reserved built-ins `org` and `client` listed first. Returns a `{data, nextCursor}` envelope.
8536
+ * Returns the scope namespaces registered in your account. Returns a `{data, nextCursor}` envelope.
8358
8537
  *
8359
8538
  * @param {Vectros.ListNamespacesRequest} request
8360
8539
  * @param {IdentityClient.RequestOptions} requestOptions - Request-specific configuration.
@@ -8366,10 +8545,11 @@ var IdentityClient = class {
8366
8545
  return HttpResponsePromise.fromPromise(this.__listNamespaces(request, requestOptions));
8367
8546
  }
8368
8547
  async __listNamespaces(request = {}, requestOptions) {
8369
- const { startFrom, limit } = request;
8548
+ const { startFrom, limit, contextId } = request;
8370
8549
  const _queryParams = {
8371
8550
  startFrom,
8372
- limit
8551
+ limit,
8552
+ contextId
8373
8553
  };
8374
8554
  const _authRequest = await this._options.authProvider.getAuthRequest();
8375
8555
  const _headers = mergeHeaders(
@@ -8404,9 +8584,9 @@ var IdentityClient = class {
8404
8584
  return handleNonStatusCodeError(_response.error, _response.rawResponse, "GET", "/v1/namespaces");
8405
8585
  }
8406
8586
  /**
8407
- * 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. The reserved names `org` and `client` are built in and cannot be registered.
8587
+ * 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.
8408
8588
  *
8409
- * @param {Vectros.NamespaceRequest} request
8589
+ * @param {Vectros.RegisterNamespaceRequest} request
8410
8590
  * @param {IdentityClient.RequestOptions} requestOptions - Request-specific configuration.
8411
8591
  *
8412
8592
  * @throws {@link Vectros.BadRequestError}
@@ -8415,14 +8595,20 @@ var IdentityClient = class {
8415
8595
  *
8416
8596
  * @example
8417
8597
  * await client.identity.registerNamespace({
8418
- * namespace: "team",
8419
- * specificityRank: 1500
8598
+ * body: {
8599
+ * namespace: "team",
8600
+ * specificityRank: 1500
8601
+ * }
8420
8602
  * })
8421
8603
  */
8422
8604
  registerNamespace(request, requestOptions) {
8423
8605
  return HttpResponsePromise.fromPromise(this.__registerNamespace(request, requestOptions));
8424
8606
  }
8425
8607
  async __registerNamespace(request, requestOptions) {
8608
+ const { contextId, body: _body } = request;
8609
+ const _queryParams = {
8610
+ contextId
8611
+ };
8426
8612
  const _authRequest = await this._options.authProvider.getAuthRequest();
8427
8613
  const _headers = mergeHeaders(
8428
8614
  _authRequest.headers,
@@ -8437,9 +8623,9 @@ var IdentityClient = class {
8437
8623
  method: "POST",
8438
8624
  headers: _headers,
8439
8625
  contentType: "application/json",
8440
- queryString: url_exports.queryBuilder().mergeAdditional(requestOptions?.queryParams).build(),
8626
+ queryString: url_exports.queryBuilder().addMany(_queryParams).mergeAdditional(requestOptions?.queryParams).build(),
8441
8627
  requestType: "json",
8442
- body: request,
8628
+ body: _body,
8443
8629
  timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1e3,
8444
8630
  maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries,
8445
8631
  abortSignal: requestOptions?.abortSignal,
@@ -8468,7 +8654,7 @@ var IdentityClient = class {
8468
8654
  return handleNonStatusCodeError(_response.error, _response.rawResponse, "POST", "/v1/namespaces");
8469
8655
  }
8470
8656
  /**
8471
- * 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.
8657
+ * 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.
8472
8658
  *
8473
8659
  * @param {Vectros.ListUsersRequest} request
8474
8660
  * @param {IdentityClient.RequestOptions} requestOptions - Request-specific configuration.
@@ -8533,7 +8719,7 @@ var IdentityClient = class {
8533
8719
  return handleNonStatusCodeError(_response.error, _response.rawResponse, "GET", "/v1/users");
8534
8720
  }
8535
8721
  /**
8536
- * 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.
8722
+ * 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`.
8537
8723
  *
8538
8724
  * @param {Vectros.CreateUserRequest} request
8539
8725
  * @param {IdentityClient.RequestOptions} requestOptions - Request-specific configuration.
@@ -8657,7 +8843,7 @@ var IdentityClient = class {
8657
8843
  return handleNonStatusCodeError(_response.error, _response.rawResponse, "GET", "/v1/users/{id}");
8658
8844
  }
8659
8845
  /**
8660
- * 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.
8846
+ * 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.
8661
8847
  *
8662
8848
  * @param {Vectros.UpdateUserRequest} request
8663
8849
  * @param {IdentityClient.RequestOptions} requestOptions - Request-specific configuration.
@@ -8724,12 +8910,13 @@ var IdentityClient = class {
8724
8910
  return handleNonStatusCodeError(_response.error, _response.rawResponse, "PUT", "/v1/users/{id}");
8725
8911
  }
8726
8912
  /**
8727
- * 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.
8913
+ * 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.
8728
8914
  *
8729
8915
  * @param {Vectros.DeleteUserRequest} request
8730
8916
  * @param {IdentityClient.RequestOptions} requestOptions - Request-specific configuration.
8731
8917
  *
8732
8918
  * @throws {@link Vectros.NotFoundError}
8919
+ * @throws {@link Vectros.ConflictError}
8733
8920
  * @throws {@link Vectros.TooManyRequestsError}
8734
8921
  *
8735
8922
  * @example
@@ -8769,6 +8956,8 @@ var IdentityClient = class {
8769
8956
  switch (_response.error.statusCode) {
8770
8957
  case 404:
8771
8958
  throw new NotFoundError(_response.error.body, _response.rawResponse);
8959
+ case 409:
8960
+ throw new ConflictError(_response.error.body, _response.rawResponse);
8772
8961
  case 429:
8773
8962
  throw new TooManyRequestsError(_response.error.body, _response.rawResponse);
8774
8963
  default:
@@ -8782,7 +8971,7 @@ var IdentityClient = class {
8782
8971
  return handleNonStatusCodeError(_response.error, _response.rawResponse, "DELETE", "/v1/users/{id}");
8783
8972
  }
8784
8973
  /**
8785
- * 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.
8974
+ * 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.
8786
8975
  *
8787
8976
  * @param {Vectros.IdentityLookupRequest} request
8788
8977
  * @param {IdentityClient.RequestOptions} requestOptions - Request-specific configuration.
@@ -9225,14 +9414,14 @@ var RecordsClient = class {
9225
9414
  this._options = normalizeClientOptionsWithAuth(options);
9226
9415
  }
9227
9416
  /**
9228
- * 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.
9417
+ * 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).
9229
9418
  *
9230
9419
  * @param {Vectros.BatchGetRequest} request
9231
9420
  * @param {RecordsClient.RequestOptions} requestOptions - Request-specific configuration.
9232
9421
  *
9422
+ * @throws {@link Vectros.BadRequestError}
9233
9423
  * @throws {@link Vectros.ForbiddenError}
9234
9424
  * @throws {@link Vectros.TooManyRequestsError}
9235
- * @throws {@link Vectros.NotImplementedError}
9236
9425
  *
9237
9426
  * @example
9238
9427
  * await client.records.batchGetRecords()
@@ -9269,12 +9458,12 @@ var RecordsClient = class {
9269
9458
  }
9270
9459
  if (_response.error.reason === "status-code") {
9271
9460
  switch (_response.error.statusCode) {
9461
+ case 400:
9462
+ throw new BadRequestError(_response.error.body, _response.rawResponse);
9272
9463
  case 403:
9273
9464
  throw new ForbiddenError(_response.error.body, _response.rawResponse);
9274
9465
  case 429:
9275
9466
  throw new TooManyRequestsError(_response.error.body, _response.rawResponse);
9276
- case 501:
9277
- throw new NotImplementedError(_response.error.body, _response.rawResponse);
9278
9467
  default:
9279
9468
  throw new VectrosError({
9280
9469
  statusCode: _response.error.statusCode,
@@ -10088,7 +10277,7 @@ var SchemasClient = class {
10088
10277
  this._options = normalizeClientOptionsWithAuth(options);
10089
10278
  }
10090
10279
  /**
10091
- * 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.
10280
+ * 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.
10092
10281
  *
10093
10282
  * @param {Vectros.ListSchemasRequest} request
10094
10283
  * @param {SchemasClient.RequestOptions} requestOptions - Request-specific configuration.
@@ -10376,6 +10565,7 @@ var SchemasClient = class {
10376
10565
  * @param {Vectros.DeleteSchemaRequest} request
10377
10566
  * @param {SchemasClient.RequestOptions} requestOptions - Request-specific configuration.
10378
10567
  *
10568
+ * @throws {@link Vectros.ForbiddenError}
10379
10569
  * @throws {@link Vectros.NotFoundError}
10380
10570
  * @throws {@link Vectros.ConflictError}
10381
10571
  * @throws {@link Vectros.TooManyRequestsError}
@@ -10415,6 +10605,8 @@ var SchemasClient = class {
10415
10605
  }
10416
10606
  if (_response.error.reason === "status-code") {
10417
10607
  switch (_response.error.statusCode) {
10608
+ case 403:
10609
+ throw new ForbiddenError(_response.error.body, _response.rawResponse);
10418
10610
  case 404:
10419
10611
  throw new NotFoundError(_response.error.body, _response.rawResponse);
10420
10612
  case 409:
@@ -10851,10 +11043,58 @@ var BlueprintSchemaSchema = import_zod.z.object({
10851
11043
  });
10852
11044
  }
10853
11045
  });
11046
+ var CAPABILITY_NAME_RE = /^[a-z][a-z0-9-]*$/;
11047
+ var GrantedCapabilitiesSchema = import_zod.z.array(import_zod.z.string().min(1)).optional().superRefine((caps, ctx) => {
11048
+ if (!caps) return;
11049
+ const seen = /* @__PURE__ */ new Set();
11050
+ caps.forEach((name, i) => {
11051
+ if (name === "") return;
11052
+ if (name === "*") {
11053
+ ctx.addIssue({
11054
+ code: import_zod.z.ZodIssueCode.custom,
11055
+ path: [i],
11056
+ message: "'*' is not a capability \u2014 granted_capabilities has no wildcard, and a '*' in allowedActions confers zero capabilities. Name each capability explicitly."
11057
+ });
11058
+ } else if (!CAPABILITY_NAME_RE.test(name)) {
11059
+ ctx.addIssue({
11060
+ code: import_zod.z.ZodIssueCode.custom,
11061
+ path: [i],
11062
+ message: `'${name}' is not a valid capability name (expected lowercase letters, digits and hyphens, starting with a letter, and no ':' \u2014 a capability is not an action)`
11063
+ });
11064
+ }
11065
+ if (seen.has(name)) {
11066
+ ctx.addIssue({
11067
+ code: import_zod.z.ZodIssueCode.custom,
11068
+ path: [i],
11069
+ message: `'${name}' is declared more than once`
11070
+ });
11071
+ }
11072
+ seen.add(name);
11073
+ });
11074
+ });
10854
11075
  var BlueprintAccessProfileSchema = import_zod.z.object({
10855
11076
  // Validated structurally here; the SCOPE GATE (in @vectros-ai/cli) is
10856
11077
  // what enforces the data-plane-only security boundary.
10857
- allowedActions: import_zod.z.array(import_zod.z.string().min(1)).min(1),
11078
+ //
11079
+ // Optional now: mutually exclusive with `roleIds` below (the
11080
+ // `.superRefine` on this schema enforces the XOR, mirroring the platform's
11081
+ // own `AccessProfileDB.validateScopesXorRole`). Exactly one of the two
11082
+ // authors this profile's grant.
11083
+ allowedActions: import_zod.z.array(import_zod.z.string().min(1)).min(1).optional(),
11084
+ // Additive role composition: this profile's effective grant is
11085
+ // each named role's own clauses, concatenated in the order listed, instead of
11086
+ // an inline clause authored here. Every id must name a role this SAME
11087
+ // blueprint declares in `roles` (`lintAccessProfileRoleIds` below — the
11088
+ // authoring-time mirror of the platform's same-`(tenantId, contextId)`
11089
+ // check, trivially satisfied here since a blueprint's own `roles` are
11090
+ // always created in its own `contextId`); duplicates are rejected outright
11091
+ // rather than silently deduplicated. Mutually exclusive
11092
+ // with `allowedActions`/`dataScope`/`capabilities` — a role-referencing
11093
+ // profile has no inline clause of its own to carry a dataScope or
11094
+ // capability grant; author those on the referenced Role instead, where
11095
+ // every profile referencing it picks them up uniformly (mirrors how
11096
+ // `assumable` already works for a `roleId`-referencing profile).
11097
+ roleIds: import_zod.z.array(import_zod.z.string().min(1)).min(1).optional(),
10858
11098
  // Optional ownership binding: { userId: [...], "scope:org": [...], "scope:client": [...] }.
10859
11099
  // A `null` element in a value list is the documented NULL SENTINEL — it
10860
11100
  // grants access to TENANT-LEVEL (owner-less) records IN ADDITION to the
@@ -10882,31 +11122,92 @@ var BlueprintAccessProfileSchema = import_zod.z.object({
10882
11122
  // below (`lintIdentityOverrides`, placeholder-aware — see that function's
10883
11123
  // docstring); this `.min(1)` is only the shape-level "non-blank" guard on
10884
11124
  // the value, not the grammar check.
10885
- identityOverrides: import_zod.z.record(import_zod.z.string().min(1)).optional()
10886
- }).strict();
10887
- var BlueprintRoleClauseSchema = import_zod.z.object({
10888
- allowedActions: import_zod.z.array(import_zod.z.string().min(1)).min(1),
10889
- dataScope: import_zod.z.record(import_zod.z.array(import_zod.z.union([import_zod.z.string().min(1), import_zod.z.null()]))).optional()
10890
- }).strict();
10891
- var BlueprintRolesSchema = import_zod.z.record(import_zod.z.array(BlueprintRoleClauseSchema).min(1));
10892
- var IDENTITY_NAMESPACE_RE = /^[a-z][a-z0-9_-]{1,31}$/;
10893
- var FORBIDDEN_IDENTITY_NAMESPACES = /* @__PURE__ */ new Set(["record", "document", "entity", "user"]);
10894
- var IdentityKindSchema = import_zod.z.string().min(1).superRefine((val, ctx) => {
10895
- if (val === "user") return;
10896
- if (!IDENTITY_NAMESPACE_RE.test(val)) {
10897
- ctx.addIssue({
10898
- code: import_zod.z.ZodIssueCode.custom,
10899
- message: `kind must be 'user' or an entity namespace (2-32 chars, a lowercase letter first, then lowercase letters/digits/_/-), e.g. 'org', 'client', 'team'`
10900
- });
10901
- } else if (FORBIDDEN_IDENTITY_NAMESPACES.has(val)) {
11125
+ identityOverrides: import_zod.z.record(import_zod.z.string().min(1)).optional(),
11126
+ // Optional capabilities granted to the blueprint's own service-principal
11127
+ // profile. DECISION: allowed here, not role-clause-only. On the platform,
11128
+ // an access profile's scopes are themselves a list of clauses — the exact
11129
+ // same wire shape a role clause carries — and the loader already builds
11130
+ // this profile as ONE such clause. Refusing `capabilities` here while
11131
+ // allowing it on a role clause would be an asymmetry this FORMAT package
11132
+ // has no basis to invent: nothing on the platform side distinguishes the
11133
+ // two, and whether granting a capability to a blueprint's own service key
11134
+ // is sound POLICY is exactly the question the CLI's scope gate exists to
11135
+ // answer (a separate package, out of scope for this format layer) — the
11136
+ // same posture this package already takes with `allowedActions` above.
11137
+ //
11138
+ // Worth naming for whoever writes that gate: this profile is a long-lived,
11139
+ // non-human-attributable machine credential (typically stored in CI/CD
11140
+ // secrets), not an ephemeral human session — a materially different
11141
+ // blast-radius question from granting the same capability to a role that
11142
+ // a person is bound to. The gate should weigh the two separately rather
11143
+ // than reuse one policy for both.
11144
+ capabilities: GrantedCapabilitiesSchema
11145
+ }).strict().superRefine((ap, ctx) => {
11146
+ const hasInline = ap.allowedActions !== void 0;
11147
+ const hasRoleIds = ap.roleIds !== void 0;
11148
+ if (hasInline === hasRoleIds) {
10902
11149
  ctx.addIssue({
10903
11150
  code: import_zod.z.ZodIssueCode.custom,
10904
- message: `'${val}' is a reserved surface name and cannot be an entity namespace \u2014 use 'user' for a user, or a namespace like 'org'/'client'/'team'`
11151
+ path: ["roleIds"],
11152
+ 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)"
10905
11153
  });
11154
+ return;
10906
11155
  }
10907
- });
10908
- var IdentityDeclSchema = import_zod.z.object({
10909
- kind: IdentityKindSchema,
11156
+ if (hasRoleIds) {
11157
+ if (ap.dataScope !== void 0) {
11158
+ ctx.addIssue({
11159
+ code: import_zod.z.ZodIssueCode.custom,
11160
+ path: ["dataScope"],
11161
+ 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"
11162
+ });
11163
+ }
11164
+ if (ap.capabilities !== void 0) {
11165
+ ctx.addIssue({
11166
+ code: import_zod.z.ZodIssueCode.custom,
11167
+ path: ["capabilities"],
11168
+ 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"
11169
+ });
11170
+ }
11171
+ }
11172
+ });
11173
+ var BlueprintRoleClauseSchema = import_zod.z.object({
11174
+ allowedActions: import_zod.z.array(import_zod.z.string().min(1)).min(1),
11175
+ dataScope: import_zod.z.record(import_zod.z.array(import_zod.z.union([import_zod.z.string().min(1), import_zod.z.null()]))).optional(),
11176
+ dataScopeRef: import_zod.z.string().min(1).optional(),
11177
+ capabilities: GrantedCapabilitiesSchema
11178
+ }).strict().superRefine((c, ctx) => {
11179
+ if (c.dataScope !== void 0 && c.dataScopeRef !== void 0) {
11180
+ ctx.addIssue({
11181
+ code: import_zod.z.ZodIssueCode.custom,
11182
+ path: ["dataScopeRef"],
11183
+ 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"
11184
+ });
11185
+ }
11186
+ });
11187
+ var BlueprintFragmentsSchema = import_zod.z.record(
11188
+ import_zod.z.record(import_zod.z.array(import_zod.z.union([import_zod.z.string().min(1), import_zod.z.null()])))
11189
+ );
11190
+ var BlueprintRolesSchema = import_zod.z.record(import_zod.z.array(BlueprintRoleClauseSchema).min(1));
11191
+ var BlueprintRoleAssumableSchema = import_zod.z.record(import_zod.z.array(import_zod.z.string().min(1)).min(1));
11192
+ var BlueprintRoleAssumableMapSchema = import_zod.z.record(BlueprintRoleAssumableSchema);
11193
+ var IDENTITY_NAMESPACE_RE = /^[a-z][a-z0-9_-]{1,31}$/;
11194
+ var FORBIDDEN_IDENTITY_NAMESPACES = /* @__PURE__ */ new Set(["record", "document", "entity", "user"]);
11195
+ var IdentityKindSchema = import_zod.z.string().min(1).superRefine((val, ctx) => {
11196
+ if (val === "user") return;
11197
+ if (!IDENTITY_NAMESPACE_RE.test(val)) {
11198
+ ctx.addIssue({
11199
+ code: import_zod.z.ZodIssueCode.custom,
11200
+ message: `kind must be 'user' or an entity namespace (2-32 chars, a lowercase letter first, then lowercase letters/digits/_/-), e.g. 'org', 'client', 'team'`
11201
+ });
11202
+ } else if (FORBIDDEN_IDENTITY_NAMESPACES.has(val)) {
11203
+ ctx.addIssue({
11204
+ code: import_zod.z.ZodIssueCode.custom,
11205
+ message: `'${val}' is a reserved surface name and cannot be an entity namespace \u2014 use 'user' for a user, or a namespace like 'org'/'client'/'team'`
11206
+ });
11207
+ }
11208
+ });
11209
+ var IdentityDeclSchema = import_zod.z.object({
11210
+ kind: IdentityKindSchema,
10910
11211
  externalId: import_zod.z.string().min(1),
10911
11212
  displayName: import_zod.z.string().min(1).optional(),
10912
11213
  metadata: import_zod.z.record(import_zod.z.unknown()).optional()
@@ -10924,7 +11225,7 @@ var BlueprintServicePrincipalSchema = import_zod.z.object({
10924
11225
  }).strict();
10925
11226
  var ISSUER_ID_RE = CONTEXT_ID_RE;
10926
11227
  var BlueprintIssuerSchema = import_zod.z.object({
10927
- /** Short slug identifying this issuer within your tenant. Immutable once registered. */
11228
+ /** Short slug identifying this issuer within your account. Immutable once registered. */
10928
11229
  issuerId: import_zod.z.string().regex(ISSUER_ID_RE, {
10929
11230
  message: "issuerId must be 3-31 chars, start with a lowercase letter, then lowercase letters/digits/dashes (e.g. 'auth0-prod')"
10930
11231
  }),
@@ -10939,7 +11240,12 @@ var BlueprintIssuerSchema = import_zod.z.object({
10939
11240
  * this as an ordinary per-API/application default).
10940
11241
  */
10941
11242
  audience: import_zod.z.string().min(1),
10942
- /** Which app context an exchanged token targets. Must be an existing app context. */
11243
+ /**
11244
+ * Which app context an exchanged token targets. **Must equal the blueprint's own
11245
+ * `contextId`** — an issuer is a trust anchor, so a blueprint may only attach one to the
11246
+ * context it actually provisions. One IdP account serving several contexts needs one entry
11247
+ * per context, each in that context's own blueprint.
11248
+ */
10943
11249
  contextId: import_zod.z.string().regex(CONTEXT_ID_RE, {
10944
11250
  message: "contextId must be 3-31 chars, start with a lowercase letter, then lowercase letters/digits/dashes (e.g. 'casework')"
10945
11251
  }),
@@ -10947,6 +11253,14 @@ var BlueprintIssuerSchema = import_zod.z.object({
10947
11253
  subClaim: import_zod.z.string().min(1).optional(),
10948
11254
  /** The claim carrying the subject's email (first-login invite matching). Platform defaults to 'email' when omitted. */
10949
11255
  emailClaim: import_zod.z.string().min(1).optional(),
11256
+ /**
11257
+ * The IdP's OIDC userinfo endpoint. Optional. Presented tokens are access tokens, which under
11258
+ * OIDC don't carry `email` unless the IdP was specifically configured to add it — if
11259
+ * `emailClaim` misses on the presented token, and `userinfoUri` is configured, Vectros falls
11260
+ * back to calling this endpoint (with the presented token as the bearer credential) and reads
11261
+ * `emailClaim` from its JSON response instead. Omit to leave the fallback disabled.
11262
+ */
11263
+ userinfoUri: import_zod.z.string().min(1).optional(),
10950
11264
  /**
10951
11265
  * Opt-in self-service signup: a list of {signupType, roleId} pairs. When a first-time exchange
10952
11266
  * caller presents no invite token but names a signupType matching one of these (or omits it and
@@ -10966,6 +11280,169 @@ var BlueprintIssuerSchema = import_zod.z.object({
10966
11280
  }).strict()
10967
11281
  ).optional()
10968
11282
  }).strict();
11283
+ var BlueprintNamespaceSchema = import_zod.z.object({
11284
+ /** The namespace to register — same grammar as a `scope:<namespace>` key. */
11285
+ namespace: import_zod.z.string().min(1),
11286
+ /**
11287
+ * When `true`, every value in this namespace must resolve to an existing
11288
+ * identity entity (create via `entities:c` at the in-context phase, or a
11289
+ * declared `identities:` entry). When `false`/omitted, values are
11290
+ * free-form strings validated by grammar only.
11291
+ */
11292
+ entityBacked: import_zod.z.boolean().optional(),
11293
+ /**
11294
+ * Request the platform's TENANT-WIDE registration form instead of this
11295
+ * blueprint's own context — visible to every context in the account, not
11296
+ * just the one this blueprint provisions. `false`/omitted (the default)
11297
+ * is the ordinary, context-owned registration every other namespace here
11298
+ * gets. Mutually exclusive with any future context-targeting field this
11299
+ * schema may grow — there is exactly one way to say "not my own context"
11300
+ * today.
11301
+ *
11302
+ * A blueprint declaring this does NOT get it automatically: the applying
11303
+ * credential must separately hold a platform capability granted only to
11304
+ * an account OWNER, and the CLI refuses to even request it unless
11305
+ * explicitly told to (see `--allow-tenant-wide-namespaces`) — no flag,
11306
+ * no request, regardless of what this field says. Setting this to `true`
11307
+ * states the blueprint's INTENT; it is not, on its own, a grant of
11308
+ * anything.
11309
+ */
11310
+ tenantWide: import_zod.z.boolean().optional(),
11311
+ /**
11312
+ * This namespace's position in your account's specificity order (used to
11313
+ * break a tie when a caller holds two scope dimensions at once). Required
11314
+ * by the platform on registration; MUST be unique across every namespace
11315
+ * in the account, including `org` (1000) and `client` (2000) — pick a
11316
+ * value outside that pair, and outside any other namespace this same
11317
+ * account might register (its own, or another blueprint's).
11318
+ */
11319
+ specificityRank: import_zod.z.number().int().min(0).max(1e6),
11320
+ /**
11321
+ * Optional membership backing — declares which record type + field hold
11322
+ * grants of THIS namespace's values. Supply together with
11323
+ * `membershipTargetField`, or omit both. Declaring this grants nobody
11324
+ * anything on its own: a role opts in explicitly with
11325
+ * `${{ member.scope.<namespace> }}` in its `dataScope`.
11326
+ */
11327
+ membershipRecordType: import_zod.z.string().min(1).optional(),
11328
+ /** The field on `membershipRecordType` naming the user a grant is FOR. Required alongside `membershipRecordType`. */
11329
+ membershipTargetField: import_zod.z.string().min(1).optional(),
11330
+ /**
11331
+ * The field on `membershipRecordType` naming a grant's LEVEL, so the same
11332
+ * user can hold different levels in different values of this namespace.
11333
+ * Supply together with `membershipLevels`, or omit both for plain
11334
+ * in-or-out membership.
11335
+ */
11336
+ membershipLevelField: import_zod.z.string().min(1).optional(),
11337
+ /** The complete set of level labels this namespace allows. Required alongside `membershipLevelField`. */
11338
+ membershipLevels: import_zod.z.array(import_zod.z.string().min(1)).optional()
11339
+ }).strict();
11340
+ var MEMBERSHIP_FIELD_NAME_RE = /^[A-Za-z0-9_-]{1,64}$/;
11341
+ function lintNamespaces(value, ctx) {
11342
+ if (!value || typeof value !== "object" || !("namespaces" in value)) return;
11343
+ const namespaces = value.namespaces;
11344
+ if (!Array.isArray(namespaces)) return;
11345
+ const rawSchemas = value.schemas;
11346
+ const declaredSchemaTypes = new Set(
11347
+ (Array.isArray(rawSchemas) ? rawSchemas : []).map((s) => s && typeof s === "object" && "typeName" in s ? s.typeName : void 0).filter((t) => typeof t === "string")
11348
+ );
11349
+ const seenNames = /* @__PURE__ */ new Set();
11350
+ const seenRanks = /* @__PURE__ */ new Map();
11351
+ namespaces.forEach((entry, i) => {
11352
+ if (!entry || typeof entry !== "object") return;
11353
+ const ns = entry;
11354
+ const path4 = ["namespaces", i];
11355
+ if (typeof ns.namespace === "string") {
11356
+ const err = scopeNamespaceGrammarError(ns.namespace);
11357
+ if (err) ctx.addIssue({ code: import_zod.z.ZodIssueCode.custom, path: [...path4, "namespace"], message: err });
11358
+ if (seenNames.has(ns.namespace)) {
11359
+ ctx.addIssue({
11360
+ code: import_zod.z.ZodIssueCode.custom,
11361
+ path: [...path4, "namespace"],
11362
+ message: `'${ns.namespace}' is declared more than once in namespaces[]`
11363
+ });
11364
+ }
11365
+ seenNames.add(ns.namespace);
11366
+ }
11367
+ if (typeof ns.specificityRank === "number") {
11368
+ const prior = seenRanks.get(ns.specificityRank);
11369
+ if (prior !== void 0) {
11370
+ ctx.addIssue({
11371
+ code: import_zod.z.ZodIssueCode.custom,
11372
+ path: [...path4, "specificityRank"],
11373
+ message: `specificityRank ${ns.specificityRank} is already used by namespaces[${prior}] \u2014 every namespace's rank must be unique`
11374
+ });
11375
+ }
11376
+ seenRanks.set(ns.specificityRank, i);
11377
+ }
11378
+ const hasRecordType = ns.membershipRecordType !== void 0;
11379
+ const hasTargetField = ns.membershipTargetField !== void 0;
11380
+ if (hasRecordType !== hasTargetField) {
11381
+ ctx.addIssue({
11382
+ code: import_zod.z.ZodIssueCode.custom,
11383
+ path: [...path4, "membershipRecordType"],
11384
+ message: "membershipRecordType and membershipTargetField must be declared together, or neither"
11385
+ });
11386
+ }
11387
+ if (typeof ns.membershipRecordType === "string" && !declaredSchemaTypes.has(ns.membershipRecordType)) {
11388
+ ctx.addIssue({
11389
+ code: import_zod.z.ZodIssueCode.custom,
11390
+ path: [...path4, "membershipRecordType"],
11391
+ message: `membershipRecordType '${ns.membershipRecordType}' must name a typeName this blueprint declares in schemas[] \u2014 a namespace's membership can only resolve over a record type the blueprint itself ships, never a type that merely happens to already exist in the target context.`
11392
+ });
11393
+ }
11394
+ for (const [field, value2] of [
11395
+ ["membershipRecordType", ns.membershipRecordType],
11396
+ ["membershipTargetField", ns.membershipTargetField],
11397
+ ["membershipLevelField", ns.membershipLevelField]
11398
+ ]) {
11399
+ if (typeof value2 === "string" && !MEMBERSHIP_FIELD_NAME_RE.test(value2)) {
11400
+ ctx.addIssue({
11401
+ code: import_zod.z.ZodIssueCode.custom,
11402
+ path: [...path4, field],
11403
+ message: `${field} '${value2}' is invalid \u2014 must be 1-64 characters of letters, digits, underscores, or hyphens`
11404
+ });
11405
+ }
11406
+ }
11407
+ const hasLevelField = ns.membershipLevelField !== void 0;
11408
+ const hasLevels = Array.isArray(ns.membershipLevels) && ns.membershipLevels.length > 0;
11409
+ if (hasLevelField !== hasLevels) {
11410
+ ctx.addIssue({
11411
+ code: import_zod.z.ZodIssueCode.custom,
11412
+ path: [...path4, "membershipLevelField"],
11413
+ message: "membershipLevelField and a non-empty membershipLevels must be declared together, or neither"
11414
+ });
11415
+ }
11416
+ if ((hasLevelField || hasLevels) && !hasRecordType) {
11417
+ ctx.addIssue({
11418
+ code: import_zod.z.ZodIssueCode.custom,
11419
+ path: [...path4, "membershipLevelField"],
11420
+ message: "membershipLevelField/membershipLevels require membershipRecordType + membershipTargetField too"
11421
+ });
11422
+ }
11423
+ if (Array.isArray(ns.membershipLevels)) {
11424
+ const seenLevels = /* @__PURE__ */ new Set();
11425
+ for (const level of ns.membershipLevels) {
11426
+ if (typeof level !== "string") continue;
11427
+ const err = scopeNamespaceGrammarError(level);
11428
+ if (err) {
11429
+ ctx.addIssue({
11430
+ code: import_zod.z.ZodIssueCode.custom,
11431
+ path: [...path4, "membershipLevels"],
11432
+ message: `level '${level}': ${err}`
11433
+ });
11434
+ } else if (seenLevels.has(level)) {
11435
+ ctx.addIssue({
11436
+ code: import_zod.z.ZodIssueCode.custom,
11437
+ path: [...path4, "membershipLevels"],
11438
+ message: `membershipLevels contains '${level}' twice`
11439
+ });
11440
+ }
11441
+ seenLevels.add(level);
11442
+ }
11443
+ }
11444
+ });
11445
+ }
10969
11446
  var SeedCommonShape = {
10970
11447
  /** The schema typeName this seed instantiates (should match a declared schema). */
10971
11448
  typeName: import_zod.z.string().min(1),
@@ -11006,7 +11483,7 @@ var BlueprintSeedRecordSchema = import_zod.z.discriminatedUnion("surface", [
11006
11483
  RecordSeedSchema,
11007
11484
  DocumentSeedSchema
11008
11485
  ]);
11009
- var SELF_TOKEN_RE = /\$\{\{\s*(?:self|under)\.[^}]*?\s*\}\}/;
11486
+ var SELF_TOKEN_RE = /\$\{\{\s*(?:self|under|member)\.[^}]*?\s*\}\}/;
11010
11487
  function lintSelfTokenPlacement(value, ctx) {
11011
11488
  const walk = (node, path4, inRoleDataScope) => {
11012
11489
  if (typeof node === "string") {
@@ -11014,7 +11491,7 @@ function lintSelfTokenPlacement(value, ctx) {
11014
11491
  ctx.addIssue({
11015
11492
  code: import_zod.z.ZodIssueCode.custom,
11016
11493
  path: path4,
11017
- message: "'${{ self.* }}' is a runtime per-principal placeholder \u2014 it is only valid inside a roles[].dataScope value"
11494
+ message: "'${{ self.* }}' / '${{ member.* }}' is a runtime placeholder \u2014 it is only valid inside a roles[].dataScope value, a roleAssumable[] value, or a fragments[] value"
11018
11495
  });
11019
11496
  }
11020
11497
  return;
@@ -11025,7 +11502,10 @@ function lintSelfTokenPlacement(value, ctx) {
11025
11502
  }
11026
11503
  if (node && typeof node === "object") {
11027
11504
  for (const [k, v] of Object.entries(node)) {
11028
- const entering = inRoleDataScope || path4.length === 3 && path4[0] === "roles" && k === "dataScope";
11505
+ const inRoleDataScopeClause = path4.length === 3 && path4[0] === "roles" && k === "dataScope";
11506
+ const inRoleAssumable = path4.length === 0 && k === "roleAssumable";
11507
+ const inFragments = path4.length === 0 && k === "fragments";
11508
+ const entering = inRoleDataScope || inRoleDataScopeClause || inRoleAssumable || inFragments;
11029
11509
  walk(v, [...path4, k], entering);
11030
11510
  }
11031
11511
  }
@@ -11196,6 +11676,140 @@ function lintDataScopeKeys(value, ctx) {
11196
11676
  }
11197
11677
  });
11198
11678
  }
11679
+ for (const [name, fragment] of Object.entries(bp.fragments ?? {})) {
11680
+ for (const key of Object.keys(fragment ?? {})) {
11681
+ lintDataScopeKey(key, ["fragments", name, key], ctx);
11682
+ }
11683
+ }
11684
+ }
11685
+ function lintDataScopeRefs(value, ctx) {
11686
+ const bp = value;
11687
+ const declared = new Set(Object.keys(bp.fragments ?? {}));
11688
+ for (const [roleId, clauses] of Object.entries(bp.roles ?? {})) {
11689
+ clauses.forEach((clause, i) => {
11690
+ if (clause.dataScopeRef === void 0) return;
11691
+ if (!declared.has(clause.dataScopeRef)) {
11692
+ ctx.addIssue({
11693
+ code: import_zod.z.ZodIssueCode.custom,
11694
+ path: ["roles", roleId, i, "dataScopeRef"],
11695
+ message: `dataScopeRef '${clause.dataScopeRef}' is not declared in fragments \u2014 declared fragments: ` + (declared.size ? [...declared].join(", ") : "(none)")
11696
+ });
11697
+ }
11698
+ });
11699
+ }
11700
+ }
11701
+ var BARE_SELF_PLACEHOLDER_RE = /^\$\{\{\s*self\.[^}]*?\s*\}\}$/;
11702
+ var UNDER_SELF_PLACEHOLDER_RE = /^\$\{\{\s*under\.self\.userId\s*\}\}$/;
11703
+ var UNDER_SELF_SCOPE_PLACEHOLDER_RE = /^\$\{\{\s*under\.self\.scope\.[a-z][a-z0-9_-]{1,31}\s*\}\}$/;
11704
+ var MEMBER_SCOPE_PLACEHOLDER_RE = /^\$\{\{\s*member\.scope\.[a-z][a-z0-9_-]{1,31}(?::[a-z][a-z0-9_-]{1,31})?\s*\}\}$/;
11705
+ var ANY_PLACEHOLDER_RE = /^\$\{\{\s*any\s*\}\}$/;
11706
+ function assumableValueError(publicKey, v) {
11707
+ if (UNDER_SELF_PLACEHOLDER_RE.test(v) || MEMBER_SCOPE_PLACEHOLDER_RE.test(v)) return null;
11708
+ if (UNDER_SELF_SCOPE_PLACEHOLDER_RE.test(v)) {
11709
+ 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)`;
11710
+ }
11711
+ if (ANY_PLACEHOLDER_RE.test(v)) {
11712
+ 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`;
11713
+ }
11714
+ if (BARE_SELF_PLACEHOLDER_RE.test(v)) {
11715
+ 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`;
11716
+ }
11717
+ if (SELF_TOKEN_RE.test(v)) {
11718
+ return `assumable['${publicKey}'] value '${v}' is not a recognized runtime placeholder \u2014 use '\${{ under.self.userId }}' or '\${{ member.scope.<namespace>[:level] }}'`;
11719
+ }
11720
+ if (!SCOPE_VALUE_RE.test(v)) {
11721
+ 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 '-'`;
11722
+ }
11723
+ return null;
11724
+ }
11725
+ function lintAccessProfileRoleIds(value, ctx) {
11726
+ const bp = value;
11727
+ const roleIds = bp.accessProfile?.roleIds;
11728
+ if (!roleIds) return;
11729
+ const declaredRoles = new Set(Object.keys(bp.roles ?? {}));
11730
+ const seen = /* @__PURE__ */ new Set();
11731
+ roleIds.forEach((roleId, i) => {
11732
+ const path4 = ["accessProfile", "roleIds", i];
11733
+ if (!declaredRoles.has(roleId)) {
11734
+ ctx.addIssue({
11735
+ code: import_zod.z.ZodIssueCode.custom,
11736
+ path: path4,
11737
+ 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)")
11738
+ });
11739
+ }
11740
+ if (seen.has(roleId)) {
11741
+ ctx.addIssue({
11742
+ code: import_zod.z.ZodIssueCode.custom,
11743
+ path: path4,
11744
+ message: `accessProfile.roleIds names '${roleId}' more than once \u2014 remove the duplicate`
11745
+ });
11746
+ }
11747
+ seen.add(roleId);
11748
+ });
11749
+ }
11750
+ function lintRoleAssumable(value, ctx) {
11751
+ const bp = value;
11752
+ const declaredRoles = new Set(Object.keys(bp.roles ?? {}));
11753
+ for (const [roleId, assumable] of Object.entries(bp.roleAssumable ?? {})) {
11754
+ if (!declaredRoles.has(roleId)) {
11755
+ ctx.addIssue({
11756
+ code: import_zod.z.ZodIssueCode.custom,
11757
+ path: ["roleAssumable", roleId],
11758
+ message: `roleAssumable names '${roleId}', which is not a role this blueprint declares in roles \u2014 ` + (declaredRoles.size ? `declared roles: ${[...declaredRoles].join(", ")}` : "declared roles: (none)")
11759
+ });
11760
+ continue;
11761
+ }
11762
+ for (const [key, values] of Object.entries(assumable)) {
11763
+ const path4 = ["roleAssumable", roleId, key];
11764
+ if (key === "userId") {
11765
+ ctx.addIssue({
11766
+ code: import_zod.z.ZodIssueCode.custom,
11767
+ path: path4,
11768
+ 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"
11769
+ });
11770
+ continue;
11771
+ }
11772
+ const ns = key.startsWith("scope:") ? key.slice("scope:".length) : null;
11773
+ const nsError = ns === null ? null : scopeNamespaceGrammarError(ns);
11774
+ if (ns === null || nsError !== null) {
11775
+ ctx.addIssue({
11776
+ code: import_zod.z.ZodIssueCode.custom,
11777
+ path: path4,
11778
+ 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}`
11779
+ });
11780
+ continue;
11781
+ }
11782
+ if (!Array.isArray(values)) continue;
11783
+ values.forEach((v, i) => {
11784
+ if (typeof v !== "string") return;
11785
+ const err = assumableValueError(key, v);
11786
+ if (err) ctx.addIssue({ code: import_zod.z.ZodIssueCode.custom, path: [...path4, i], message: err });
11787
+ });
11788
+ }
11789
+ }
11790
+ }
11791
+ function expandDataScopeRefs(bp) {
11792
+ if (!bp.roles) return bp;
11793
+ const fragments = bp.fragments ?? {};
11794
+ let changedAnyRole = false;
11795
+ const roles = {};
11796
+ for (const [roleId, clauses] of Object.entries(bp.roles)) {
11797
+ let changedThisRole = false;
11798
+ const expandedClauses = clauses.map((clause) => {
11799
+ if (clause.dataScopeRef === void 0) return clause;
11800
+ changedThisRole = true;
11801
+ const { dataScopeRef, ...rest } = clause;
11802
+ if (!(dataScopeRef in fragments)) {
11803
+ throw new Error(
11804
+ `expandDataScopeRefs: dataScopeRef '${dataScopeRef}' does not resolve against fragments \u2014 this should have been caught by lintDataScopeRefs before parseBlueprint reaches this function`
11805
+ );
11806
+ }
11807
+ return { ...rest, dataScope: fragments[dataScopeRef] };
11808
+ });
11809
+ roles[roleId] = changedThisRole ? expandedClauses : clauses;
11810
+ changedAnyRole = changedAnyRole || changedThisRole;
11811
+ }
11812
+ return changedAnyRole ? { ...bp, roles } : bp;
11199
11813
  }
11200
11814
  function lintSeedSurfaces(value, ctx) {
11201
11815
  const bp = value;
@@ -11214,9 +11828,36 @@ function lintSeedSurfaces(value, ctx) {
11214
11828
  }
11215
11829
  });
11216
11830
  }
11831
+ function lintIdentitySurfaces(value, ctx) {
11832
+ const bp = value;
11833
+ if (!Array.isArray(bp.schemas)) return;
11834
+ bp.schemas.forEach((schema, i) => {
11835
+ if (!schema.allowedSurfaces?.includes("user")) return;
11836
+ ctx.addIssue({
11837
+ code: import_zod.z.ZodIssueCode.custom,
11838
+ path: ["schemas", i, "allowedSurfaces"],
11839
+ 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).`
11840
+ });
11841
+ });
11842
+ }
11843
+ function lintIssuerContext(value, ctx) {
11844
+ const bp = value;
11845
+ if (!Array.isArray(bp.issuers)) return;
11846
+ bp.issuers.forEach((issuer, i) => {
11847
+ if (issuer.contextId === void 0 || issuer.contextId === bp.contextId) return;
11848
+ ctx.addIssue({
11849
+ code: import_zod.z.ZodIssueCode.custom,
11850
+ path: ["issuers", i, "contextId"],
11851
+ 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}'.`
11852
+ });
11853
+ });
11854
+ }
11217
11855
  var BLUEPRINT_FIELD_PHASES = Object.freeze({
11218
11856
  servicePrincipal: "bootstrap",
11219
11857
  issuers: "bootstrap",
11858
+ // Registration requires the platform's provisioning:c capability, which
11859
+ // only the bootstrap credential carries — same reasoning as issuers.
11860
+ namespaces: "bootstrap",
11220
11861
  schemas: "in-context",
11221
11862
  accessProfile: "in-context",
11222
11863
  roles: "in-context",
@@ -11239,6 +11880,24 @@ var BlueprintSchema = import_zod.z.object({
11239
11880
  seed: import_zod.z.array(BlueprintSeedRecordSchema).optional(),
11240
11881
  /** Optional multi-clause roles, bound to principals via `access grant --role`. */
11241
11882
  roles: BlueprintRolesSchema.optional(),
11883
+ /**
11884
+ * Optional roleId → `POST /v1/auth/token/assume`
11885
+ * entitlement grant, mirroring the platform's role create/update request
11886
+ * and response shape for this field. Kept separate from `roles` rather
11887
+ * than folded into a role entry — see {@link BlueprintRoleAssumableMapSchema}'s
11888
+ * own doc comment for why. Every key must name a role this blueprint
11889
+ * declares in `roles` (`lintRoleAssumable`).
11890
+ */
11891
+ roleAssumable: BlueprintRoleAssumableMapSchema.optional(),
11892
+ /**
11893
+ * Optional named `dataScope` fragments, each
11894
+ * referenceable from a role clause via `dataScopeRef: <name>` instead of
11895
+ * repeating the same map inline. Purely local authoring sugar: not a
11896
+ * provisioned resource (absent from `BLUEPRINT_FIELD_PHASES`), and never
11897
+ * survives `parseBlueprint` as a live reference — see
11898
+ * {@link expandDataScopeRefs}.
11899
+ */
11900
+ fragments: BlueprintFragmentsSchema.optional(),
11242
11901
  /** Optional principals ensured-exist at apply; referenced via ${{ identities.* }}. */
11243
11902
  identities: IdentitiesDeclSchema.optional(),
11244
11903
  /**
@@ -11247,14 +11906,29 @@ var BlueprintSchema = import_zod.z.object({
11247
11906
  * service-principal creation — NOT the in-context load (see
11248
11907
  * {@link BlueprintIssuerSchema}).
11249
11908
  */
11250
- issuers: import_zod.z.array(BlueprintIssuerSchema).optional()
11909
+ issuers: import_zod.z.array(BlueprintIssuerSchema).optional(),
11910
+ /**
11911
+ * Optional namespaces to register before anything else needs them — see
11912
+ * {@link BlueprintNamespaceSchema}. Applied in the BOOTSTRAP-token phase,
11913
+ * alongside app-context/service-principal creation and issuers, BEFORE
11914
+ * schemas/seed (an `entityBacked` namespace's entities, and an
11915
+ * `allowedSurfaces: ['entity']` schema, both need the registration to
11916
+ * already exist). `org`/`client` need no entry — they exist already.
11917
+ */
11918
+ namespaces: import_zod.z.array(BlueprintNamespaceSchema).optional()
11251
11919
  }).strict().superRefine((bp, ctx) => {
11252
11920
  lintSelfTokenPlacement(bp, ctx);
11253
11921
  lintIdentityRefs(bp, ctx);
11254
11922
  lintIdentityOverrides(bp, ctx);
11255
11923
  lintNamespacedScopeArrays(bp, ctx);
11256
11924
  lintDataScopeKeys(bp, ctx);
11925
+ lintDataScopeRefs(bp, ctx);
11926
+ lintRoleAssumable(bp, ctx);
11927
+ lintAccessProfileRoleIds(bp, ctx);
11257
11928
  lintSeedSurfaces(bp, ctx);
11929
+ lintIdentitySurfaces(bp, ctx);
11930
+ lintIssuerContext(bp, ctx);
11931
+ lintNamespaces(bp, ctx);
11258
11932
  });
11259
11933
  function formatIssuePath(path4) {
11260
11934
  let out = "";
@@ -11290,7 +11964,7 @@ function parseBlueprint(input) {
11290
11964
  throw new BlueprintValidationError(`Malformed blueprint:
11291
11965
  ${renderIssues(issues)}`, issues);
11292
11966
  }
11293
- return result.data;
11967
+ return expandDataScopeRefs(result.data);
11294
11968
  }
11295
11969
  function contextNameOf(blueprint) {
11296
11970
  return blueprint.contextName ?? `MCP \u2014 ${blueprint.name}`;
@@ -11409,9 +12083,12 @@ function deriveSuffix(contextId) {
11409
12083
  var WHOLE_TOKEN_RE = /^\$\{\{\s*([A-Za-z_]\w*)\.([A-Za-z_]\w*)\s*\}\}$/;
11410
12084
  var TOKEN_SCAN_RE = /\$\$\{\{|\$\{\{([\s\S]*?)\}\}|\$\{\{/g;
11411
12085
  var INNER_REF_RE = /^([A-Za-z_]\w*)\.([A-Za-z_]\w*)$/;
11412
- var DEFERRED_NAMESPACES = /* @__PURE__ */ new Set(["self", "identities", "under"]);
12086
+ var DEFERRED_NAMESPACES = /* @__PURE__ */ new Set(["self", "identities", "under", "member"]);
12087
+ var DEFERRED_BARE_TOKENS = /* @__PURE__ */ new Set(["any"]);
11413
12088
  var DEFERRED_NS_ALTERNATION = [...DEFERRED_NAMESPACES].join("|");
11414
- var DEFERRED_INNER_RE = new RegExp(`^(?:${DEFERRED_NS_ALTERNATION})(?:\\.[A-Za-z_]\\w*)+$`);
12089
+ var DEFERRED_INNER_RE = new RegExp(
12090
+ `^(?:${DEFERRED_NS_ALTERNATION})(?:\\.[A-Za-z_]\\w*)+(?::[A-Za-z_]\\w*)?$`
12091
+ );
11415
12092
  function resolveRef(ns, name, ctx, path4) {
11416
12093
  if (ns === "inputs") {
11417
12094
  if (!(name in ctx.declared)) {
@@ -11469,7 +12146,7 @@ function interpolate(str, ctx, path4) {
11469
12146
  });
11470
12147
  } else {
11471
12148
  const inner = (m[1] ?? "").trim();
11472
- if (DEFERRED_INNER_RE.test(inner)) {
12149
+ if (DEFERRED_INNER_RE.test(inner) || DEFERRED_BARE_TOKENS.has(inner)) {
11473
12150
  out += m[0];
11474
12151
  last = TOKEN_SCAN_RE.lastIndex;
11475
12152
  continue;
@@ -11671,314 +12348,72 @@ var taskManagement = {
11671
12348
  renderHints: { label: "Assignee", widget: "text", order: 5, section: "Tracking" }
11672
12349
  },
11673
12350
  {
11674
- fieldId: "project",
11675
- fieldType: "string",
11676
- filterable: true,
11677
- description: "Grouping key for cross-session continuity.",
11678
- renderHints: { label: "Project", widget: "text", order: 6, section: "Tracking" }
11679
- },
11680
- {
11681
- fieldId: "dueDate",
11682
- fieldType: "date",
11683
- description: "ISO-8601. Range-queryable (see lookupFields) \u2014 list tasks due in a window.",
11684
- renderHints: { label: "Due date", widget: "date", order: 7, section: "Tracking" }
11685
- },
11686
- {
11687
- fieldId: "tags",
11688
- fieldType: "array",
11689
- filterable: true,
11690
- renderHints: { label: "Tags", order: 8, section: "Tracking" }
11691
- }
11692
- // externalId is the record's FIRST-CLASS identifier (the dedup/lookup key + the
11693
- // value a `reference` resolves against) — not a payload field; the loader sends
11694
- // it top-level on the RecordRequest.
11695
- ],
11696
- // Two lookup shapes, chosen deliberately because the equality-vs-range choice
11697
- // per field is MIGRATION-LOCKED once the schema is live (you cannot flip a
11698
- // field slot↔range later, even by removing and re-adding it):
11699
- // • `project` — EQUALITY (a grouping key, not ordered): enumerate one
11700
- // project's tasks directly, no search. Each equality lookup uses 1 of the
11701
- // schema's 7 fast index slots.
11702
- // • `dueDate` — RANGE: ordered `from`/`to`/`prefix` queries ("tasks due this
11703
- // week", "due in 2026-06"). Range lookups use a relationship row, not a
11704
- // slot, and are billed at the range rate. ISO-8601 sorts chronologically,
11705
- // so the order is correct.
11706
- // `externalId` has a built-in first-class finder and must NOT be redeclared here.
11707
- lookupFields: ["project", { fieldName: "dueDate", rangeEnabled: true }]
11708
- }
11709
- ],
11710
- // Least-privilege profile — MUST pass the CLI scope gate.
11711
- // r/c/u + search + schema discovery. NOT records:d (least privilege).
11712
- accessProfile: {
11713
- allowedActions: ["records:r", "records:c", "records:u", "search:r", "schemas:r"]
11714
- // dataScope omitted → tenant-level shared tracker. Add a `scope:org`
11715
- // entry for per-org isolation.
11716
- },
11717
- servicePrincipal: {
11718
- externalId: "task-management",
11719
- displayName: "Task Management"
11720
- },
11721
- seed: [
11722
- {
11723
- surface: "record",
11724
- typeName: "task",
11725
- externalId: "seed-welcome",
11726
- fields: {
11727
- title: "Welcome to your Vectros task tracker",
11728
- description: "Created by the bootstrap loader. Ask your agent to add tasks.",
11729
- status: "todo",
11730
- priority: "low",
11731
- project: "getting-started",
11732
- dueDate: "2026-06-30"
11733
- }
11734
- }
11735
- ]
11736
- };
11737
- var task_management_default = taskManagement;
11738
- var codingAgentMemory = {
11739
- name: "coding-agent-memory",
11740
- version: "1.0.0",
11741
- description: "Persistent project memory for a coding agent \u2014 decisions, conventions, and gotchas that survive across sessions.",
11742
- contextId: "coding-memory",
11743
- contextName: "Coding Agent \u2014 Project Memory",
11744
- schemas: [
11745
- {
11746
- // A durable architectural/product decision: what was decided, and WHY.
11747
- // The rationale is the high-value, RAG-able body — "why did we do X?"
11748
- // is the question a cold-context agent most needs answered.
11749
- typeName: "decision",
11750
- displayName: "Decision",
11751
- indexMode: "HYBRID",
11752
- // keyword on titles + semantic on statement/rationale
11753
- fields: [
11754
- {
11755
- fieldId: "title",
11756
- fieldType: "string",
11757
- required: true,
11758
- searchable: true,
11759
- validation: { minLength: 1, maxLength: 200 },
11760
- renderHints: { label: "Title", widget: "text", order: 1, displayField: true }
11761
- },
11762
- {
11763
- fieldId: "statement",
11764
- fieldType: "string",
11765
- searchable: true,
11766
- description: "What was decided, in one or two sentences.",
11767
- renderHints: { label: "Statement", widget: "textarea", order: 2 }
11768
- },
11769
- {
11770
- fieldId: "rationale",
11771
- fieldType: "string",
11772
- searchable: true,
11773
- description: "Why \u2014 the trade-offs and the context. The most-recalled field.",
11774
- renderHints: { label: "Rationale", widget: "textarea", order: 3 }
11775
- },
11776
- {
11777
- fieldId: "status",
11778
- fieldType: "enum",
11779
- filterable: true,
11780
- enumValues: ["proposed", "active", "superseded"],
11781
- renderHints: { label: "Status", widget: "select", order: 4 }
11782
- },
11783
- {
11784
- fieldId: "area",
11785
- fieldType: "string",
11786
- filterable: true,
11787
- description: 'Subsystem / module the decision applies to (e.g. "auth", "search").',
11788
- renderHints: { label: "Area", widget: "text", order: 5 }
11789
- },
11790
- {
11791
- fieldId: "decidedOn",
11792
- fieldType: "date",
11793
- description: 'ISO-8601 date. Range-queryable \u2014 "decisions made this quarter".',
11794
- renderHints: { label: "Decided on", widget: "date", order: 6 }
11795
- }
11796
- // externalId is the record's FIRST-CLASS identifier (the dedup/upsert key + the
11797
- // value a `reference` resolves against) — it is NOT a payload field, so it is not
11798
- // declared here; the loader sends it top-level on the RecordRequest.
11799
- ],
11800
- // `externalId` is a first-class identifier with its own finder (exact
11801
- // get/upsert) — look it up directly, never redeclare it as a schema lookup.
11802
- // The choice below is MIGRATION-LOCKED once the schema is live:
11803
- // • `area`/`status` — EQUALITY (categorical): "list active decisions",
11804
- // "all decisions in the auth area". 2 of the 7 fast index slots.
11805
- // • `decidedOn` — RANGE: ordered `from`/`to`/`prefix` over the date.
11806
- lookupFields: ["area", "status", { fieldName: "decidedOn", rangeEnabled: true }]
11807
- },
11808
- {
11809
- // A coding convention the team follows — the agent reads these before it
11810
- // writes code so it matches the surrounding style instead of inventing one.
11811
- typeName: "convention",
11812
- displayName: "Convention",
11813
- indexMode: "HYBRID",
11814
- fields: [
11815
- {
11816
- fieldId: "name",
11817
- fieldType: "string",
11818
- required: true,
11819
- searchable: true,
11820
- validation: { minLength: 1, maxLength: 200 },
11821
- renderHints: { label: "Name", widget: "text", order: 1, displayField: true }
11822
- },
11823
- {
11824
- fieldId: "rule",
11825
- fieldType: "string",
11826
- searchable: true,
11827
- description: "The convention itself, stated as an imperative.",
11828
- renderHints: { label: "Rule", widget: "textarea", order: 2 }
11829
- },
11830
- {
11831
- fieldId: "area",
11832
- fieldType: "string",
11833
- filterable: true,
11834
- renderHints: { label: "Area", widget: "text", order: 3 }
11835
- },
11836
- {
11837
- // Parallels `decision.status` so a convention can be RETIRED rather than
11838
- // deleted (the blueprint omits records:d — memory is superseded, not lost).
11839
- fieldId: "status",
11840
- fieldType: "enum",
11841
- filterable: true,
11842
- enumValues: ["active", "retired"],
11843
- renderHints: { label: "Status", widget: "select", order: 4 }
11844
- },
11845
- {
11846
- // A typed REFERENCE to the decision that established this convention — a
11847
- // foreign-key link between record types (the knowledge-graph edge). The
11848
- // platform requires both the target type AND its surface; the value is the
11849
- // target decision's externalId (the default `targetField`). Provenance:
11850
- // "why does this convention exist? → open the decision behind it." Not
11851
- // searchable (a foreign-key id is search noise); declared as an equality
11852
- // lookup below so you can also enumerate "conventions from decision X".
11853
- fieldId: "establishedByDecision",
11854
- fieldType: "reference",
11855
- targetTypeName: "decision",
11856
- targetSurface: "record",
11857
- targetField: "externalId",
11858
- cardinality: "one",
11859
- renderHints: { label: "Established by (decision)", order: 5 }
11860
- },
11861
- {
11862
- fieldId: "adoptedOn",
11863
- fieldType: "date",
11864
- description: "ISO-8601 \u2014 when this convention was adopted. Range-queryable.",
11865
- renderHints: { label: "Adopted on", widget: "date", order: 6 }
11866
- }
11867
- // externalId: first-class identifier, not a payload field (see `decision`).
11868
- ],
11869
- // externalId has its own first-class finder for exact get — look it up
11870
- // directly, not as a schema lookup. Locked equality-vs-range choices:
11871
- // • `area`/`status` — EQUALITY: "active conventions for auth".
11872
- // • `establishedByDecision` — EQUALITY on the reference value: enumerate
11873
- // "conventions established by decision X" (a forward link query; the
11874
- // platform has no reverse-reference index on this surface).
11875
- // • `adoptedOn` — RANGE over the adoption date.
11876
- // 3 equality lookups (of 7 fast slots) + 1 range row.
11877
- lookupFields: ["area", "status", "establishedByDecision", { fieldName: "adoptedOn", rangeEnabled: true }]
11878
- },
11879
- {
11880
- // A gotcha / sharp edge: a symptom, its cause, and the fix. Saves the
11881
- // agent (and the human) from re-discovering the same trap.
11882
- typeName: "gotcha",
11883
- displayName: "Gotcha",
11884
- indexMode: "HYBRID",
11885
- fields: [
11886
- {
11887
- fieldId: "symptom",
11888
- fieldType: "string",
11889
- required: true,
11890
- searchable: true,
11891
- validation: { minLength: 1, maxLength: 500 },
11892
- renderHints: { label: "Symptom", widget: "textarea", order: 1, displayField: true }
11893
- },
11894
- {
11895
- fieldId: "cause",
11896
- fieldType: "string",
11897
- searchable: true,
11898
- renderHints: { label: "Cause", widget: "textarea", order: 2 }
11899
- },
11900
- {
11901
- fieldId: "fix",
11902
- fieldType: "string",
11903
- searchable: true,
11904
- renderHints: { label: "Fix", widget: "textarea", order: 3 }
11905
- },
11906
- {
11907
- fieldId: "area",
11908
- fieldType: "string",
11909
- filterable: true,
11910
- renderHints: { label: "Area", widget: "text", order: 4 }
11911
- },
11912
- {
11913
- // A gotcha can be RETIRED once the underlying trap is fixed for good —
11914
- // superseded, not deleted (the blueprint omits records:d).
11915
- fieldId: "status",
11916
- fieldType: "enum",
12351
+ fieldId: "project",
12352
+ fieldType: "string",
11917
12353
  filterable: true,
11918
- enumValues: ["active", "retired"],
11919
- renderHints: { label: "Status", widget: "select", order: 5 }
12354
+ description: "Grouping key for cross-session continuity.",
12355
+ renderHints: { label: "Project", widget: "text", order: 6, section: "Tracking" }
11920
12356
  },
11921
12357
  {
11922
- fieldId: "discoveredOn",
12358
+ fieldId: "dueDate",
11923
12359
  fieldType: "date",
11924
- description: "ISO-8601 \u2014 when this gotcha was first hit. Range-queryable.",
11925
- renderHints: { label: "Discovered on", widget: "date", order: 6 }
12360
+ description: "ISO-8601. Range-queryable (see lookupFields) \u2014 list tasks due in a window.",
12361
+ renderHints: { label: "Due date", widget: "date", order: 7, section: "Tracking" }
12362
+ },
12363
+ {
12364
+ fieldId: "tags",
12365
+ fieldType: "array",
12366
+ filterable: true,
12367
+ renderHints: { label: "Tags", order: 8, section: "Tracking" }
11926
12368
  }
11927
- // externalId: first-class identifier, not a payload field (see `decision`).
12369
+ // externalId is the record's FIRST-CLASS identifier (the dedup/lookup key + the
12370
+ // value a `reference` resolves against) — not a payload field; the loader sends
12371
+ // it top-level on the RecordRequest.
11928
12372
  ],
11929
- // externalId has its own first-class finder for exact get — look it up
11930
- // directly, not as a schema lookup. `area`/`status` EQUALITY ("active
11931
- // gotchas in search"); `discoveredOn` RANGE over the discovery date.
11932
- lookupFields: ["area", "status", { fieldName: "discoveredOn", rangeEnabled: true }]
12373
+ // Two lookup shapes, chosen deliberately because the equality-vs-range choice
12374
+ // per field is MIGRATION-LOCKED once the schema is live (you cannot flip a
12375
+ // field slot↔range later, even by removing and re-adding it):
12376
+ // • `project` — EQUALITY (a grouping key, not ordered): enumerate one
12377
+ // project's tasks directly, no search. Each equality lookup uses 1 of the
12378
+ // schema's 7 fast index slots.
12379
+ // • `dueDate` — RANGE: ordered `from`/`to`/`prefix` queries ("tasks due this
12380
+ // week", "due in 2026-06"). Range lookups use a relationship row, not a
12381
+ // slot, and are billed at the range rate. ISO-8601 sorts chronologically,
12382
+ // so the order is correct.
12383
+ // `externalId` has a built-in first-class finder and must NOT be redeclared here.
12384
+ lookupFields: ["project", { fieldName: "dueDate", rangeEnabled: true }]
11933
12385
  }
11934
12386
  ],
11935
12387
  // Least-privilege profile — MUST pass the CLI scope gate.
11936
- // r/c/u + search + schema discovery + inference:r (grounded "why did we do X?"
11937
- // recall over the captured rationale). NOT records:d: memory is superseded,
11938
- // not deleted, so the project's decision history stays auditable.
12388
+ // r/c/u + search + schema discovery. NOT records:d (least privilege).
11939
12389
  accessProfile: {
11940
- allowedActions: ["records:r", "records:c", "records:u", "search:r", "schemas:r", "inference:r"]
12390
+ allowedActions: ["records:r", "records:c", "records:u", "search:r", "schemas:r"],
12391
+ // dataScope: {} (present, empty) is the deliberate marker for a
12392
+ // tenant-level shared tracker — every task visible to every holder of
12393
+ // this credential. Add a `scope:org` entry instead for per-org isolation.
12394
+ dataScope: {}
11941
12395
  },
11942
12396
  servicePrincipal: {
11943
- externalId: "coding-agent-memory",
11944
- displayName: "Coding Agent \u2014 Project Memory"
12397
+ externalId: "task-management",
12398
+ displayName: "Task Management"
11945
12399
  },
11946
12400
  seed: [
11947
12401
  {
11948
- // Seeded FIRST so the convention below resolves its reference to it — the
11949
- // loader creates seeds in array order, and a reference target must exist when
11950
- // the referencing record is written.
11951
- surface: "record",
11952
- typeName: "decision",
11953
- externalId: "seed-use-vectros-for-memory",
11954
- fields: {
11955
- title: "Use Vectros as the coding agent\u2019s project memory",
11956
- statement: "Persist decisions, conventions, and gotchas as Vectros records so they survive across agent sessions.",
11957
- rationale: "A coding agent loses context every cold start. A governed, searchable memory lets it recall prior decisions by meaning instead of re-asking \u2014 and the version history shows how the thinking evolved.",
11958
- status: "active",
11959
- area: "getting-started",
11960
- decidedOn: "2026-06-14"
11961
- }
11962
- },
11963
- {
11964
- // Demonstrates a live typed link at bootstrap: this convention references the
11965
- // decision above by its externalId, so "open the decision behind this rule"
11966
- // works the moment the blueprint is applied.
11967
12402
  surface: "record",
11968
- typeName: "convention",
11969
- externalId: "seed-record-the-why",
12403
+ typeName: "task",
12404
+ externalId: "seed-welcome",
11970
12405
  fields: {
11971
- name: "Record the why, not just the what",
11972
- rule: "When you record a decision or convention, always capture the rationale \u2014 the trade-offs a future session cannot re-derive from the code.",
11973
- area: "getting-started",
11974
- status: "active",
11975
- establishedByDecision: "seed-use-vectros-for-memory",
11976
- adoptedOn: "2026-06-14"
12406
+ title: "Welcome to your Vectros task tracker",
12407
+ description: "Created by the bootstrap loader. Ask your agent to add tasks.",
12408
+ status: "todo",
12409
+ priority: "low",
12410
+ project: "getting-started",
12411
+ dueDate: "2026-06-30"
11977
12412
  }
11978
12413
  }
11979
12414
  ]
11980
12415
  };
11981
- var coding_agent_memory_default = codingAgentMemory;
12416
+ var task_management_default = taskManagement;
11982
12417
  var DATA_PLANE_ACTIONS = [
11983
12418
  "records:r",
11984
12419
  "records:c",
@@ -13105,7 +13540,11 @@ var agenticSdlc = {
13105
13540
  // mints for THIS blueprint's service principal (the MCP/API runtime). See
13106
13541
  // DATA_PLANE_ACTIONS above for the action set + rationale.
13107
13542
  accessProfile: {
13108
- allowedActions: DATA_PLANE_ACTIONS
13543
+ allowedActions: DATA_PLANE_ACTIONS,
13544
+ // dataScope: {} (present, empty) is the deliberate marker for tenant-wide
13545
+ // reach — this service key IS the runtime's own access to the whole KB,
13546
+ // not a multi-owner resource to isolate.
13547
+ dataScope: {}
13109
13548
  },
13110
13549
  // A reusable `editor` role for the HUMAN owner — DISTINCT from `accessProfile`
13111
13550
  // (which scopes only the service-principal key). `bootstrap` provisions this
@@ -13125,7 +13564,9 @@ var agenticSdlc = {
13125
13564
  // which includes members' private memory — the trusted-administrator tradeoff,
13126
13565
  // called out in the bundled guide. Scope it down if your deployment wants
13127
13566
  // curator access without private-memory visibility.
13128
- editor: [{ allowedActions: EDITOR_ACTIONS }],
13567
+ // dataScope: {} echoes the "deliberately UNSCOPED (whole-context)" call-out
13568
+ // above as an explicit marker, not just prose — see it for the tradeoff.
13569
+ editor: [{ allowedActions: EDITOR_ACTIONS, dataScope: {} }],
13129
13570
  // A team member (or their agent) — TWO memory tiers as two UNIONed clauses
13130
13571
  // (a record is accessible when ANY clause grants it; within one clause every
13131
13572
  // dataScope dimension must match):
@@ -13145,7 +13586,9 @@ var agenticSdlc = {
13145
13586
  // `vectros join agentic-sdlc --role member`; verify a binding with
13146
13587
  // `vectros access explain --principal me --context agentic-sdlc`.
13147
13588
  member: [
13148
- { allowedActions: MEMBER_SHARED_READ_ACTIONS },
13589
+ // dataScope: {} echoes clause 1's own "UNSCOPED, but safe" call-out above
13590
+ // as an explicit marker, not just prose.
13591
+ { allowedActions: MEMBER_SHARED_READ_ACTIONS, dataScope: {} },
13149
13592
  {
13150
13593
  // `inference:r` is IN this self-scoped clause (not only clause 1) so a
13151
13594
  // member's `rag_ask` can ground on its OWN private memory — the read
@@ -13260,7 +13703,11 @@ var secondBrain = {
13260
13703
  // (so the agent can `rag_ask` a grounded, cited question over your notes). No
13261
13704
  // records:d: notes are archived (status → 'archived'), never deleted.
13262
13705
  accessProfile: {
13263
- allowedActions: ["records:r", "records:c", "records:u", "search:r", "schemas:r", "inference:r"]
13706
+ allowedActions: ["records:r", "records:c", "records:u", "search:r", "schemas:r", "inference:r"],
13707
+ // dataScope: {} (present, empty) is the deliberate marker for tenant-wide
13708
+ // reach — this is a single personal-knowledge-base credential with one
13709
+ // holder, not a multi-owner shared resource to isolate.
13710
+ dataScope: {}
13264
13711
  },
13265
13712
  servicePrincipal: {
13266
13713
  externalId: "second-brain",
@@ -13392,7 +13839,11 @@ var clinicalIntake = {
13392
13839
  // reveal qualifier (the demo key literally cannot un-redact the PHI fields),
13393
13840
  // and NO inference:r (we never point a RAG capability at a PHI corpus here).
13394
13841
  accessProfile: {
13395
- allowedActions: ["records:r", "records:c", "records:u", "search:r", "schemas:r"]
13842
+ allowedActions: ["records:r", "records:c", "records:u", "search:r", "schemas:r"],
13843
+ // dataScope: {} (present, empty) is the deliberate marker for tenant-wide
13844
+ // reach, unchanged from this demo's original intent — a real clinical
13845
+ // deployment should scope this (e.g. `scope:org`) before handling real PHI.
13846
+ dataScope: {}
13396
13847
  },
13397
13848
  servicePrincipal: {
13398
13849
  externalId: "clinical-intake",
@@ -13421,7 +13872,6 @@ var clinicalIntake = {
13421
13872
  var clinical_intake_default = clinicalIntake;
13422
13873
  var BUNDLED_BLUEPRINTS = [
13423
13874
  task_management_default,
13424
- coding_agent_memory_default,
13425
13875
  agentic_sdlc_default,
13426
13876
  second_brain_default,
13427
13877
  clinical_intake_default
@@ -13720,18 +14170,17 @@ async function mintBridgeToken(opts) {
13720
14170
  ttlSeconds = DEFAULT_BRIDGE_TTL_SECONDS,
13721
14171
  tenant = "live",
13722
14172
  useScopedToken = false,
14173
+ contextId,
13723
14174
  fetchImpl = fetch
13724
14175
  } = opts;
13725
14176
  const base = apiBaseUrl.replace(/\/$/, "");
13726
14177
  const path4 = bridgeTokenPath(useScopedToken);
13727
- const url = `${base}${path4}?ttl=${encodeURIComponent(String(ttlSeconds))}&tenant=${encodeURIComponent(tenant)}`;
13728
- return fetchStToken(
13729
- url,
13730
- cognitoToken,
13731
- fetchImpl,
13732
- path4,
13733
- (status) => status === 404 && !useScopedToken ? ` (the dedicated ${CLI_BOOTSTRAP_TOKEN_PATH} endpoint may not be deployed yet \u2014 retry with --use-scoped-token for the fallback path)` : ""
13734
- );
14178
+ let url = `${base}${path4}?ttl=${encodeURIComponent(String(ttlSeconds))}&tenant=${encodeURIComponent(tenant)}`;
14179
+ if (contextId) url += `&context=${encodeURIComponent(contextId)}`;
14180
+ return fetchStToken(url, cognitoToken, fetchImpl, path4, (status) => {
14181
+ if (status !== 404 || useScopedToken || contextId) return "";
14182
+ return ` (the dedicated ${CLI_BOOTSTRAP_TOKEN_PATH} endpoint may not be deployed yet \u2014 retry with --use-scoped-token for the fallback path)`;
14183
+ });
13735
14184
  }
13736
14185
  async function mintContextToken(opts) {
13737
14186
  const {
@@ -13995,7 +14444,12 @@ async function resolveAuthContext(opts) {
13995
14444
  const baseUrl = envToBaseUrl(opts.env, opts.baseUrl ?? process.env.VECTROS_API_BASE_URL);
13996
14445
  const cognitoToken = await resolveCognitoToken(opts.env, opts.token);
13997
14446
  const tenant = opts.tenant ?? "live";
13998
- const bridge = opts.provisioning ? await mintBridgeToken({ cognitoToken, apiBaseUrl: baseUrl, tenant }) : await mintContextToken({ cognitoToken, apiBaseUrl: baseUrl, contextId: opts.context ?? "default", tenant });
14447
+ const bridge = opts.provisioning ? await mintBridgeToken({
14448
+ cognitoToken,
14449
+ apiBaseUrl: baseUrl,
14450
+ tenant,
14451
+ ...opts.context ? { contextId: opts.context } : {}
14452
+ }) : await mintContextToken({ cognitoToken, apiBaseUrl: baseUrl, contextId: opts.context ?? "default", tenant });
13999
14453
  const client = new VectrosClient({ token: bridge.token, environment: baseUrl });
14000
14454
  const identity = await resolveIdentity({ log: opts.log, apiKey: bridge.token, environment: baseUrl });
14001
14455
  return {
@@ -14114,8 +14568,32 @@ var KNOWN_CONTROL_PLANE_RESOURCES = [
14114
14568
  // control-plane, unlike per-namespace entity CRUD. (Cosmetic: any
14115
14569
  // non-data-plane resource hard-rejects whether or not it is listed here;
14116
14570
  // this list only sharpens the error message.)
14571
+ //
14572
+ // ⚠️ This bars `namespaces` as a SCOPE — no partner-authored role can ever
14573
+ // be granted it. It does NOT bar a blueprint's own top-level `namespaces[]`
14574
+ // FORMAT field (@vectros-ai/blueprints), which deliberately DOES write this
14575
+ // same registry — the same "DISCRETIONARY vs UNCONDITIONAL" distinction
14576
+ // {@link PARTIALLY_ADMITTED_OPS} draws for `entities`, applied here: the
14577
+ // write rides `provisioning:c` (non-grantable — no partner role can ever
14578
+ // hold it, the same bearer `keys`/`app-contexts` writes already require)
14579
+ // and the platform confines it to the blueprint's OWN context
14580
+ // unconditionally, never tenant-wide, so the reach is bounded to what the
14581
+ // blueprint already provisions, never the pack's say-so alone. See
14582
+ // `@vectros-ai/blueprints`' `namespaces[]` doc for the full design.
14117
14583
  "namespaces"
14118
14584
  ];
14585
+ var PARTIALLY_ADMITTED_OPS = /* @__PURE__ */ new Map([
14586
+ ["profiles", /* @__PURE__ */ new Set(["r", "u", "d"])]
14587
+ ]);
14588
+ var SELF_QUALIFIED_OPS = /* @__PURE__ */ new Map([
14589
+ ["profiles", /* @__PURE__ */ new Set(["c"])]
14590
+ ]);
14591
+ var UNQUALIFIABLE_WITH_ANY_TAIL = /* @__PURE__ */ new Map([
14592
+ ["profiles", /* @__PURE__ */ new Set(["r"])]
14593
+ ]);
14594
+ var CAPABILITY_UNLOCKED_OPS = /* @__PURE__ */ new Map([
14595
+ ["profiles", /* @__PURE__ */ new Map([["c", "member-lifecycle"]])]
14596
+ ]);
14119
14597
  var VALID_OPS = /* @__PURE__ */ new Set(["c", "r", "u", "d"]);
14120
14598
  var DATA_PLANE_SET = new Set(DATA_PLANE_ALLOWLIST);
14121
14599
  var CONTROL_PLANE_SET = new Set(KNOWN_CONTROL_PLANE_RESOURCES);
@@ -14132,7 +14610,60 @@ ${summary}`
14132
14610
  this.rejected = rejected;
14133
14611
  }
14134
14612
  };
14135
- function evaluateScopeToken(action) {
14613
+ var GRANTED_CAPABILITIES_ALLOWLIST = ["member-lifecycle"];
14614
+ var GRANTED_CAPABILITIES_SET = new Set(GRANTED_CAPABILITIES_ALLOWLIST);
14615
+ var CAPABILITIES_HINT = GRANTED_CAPABILITIES_ALLOWLIST.join("/");
14616
+ var CapabilityGateError = class extends Error {
14617
+ rejected;
14618
+ constructor(rejected) {
14619
+ const summary = rejected.map((r) => ` \u2022 ${r.action}: ${r.reason}`).join("\n");
14620
+ super(
14621
+ `Capability gate rejected ${rejected.length} requested capability(ies); the bootstrap loader grants (${CAPABILITIES_HINT}) only and mints nothing on reject:
14622
+ ${summary}`
14623
+ );
14624
+ this.name = "CapabilityGateError";
14625
+ this.rejected = rejected;
14626
+ }
14627
+ };
14628
+ function evaluateCapabilityToken(name) {
14629
+ const token = name.trim();
14630
+ if (token.length === 0) {
14631
+ return { action: name, reason: "empty capability name" };
14632
+ }
14633
+ if (!GRANTED_CAPABILITIES_SET.has(token)) {
14634
+ return {
14635
+ action: name,
14636
+ reason: `requests capability '${token}', which the bootstrap loader does not grant (${CAPABILITIES_HINT} only). A capability outside this list must be requested deliberately in the developer portal, never via the bootstrap loader`
14637
+ };
14638
+ }
14639
+ return null;
14640
+ }
14641
+ function evaluateCapabilityGate(capabilities) {
14642
+ const accepted = [];
14643
+ const rejected = [];
14644
+ for (const name of capabilities ?? []) {
14645
+ const rejection = evaluateCapabilityToken(name);
14646
+ if (rejection) {
14647
+ rejected.push(rejection);
14648
+ } else {
14649
+ accepted.push(name);
14650
+ }
14651
+ }
14652
+ return { ok: rejected.length === 0, accepted, rejected };
14653
+ }
14654
+ function assertCapabilityGate(capabilities) {
14655
+ const result = evaluateCapabilityGate(capabilities);
14656
+ if (!result.ok) {
14657
+ throw new CapabilityGateError(result.rejected);
14658
+ }
14659
+ }
14660
+ function firstInvalidOp(ops) {
14661
+ for (const ch of ops) {
14662
+ if (!VALID_OPS.has(ch)) return ch;
14663
+ }
14664
+ return void 0;
14665
+ }
14666
+ function evaluateScopeToken(action, capabilities) {
14136
14667
  const token = action.trim();
14137
14668
  if (token.length === 0) {
14138
14669
  return { action, reason: "empty scope token" };
@@ -14153,6 +14684,51 @@ function evaluateScopeToken(action) {
14153
14684
  };
14154
14685
  }
14155
14686
  if (!DATA_PLANE_SET.has(resource)) {
14687
+ const admittedOps = PARTIALLY_ADMITTED_OPS.get(resource);
14688
+ const selfOnlyOps = SELF_QUALIFIED_OPS.get(resource);
14689
+ if (admittedOps || selfOnlyOps) {
14690
+ const admittedHint = [.../* @__PURE__ */ new Set([...admittedOps ?? [], ...selfOnlyOps ?? []])].sort().join("/");
14691
+ if (ops === "*") {
14692
+ return {
14693
+ action,
14694
+ reason: `requests '${resource}:*', which includes operation(s) outside the narrow allowlist (${admittedHint}) admitted for this control-plane resource. Request only the specific operations you need`
14695
+ };
14696
+ }
14697
+ const invalidOp2 = firstInvalidOp(ops);
14698
+ if (invalidOp2) {
14699
+ return {
14700
+ action,
14701
+ reason: `invalid operation character '${invalidOp2}' (valid operations are c, r, u, d, or '*')`
14702
+ };
14703
+ }
14704
+ const qualifier = parts.length > 2 ? parts.slice(2).join(":") : void 0;
14705
+ const isSelfQualified = qualifier === "self";
14706
+ const unqualifiableOps = UNQUALIFIABLE_WITH_ANY_TAIL.get(resource);
14707
+ for (const ch of ops) {
14708
+ if (qualifier !== void 0 && unqualifiableOps?.has(ch)) {
14709
+ return {
14710
+ action,
14711
+ reason: `requests '${resource}:${ch}' alongside a qualifier ('${qualifier}'), but the platform does not resolve a qualifier for '${ch}' on '${resource}' at all \u2014 it will reject the WHOLE entry as malformed, not just ignore the qualifier. Split into separate entries (e.g. '${resource}:${ch}' + the other op(s) with their qualifier).`
14712
+ };
14713
+ }
14714
+ if (admittedOps?.has(ch)) continue;
14715
+ if (selfOnlyOps?.has(ch)) {
14716
+ if (isSelfQualified) continue;
14717
+ const unlockCapability = CAPABILITY_UNLOCKED_OPS.get(resource)?.get(ch);
14718
+ if (unlockCapability && qualifier === void 0 && capabilities?.includes(unlockCapability)) continue;
14719
+ const capabilityHintApplies = unlockCapability && !capabilities?.includes(unlockCapability);
14720
+ return {
14721
+ action,
14722
+ 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`
14723
+ };
14724
+ }
14725
+ return {
14726
+ action,
14727
+ reason: `requests control-plane scope '${resource}:${ch}', which is outside the narrow allowlist (${admittedHint}) admitted for this resource. That operation must be done deliberately in the developer portal, never via the bootstrap loader`
14728
+ };
14729
+ }
14730
+ return null;
14731
+ }
14156
14732
  if (CONTROL_PLANE_SET.has(resource)) {
14157
14733
  return {
14158
14734
  action,
@@ -14167,21 +14743,20 @@ function evaluateScopeToken(action) {
14167
14743
  if (ops === "*") {
14168
14744
  return null;
14169
14745
  }
14170
- for (const ch of ops) {
14171
- if (!VALID_OPS.has(ch)) {
14172
- return {
14173
- action,
14174
- reason: `invalid operation character '${ch}' (valid operations are c, r, u, d, or '*')`
14175
- };
14176
- }
14746
+ const invalidOp = firstInvalidOp(ops);
14747
+ if (invalidOp) {
14748
+ return {
14749
+ action,
14750
+ reason: `invalid operation character '${invalidOp}' (valid operations are c, r, u, d, or '*')`
14751
+ };
14177
14752
  }
14178
14753
  return null;
14179
14754
  }
14180
- function evaluateScopeGate(allowedActions) {
14755
+ function evaluateScopeGate(allowedActions, capabilities) {
14181
14756
  const accepted = [];
14182
14757
  const rejected = [];
14183
14758
  for (const action of allowedActions) {
14184
- const rejection = evaluateScopeToken(action);
14759
+ const rejection = evaluateScopeToken(action, capabilities);
14185
14760
  if (rejection) {
14186
14761
  rejected.push(rejection);
14187
14762
  } else {
@@ -14190,12 +14765,34 @@ function evaluateScopeGate(allowedActions) {
14190
14765
  }
14191
14766
  return { ok: rejected.length === 0, accepted, rejected };
14192
14767
  }
14193
- function assertScopeGate(allowedActions) {
14194
- const result = evaluateScopeGate(allowedActions);
14768
+ function assertScopeGate(allowedActions, capabilities) {
14769
+ const result = evaluateScopeGate(allowedActions, capabilities);
14195
14770
  if (!result.ok) {
14196
14771
  throw new ScopeGateError(result.rejected);
14197
14772
  }
14198
14773
  }
14774
+ function assertBlueprintGates(blueprint) {
14775
+ if (blueprint.accessProfile.allowedActions !== void 0) {
14776
+ assertScopeGate(blueprint.accessProfile.allowedActions, blueprint.accessProfile.capabilities);
14777
+ assertCapabilityGate(blueprint.accessProfile.capabilities);
14778
+ }
14779
+ for (const clauses of Object.values(blueprint.roles ?? {})) {
14780
+ for (const clause of clauses) {
14781
+ assertScopeGate(clause.allowedActions, clause.capabilities);
14782
+ assertCapabilityGate(clause.capabilities);
14783
+ }
14784
+ }
14785
+ }
14786
+ function effectiveAccessProfileActions(blueprint) {
14787
+ if (blueprint.accessProfile.allowedActions !== void 0) {
14788
+ return [...blueprint.accessProfile.allowedActions];
14789
+ }
14790
+ const out = [];
14791
+ for (const roleId of blueprint.accessProfile.roleIds ?? []) {
14792
+ for (const clause of blueprint.roles?.[roleId] ?? []) out.push(...clause.allowedActions);
14793
+ }
14794
+ return out;
14795
+ }
14199
14796
 
14200
14797
  // src/machine-id.ts
14201
14798
  var import_node_fs = require("fs");
@@ -14300,6 +14897,70 @@ async function resolveApplyTimeIdentities(blueprint, client, log) {
14300
14897
  function actionOf(created) {
14301
14898
  return created ? "created" : "reconciled";
14302
14899
  }
14900
+ async function registerNamespaceIdempotently(client, blueprintContextId, decl) {
14901
+ const contextIdForRequest = decl.tenantWide ? void 0 : blueprintContextId;
14902
+ const body = {
14903
+ namespace: decl.namespace,
14904
+ entityBacked: decl.entityBacked,
14905
+ specificityRank: decl.specificityRank,
14906
+ membershipRecordType: decl.membershipRecordType,
14907
+ membershipTargetField: decl.membershipTargetField,
14908
+ membershipLevelField: decl.membershipLevelField,
14909
+ membershipLevels: decl.membershipLevels
14910
+ };
14911
+ try {
14912
+ await client.registerNamespace({ contextId: contextIdForRequest, body });
14913
+ return "created";
14914
+ } catch (err) {
14915
+ const info = extractServerError(err);
14916
+ if (info.statusCode !== 400) throw err;
14917
+ if (decl.tenantWide) throw err;
14918
+ let existing;
14919
+ try {
14920
+ existing = await client.getNamespace({ namespace: decl.namespace, contextId: contextIdForRequest });
14921
+ } catch {
14922
+ throw err;
14923
+ }
14924
+ if (existing.contextId !== contextIdForRequest) throw err;
14925
+ const mismatch = describeNamespaceMismatch(decl, existing);
14926
+ if (mismatch) {
14927
+ throw new Error(
14928
+ `namespace '${decl.namespace}' is already registered with different settings than this blueprint declares (${mismatch}) \u2014 this looks like a collision with another blueprint or a manual registration, not a benign re-apply. Resolve the disagreement (or rename this blueprint's namespace) before re-applying.`
14929
+ );
14930
+ }
14931
+ return "reconciled";
14932
+ }
14933
+ }
14934
+ function describeNamespaceMismatch(decl, existing) {
14935
+ const mismatches = [];
14936
+ const declaredEntityBacked = decl.entityBacked ?? false;
14937
+ const existingEntityBacked = existing.entityBacked ?? false;
14938
+ if (declaredEntityBacked !== existingEntityBacked) {
14939
+ mismatches.push(
14940
+ `entityBacked: declared ${JSON.stringify(declaredEntityBacked)}, registered ${JSON.stringify(existingEntityBacked)}`
14941
+ );
14942
+ }
14943
+ const fields = [
14944
+ "specificityRank",
14945
+ "membershipRecordType",
14946
+ "membershipTargetField",
14947
+ "membershipLevelField"
14948
+ ];
14949
+ for (const field of fields) {
14950
+ const declared = decl[field];
14951
+ const existingValue = existing[field];
14952
+ if (declared === void 0 && (existingValue === void 0 || existingValue === null)) continue;
14953
+ if (declared === existingValue) continue;
14954
+ mismatches.push(`${field}: declared ${JSON.stringify(declared)}, registered ${JSON.stringify(existingValue)}`);
14955
+ }
14956
+ const declaredLevels = decl.membershipLevels;
14957
+ const existingLevels = existing.membershipLevels;
14958
+ const levelsDiffer = declaredLevels === void 0 ? existingLevels != null && existingLevels.length > 0 : JSON.stringify([...declaredLevels].sort()) !== JSON.stringify([...existingLevels ?? []].sort());
14959
+ if (levelsDiffer) {
14960
+ mismatches.push(`membershipLevels: declared ${JSON.stringify(declaredLevels)}, registered ${JSON.stringify(existingLevels)}`);
14961
+ }
14962
+ return mismatches.length > 0 ? mismatches.join("; ") : null;
14963
+ }
14303
14964
  var LostKeyError = class extends Error {
14304
14965
  constructor(keyName) {
14305
14966
  super(
@@ -14310,6 +14971,20 @@ var LostKeyError = class extends Error {
14310
14971
  }
14311
14972
  keyName;
14312
14973
  };
14974
+ var ExistingPrincipalNotConfirmedError = class extends Error {
14975
+ constructor(contextId, userId, externalId) {
14976
+ super(
14977
+ `About to bind a NEW scoped key to a principal that already existed in this tenant before this run (externalId '${externalId}', userId '${userId}', context '${contextId}'). This is either an intentional multi-machine setup for an integration you already own, or this blueprint's servicePrincipal.externalId collided with an unrelated existing identity \u2014 confirm before a new credential is minted bound to it.`
14978
+ );
14979
+ this.contextId = contextId;
14980
+ this.userId = userId;
14981
+ this.externalId = externalId;
14982
+ this.name = "ExistingPrincipalNotConfirmedError";
14983
+ }
14984
+ contextId;
14985
+ userId;
14986
+ externalId;
14987
+ };
14313
14988
  function mapEnumValues(enumValues) {
14314
14989
  if (!enumValues) return void 0;
14315
14990
  return enumValues.map((value) => ({ value }));
@@ -14384,7 +15059,8 @@ function toCapabilities(capabilities) {
14384
15059
  function toRoleScopeClauses(clauses) {
14385
15060
  return clauses.map((c) => ({
14386
15061
  allowed_actions: c.allowedActions,
14387
- data_scope: c.dataScope
15062
+ data_scope: c.dataScope,
15063
+ granted_capabilities: c.capabilities
14388
15064
  }));
14389
15065
  }
14390
15066
  var UnresolvedIdentitiesError = class extends Error {
@@ -14403,13 +15079,24 @@ function assertNoUnresolvedIdentities(blueprint) {
14403
15079
  );
14404
15080
  }
14405
15081
  }
14406
- async function runLoader(client, input) {
14407
- const { blueprint, tenantId, machineId, rotate = false, skipSeed = false, readExistingKey, persistMintedKey, log } = input;
14408
- assertScopeGate(blueprint.accessProfile.allowedActions);
14409
- for (const clauses of Object.values(blueprint.roles ?? {})) {
14410
- for (const clause of clauses) assertScopeGate(clause.allowedActions);
15082
+ var TenantWideNamespacesNotAllowedError = class extends Error {
15083
+ constructor(message) {
15084
+ super(message);
15085
+ this.name = "TenantWideNamespacesNotAllowedError";
14411
15086
  }
14412
- assertNoUnresolvedIdentities(blueprint);
15087
+ };
15088
+ function assertTenantWideNamespacesAllowed(blueprint, allow) {
15089
+ if (allow) return;
15090
+ const names = (blueprint.namespaces ?? []).filter((ns) => ns.tenantWide === true).map((ns) => ns.namespace);
15091
+ if (names.length === 0) return;
15092
+ throw new TenantWideNamespacesNotAllowedError(
15093
+ `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).`
15094
+ );
15095
+ }
15096
+ async function provisionContext(client, input) {
15097
+ const { blueprint, allowTenantWideNamespaces = false, readExistingKey, confirmExistingPrincipal, log } = input;
15098
+ assertBlueprintGates(blueprint);
15099
+ assertTenantWideNamespacesAllowed(blueprint, allowTenantWideNamespaces);
14413
15100
  const ctxResp = await client.auth.createAppContext({ body: { contextId: blueprint.contextId, name: contextNameOf(blueprint) } });
14414
15101
  const contextAction = actionOf(ctxResp.created);
14415
15102
  log.debug({ component: "loader", contextId: blueprint.contextId, contextAction }, "app-context ensured");
@@ -14426,10 +15113,77 @@ async function runLoader(client, input) {
14426
15113
  if (!userId) {
14427
15114
  throw new Error("Service-principal create returned no user id; cannot bind AccessProfile/key.");
14428
15115
  }
14429
- log.debug({ component: "loader", externalId: blueprint.servicePrincipal.externalId, userId }, "service principal ensured");
15116
+ const userAction = actionOf(user.created);
15117
+ log.debug(
15118
+ { component: "loader", externalId: blueprint.servicePrincipal.externalId, userId, userAction },
15119
+ "service principal ensured"
15120
+ );
15121
+ const localKeyForContext = await readExistingKey(blueprint.contextId);
15122
+ const storedPrincipalId = localKeyForContext?.principalId;
15123
+ const localKeyNamesThisPrincipal = storedPrincipalId === `usr_${userId}` || storedPrincipalId === userId;
15124
+ if (userAction === "reconciled" && !localKeyNamesThisPrincipal) {
15125
+ const proceed = confirmExistingPrincipal ? await confirmExistingPrincipal({
15126
+ contextId: blueprint.contextId,
15127
+ userId,
15128
+ externalId: blueprint.servicePrincipal.externalId
15129
+ }) : true;
15130
+ if (!proceed) {
15131
+ throw new ExistingPrincipalNotConfirmedError(blueprint.contextId, userId, blueprint.servicePrincipal.externalId);
15132
+ }
15133
+ log.debug(
15134
+ { component: "loader", contextId: blueprint.contextId, userId },
15135
+ "existing-principal collision confirmed (or unwired) \u2014 proceeding with any further write to this principal"
15136
+ );
15137
+ }
15138
+ const needsContextPinnedBootstrap = (blueprint.namespaces ?? []).length > 0;
15139
+ const ctxBootstrapClient = needsContextPinnedBootstrap ? input.remintBootstrapForContext ? await input.remintBootstrapForContext(blueprint.contextId) : client : void 0;
15140
+ if (ctxBootstrapClient && input.remintBootstrapForContext) {
15141
+ log.debug({ component: "loader", contextId: blueprint.contextId }, "re-minted context-pinned bootstrap client (namespaces)");
15142
+ }
15143
+ const namespaceActions = [];
15144
+ for (const ns of blueprint.namespaces ?? []) {
15145
+ const action = await registerNamespaceIdempotently(ctxBootstrapClient.identity, blueprint.contextId, ns);
15146
+ namespaceActions.push({ namespace: ns.namespace, action });
15147
+ log.debug({ component: "loader", namespace: ns.namespace, contextId: blueprint.contextId, action }, "namespace ensured");
15148
+ }
15149
+ return { contextAction, servicePrincipalUserId: userId, userAction, namespaceActions, ctxBootstrapClient };
15150
+ }
15151
+ async function runLoader(client, input) {
15152
+ const {
15153
+ blueprint,
15154
+ tenantId,
15155
+ machineId,
15156
+ rotate = false,
15157
+ skipSeed = false,
15158
+ allowTenantWideNamespaces = false,
15159
+ readExistingKey,
15160
+ persistMintedKey,
15161
+ confirmExistingPrincipal,
15162
+ log
15163
+ } = input;
15164
+ assertNoUnresolvedIdentities(blueprint);
15165
+ const provisioned = input.provisioned ?? await provisionContext(client, {
15166
+ blueprint,
15167
+ allowTenantWideNamespaces,
15168
+ remintBootstrapForContext: input.remintBootstrapForContext,
15169
+ readExistingKey,
15170
+ confirmExistingPrincipal,
15171
+ log
15172
+ });
15173
+ const { contextAction, servicePrincipalUserId: userId, userAction, namespaceActions } = provisioned;
15174
+ assertBlueprintGates(blueprint);
15175
+ const needsContextPinnedBootstrapForIssuers = (blueprint.issuers ?? []).length > 0;
15176
+ const ctxBootstrapClient = provisioned.ctxBootstrapClient ?? (needsContextPinnedBootstrapForIssuers ? input.remintBootstrapForContext ? await input.remintBootstrapForContext(blueprint.contextId) : client : void 0);
15177
+ if (ctxBootstrapClient && !provisioned.ctxBootstrapClient && input.remintBootstrapForContext) {
15178
+ log.debug({ component: "loader", contextId: blueprint.contextId }, "re-minted context-pinned bootstrap client (issuers)");
15179
+ }
14430
15180
  const issuerActions = [];
14431
15181
  for (const issuer of blueprint.issuers ?? []) {
14432
- const issuerResp = await client.auth.registerIssuer({
15182
+ const selfSignupPolicies = issuer.selfSignupPolicies?.map((p) => ({
15183
+ signup_type: p.signupType,
15184
+ role_id: p.roleId
15185
+ }));
15186
+ const issuerResp = await ctxBootstrapClient.auth.registerIssuer({
14433
15187
  issuerId: issuer.issuerId,
14434
15188
  issuer: issuer.issuer,
14435
15189
  jwksUri: issuer.jwksUri,
@@ -14437,18 +15191,26 @@ async function runLoader(client, input) {
14437
15191
  contextId: issuer.contextId,
14438
15192
  subClaim: issuer.subClaim,
14439
15193
  emailClaim: issuer.emailClaim,
14440
- selfSignupPolicies: issuer.selfSignupPolicies?.map((p) => ({
14441
- signup_type: p.signupType,
14442
- role_id: p.roleId
14443
- }))
15194
+ userinfoUri: issuer.userinfoUri,
15195
+ selfSignupPolicies
14444
15196
  });
14445
15197
  const action = actionOf(issuerResp.created);
14446
15198
  issuerActions.push({ issuerId: issuer.issuerId, action });
14447
15199
  if (action === "reconciled") {
14448
- log.warn(
14449
- { component: "loader", issuerId: issuer.issuerId },
14450
- "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"
14451
- );
15200
+ await ctxBootstrapClient.auth.updateIssuer({
15201
+ issuerId: issuer.issuerId,
15202
+ // Trust-anchor/routing-pin fields — echoed back, never actually
15203
+ // changed here (the platform rejects a differing value).
15204
+ issuer: issuer.issuer,
15205
+ jwksUri: issuer.jwksUri,
15206
+ audience: issuer.audience,
15207
+ contextId: issuer.contextId,
15208
+ subClaim: issuer.subClaim,
15209
+ emailClaim: issuer.emailClaim,
15210
+ userinfoUri: issuer.userinfoUri,
15211
+ selfSignupPolicies
15212
+ });
15213
+ log.debug({ component: "loader", issuerId: issuer.issuerId }, "issuer already registered \u2014 reconciled safe fields to the declared values");
14452
15214
  } else {
14453
15215
  log.debug({ component: "loader", issuerId: issuer.issuerId, action }, "issuer registered");
14454
15216
  }
@@ -14488,10 +15250,35 @@ async function runLoader(client, input) {
14488
15250
  schemaActions.push({ typeName: s.typeName, action: actionOf(resp.created) });
14489
15251
  log.debug({ component: "loader", typeName: s.typeName, schemaId: resp.id, action: actionOf(resp.created) }, "schema ensured (upsert)");
14490
15252
  }
14491
- const profileBody = {
15253
+ const roleIds = [];
15254
+ const roleActions = [];
15255
+ for (const [roleId, clauses] of Object.entries(blueprint.roles ?? {})) {
15256
+ const roleResp = await ctxClient.auth.createRole({
15257
+ contextId: blueprint.contextId,
15258
+ upsert: true,
15259
+ body: {
15260
+ roleId,
15261
+ // Blueprint roles carry no display name → default it to the roleId.
15262
+ name: roleId,
15263
+ scopes: toRoleScopeClauses(clauses),
15264
+ assumable: blueprint.roleAssumable?.[roleId]
15265
+ }
15266
+ });
15267
+ roleIds.push(roleId);
15268
+ roleActions.push({ roleId, action: actionOf(roleResp.created) });
15269
+ log.debug({ component: "loader", roleId, clauses: clauses.length, action: actionOf(roleResp.created) }, "role ensured (upsert)");
15270
+ }
15271
+ const profileBody = blueprint.accessProfile.roleIds ? {
15272
+ principalId: `usr_${userId}`,
15273
+ roleIds: blueprint.accessProfile.roleIds,
15274
+ identityOverrides: blueprint.accessProfile.identityOverrides
15275
+ } : {
14492
15276
  principalId: `usr_${userId}`,
14493
15277
  scopes: [
14494
15278
  {
15279
+ // Non-null: this is the `else` branch of the roleIds XOR above —
15280
+ // `@vectros-ai/blueprints`' BlueprintAccessProfileSchema guarantees
15281
+ // allowedActions is set whenever roleIds is not.
14495
15282
  allowed_actions: blueprint.accessProfile.allowedActions,
14496
15283
  // Pass dataScope straight through to the wire. The blueprint type is
14497
15284
  // Record<string, (string|null)[]> — a `null` element is the NULL
@@ -14499,7 +15286,10 @@ async function runLoader(client, input) {
14499
15286
  // literal JSON null, which is exactly what the platform's scope
14500
15287
  // matcher tests via `allowedValues.contains(null)`. The cast bridges
14501
15288
  // to the SDK's loosely-typed Record<string, Record<string,unknown>>.
14502
- data_scope: blueprint.accessProfile.dataScope
15289
+ data_scope: blueprint.accessProfile.dataScope,
15290
+ // Already gate-validated (assertBlueprintGates, above this function's
15291
+ // call site) before this body is ever built.
15292
+ granted_capabilities: blueprint.accessProfile.capabilities
14503
15293
  }
14504
15294
  ],
14505
15295
  // Optional identity overrides — scope values the service key STAMPS onto
@@ -14522,22 +15312,6 @@ async function runLoader(client, input) {
14522
15312
  body: profileBody
14523
15313
  });
14524
15314
  log.debug({ component: "loader", principalId: `usr_${userId}`, accessProfileId: profile.id, profileAction }, "access profile ensured + reconciled to declared scopes");
14525
- const roleIds = [];
14526
- const roleActions = [];
14527
- for (const [roleId, clauses] of Object.entries(blueprint.roles ?? {})) {
14528
- const roleResp = await ctxClient.auth.createRole({
14529
- contextId: blueprint.contextId,
14530
- body: {
14531
- roleId,
14532
- // Blueprint roles carry no display name → default it to the roleId.
14533
- name: roleId,
14534
- scopes: toRoleScopeClauses(clauses)
14535
- }
14536
- });
14537
- roleIds.push(roleId);
14538
- roleActions.push({ roleId, action: actionOf(roleResp.created) });
14539
- log.debug({ component: "loader", roleId, clauses: clauses.length, action: actionOf(roleResp.created) }, "role ensured");
14540
- }
14541
15315
  const keyName = keyNameFor(blueprint.name, machineId);
14542
15316
  let keyAction = "created";
14543
15317
  let revokedKeyId;
@@ -14557,7 +15331,7 @@ async function runLoader(client, input) {
14557
15331
  keyId = keyResp.keyId ?? "";
14558
15332
  rawKey = keyResp.rawKey;
14559
15333
  if (keyId) {
14560
- await persistMintedKey(blueprint.contextId, { keyId, rawKey, keyName, principalId: userId, revokedKeyId });
15334
+ await persistMintedKey(blueprint.contextId, { keyId, rawKey, keyName, principalId: `usr_${userId}`, revokedKeyId });
14561
15335
  }
14562
15336
  if (keyAction !== "rotated") keyAction = "created";
14563
15337
  } else {
@@ -14631,10 +15405,12 @@ async function runLoader(client, input) {
14631
15405
  keyName,
14632
15406
  keyAction,
14633
15407
  contextAction,
15408
+ userAction,
14634
15409
  profileAction,
14635
15410
  schemaActions,
14636
15411
  roleActions,
14637
15412
  issuerActions,
15413
+ namespaceActions,
14638
15414
  rawKey,
14639
15415
  keyId,
14640
15416
  revokedKeyId,
@@ -14646,6 +15422,11 @@ async function runLoader(client, input) {
14646
15422
  seedIndexFailures
14647
15423
  };
14648
15424
  }
15425
+ function renderPlanDataScope(ds) {
15426
+ const entries = Object.entries(ds ?? {});
15427
+ if (entries.length === 0) return "(none \u2014 unconstrained)";
15428
+ return entries.map(([dim, values]) => `${dim}=[${values.map((v) => v === null ? "null" : v).join(", ")}]`).join(", ");
15429
+ }
14649
15430
  function buildPlan(blueprint, opts = {}) {
14650
15431
  const steps = [];
14651
15432
  steps.push({ kind: "context", summary: `context '${blueprint.contextId}' (${contextNameOf(blueprint)})` });
@@ -14653,6 +15434,15 @@ function buildPlan(blueprint, opts = {}) {
14653
15434
  kind: "principal",
14654
15435
  summary: `principal '${blueprint.servicePrincipal.externalId}' (SERVICE)`
14655
15436
  });
15437
+ for (const ns of blueprint.namespaces ?? []) {
15438
+ const ownerNote = ns.tenantWide ? "TENANT-WIDE \u2014 visible to every context" : `owned by '${blueprint.contextId}'`;
15439
+ const entityNote = ns.entityBacked ? ", entity-backed" : "";
15440
+ const membershipNote = ns.membershipRecordType ? `, membership: ${ns.membershipRecordType}.${ns.membershipTargetField}` + (ns.membershipLevelField ? ` (levels: ${ns.membershipLevels?.join("/")})` : "") : "";
15441
+ steps.push({
15442
+ kind: "namespace",
15443
+ summary: `namespace '${ns.namespace}' (${ownerNote}${entityNote}${membershipNote}) [${BLUEPRINT_FIELD_PHASES.namespaces}-token phase]`
15444
+ });
15445
+ }
14656
15446
  for (const iss of blueprint.issuers ?? []) {
14657
15447
  const selfSignup = iss.selfSignupPolicies?.length ? `, self-signup: ${iss.selfSignupPolicies.length} polic${iss.selfSignupPolicies.length === 1 ? "y" : "ies"}` : "";
14658
15448
  steps.push({
@@ -14673,17 +15463,40 @@ function buildPlan(blueprint, opts = {}) {
14673
15463
  summary: `schema '${s.typeName}' (${s.fields.length} field${s.fields.length === 1 ? "" : "s"}, ${indexNote})${lookupNote}${surfaceNote}${sensitiveNote}`
14674
15464
  });
14675
15465
  }
14676
- const actions = blueprint.accessProfile.allowedActions;
14677
- const scopeNote = blueprint.accessProfile.dataScope && Object.keys(blueprint.accessProfile.dataScope).length ? ` dataScope: ${Object.keys(blueprint.accessProfile.dataScope).join(", ")}` : "";
14678
- steps.push({
14679
- kind: "profile",
14680
- summary: `profile scopes [${actions.join(", ")}] (data-plane \u2713)${scopeNote}`
14681
- });
14682
15466
  for (const [roleId, clauses] of Object.entries(blueprint.roles ?? {})) {
14683
15467
  const actionSet = [...new Set(clauses.flatMap((c) => c.allowedActions))];
15468
+ const capSet = [...new Set(clauses.flatMap((c) => c.capabilities ?? []))];
15469
+ const roleCapabilityNote = capSet.length ? `, capabilities [${capSet.join(", ")}]` : "";
15470
+ const clauseLines = clauses.map((c, i) => {
15471
+ const capNote = c.capabilities?.length ? `, capabilities [${c.capabilities.join(", ")}]` : "";
15472
+ return ` clause ${i + 1}: actions [${c.allowedActions.join(", ")}], dataScope: ${renderPlanDataScope(c.dataScope)}${capNote}`;
15473
+ });
15474
+ const assumable = blueprint.roleAssumable?.[roleId];
15475
+ const assumableLine = assumable ? `
15476
+ assumable: ${renderPlanDataScope(assumable)}` : "";
14684
15477
  steps.push({
14685
15478
  kind: "role",
14686
- summary: `role '${roleId}' (${clauses.length} clause${clauses.length === 1 ? "" : "s"}, actions [${actionSet.join(", ")}])`
15479
+ summary: `role '${roleId}' (${clauses.length} clause${clauses.length === 1 ? "" : "s"}, actions [${actionSet.join(", ")}]${roleCapabilityNote})
15480
+ ` + clauseLines.join("\n") + assumableLine
15481
+ });
15482
+ }
15483
+ const roleIdsNote = blueprint.accessProfile.roleIds;
15484
+ if (roleIdsNote) {
15485
+ steps.push({
15486
+ kind: "profile",
15487
+ summary: `profile via role(s) [${roleIdsNote.join(", ")}] (composed \u2014 each role's own clauses, concatenated in order)`
15488
+ });
15489
+ } else {
15490
+ const actions = blueprint.accessProfile.allowedActions ?? [];
15491
+ const scopeNote = blueprint.accessProfile.dataScope && Object.keys(blueprint.accessProfile.dataScope).length ? ` dataScope: ${Object.keys(blueprint.accessProfile.dataScope).join(", ")}` : "";
15492
+ const capabilityNote = blueprint.accessProfile.capabilities?.length ? ` capabilities: [${blueprint.accessProfile.capabilities.join(", ")}]` : "";
15493
+ const allDataPlane = !actions.some(
15494
+ (a) => KNOWN_CONTROL_PLANE_RESOURCES.includes(a.split(":", 1)[0])
15495
+ );
15496
+ const planeNote = allDataPlane ? " (data-plane \u2713)" : " (includes a control-plane exception)";
15497
+ steps.push({
15498
+ kind: "profile",
15499
+ summary: `profile scopes [${actions.join(", ")}]${planeNote}${scopeNote}${capabilityNote}`
14687
15500
  });
14688
15501
  }
14689
15502
  steps.push({ kind: "key", summary: `key ${blueprint.name}-<machine> (mint 1 scoped ssk_*)` });
@@ -14706,10 +15519,25 @@ function renderPlan(blueprint, steps) {
14706
15519
  ];
14707
15520
  if (steps.some((s) => s.kind === "issuer")) {
14708
15521
  legendLines.push(
14709
- ` Exception: issuers cannot be updated once registered \u2014 an existing`,
14710
- ` issuer is left unchanged, not reconciled to match.`
15522
+ ` Exception: an issuer's trust-anchor fields (issuer, jwksUri, audience,`,
15523
+ ` contextId) are immutable once registered \u2014 only subClaim/emailClaim/`,
15524
+ ` userinfoUri/selfSignupPolicies actually reconcile on a re-apply (status is not`,
15525
+ ` authorable in a blueprint at all).`
14711
15526
  );
14712
15527
  }
15528
+ if (steps.some((s) => s.kind === "namespace")) {
15529
+ legendLines.push(
15530
+ ` Exception: namespace registration is not idempotent \u2014 a re-apply matching`,
15531
+ ` an existing registration converges silently, but one that disagrees FAILS`,
15532
+ ` the apply rather than reconciling it.`
15533
+ );
15534
+ if (steps.some((s) => s.kind === "namespace" && /TENANT-WIDE/.test(s.summary))) {
15535
+ legendLines.push(
15536
+ ` Stricter still for a TENANT-WIDE namespace: it never converges, even on an`,
15537
+ ` exact match \u2014 any existing registration of that name fails the apply.`
15538
+ );
15539
+ }
15540
+ }
14713
15541
  const legend = legendLines.join("\n");
14714
15542
  const lines = steps.map((s) => ` + ${s.summary}`);
14715
15543
  return [header, legend, "", ...lines].join("\n");
@@ -14958,6 +15786,15 @@ var OsKeychainSecretStore = class {
14958
15786
  * against the real Windows store on every MR — the dev-machine probe is a point
14959
15787
  * measurement, not a regression gate. (Tracked internally; this file ships to
14960
15788
  * public npm, so no tracker number here.)
15789
+ *
15790
+ * ⚠️ THIS METHOD'S FAIL-CLOSED GUARANTEE IS AN UPSTREAM BEHAVIOURAL CONTRACT, NOT
15791
+ * SOMETHING THIS CODE ENFORCES. `@napi-rs/keyring` is pinned to an EXACT version
15792
+ * in `package.json` for exactly this reason: if a future release returned `[]`
15793
+ * from `findCredentials()` on a locked store instead of throwing, the `catch`
15794
+ * block above would never run — `!.some(...)` on an empty array reads as "gone",
15795
+ * `deleteSecret` returns `true`, and `remove`/`prune` drop the index row of a
15796
+ * still-live credential. A version bump of this dependency is a security review
15797
+ * of the paragraph above against the new release, not a routine dependency bump.
14961
15798
  */
14962
15799
  async deleteSecret(ref) {
14963
15800
  try {
@@ -15891,6 +16728,26 @@ The key itself WAS created server-side and is live \u2014 its id is ${entry.keyI
15891
16728
  const entry = await this.getActive();
15892
16729
  return entry ? this.readSecret(entry) : void 0;
15893
16730
  }
16731
+ /**
16732
+ * The ACTIVE entry's tenant (test/live), or undefined if there is no active
16733
+ * entry or its tenant cannot be determined — WITHOUT migrating its secret.
16734
+ *
16735
+ * Deliberately NOT built on {@link resolveActiveSecret}/{@link readSecret}:
16736
+ * both migrate the secret to this machine's preferred store as a side effect
16737
+ * of reading it, and a caller that only wants to CLASSIFY the active identity
16738
+ * (e.g. "is it safe to activate a different one over it?") must not silently
16739
+ * relocate — and take the cross-process index lock for — a credential it has
16740
+ * no other reason to touch. Same non-migrating shape {@link assess} already
16741
+ * uses per entry (`store.getSecret` directly, never `readSecret`).
16742
+ */
16743
+ async activeTenant() {
16744
+ const entry = await this.getActive();
16745
+ if (!entry) return void 0;
16746
+ const store = this.storeFor(entry);
16747
+ if (!store) return void 0;
16748
+ const secret = await store.getSecret(entry.secretRef);
16749
+ return secret !== void 0 ? keyTenant(secret) : void 0;
16750
+ }
15894
16751
  /**
15895
16752
  * Read one entry's secret, moving it to this machine's preferred store if it is
15896
16753
  * somewhere else.
@@ -16017,26 +16874,31 @@ var readline = __toESM(require("readline/promises"));
16017
16874
  async function runKeyringList(opts, deps = {}) {
16018
16875
  try {
16019
16876
  const keyring = deps.keyring ?? await Keyring.open();
16020
- const entries = await keyring.list();
16021
- const active = await keyring.activeAlias();
16022
16877
  if (opts.json) {
16878
+ const entries = await keyring.list();
16879
+ const active = await keyring.activeAlias();
16023
16880
  process.stdout.write(`${formatJson({ active, entries })}
16024
16881
  `);
16025
16882
  return 0;
16026
16883
  }
16027
- if (entries.length === 0) {
16884
+ const { health } = await assessEntries(keyring);
16885
+ if (health.length === 0) {
16028
16886
  process.stdout.write("\n (no keyring entries yet \u2014 run `vectros join <context>` to add one)\n");
16029
16887
  return 0;
16030
16888
  }
16031
16889
  process.stdout.write(
16032
- "\n" + formatTable(entries, [
16033
- { header: "", get: (e) => e.alias === active ? "*" : "" },
16034
- { header: "alias", get: (e) => e.alias },
16035
- { header: "context", get: (e) => e.context },
16036
- { header: "principal", get: (e) => e.principalId },
16037
- { header: "role", get: (e) => e.role },
16038
- { header: "env", get: (e) => e.env },
16039
- { header: "key id", get: (e) => e.keyId }
16890
+ "\n" + formatTable(health, [
16891
+ { header: "", get: (h) => h.active ? "*" : "" },
16892
+ { header: "alias", get: (h) => h.entry.alias },
16893
+ { header: "context", get: (h) => h.entry.context },
16894
+ { header: "principal", get: (h) => h.entry.principalId },
16895
+ { header: "role", get: (h) => h.entry.role },
16896
+ // `tenant` (test/live) replaces the old `env` (staging/production) column
16897
+ // here: env is an internal-team concern an external developer's CLI never
16898
+ // sees, while tenant is the distinction every user routinely holds two
16899
+ // credentials side by side over.
16900
+ { header: "tenant", get: (h) => tenantCell(h, { upper: true }) },
16901
+ { header: "key id", get: (h) => h.entry.keyId }
16040
16902
  ]) + "\n"
16041
16903
  );
16042
16904
  return 0;
@@ -16218,6 +17080,10 @@ async function assessEntries(keyring) {
16218
17080
  }))
16219
17081
  };
16220
17082
  }
17083
+ function tenantCell(h, opts) {
17084
+ if (h.status !== "ok") return void 0;
17085
+ return h.tenant ?? (opts.upper ? "UNKNOWN" : "unknown");
17086
+ }
16221
17087
  function describeEntry(h) {
16222
17088
  return `${h.entry.alias} (${h.entry.context}, ${h.entry.keyId})`;
16223
17089
  }
@@ -16362,9 +17228,7 @@ async function runKeyringDoctor(opts, deps = {}) {
16362
17228
  // `unknown` can escalate; one that sees a confident wrong answer cannot.
16363
17229
  store: storeKindOf(h.entry) ?? "unknown",
16364
17230
  secret: h.status,
16365
- // `unknown` rather than absent: a consumer must be able to tell "not a
16366
- // live key" from "we could not read the shape of this one".
16367
- tenant: h.status === "ok" ? h.tenant ?? "unknown" : void 0
17231
+ tenant: tenantCell(h, { upper: false })
16368
17232
  })),
16369
17233
  liveKeys: liveKeys.map((h) => h.entry.alias),
16370
17234
  findings
@@ -16379,7 +17243,7 @@ async function runKeyringDoctor(opts, deps = {}) {
16379
17243
  { header: "", get: (h) => h.active ? "*" : "" },
16380
17244
  { header: "alias", get: (h) => h.entry.alias },
16381
17245
  { header: "context", get: (h) => h.entry.context },
16382
- { header: "tenant", get: (h) => h.status === "ok" ? h.tenant ?? "UNKNOWN" : void 0 },
17246
+ { header: "tenant", get: (h) => tenantCell(h, { upper: true }) },
16383
17247
  { header: "key id", get: (h) => h.entry.keyId },
16384
17248
  { header: "secret", get: (h) => h.status === "ok" ? "ok" : h.status.toUpperCase() }
16385
17249
  ]) + "\n"
@@ -16573,6 +17437,47 @@ function keyringAliasFor(alias, tenant) {
16573
17437
  }
16574
17438
  return tenant === "test" ? `${alias}.test` : alias;
16575
17439
  }
17440
+ function resolveActivateDecision(opts) {
17441
+ if (opts.explicitActivate !== void 0) return opts.explicitActivate;
17442
+ if (opts.tenant === "live") return true;
17443
+ return opts.currentActiveTenant !== "live";
17444
+ }
17445
+ async function determineActivateDecision(keyring, opts) {
17446
+ if (opts.explicitActivate !== void 0 || opts.tenant === "live") {
17447
+ return resolveActivateDecision(opts);
17448
+ }
17449
+ let currentActiveTenant;
17450
+ try {
17451
+ if (opts.hint && opts.hint.alias === await keyring.activeAlias()) {
17452
+ currentActiveTenant = keyTenant(opts.hint.secret);
17453
+ } else {
17454
+ currentActiveTenant = await keyring.activeTenant();
17455
+ }
17456
+ } catch {
17457
+ }
17458
+ return resolveActivateDecision({ ...opts, currentActiveTenant });
17459
+ }
17460
+ function tenantActivateMessage(opts) {
17461
+ if (opts.willActivate) {
17462
+ if (opts.tenant !== "test") return "";
17463
+ return `
17464
+ \u26A0 '${opts.alias}' is now your ACTIVE identity \u2014 a TEST tenant credential. Anything
17465
+ resolving a key with no explicit --alias (including the MCP server) follows it.
17466
+ `;
17467
+ }
17468
+ if (opts.explicitlyDeclined) {
17469
+ return `
17470
+ \u26A0 Stored '${opts.alias}' (${opts.tenant} tenant) without activating it \u2014 you passed --no-activate.
17471
+ Run \`vectros switch ${opts.alias}\` when you're ready to use it.
17472
+ `;
17473
+ }
17474
+ return `
17475
+ \u26A0 Stored '${opts.alias}' (test tenant) WITHOUT activating it \u2014 your currently-active
17476
+ identity is a LIVE credential, and switching it out from under you silently is
17477
+ exactly the mistake this check exists to prevent. Run \`vectros switch ${opts.alias}\`
17478
+ to use the test key, or re-run with --activate to do so automatically.
17479
+ `;
17480
+ }
16576
17481
  function nowIso() {
16577
17482
  return (/* @__PURE__ */ new Date()).toISOString();
16578
17483
  }
@@ -16669,10 +17574,7 @@ async function storeMintedKey(keyring, binding, secret, opts = {}) {
16669
17574
  // src/cli-bootstrap.ts
16670
17575
  function validateBlueprint(input) {
16671
17576
  const blueprint = parseBlueprint(input);
16672
- assertScopeGate(blueprint.accessProfile.allowedActions);
16673
- for (const clauses of Object.values(blueprint.roles ?? {})) {
16674
- for (const clause of clauses) assertScopeGate(clause.allowedActions);
16675
- }
17577
+ assertBlueprintGates(blueprint);
16676
17578
  return blueprint;
16677
17579
  }
16678
17580
  var BLANK_SLATE = {
@@ -16694,9 +17596,11 @@ function parseBootstrapArgs(argv) {
16694
17596
  yes: false,
16695
17597
  noMerge: false,
16696
17598
  rotate: false,
17599
+ confirmExistingPrincipal: false,
16697
17600
  useScopedToken: false,
16698
17601
  blankSlate: false,
16699
17602
  noSeed: false,
17603
+ allowTenantWideNamespaces: false,
16700
17604
  client: "desktop",
16701
17605
  tenant: "live",
16702
17606
  help: false
@@ -16745,6 +17649,9 @@ function parseBootstrapArgs(argv) {
16745
17649
  case "--rotate":
16746
17650
  args.rotate = true;
16747
17651
  break;
17652
+ case "--confirm-existing-principal":
17653
+ args.confirmExistingPrincipal = true;
17654
+ break;
16748
17655
  case "--use-scoped-token":
16749
17656
  args.useScopedToken = true;
16750
17657
  break;
@@ -16755,6 +17662,9 @@ function parseBootstrapArgs(argv) {
16755
17662
  case "--no-seed":
16756
17663
  args.noSeed = true;
16757
17664
  break;
17665
+ case "--allow-tenant-wide-namespaces":
17666
+ args.allowTenantWideNamespaces = true;
17667
+ break;
16758
17668
  case "--tenant": {
16759
17669
  const v = argv[++i];
16760
17670
  if (v !== "test" && v !== "live") {
@@ -16766,6 +17676,12 @@ function parseBootstrapArgs(argv) {
16766
17676
  case "--test":
16767
17677
  args.tenant = "test";
16768
17678
  break;
17679
+ case "--activate":
17680
+ args.activate = true;
17681
+ break;
17682
+ case "--no-activate":
17683
+ args.activate = false;
17684
+ break;
16769
17685
  case "--help":
16770
17686
  case "-h":
16771
17687
  args.help = true;
@@ -16783,6 +17699,26 @@ function seedDeclined(answer) {
16783
17699
  const a = answer.trim().toLowerCase();
16784
17700
  return a === "n" || a === "no";
16785
17701
  }
17702
+ function existingPrincipalConfirmed(answer) {
17703
+ const a = answer.trim().toLowerCase();
17704
+ return a === "y" || a === "yes";
17705
+ }
17706
+ function buildConfirmExistingPrincipal(args, prompt, isTty) {
17707
+ return async (details) => {
17708
+ if (!args.yes && isTty) {
17709
+ const ans = await prompt(
17710
+ `
17711
+ \u26A0 '${details.externalId}' already exists in your tenant (context '${details.contextId}') \u2014 this key will bind to that EXISTING identity, not a new one.
17712
+ Expected if you're re-applying a blueprint from a new machine, or setting up a second
17713
+ credential for an integration you already own. If you don't recognize this identity,
17714
+ do NOT proceed \u2014 this may be a name collision with something you don't control.
17715
+ Proceed and bind to the existing identity? [y/N]: `
17716
+ );
17717
+ return existingPrincipalConfirmed(ans);
17718
+ }
17719
+ return args.confirmExistingPrincipal;
17720
+ };
17721
+ }
16786
17722
  function seedSummaryLine(opts) {
16787
17723
  if (opts.noSeed) {
16788
17724
  return opts.declinedInteractively ? " \u2714 Seeded 0 record(s) \u2014 skipped (you declined sample data)\n" : " \u2714 Seeded 0 record(s) \u2014 skipped (--no-seed)\n";
@@ -16810,25 +17746,30 @@ function reconcileTally(items, noun) {
16810
17746
  if (reconciled) seg.push(`${reconciled} reconciled`);
16811
17747
  return `${items.length} ${noun}${items.length === 1 ? "" : "s"} (${seg.join(", ")})`;
16812
17748
  }
16813
- function issuerTally(items) {
17749
+ function namespaceTally(items) {
16814
17750
  if (items.length === 0) return null;
16815
17751
  const created = items.filter((i) => i.action === "created").length;
16816
- const unchanged = items.length - created;
17752
+ const matched = items.length - created;
16817
17753
  const seg = [];
16818
17754
  if (created) seg.push(`${created} created`);
16819
- if (unchanged) seg.push(`${unchanged} already registered \u2014 unchanged (issuers cannot be updated once registered)`);
16820
- return `${items.length} issuer${items.length === 1 ? "" : "s"} (${seg.join(", ")})`;
17755
+ if (matched) seg.push(`${matched} already registered \u2014 confirmed matching`);
17756
+ return `${items.length} namespace${items.length === 1 ? "" : "s"} (${seg.join(", ")})`;
16821
17757
  }
16822
17758
  function applyOutcomeSummary(result) {
16823
17759
  const parts = [
16824
17760
  reconcileTally(result.schemaActions, "schema"),
16825
17761
  reconcileTally(result.roleActions, "role"),
16826
- issuerTally(result.issuerActions),
17762
+ // #1023 — the platform's PUT /v1/auth/issuers/{issuerId} makes an issuer's
17763
+ // 'reconciled' mean the same "updated to match" as every other kind here.
17764
+ reconcileTally(result.issuerActions, "issuer"),
17765
+ namespaceTally(result.namespaceActions),
16827
17766
  `profile ${result.profileAction}`
16828
17767
  ].filter((p) => p !== null);
16829
17768
  const detail = parts.length ? ` \u2014 ${parts.join("; ")}` : "";
17769
+ const principalNote = result.userAction === "reconciled" ? ` \u26A0 service principal already existed in this tenant before this run (not newly created)
17770
+ ` : "";
16830
17771
  return ` \u2714 context '${result.contextId}' ${result.contextAction}${detail}
16831
- `;
17772
+ ${principalNote}`;
16832
17773
  }
16833
17774
  var BOOTSTRAP_HELP = `vectros bootstrap \u2014 provision a least-privilege MCP credential.
16834
17775
 
@@ -16836,7 +17777,7 @@ Usage:
16836
17777
  vectros bootstrap [--blueprint <name> | --blank] [--no-seed]
16837
17778
  [--set name=value ...] [--values <file>]
16838
17779
  [--tenant test|live | --test] [--client desktop|code]
16839
- [--yes] [--no-merge] [--rotate]
17780
+ [--yes] [--no-merge] [--rotate] [--confirm-existing-principal]
16840
17781
  [--token <t> | env VECTROS_BOOTSTRAP_TOKEN]
16841
17782
  [--base-url <url>] [--use-scoped-token]
16842
17783
 
@@ -16853,12 +17794,24 @@ Options:
16853
17794
  sample seed records \u2014 the right choice for a production context
16854
17795
  (a blueprint's seeds are illustrative, not real data). Unlike
16855
17796
  --blank, the schemas are still created.
17797
+ --allow-tenant-wide-namespaces
17798
+ Required to apply a blueprint that declares a namespace with
17799
+ tenantWide: true \u2014 visible to every context in the account, not
17800
+ just this one. Without it, bootstrap refuses before provisioning
17801
+ anything, naming the namespace(s). Only an account OWNER's
17802
+ credential can actually complete the registration either way \u2014
17803
+ this flag doesn't change who's allowed, only whether this run
17804
+ tries. Never needed for a blueprint with no tenantWide namespace.
16856
17805
  --tenant test|live Which tenant the minted key scopes to (default: live).
16857
17806
  'test' provisions into your isolated test tenant \u2014 a
16858
17807
  dry-run before pointing real data at the same blueprint.
16859
17808
  The tenant rides the key (ssk_test_* vs ssk_live_*), so
16860
17809
  the MCP/client config is otherwise identical.
16861
17810
  --test Shorthand for --tenant test.
17811
+ --activate Force the freshly-stored key to become your active identity, even
17812
+ if that would switch away from a currently-active LIVE credential
17813
+ (the one case --tenant test does NOT do this automatically).
17814
+ --no-activate Store the key without activating it, regardless of tenant.
16862
17815
  --base-url <url> Override the API base URL (beats VECTROS_API_BASE_URL).
16863
17816
  --client <desktop|code> MCP host to configure (default: desktop). 'desktop' merges
16864
17817
  claude_desktop_config.json (also works for Cursor/Cline);
@@ -16873,6 +17826,13 @@ Options:
16873
17826
  print the config snippet instead of merging it into your client's
16874
17827
  config file. Not a dry run; see 'blueprint plan' for that.
16875
17828
  --rotate Revoke + re-mint THIS machine's key.
17829
+ --confirm-existing-principal
17830
+ Required (with --yes, or with no TTY) to mint a NEW key bound to a
17831
+ service principal that already existed in your tenant before this run
17832
+ \u2014 e.g. a deliberate second machine for an integration you already own.
17833
+ Refused by default so an unexpected collision halts a CI run loudly
17834
+ instead of silently minting a credential bound to the wrong identity.
17835
+ Never needed for --rotate or an ordinary same-machine re-apply.
16876
17836
  --use-scoped-token Legacy fallback.
16877
17837
  NOTE: it mints a token WITHOUT the provisioning
16878
17838
  capability, so it cannot create a blueprint's
@@ -16960,6 +17920,22 @@ function buildRemintForContext(opts) {
16960
17920
  });
16961
17921
  };
16962
17922
  }
17923
+ function buildRemintBootstrapForContext(opts) {
17924
+ const mintBridge = opts.mintBridge ?? mintBridgeToken;
17925
+ return async (contextId) => {
17926
+ const ctxToken = await mintBridge({
17927
+ cognitoToken: opts.cognitoToken,
17928
+ apiBaseUrl: opts.apiBaseUrl,
17929
+ contextId,
17930
+ tenant: opts.tenant,
17931
+ useScopedToken: opts.useScopedToken
17932
+ });
17933
+ return new VectrosClient({
17934
+ token: ctxToken.token,
17935
+ environment: opts.apiBaseUrl
17936
+ });
17937
+ };
17938
+ }
16963
17939
  async function runBootstrapCli(argv, deps = {}) {
16964
17940
  const log = deps.log ?? createLogger2();
16965
17941
  let args;
@@ -16993,17 +17969,36 @@ ${BOOTSTRAP_HELP}
16993
17969
  \u2716 Scope gate refused blueprint '${blueprint.name}':
16994
17970
  `);
16995
17971
  for (const r of err.rejected) process.stderr.write(` \u2022 ${r.action}: ${r.reason}
17972
+ `);
17973
+ process.stderr.write(" Minting nothing.\n");
17974
+ return 2;
17975
+ }
17976
+ if (err instanceof CapabilityGateError) {
17977
+ process.stderr.write(`
17978
+ \u2716 Capability gate refused blueprint '${blueprint.name}':
17979
+ `);
17980
+ for (const r of err.rejected) process.stderr.write(` \u2022 ${r.action}: ${r.reason}
16996
17981
  `);
16997
17982
  process.stderr.write(" Minting nothing.\n");
16998
17983
  return 2;
16999
17984
  }
17000
17985
  throw err;
17001
17986
  }
17002
- const gate = evaluateScopeGate(blueprint.accessProfile.allowedActions);
17003
- process.stdout.write(`
17987
+ const roleNote = blueprint.accessProfile.roleIds ? ` via role(s) [${blueprint.accessProfile.roleIds.join(", ")}]` : "";
17988
+ const acceptedActions = effectiveAccessProfileActions(blueprint);
17989
+ const allDataPlane = !acceptedActions.some(
17990
+ (a) => KNOWN_CONTROL_PLANE_RESOURCES.includes(a.split(":", 1)[0])
17991
+ );
17992
+ process.stdout.write(
17993
+ `
17004
17994
  Blueprint: ${blueprint.name}
17005
- \u2714 Scope gate: [${gate.accepted.join(", ")}] \u2014 all data-plane \u2713
17995
+ \u2714 Scope gate: [${acceptedActions.join(", ")}]${roleNote}` + (allDataPlane ? " \u2014 all data-plane \u2713\n" : " \u2014 includes a narrow, allowlisted control-plane exception (see above)\n")
17996
+ );
17997
+ if (blueprint.accessProfile.capabilities?.length) {
17998
+ const capGate = evaluateCapabilityGate(blueprint.accessProfile.capabilities);
17999
+ process.stdout.write(` \u2714 Capability gate: [${capGate.accepted.join(", ")}] \u2014 granted \u2713
17006
18000
  `);
18001
+ }
17007
18002
  let noSeed = args.noSeed;
17008
18003
  let seedDeclinedInteractively = false;
17009
18004
  if (shouldOfferSeedOptOut(args, blueprint, isTty)) {
@@ -17054,6 +18049,7 @@ ${renderPlan(blueprint, planSteps)}
17054
18049
  process.stdout.write(` \u2714 Bootstrap token minted (5-min TTL, ${args.tenant} tenant)
17055
18050
  `);
17056
18051
  const pingIdentity = deps.resolveIdentity ?? resolveIdentity;
18052
+ const provision = deps.provisionContext ?? provisionContext;
17057
18053
  const resolveIdentities = deps.resolveApplyTimeIdentities ?? resolveApplyTimeIdentities;
17058
18054
  const loader = deps.runLoader ?? runLoader;
17059
18055
  const client = new VectrosClient({ token: bridge.token, environment: baseUrl });
@@ -17064,11 +18060,6 @@ ${renderPlan(blueprint, planSteps)}
17064
18060
  "Could not resolve tenantId from /v1/ping \u2014 the extended ping (tenant_id) must be deployed for bootstrap."
17065
18061
  );
17066
18062
  }
17067
- const resolvedBlueprint = await resolveIdentities(
17068
- blueprint,
17069
- client,
17070
- log
17071
- );
17072
18063
  let keyring;
17073
18064
  let keyringOpenError;
17074
18065
  try {
@@ -17077,6 +18068,7 @@ ${renderPlan(blueprint, planSteps)}
17077
18068
  keyringOpenError = err;
17078
18069
  }
17079
18070
  let storedFreshInKeyring = false;
18071
+ const lastResolvedSecret = /* @__PURE__ */ new Map();
17080
18072
  const readExistingKey = async (contextId) => {
17081
18073
  if (!keyring) return void 0;
17082
18074
  const alias = keyringAliasFor(contextId, args.tenant);
@@ -17084,7 +18076,8 @@ ${renderPlan(blueprint, planSteps)}
17084
18076
  const existing = await keyring.get(alias);
17085
18077
  if (!existing?.keyId) return void 0;
17086
18078
  const rawKey = await keyring.resolveSecret(alias);
17087
- return rawKey ? { keyId: existing.keyId, rawKey } : void 0;
18079
+ lastResolvedSecret.set(alias, rawKey);
18080
+ return rawKey ? { keyId: existing.keyId, rawKey, principalId: existing.principalId } : void 0;
17088
18081
  } catch {
17089
18082
  return void 0;
17090
18083
  }
@@ -17103,6 +18096,11 @@ ${renderPlan(blueprint, planSteps)}
17103
18096
  const alias = keyringAliasFor(contextId, args.tenant);
17104
18097
  try {
17105
18098
  warnReplacingAlias(await keyring.get(alias), alias, record.revokedKeyId);
18099
+ const willActivate = await determineActivateDecision(keyring, {
18100
+ tenant: args.tenant,
18101
+ explicitActivate: args.activate,
18102
+ hint: lastResolvedSecret.has(alias) ? { alias, secret: lastResolvedSecret.get(alias) } : void 0
18103
+ });
17106
18104
  await storeMintedKey(
17107
18105
  keyring,
17108
18106
  {
@@ -17117,11 +18115,14 @@ ${renderPlan(blueprint, planSteps)}
17117
18115
  env: deriveEnvironment(baseUrl) ?? args.env
17118
18116
  },
17119
18117
  record.rawKey,
17120
- { activate: true, revokedKeyId: record.revokedKeyId }
18118
+ { activate: willActivate, revokedKeyId: record.revokedKeyId }
17121
18119
  );
17122
18120
  if (record.revokedKeyId) {
17123
18121
  await warnOtherAliasesNaming(keyring, record.revokedKeyId, alias);
17124
18122
  }
18123
+ process.stdout.write(
18124
+ tenantActivateMessage({ alias, tenant: args.tenant, willActivate, explicitlyDeclined: args.activate === false })
18125
+ );
17125
18126
  storedFreshInKeyring = true;
17126
18127
  } catch (err) {
17127
18128
  process.stderr.write(
@@ -17133,6 +18134,27 @@ ${renderPlan(blueprint, planSteps)}
17133
18134
  );
17134
18135
  }
17135
18136
  };
18137
+ const remintBootstrapForContext = buildRemintBootstrapForContext({
18138
+ cognitoToken,
18139
+ apiBaseUrl: baseUrl,
18140
+ tenant: args.tenant,
18141
+ useScopedToken: args.useScopedToken,
18142
+ mintBridge
18143
+ });
18144
+ const confirmExistingPrincipal = buildConfirmExistingPrincipal(args, promptCtx.prompt, isTty);
18145
+ const provisioned = await provision(client, {
18146
+ blueprint,
18147
+ allowTenantWideNamespaces: args.allowTenantWideNamespaces,
18148
+ remintBootstrapForContext,
18149
+ readExistingKey,
18150
+ confirmExistingPrincipal,
18151
+ log
18152
+ });
18153
+ const resolvedBlueprint = await resolveIdentities(
18154
+ blueprint,
18155
+ provisioned.ctxBootstrapClient ?? client,
18156
+ log
18157
+ );
17136
18158
  const machineId = await resolveMachineId();
17137
18159
  const result = await loader(client, {
17138
18160
  blueprint: resolvedBlueprint,
@@ -17140,14 +18162,18 @@ ${renderPlan(blueprint, planSteps)}
17140
18162
  machineId,
17141
18163
  rotate: args.rotate,
17142
18164
  skipSeed: noSeed,
18165
+ allowTenantWideNamespaces: args.allowTenantWideNamespaces,
17143
18166
  readExistingKey,
17144
18167
  persistMintedKey,
18168
+ provisioned,
18169
+ remintBootstrapForContext,
17145
18170
  remintForContext: buildRemintForContext({
17146
18171
  cognitoToken,
17147
18172
  apiBaseUrl: baseUrl,
17148
18173
  tenant: args.tenant,
17149
18174
  mintContext
17150
18175
  }),
18176
+ confirmExistingPrincipal,
17151
18177
  log
17152
18178
  });
17153
18179
  process.stdout.write(
@@ -17225,10 +18251,31 @@ ${codeAddHint}`);
17225
18251
  `);
17226
18252
  return 3;
17227
18253
  }
18254
+ if (err instanceof ExistingPrincipalNotConfirmedError) {
18255
+ process.stderr.write(
18256
+ `
18257
+ \u2716 ${err.message}
18258
+
18259
+ If this is expected (a re-apply, or a deliberate second machine for an integration you already own),
18260
+ re-run with --confirm-existing-principal, or confirm interactively.
18261
+ If you don't recognize this identity, investigate the blueprint's servicePrincipal.externalId before re-running.
18262
+ `
18263
+ );
18264
+ return 3;
18265
+ }
17228
18266
  if (err instanceof BlueprintIdentityError || err instanceof UnresolvedIdentitiesError) {
18267
+ process.stderr.write(
18268
+ `
18269
+ \u2716 ${err.message}
18270
+ The app-context, service principal, and any declared namespaces were already provisioned. No schemas, profile, roles, key, or seed records were.
18271
+ `
18272
+ );
18273
+ return 2;
18274
+ }
18275
+ if (err instanceof TenantWideNamespacesNotAllowedError) {
17229
18276
  process.stderr.write(`
17230
18277
  \u2716 ${err.message}
17231
- No context or key was provisioned.
18278
+ Nothing was provisioned.
17232
18279
  `);
17233
18280
  return 2;
17234
18281
  }
@@ -17303,11 +18350,14 @@ function assertIdentityOverrideValues(overrides, flag) {
17303
18350
  }
17304
18351
  }
17305
18352
  function placeholderScopeNamespace(value) {
17306
- const m = /^\$\{\{\s*(?:under\.)?self\.scope\.([^\s}]+)\s*\}\}$/.exec(value);
17307
- return m ? m[1] : null;
18353
+ const self2 = /^\$\{\{\s*(?:under\.)?self\.scope\.([^\s}]+)\s*\}\}$/.exec(value);
18354
+ if (self2) return self2[1];
18355
+ const member = MEMBER_SCOPE_PLACEHOLDER_RE2.exec(value);
18356
+ return member ? member[1] : null;
17308
18357
  }
17309
18358
  var SELF_PLACEHOLDER_RE = /^\$\{\{\s*self\.(?:userId|partnerUserId|scope\.[a-z][a-z0-9_-]{1,31})\s*\}\}$/;
17310
- var UNDER_SELF_PLACEHOLDER_RE = /^\$\{\{\s*under\.self\.(?:userId|partnerUserId|scope\.[a-z][a-z0-9_-]{1,31})\s*\}\}$/;
18359
+ var UNDER_SELF_PLACEHOLDER_RE2 = /^\$\{\{\s*under\.self\.(?:userId|partnerUserId|scope\.[a-z][a-z0-9_-]{1,31})\s*\}\}$/;
18360
+ var MEMBER_SCOPE_PLACEHOLDER_RE2 = /^\$\{\{\s*member\.scope\.([a-z][a-z0-9_-]{1,31})(?::[a-z][a-z0-9_-]{1,31})?\s*\}\}$/;
17311
18361
 
17312
18362
  // src/lint.ts
17313
18363
  var ENUM_COMPATIBLE_TYPES = /* @__PURE__ */ new Set(["enum", "string", "string[]"]);
@@ -17445,6 +18495,10 @@ function lintBlueprint(bp) {
17445
18495
  }
17446
18496
  }
17447
18497
  }
18498
+ if (bp.accessProfile.allowedActions !== void 0) {
18499
+ const reached = ownerScopedReachWithoutDataScope(bp.accessProfile.allowedActions, ds);
18500
+ if (reached.length > 0) findings.push(unconstrainedReachFinding("accessProfile", reached));
18501
+ }
17448
18502
  for (const roleId of Object.keys(bp.roles ?? {})) {
17449
18503
  if (!ROLE_ID_RE.test(roleId)) {
17450
18504
  findings.push({
@@ -17457,6 +18511,10 @@ function lintBlueprint(bp) {
17457
18511
  for (const [roleId, clauses] of Object.entries(bp.roles ?? {})) {
17458
18512
  clauses.forEach((clause, i) => {
17459
18513
  const cds = clause.dataScope;
18514
+ const reached = ownerScopedReachWithoutDataScope(clause.allowedActions, cds);
18515
+ if (reached.length > 0) {
18516
+ findings.push(unconstrainedReachFinding(`role '${roleId}' clause ${i}`, reached));
18517
+ }
17460
18518
  if (!cds) return;
17461
18519
  for (const [key, values] of Object.entries(cds)) {
17462
18520
  for (const value of values) {
@@ -17488,7 +18546,12 @@ function lintBlueprint(bp) {
17488
18546
  }
17489
18547
  var ROLE_ID_RE = /^[a-z][a-z0-9-]{2,30}$/;
17490
18548
  var PLACEHOLDER_SHAPED_RE = /^\s*\$\{\{.*\}\}\s*$/s;
17491
- var RECOGNIZED_PLACEHOLDER_RES = [/^\$\{\{\s*any\s*\}\}$/, SELF_PLACEHOLDER_RE, UNDER_SELF_PLACEHOLDER_RE];
18549
+ var RECOGNIZED_PLACEHOLDER_RES = [
18550
+ /^\$\{\{\s*any\s*\}\}$/,
18551
+ SELF_PLACEHOLDER_RE,
18552
+ UNDER_SELF_PLACEHOLDER_RE2,
18553
+ MEMBER_SCOPE_PLACEHOLDER_RE2
18554
+ ];
17492
18555
  var BLUEPRINT_TOKEN_RE = /^\s*\$\{\{\s*(identities|inputs|vectros|ns)\./;
17493
18556
  function placeholderFinding(where, key, value) {
17494
18557
  if (typeof value !== "string" || !PLACEHOLDER_SHAPED_RE.test(value)) return null;
@@ -17506,7 +18569,26 @@ function placeholderFinding(where, key, value) {
17506
18569
  return {
17507
18570
  severity: "error",
17508
18571
  rule: "datascope-unknown-placeholder",
17509
- 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.`
18572
+ 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.`
18573
+ };
18574
+ }
18575
+ var OWNER_SCOPED_RESOURCES = /* @__PURE__ */ new Set(["records", "documents", "entities"]);
18576
+ var UNCONSTRAINED_REACH_OPS = /* @__PURE__ */ new Set(["r", "u", "d"]);
18577
+ function ownerScopedReachWithoutDataScope(allowedActions, dataScope) {
18578
+ const reached = /* @__PURE__ */ new Set();
18579
+ for (const action of allowedActions) {
18580
+ const [resource, ops] = action.split(":", 2);
18581
+ if (!resource || !ops || !OWNER_SCOPED_RESOURCES.has(resource)) continue;
18582
+ if (ops === "*" || [...ops].some((op) => UNCONSTRAINED_REACH_OPS.has(op))) reached.add(resource);
18583
+ }
18584
+ if (dataScope !== void 0) return [];
18585
+ return [...reached].sort();
18586
+ }
18587
+ function unconstrainedReachFinding(where, resources) {
18588
+ return {
18589
+ severity: "warning",
18590
+ rule: "datascope-absent-unconstrained-reach",
18591
+ 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.`
17510
18592
  };
17511
18593
  }
17512
18594
  function lintPasses(findings) {
@@ -17547,8 +18629,12 @@ schemas:
17547
18629
  lookupFields: [title] # extra O(1) exact-match lookup indexes (max 10)
17548
18630
 
17549
18631
  accessProfile:
17550
- # SCOPE GATE: data-plane only (records/schemas/search/documents/folders/inference/entities).
17551
- # Anything else (keys/profiles/users/admin/namespaces/*) is HARD-REJECTED \u2014 by design, no override.
18632
+ # SCOPE GATE: data-plane only (records/schemas/search/documents/folders/inference/entities),
18633
+ # PLUS profiles:r/u/d (bounded by the platform to redistributing access you already have,
18634
+ # never granting more) and profiles:c:self (create a profile for yourself only \u2014 the
18635
+ # platform resolves "self" from the caller, not from anything you declare here). A bare
18636
+ # profiles:c, or profiles:c:usr_<id> naming another principal, is HARD-REJECTED \u2014 by
18637
+ # design, no override. Same for keys/users/admin/namespaces/*.
17552
18638
  # Ops: c|r|u|d, combinable (records:cru). Prefer the narrowest set (least privilege).
17553
18639
  allowedActions: [records:r, records:c, records:u, search:r, schemas:r]
17554
18640
  # dataScope binds the key to specific owners. Include null to ALSO see
@@ -17556,6 +18642,17 @@ accessProfile:
17556
18642
  # owners' records, not shared/seed data. Keys are userId (the principal) and
17557
18643
  # namespaced scopes (scope:org, scope:client, ...):
17558
18644
  # dataScope: { "scope:org": [<org-uuid>, null] }
18645
+ # Left empty ({}) below \u2014 the explicit marker for "deliberately tenant-wide,
18646
+ # every record visible" ('blueprint validate' warns on a MISSING dataScope
18647
+ # key, but not on this one). This is a SCAFFOLD PLACEHOLDER, not a
18648
+ # recommendation: it also grants records:c/u tenant-wide, so before this
18649
+ # profile touches real data, replace it with a real scope (the example
18650
+ # above) unless every owner genuinely sharing every record is what you want.
18651
+ dataScope: {}
18652
+ # capabilities is a SEPARATE gated dimension from allowedActions \u2014 named platform
18653
+ # effects a resource:ops verb can't express. Only 'member-lifecycle' is admitted
18654
+ # today; every other name is HARD-REJECTED, same posture as the scope gate above.
18655
+ # capabilities: [member-lifecycle]
17559
18656
 
17560
18657
  servicePrincipal:
17561
18658
  externalId: ${name}-sp # stable; the principal the profile + key bind to
@@ -17675,15 +18772,19 @@ async function resolveAndValidate(arg, verb, supplied) {
17675
18772
  return null;
17676
18773
  }
17677
18774
  try {
17678
- assertScopeGate(blueprint.accessProfile.allowedActions);
17679
- for (const clauses of Object.values(blueprint.roles ?? {})) {
17680
- for (const clause of clauses) assertScopeGate(clause.allowedActions);
17681
- }
18775
+ assertBlueprintGates(blueprint);
17682
18776
  } catch (err) {
17683
18777
  if (err instanceof ScopeGateError) {
17684
18778
  process.stderr.write(` \u2716 Scope gate rejected '${blueprint.name}':
17685
18779
  `);
17686
18780
  for (const r of err.rejected) process.stderr.write(` \u2022 ${r.action}: ${r.reason}
18781
+ `);
18782
+ return null;
18783
+ }
18784
+ if (err instanceof CapabilityGateError) {
18785
+ process.stderr.write(` \u2716 Capability gate rejected '${blueprint.name}':
18786
+ `);
18787
+ for (const r of err.rejected) process.stderr.write(` \u2022 ${r.action}: ${r.reason}
17687
18788
  `);
17688
18789
  return null;
17689
18790
  }
@@ -17714,10 +18815,11 @@ async function runValidate(args) {
17714
18815
  }
17715
18816
  const blueprint = await resolveAndValidate(args.file, "validate", supplied);
17716
18817
  if (!blueprint) return 2;
17717
- const gate = evaluateScopeGate(blueprint.accessProfile.allowedActions);
18818
+ const capGate = evaluateCapabilityGate(blueprint.accessProfile.capabilities);
17718
18819
  const warnCount = lintBlueprint(blueprint).length;
18820
+ const roleNote = blueprint.accessProfile.roleIds ? ` via role(s) [${blueprint.accessProfile.roleIds.join(", ")}]` : "";
17719
18821
  process.stdout.write(
17720
- ` \u2714 '${blueprint.name}' is valid \u2014 structural \u2713, scope gate \u2713 [${gate.accepted.join(", ")}], lint \u2713` + (warnCount ? ` (${warnCount} warning${warnCount === 1 ? "" : "s"})` : "") + "\n"
18822
+ ` \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"
17721
18823
  );
17722
18824
  return 0;
17723
18825
  }
@@ -18048,7 +19150,14 @@ async function resolveKeyDestination(opts, deps) {
18048
19150
  `);
18049
19151
  return 2;
18050
19152
  }
18051
- const alias = opts.alias ?? opts.context;
19153
+ let alias;
19154
+ try {
19155
+ alias = keyringAliasFor(opts.alias ?? opts.context, opts.tenant ?? "live");
19156
+ } catch (err) {
19157
+ process.stderr.write(` \u2716 ${formatError(err)}
19158
+ `);
19159
+ return 2;
19160
+ }
18052
19161
  if (opts.activate === false && await keyring.activeAlias() === alias) {
18053
19162
  process.stderr.write(
18054
19163
  ` \u2716 --no-activate cannot be used on '${alias}': it is your ACTIVE identity, and storing
@@ -18069,6 +19178,8 @@ async function storeAndReport(dest, key, auth, opts) {
18069
19178
  return 1;
18070
19179
  }
18071
19180
  const principalId = opts.principalId;
19181
+ const tenant = keyTenant(key.rawKey) ?? "test";
19182
+ const willActivate = await determineActivateDecision(dest.keyring, { tenant, explicitActivate: opts.activate });
18072
19183
  let entry;
18073
19184
  try {
18074
19185
  warnReplacingAlias(await dest.keyring.get(dest.alias), dest.alias, opts.revokedKeyId);
@@ -18083,7 +19194,7 @@ async function storeAndReport(dest, key, auth, opts) {
18083
19194
  env: auth.env
18084
19195
  },
18085
19196
  key.rawKey,
18086
- { activate: opts.activate !== false, revokedKeyId: opts.revokedKeyId }
19197
+ { activate: willActivate, revokedKeyId: opts.revokedKeyId }
18087
19198
  ));
18088
19199
  } catch (err) {
18089
19200
  process.stderr.write(
@@ -18095,7 +19206,6 @@ async function storeAndReport(dest, key, auth, opts) {
18095
19206
  );
18096
19207
  return 1;
18097
19208
  }
18098
- const active = opts.activate !== false;
18099
19209
  process.stdout.write(
18100
19210
  `
18101
19211
  \u2714 Stored the new key in your ${describeEntryStoreNoun(entry)} \u2014 it was not printed.
@@ -18105,9 +19215,19 @@ async function storeAndReport(dest, key, auth, opts) {
18105
19215
  ["principal", entry.principalId || void 0],
18106
19216
  ["key id", entry.keyId],
18107
19217
  ["env", entry.env],
18108
- ["active", active ? "yes" : "no"]
18109
- ]) + "\n\n" + (active ? " Anything that resolves a key through the CLI \u2014 the MCP server included \u2014 now\n reads this one. No VECTROS_API_KEY needed.\n" : ` Not active. Run \`vectros switch ${entry.alias}\` to use it.
18110
- `)
19218
+ ["active", willActivate ? "yes" : "no"]
19219
+ ]) + "\n\n" + // The plain "now active" note fires only on the activated path. When it
19220
+ // did NOT activate, tenantActivateMessage below is the ONLY explanation —
19221
+ // it already says to run `switch`, AND (unlike a bare "Not active") says
19222
+ // WHY: an explicit --no-activate vs. this call's tenant-safety default.
19223
+ // Saying both here would print two different "run switch" instructions
19224
+ // for one outcome.
19225
+ (willActivate ? " Anything that resolves a key through the CLI \u2014 the MCP server included \u2014 now\n reads this one. No VECTROS_API_KEY needed.\n" : "") + tenantActivateMessage({
19226
+ alias: entry.alias,
19227
+ tenant,
19228
+ willActivate,
19229
+ explicitlyDeclined: opts.activate === false
19230
+ })
18111
19231
  );
18112
19232
  return 0;
18113
19233
  }
@@ -18570,6 +19690,52 @@ async function runIdentityDelete(opts, deps = {}) {
18570
19690
  }
18571
19691
  });
18572
19692
  }
19693
+ function renderInvite(v) {
19694
+ return formatKeyValues([
19695
+ ["userId", v.userId],
19696
+ ["inviteExpiresAt", v.inviteExpiresAt],
19697
+ ["emailSent", v.emailSent === void 0 ? void 0 : String(v.emailSent)],
19698
+ ["inviteToken", v.inviteToken],
19699
+ ["acceptLink", v.acceptLink]
19700
+ ]);
19701
+ }
19702
+ async function runIdentityInvite(opts, deps = {}) {
19703
+ const sendEmail = !opts.noSendEmail;
19704
+ if (sendEmail && !opts.acceptUrl) {
19705
+ process.stderr.write(
19706
+ " \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"
19707
+ );
19708
+ return 2;
19709
+ }
19710
+ return withAuth(opts, deps, async (auth) => {
19711
+ const client = auth.client;
19712
+ try {
19713
+ const invite = await client.auth.createInvite({
19714
+ email: opts.email,
19715
+ contextId: opts.context,
19716
+ accessProfile: { roleId: opts.role },
19717
+ firstName: opts.firstName,
19718
+ acceptUrl: opts.acceptUrl,
19719
+ sendEmail
19720
+ });
19721
+ if (opts.json) {
19722
+ process.stdout.write(`${formatJson(invite)}
19723
+ `);
19724
+ return 0;
19725
+ }
19726
+ process.stdout.write(
19727
+ `
19728
+ \u2714 Invited '${opts.email}' to '${opts.context}' as '${opts.role}'
19729
+ ${renderInvite(invite)}
19730
+ `
19731
+ );
19732
+ return 0;
19733
+ } catch (err) {
19734
+ process.stderr.write(formatCliError(err, { action: "identity invite failed" }));
19735
+ return 1;
19736
+ }
19737
+ });
19738
+ }
18573
19739
 
18574
19740
  // src/commands/access.ts
18575
19741
  function assertPrincipalShape(principal) {
@@ -18588,20 +19754,24 @@ async function resolvePrincipalArg(raw, auth) {
18588
19754
  }
18589
19755
  return self2.principalId;
18590
19756
  }
19757
+ function roleSummary(p) {
19758
+ if (p.roleIds?.length) return p.roleIds.join(", ");
19759
+ return p.roleId;
19760
+ }
18591
19761
  function renderProfile(p) {
18592
19762
  return formatKeyValues([
18593
19763
  ["context", p.contextId],
18594
19764
  ["principal", p.principalId],
18595
- ["role", p.roleId],
19765
+ ["role", roleSummary(p)],
18596
19766
  ["scopes", p.scopes?.length ? `${p.scopes.length} clause(s)` : void 0],
18597
19767
  ["status", p.status]
18598
19768
  ]);
18599
19769
  }
18600
19770
  async function runAccessGrant(opts, deps = {}) {
18601
- const hasRole = Boolean(opts.role);
19771
+ const hasRole = Boolean(opts.role?.length);
18602
19772
  const hasActions = Boolean(opts.actions);
18603
19773
  if (hasRole === hasActions) {
18604
- process.stderr.write(" \u2716 Provide exactly one of --role <roleId> or --actions <csv>.\n");
19774
+ process.stderr.write(" \u2716 Provide exactly one of --role <roleId> (repeatable) or --actions <csv>.\n");
18605
19775
  return 2;
18606
19776
  }
18607
19777
  if (hasActions && splitCsv(opts.actions).length === 0) {
@@ -18641,7 +19811,11 @@ async function runAccessGrant(opts, deps = {}) {
18641
19811
  upsert: true,
18642
19812
  body: {
18643
19813
  principalId: principal,
18644
- roleId: opts.role,
19814
+ // Always send the plural `roleIds`, even for a single
19815
+ // `--role`: it is a strict superset of the deprecated singular
19816
+ // `roleId` (equivalent to `roleIds: ["<value>"]`), and the platform
19817
+ // reads it uniformly regardless of how many roles compose.
19818
+ roleIds: opts.role,
18645
19819
  scopes,
18646
19820
  identityOverrides
18647
19821
  }
@@ -18653,7 +19827,7 @@ async function runAccessGrant(opts, deps = {}) {
18653
19827
  }
18654
19828
  process.stdout.write(
18655
19829
  `
18656
- \u2714 Granted ${principal} in '${opts.context}'` + (hasRole ? ` via role '${opts.role}'` : ` with ${splitCsv(opts.actions).length} action(s)`) + `
19830
+ \u2714 Granted ${principal} in '${opts.context}'` + (hasRole ? ` via role(s) [${opts.role.join(", ")}]` : ` with ${splitCsv(opts.actions).length} action(s)`) + `
18657
19831
  ${renderProfile(profile)}
18658
19832
  `
18659
19833
  );
@@ -18722,7 +19896,7 @@ async function runAccessList(opts, deps = {}) {
18722
19896
  "\n" + formatTable(rows, [
18723
19897
  { header: "context", get: (r) => r.contextId },
18724
19898
  { header: "principal", get: (r) => r.principalId },
18725
- { header: "role", get: (r) => r.roleId ?? (r.scopes?.length ? "(inline)" : void 0) },
19899
+ { header: "role", get: (r) => roleSummary(r) ?? (r.scopes?.length ? "(inline)" : void 0) },
18726
19900
  { header: "status", get: (r) => r.status }
18727
19901
  ]) + "\n"
18728
19902
  );
@@ -18770,6 +19944,7 @@ ${renderProfile(p)}
18770
19944
  var SELF = /* @__PURE__ */ Symbol("self");
18771
19945
  var ANY = /* @__PURE__ */ Symbol("any");
18772
19946
  var UNDER_SELF = /* @__PURE__ */ Symbol("under.self");
19947
+ var MEMBER = /* @__PURE__ */ Symbol("member.scope");
18773
19948
  function canonicalDim(key) {
18774
19949
  if (key === "userId" || key === "partnerUserId") return "user";
18775
19950
  return key;
@@ -18799,7 +19974,8 @@ function normalizeClauses(raw) {
18799
19974
  if (val === null) return null;
18800
19975
  const s = String(val);
18801
19976
  if (ANY_TOKEN_RE.test(s)) return ANY;
18802
- if (UNDER_SELF_PLACEHOLDER_RE.test(s)) return UNDER_SELF;
19977
+ if (UNDER_SELF_PLACEHOLDER_RE2.test(s)) return UNDER_SELF;
19978
+ if (MEMBER_SCOPE_PLACEHOLDER_RE2.test(s)) return MEMBER;
18803
19979
  return SELF_PLACEHOLDER_RE.test(s) ? SELF : s;
18804
19980
  });
18805
19981
  if (!isRecognizedOwnershipKey(k)) unrecognizedDims.push(k);
@@ -18854,7 +20030,7 @@ function buildArchetypes(clauses) {
18854
20030
  const set = scopeDims.get(dim) ?? /* @__PURE__ */ new Set();
18855
20031
  set.add(SELF);
18856
20032
  for (const v of values) {
18857
- if (v === null || v === ANY || v === UNDER_SELF) continue;
20033
+ if (v === null || v === ANY || v === UNDER_SELF || v === MEMBER) continue;
18858
20034
  set.add(v);
18859
20035
  }
18860
20036
  scopeDims.set(dim, set);
@@ -18881,6 +20057,7 @@ function listAdmits(allowed, recordValue) {
18881
20057
  if (a === SELF) return recordValue === SELF;
18882
20058
  if (a === ANY) return recordValue !== void 0;
18883
20059
  if (a === UNDER_SELF) return false;
20060
+ if (a === MEMBER) return false;
18884
20061
  return recordValue === a;
18885
20062
  });
18886
20063
  }
@@ -18906,7 +20083,7 @@ function undecidableDims(clauses) {
18906
20083
  const dims = /* @__PURE__ */ new Set();
18907
20084
  for (const c of clauses) {
18908
20085
  for (const [dim, values] of Object.entries(c.dataScope)) {
18909
- if (values.includes(UNDER_SELF)) dims.add(dim);
20086
+ if (values.includes(UNDER_SELF) || values.includes(MEMBER)) dims.add(dim);
18910
20087
  }
18911
20088
  }
18912
20089
  return [...dims];
@@ -18939,6 +20116,7 @@ function renderDataScope(ds) {
18939
20116
  if (v === SELF) return "self";
18940
20117
  if (v === ANY) return "any";
18941
20118
  if (v === UNDER_SELF) return "under-self";
20119
+ if (v === MEMBER) return "member";
18942
20120
  if (v === null) return "null";
18943
20121
  return v;
18944
20122
  });
@@ -18970,14 +20148,25 @@ async function runAccessExplain(opts, deps = {}) {
18970
20148
  const profile = await client.auth.getAccessProfile({ contextId: opts.context, principalId: principal });
18971
20149
  let rawClauses = profile.scopes ?? [];
18972
20150
  let via = "inline scopes";
18973
- if (profile.roleId) {
18974
- const role = await client.auth.getRole({ contextId: opts.context, roleId: profile.roleId });
18975
- rawClauses = role.scopes ?? [];
18976
- via = `role '${profile.roleId}'`;
20151
+ const roleIds = profile.roleIds ?? (profile.roleId ? [profile.roleId] : []);
20152
+ if (roleIds.length > 0) {
20153
+ const roleClauses = [];
20154
+ for (const roleId of roleIds) {
20155
+ const role = await client.auth.getRole({ contextId: opts.context, roleId });
20156
+ roleClauses.push(...role.scopes ?? []);
20157
+ }
20158
+ rawClauses = roleClauses;
20159
+ via = roleIds.length === 1 ? `role '${roleIds[0]}'` : `role(s) [${roleIds.join(", ")}]`;
18977
20160
  }
18978
20161
  const clauses = normalizeClauses(rawClauses);
18979
20162
  const rows = explainClauses(clauses);
18980
20163
  const undecidable = undecidableDims(clauses);
20164
+ const hasUnderSelfUndecidable = clauses.some(
20165
+ (c) => Object.values(c.dataScope).some((values) => values.includes(UNDER_SELF))
20166
+ );
20167
+ const hasMemberUndecidable = clauses.some(
20168
+ (c) => Object.values(c.dataScope).some((values) => values.includes(MEMBER))
20169
+ );
18981
20170
  if (opts.json) {
18982
20171
  process.stdout.write(
18983
20172
  `${formatJson({
@@ -19034,10 +20223,7 @@ async function runAccessExplain(opts, deps = {}) {
19034
20223
  ]) + (undecidable.length > 0 ? `
19035
20224
 
19036
20225
  \u26A0 UNDECIDED offline: ${undecidable.join(", ")}
19037
- A clause matching on \`\${{ under.self.* }}\` needs the parent graph, which only the server has,
19038
- so rows granted through it are shown as NO above. Read those cells as "cannot tell here",
19039
- NOT as a denial \u2014 the matrix under-reports this clause.
19040
- ` : "") + '\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'
20226
+ ` + (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'
19041
20227
  );
19042
20228
  return 0;
19043
20229
  } catch (err) {
@@ -19055,7 +20241,8 @@ function renderRole(r) {
19055
20241
  ["name", r.name],
19056
20242
  ["context", r.contextId],
19057
20243
  ["description", r.description],
19058
- ["scopes", r.scopes?.length ? `${r.scopes.length} clause(s)` : void 0]
20244
+ ["scopes", r.scopes?.length ? `${r.scopes.length} clause(s)` : void 0],
20245
+ ["assumable", r.assumable && Object.keys(r.assumable).length ? formatJson(r.assumable) : void 0]
19059
20246
  ]);
19060
20247
  }
19061
20248
  async function runRoleCreate(opts, deps = {}) {
@@ -19326,6 +20513,75 @@ ${renderContext(ctx)}
19326
20513
  });
19327
20514
  }
19328
20515
 
20516
+ // src/commands/issuers.ts
20517
+ function renderSelfSignup(policies) {
20518
+ if (!policies || policies.length === 0) return "\u2014";
20519
+ return policies.map((p) => `${p.signup_type} \u2192 ${p.role_id}`).join(", ");
20520
+ }
20521
+ function renderIssuer(i) {
20522
+ return formatKeyValues([
20523
+ ["issuerId", i.issuerId],
20524
+ ["issuer", i.issuer],
20525
+ ["jwksUri", i.jwksUri],
20526
+ ["audience", i.audience],
20527
+ ["contextId", i.contextId],
20528
+ ["subClaim", i.subClaim],
20529
+ ["emailClaim", i.emailClaim],
20530
+ ["userinfoUri", i.userinfoUri],
20531
+ ["status", i.status],
20532
+ ["createdAt", i.createdAt],
20533
+ ["selfSignup", renderSelfSignup(i.selfSignupPolicies)]
20534
+ ]);
20535
+ }
20536
+ async function runIssuersList(opts, deps = {}) {
20537
+ return withAuth({ ...opts, provisioning: true, context: opts.context }, deps, async (auth) => {
20538
+ const client = auth.client;
20539
+ try {
20540
+ const rows = await drainPages(
20541
+ (startFrom) => client.auth.listIssuers({ limit: PAGE_LIMIT, ...startFrom ? { startFrom } : {} })
20542
+ );
20543
+ if (opts.json) {
20544
+ process.stdout.write(`${formatJson(rows)}
20545
+ `);
20546
+ return 0;
20547
+ }
20548
+ process.stdout.write(
20549
+ "\n" + formatTable(rows, [
20550
+ { header: "issuerId", get: (i) => i.issuerId },
20551
+ { header: "issuer", get: (i) => i.issuer },
20552
+ { header: "audience", get: (i) => i.audience },
20553
+ { header: "status", get: (i) => i.status },
20554
+ { header: "self-signup", get: (i) => renderSelfSignup(i.selfSignupPolicies) }
20555
+ ]) + "\n"
20556
+ );
20557
+ return 0;
20558
+ } catch (err) {
20559
+ process.stderr.write(formatCliError(err, { action: "issuers list failed" }));
20560
+ return 1;
20561
+ }
20562
+ });
20563
+ }
20564
+ async function runIssuersGet(opts, deps = {}) {
20565
+ return withAuth({ ...opts, provisioning: true, context: opts.context }, deps, async (auth) => {
20566
+ const client = auth.client;
20567
+ try {
20568
+ const issuer = await client.auth.getIssuer({ issuerId: opts.issuerId });
20569
+ if (opts.json) {
20570
+ process.stdout.write(`${formatJson(issuer)}
20571
+ `);
20572
+ return 0;
20573
+ }
20574
+ process.stdout.write(`
20575
+ ${renderIssuer(issuer)}
20576
+ `);
20577
+ return 0;
20578
+ } catch (err) {
20579
+ process.stderr.write(formatCliError(err, { action: "issuers get failed" }));
20580
+ return 1;
20581
+ }
20582
+ });
20583
+ }
20584
+
19329
20585
  // src/commands/login.ts
19330
20586
  var import_node_http = require("http");
19331
20587
  var import_open = __toESM(require("open"));
@@ -19547,7 +20803,7 @@ async function runAssertions(client, bootstrapClient, deps, blueprint, tenantId,
19547
20803
  const assertions = [];
19548
20804
  try {
19549
20805
  const ping = await deps.pingWithKey(result.rawKey);
19550
- const wantActions = blueprint.accessProfile.allowedActions;
20806
+ const wantActions = effectiveAccessProfileActions(blueprint);
19551
20807
  const got = ping.allowedActions ?? [];
19552
20808
  const missing = wantActions.filter((a) => !got.includes(a));
19553
20809
  const ok = ping.principalType === "scoped_key" && ping.tenantId === tenantId && missing.length === 0;
@@ -19641,6 +20897,19 @@ async function runAssertions(client, bootstrapClient, deps, blueprint, tenantId,
19641
20897
  assertions.push({ name: `issuer:${issuerId}`, ok: false, detail: `getIssuer(${issuerId}) failed: ${errMsg(e)}` });
19642
20898
  }
19643
20899
  }
20900
+ for (const { namespace } of result.namespaceActions) {
20901
+ try {
20902
+ const ns = await bootstrapClient.identity.getNamespace({ namespace, contextId: blueprint.contextId });
20903
+ const ok = ns.namespace === namespace && ns.contextId === blueprint.contextId;
20904
+ assertions.push({
20905
+ name: `namespace:${namespace}`,
20906
+ ok,
20907
+ detail: ok ? `namespace '${namespace}' registered + readable in context '${blueprint.contextId}'` : `getNamespace(${namespace}) returned namespace '${ns.namespace ?? "(none)"}' in context '${ns.contextId ?? "(tenant-wide)"}', expected '${blueprint.contextId}'`
20908
+ });
20909
+ } catch (e) {
20910
+ assertions.push({ name: `namespace:${namespace}`, ok: false, detail: `getNamespace(${namespace}) failed: ${errMsg(e)}` });
20911
+ }
20912
+ }
19644
20913
  return assertions;
19645
20914
  }
19646
20915
  async function runTeardown(client, bootstrapClient, blueprint, machineId, snapshot, result) {
@@ -19753,19 +21022,36 @@ async function runTeardown(client, bootstrapClient, blueprint, machineId, snapsh
19753
21022
  steps.push({ step: `delete issuer ${issuerId}`, ok: false, error: errMsg(e) });
19754
21023
  }
19755
21024
  }
21025
+ for (const { namespace } of result.namespaceActions) {
21026
+ steps.push({
21027
+ step: `delete namespace ${namespace}`,
21028
+ ok: true,
21029
+ skipped: true,
21030
+ error: "namespace deletion requires a root API key (sk_*) \u2014 blueprint-test never holds one; the registration is left in place"
21031
+ });
21032
+ }
19756
21033
  return steps;
19757
21034
  }
19758
21035
  async function runBlueprintHarness(client, input, deps) {
19759
- const { tenantId, machineId, keep = false, log, remintForContext } = input;
21036
+ const { tenantId, machineId, keep = false, log, remintForContext, remintBootstrapForContext } = input;
19760
21037
  const readExistingKey = async () => void 0;
19761
21038
  const persistMintedKey = async () => {
19762
21039
  };
21040
+ const provisioned = await provisionContext(client, {
21041
+ blueprint: input.blueprint,
21042
+ remintBootstrapForContext,
21043
+ readExistingKey,
21044
+ log
21045
+ });
19763
21046
  const blueprint = await resolveApplyTimeIdentities(
19764
21047
  input.blueprint,
19765
- client,
21048
+ provisioned.ctxBootstrapClient ?? client,
19766
21049
  log
19767
21050
  );
19768
21051
  const remint = remintForContext ?? (async () => client);
21052
+ const remintBootstrap = remintBootstrapForContext ?? (async () => client);
21053
+ const hasIssuers = (blueprint.issuers ?? []).length > 0;
21054
+ const hasNamespaces = provisioned.namespaceActions.length > 0;
19769
21055
  let contextSnapshot;
19770
21056
  if (remintForContext) {
19771
21057
  try {
@@ -19781,7 +21067,18 @@ async function runBlueprintHarness(client, input, deps) {
19781
21067
  } else {
19782
21068
  contextSnapshot = await captureSnapshot(client, blueprint);
19783
21069
  }
19784
- const issuerSnapshot = await captureIssuerSnapshot(client);
21070
+ let issuerSnapshot;
21071
+ if (!hasIssuers) {
21072
+ issuerSnapshot = { existingIssuerIds: /* @__PURE__ */ new Set(), failed: false };
21073
+ } else if (remintBootstrapForContext) {
21074
+ try {
21075
+ issuerSnapshot = await captureIssuerSnapshot(await remintBootstrapForContext(blueprint.contextId));
21076
+ } catch (e) {
21077
+ issuerSnapshot = { existingIssuerIds: /* @__PURE__ */ new Set(), failed: !isNotFound(e) };
21078
+ }
21079
+ } else {
21080
+ issuerSnapshot = await captureIssuerSnapshot(client);
21081
+ }
19785
21082
  const snapshot = {
19786
21083
  ...contextSnapshot,
19787
21084
  existingIssuerIds: issuerSnapshot.existingIssuerIds,
@@ -19791,9 +21088,26 @@ async function runBlueprintHarness(client, input, deps) {
19791
21088
  let assertions = [];
19792
21089
  let applyError;
19793
21090
  try {
19794
- result = await runLoader(client, { blueprint, tenantId, machineId, readExistingKey, persistMintedKey, log, remintForContext });
21091
+ result = await runLoader(client, {
21092
+ blueprint,
21093
+ tenantId,
21094
+ machineId,
21095
+ readExistingKey,
21096
+ persistMintedKey,
21097
+ log,
21098
+ provisioned,
21099
+ remintForContext,
21100
+ remintBootstrapForContext
21101
+ });
19795
21102
  log.debug({ component: "harness", contextId: result.contextId, keyName: result.keyName }, "applied");
19796
- assertions = await runAssertions(await remint(blueprint.contextId), client, deps, blueprint, tenantId, result);
21103
+ assertions = await runAssertions(
21104
+ await remint(blueprint.contextId),
21105
+ hasIssuers || hasNamespaces ? await remintBootstrap(blueprint.contextId) : client,
21106
+ deps,
21107
+ blueprint,
21108
+ tenantId,
21109
+ result
21110
+ );
19797
21111
  } catch (e) {
19798
21112
  applyError = errMsg(e);
19799
21113
  log.warn({ component: "harness", err: applyError }, "apply phase failed");
@@ -19805,7 +21119,14 @@ async function runBlueprintHarness(client, input, deps) {
19805
21119
  tdClient = await remint(blueprint.contextId);
19806
21120
  } catch {
19807
21121
  }
19808
- teardown = await runTeardown(tdClient, client, blueprint, machineId, snapshot, result);
21122
+ let tdBootstrapClient = client;
21123
+ if (hasIssuers) {
21124
+ try {
21125
+ tdBootstrapClient = await remintBootstrap(blueprint.contextId);
21126
+ } catch {
21127
+ }
21128
+ }
21129
+ teardown = await runTeardown(tdClient, tdBootstrapClient, blueprint, machineId, snapshot, result);
19809
21130
  }
19810
21131
  const passed = applyError === void 0 && assertions.length > 0 && assertions.every((a) => a.ok);
19811
21132
  return {
@@ -19879,6 +21200,13 @@ async function runBlueprintTest(fileOrName, opts, deps = {}) {
19879
21200
  process.stderr.write(` \u2716 Scope gate refused blueprint:
19880
21201
  `);
19881
21202
  for (const r of err.rejected) process.stderr.write(` \u2022 ${r.action}: ${r.reason}
21203
+ `);
21204
+ return 2;
21205
+ }
21206
+ if (err instanceof CapabilityGateError) {
21207
+ process.stderr.write(` \u2716 Capability gate refused blueprint:
21208
+ `);
21209
+ for (const r of err.rejected) process.stderr.write(` \u2022 ${r.action}: ${r.reason}
19882
21210
  `);
19883
21211
  return 2;
19884
21212
  }
@@ -19914,9 +21242,22 @@ async function runBlueprintTest(fileOrName, opts, deps = {}) {
19914
21242
  environment: auth.baseUrl
19915
21243
  });
19916
21244
  };
21245
+ const remintBootstrapForContext = async (contextId) => {
21246
+ const ctxToken = await mintBridgeToken({
21247
+ cognitoToken: auth.cognitoToken,
21248
+ apiBaseUrl: auth.baseUrl,
21249
+ contextId,
21250
+ tenant: auth.tenant,
21251
+ useScopedToken: opts.useScopedToken
21252
+ });
21253
+ return new VectrosClient({
21254
+ token: ctxToken.token,
21255
+ environment: auth.baseUrl
21256
+ });
21257
+ };
19917
21258
  const report = await runBlueprintHarness(
19918
21259
  client,
19919
- { blueprint, tenantId, machineId, keep: opts.keep, log, remintForContext },
21260
+ { blueprint, tenantId, machineId, keep: opts.keep, log, remintForContext, remintBootstrapForContext },
19920
21261
  { pingWithKey }
19921
21262
  );
19922
21263
  if (opts.json) {
@@ -19973,12 +21314,13 @@ async function issueOrRotateKey(client, tenantId, context, userId, keyName) {
19973
21314
  return { key: rotated, revokedKeyId: existing?.keyId };
19974
21315
  }
19975
21316
  async function runJoin(opts, deps = {}) {
19976
- const hasRole = Boolean(opts.role);
21317
+ const hasRole = Boolean(opts.role?.length);
19977
21318
  const hasActions = Boolean(opts.actions);
19978
21319
  if (hasRole === hasActions) {
19979
- process.stderr.write(" \u2716 Provide exactly one of --role <roleId> or --actions <csv>.\n");
21320
+ process.stderr.write(" \u2716 Provide exactly one of --role <roleId> (repeatable) or --actions <csv>.\n");
19980
21321
  return 2;
19981
21322
  }
21323
+ const roleDisplay = opts.role?.join(", ");
19982
21324
  if (hasActions && splitCsv(opts.actions).length === 0) {
19983
21325
  process.stderr.write(" \u2716 --actions needs at least one action (an empty/blank list grants nothing).\n");
19984
21326
  return 2;
@@ -19997,9 +21339,10 @@ async function runJoin(opts, deps = {}) {
19997
21339
  return 2;
19998
21340
  }
19999
21341
  const context = opts.contextArg;
21342
+ const tenant = opts.tenant ?? "live";
20000
21343
  let alias;
20001
21344
  try {
20002
- alias = keyringAliasFor(opts.alias ?? context, opts.tenant ?? "live");
21345
+ alias = keyringAliasFor(opts.alias ?? context, tenant);
20003
21346
  } catch (err) {
20004
21347
  process.stderr.write(` \u2716 ${formatError(err)}
20005
21348
  `);
@@ -20025,7 +21368,10 @@ async function runJoin(opts, deps = {}) {
20025
21368
  upsert: true,
20026
21369
  body: {
20027
21370
  principalId,
20028
- roleId: opts.role,
21371
+ // Always send the plural `roleIds`, even for a single
21372
+ // `--role`: a strict superset of the deprecated singular `roleId`,
21373
+ // read uniformly by the platform regardless of how many compose.
21374
+ roleIds: opts.role,
20029
21375
  scopes,
20030
21376
  identityOverrides
20031
21377
  }
@@ -20034,8 +21380,23 @@ async function runJoin(opts, deps = {}) {
20034
21380
  const heldSecret = existingEntry ? await keyring.resolveSecret(alias) : void 0;
20035
21381
  const canReuse = existingEntry && heldSecret && existingEntry.context === context && existingEntry.principalId === principalId && existingEntry.env === auth.env;
20036
21382
  if (canReuse && existingEntry) {
20037
- await keyring.setActive(alias);
20038
- emitJoined(alias, context, principalId, opts.role, existingEntry.keyId, true);
21383
+ const willActivate2 = await determineActivateDecision(keyring, {
21384
+ tenant,
21385
+ explicitActivate: opts.activate,
21386
+ hint: { alias, secret: heldSecret }
21387
+ });
21388
+ if (willActivate2) await keyring.setActive(alias);
21389
+ emitJoined({
21390
+ alias,
21391
+ context,
21392
+ principalId,
21393
+ role: roleDisplay,
21394
+ keyId: existingEntry.keyId,
21395
+ reused: true,
21396
+ tenant,
21397
+ willActivate: willActivate2,
21398
+ explicitlyDeclined: opts.activate === false
21399
+ });
20039
21400
  return 0;
20040
21401
  }
20041
21402
  warnReplacingAlias(existingEntry, alias);
@@ -20047,13 +21408,24 @@ async function runJoin(opts, deps = {}) {
20047
21408
  );
20048
21409
  }
20049
21410
  if (revokedKeyId) await warnOtherAliasesNaming(keyring, revokedKeyId, alias);
21411
+ const willActivate = await determineActivateDecision(keyring, { tenant, explicitActivate: opts.activate });
20050
21412
  const { entry } = await storeMintedKey(
20051
21413
  keyring,
20052
- { alias, context, principalId, role: opts.role, keyId: key.keyId, env: auth.env },
21414
+ { alias, context, principalId, role: roleDisplay, keyId: key.keyId, env: auth.env },
20053
21415
  key.rawKey,
20054
- { revokedKeyId }
21416
+ { activate: willActivate, revokedKeyId }
20055
21417
  );
20056
- emitJoined(alias, context, principalId, opts.role, entry.keyId, false);
21418
+ emitJoined({
21419
+ alias,
21420
+ context,
21421
+ principalId,
21422
+ role: roleDisplay,
21423
+ keyId: entry.keyId,
21424
+ reused: false,
21425
+ tenant,
21426
+ willActivate,
21427
+ explicitlyDeclined: opts.activate === false
21428
+ });
20057
21429
  return 0;
20058
21430
  } catch (err) {
20059
21431
  process.stderr.write(formatCliError(err, { action: "join failed" }));
@@ -20061,27 +21433,34 @@ async function runJoin(opts, deps = {}) {
20061
21433
  }
20062
21434
  });
20063
21435
  }
20064
- function emitJoined(alias, context, principalId, role, keyId, reused) {
20065
- process.stdout.write(
20066
- `
20067
- \u2714 Joined '${context}' as ${principalId}` + (reused ? " (existing key reused)" : "") + "\n" + formatKeyValues([
20068
- ["alias", alias],
20069
- ["context", context],
20070
- ["principal", principalId],
20071
- ["role", role],
20072
- ["key id", keyId]
20073
- ]) + `
21436
+ function emitJoined(opts) {
21437
+ const header = `
21438
+ \u2714 Joined '${opts.context}' as ${opts.principalId}` + (opts.reused ? " (existing key reused)" : "") + "\n" + formatKeyValues([
21439
+ ["alias", opts.alias],
21440
+ ["context", opts.context],
21441
+ ["principal", opts.principalId],
21442
+ ["role", opts.role],
21443
+ ["key id", opts.keyId]
21444
+ ]);
21445
+ const activationNote = opts.willActivate ? `
20074
21446
 
20075
- Now active. Use \`vectros switch ${alias}\` to return to it, \`vectros keyring show --format env\` to export the key for the MCP server.
20076
- `
21447
+ Now active. Use \`vectros switch ${opts.alias}\` to return to it, \`vectros keyring show --format env\` to export the key for the MCP server.
21448
+ ` : "";
21449
+ process.stdout.write(
21450
+ header + activationNote + tenantActivateMessage({
21451
+ alias: opts.alias,
21452
+ tenant: opts.tenant,
21453
+ willActivate: opts.willActivate,
21454
+ explicitlyDeclined: opts.explicitlyDeclined
21455
+ })
20077
21456
  );
20078
21457
  }
20079
21458
 
20080
21459
  // src/build-info.ts
20081
21460
  var BUILD_INFO = {
20082
- cli: true ? "0.15.0" : "dev",
20083
- sdk: true ? "0.39.0" : "dev",
20084
- blueprints: true ? "0.12.0" : "dev"
21461
+ cli: true ? "0.19.0" : "dev",
21462
+ sdk: true ? "0.41.0" : "dev",
21463
+ blueprints: true ? "0.16.0" : "dev"
20085
21464
  };
20086
21465
  function formatBuildInfo() {
20087
21466
  return `vectros ${BUILD_INFO.cli} (sdk ${BUILD_INFO.sdk}, blueprints ${BUILD_INFO.blueprints})`;
@@ -20105,6 +21484,14 @@ function addCommonAuthOptions(cmd) {
20105
21484
  "emit machine-readable JSON instead of the human-readable view"
20106
21485
  );
20107
21486
  }
21487
+ function parseTenantFlag(raw) {
21488
+ if (raw !== void 0 && raw !== "test" && raw !== "live") {
21489
+ process.stderr.write(` \u2716 --tenant must be 'test' or 'live' (got '${raw}')
21490
+ `);
21491
+ process.exit(2);
21492
+ }
21493
+ return raw;
21494
+ }
20108
21495
  function toCommonOptions(flags) {
20109
21496
  return {
20110
21497
  env: flags.env,
@@ -20153,12 +21540,16 @@ function buildProgram(deps = {}) {
20153
21540
  registerAccessCommands(program, deps);
20154
21541
  registerRoleCommands(program, deps);
20155
21542
  registerContextCommands(program, deps);
21543
+ registerIssuersCommands(program, deps);
20156
21544
  registerJoinCommand(program, deps);
20157
21545
  registerKeyringCommands(program);
20158
21546
  return program;
20159
21547
  }
20160
21548
  function registerJoinCommand(program, deps) {
20161
- 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)");
21549
+ 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(
21550
+ "--activate",
21551
+ "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)"
21552
+ ).option("--no-activate", "store the key without activating it, regardless of tenant");
20162
21553
  addAuthOptions(join3).action(
20163
21554
  async (contextArg, flags) => {
20164
21555
  if (flags.tenant !== void 0 && flags.tenant !== "test" && flags.tenant !== "live") {
@@ -20176,7 +21567,8 @@ function registerJoinCommand(program, deps) {
20176
21567
  as: flags.as,
20177
21568
  scope: flags.scope,
20178
21569
  alias: flags.alias,
20179
- tenant: flags.tenant
21570
+ tenant: flags.tenant,
21571
+ activate: flags.activate
20180
21572
  },
20181
21573
  deps
20182
21574
  )
@@ -20288,6 +21680,27 @@ function registerContextCommands(program, deps) {
20288
21680
  }
20289
21681
  );
20290
21682
  }
21683
+ function registerIssuersCommands(program, deps) {
21684
+ const issuers = program.command("issuers").description("Inspect registered third-party IdP issuers (token-exchange trust anchors).");
21685
+ 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)");
21686
+ addCommonAuthOptions(list).action(
21687
+ async (flags) => {
21688
+ const tenant = parseTenantFlag(flags.tenant);
21689
+ process.exit(
21690
+ await runIssuersList({ ...toCommonOptions(flags), context: flags.context, tenant }, deps)
21691
+ );
21692
+ }
21693
+ );
21694
+ 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)");
21695
+ addCommonAuthOptions(get).action(
21696
+ async (issuerId, flags) => {
21697
+ const tenant = parseTenantFlag(flags.tenant);
21698
+ process.exit(
21699
+ await runIssuersGet({ ...toCommonOptions(flags), context: flags.context, issuerId, tenant }, deps)
21700
+ );
21701
+ }
21702
+ );
21703
+ }
20291
21704
  function registerRoleCommands(program, deps) {
20292
21705
  const role = program.command("role").description("Manage context-scoped roles (reusable scope rules; bind via `access grant --role`).");
20293
21706
  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(
@@ -20324,7 +21737,7 @@ function registerRoleCommands(program, deps) {
20324
21737
  }
20325
21738
  function registerAccessCommands(program, deps) {
20326
21739
  const access2 = program.command("access").description("Manage access bindings: principal \u2194 context \u2194 role/scopes.");
20327
- 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)");
21740
+ 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)");
20328
21741
  addCommonAuthOptions(grant).action(
20329
21742
  async (flags) => {
20330
21743
  process.exit(
@@ -20405,6 +21818,25 @@ function registerIdentityCommands(program, deps) {
20405
21818
  addAuthOptions(del).action(async (flags) => {
20406
21819
  process.exit(await runIdentityDelete({ ...toCommonOptions(flags), type: flags.type, id: flags.id }, deps));
20407
21820
  });
21821
+ 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");
21822
+ addCommonAuthOptions(invite).action(
21823
+ async (flags) => {
21824
+ process.exit(
21825
+ await runIdentityInvite(
21826
+ {
21827
+ ...toCommonOptions(flags),
21828
+ context: flags.context,
21829
+ role: flags.role,
21830
+ email: flags.email,
21831
+ firstName: flags.firstName,
21832
+ acceptUrl: flags.acceptUrl,
21833
+ noSendEmail: flags.sendEmail === false
21834
+ },
21835
+ deps
21836
+ )
21837
+ );
21838
+ }
21839
+ );
20408
21840
  }
20409
21841
  function addKeyDestinationOptions(cmd) {
20410
21842
  return cmd.addOption(new import_commander.Option("--format <fmt>", "how to emit the secret").choices(KEY_FORMATS).default("human")).option(
@@ -20423,9 +21855,10 @@ function toKeyDestination(flags) {
20423
21855
  function registerKeyCommands(program, deps) {
20424
21856
  const key = program.command("key").description("Manage scoped API keys (ssk_*) \u2014 issue, list, revoke, rotate.");
20425
21857
  const issue = addKeyDestinationOptions(
20426
- key.command("issue").description("Mint an ssk_* for a principal that already has a profile in the context.").requiredOption("--principal <p>", "principal (usr_<id> or <id>) with an AccessProfile in the context").requiredOption("--context <c>", "app context the key operates in").option("--name <name>", "key name (idempotency tuple component)", DEFAULT_KEY_NAME).option("--label <label>", "optional principal label (surfaced in /v1/ping)")
21858
+ key.command("issue").description("Mint an ssk_* for a principal that already has a profile in the context.").requiredOption("--principal <p>", "principal (usr_<id> or <id>) with an AccessProfile in the context").requiredOption("--context <c>", "app context the key operates in").option("--name <name>", "key name (idempotency tuple component)", DEFAULT_KEY_NAME).option("--label <label>", "optional principal label (surfaced in /v1/ping)").option("--tenant <tenant>", "tenant the key lives in: 'test' or 'live' (default live)")
20427
21859
  );
20428
21860
  addAuthOptions(issue).action(async (flags) => {
21861
+ const tenant = parseTenantFlag(flags.tenant);
20429
21862
  process.exit(
20430
21863
  await runKeyIssue({
20431
21864
  ...toCommonOptions(flags),
@@ -20433,6 +21866,7 @@ function registerKeyCommands(program, deps) {
20433
21866
  context: flags.context,
20434
21867
  name: flags.name,
20435
21868
  label: flags.label,
21869
+ tenant,
20436
21870
  ...toKeyDestination(flags)
20437
21871
  }, deps)
20438
21872
  );
@@ -20440,37 +21874,50 @@ function registerKeyCommands(program, deps) {
20440
21874
  const list = key.command("list").description("List active scoped keys in one context (default, or --context <id>); --all-contexts for every context.").option("--principal <p>", "only keys for this principal (usr_<id> or <id>)").option("--context <c>", "only keys in this context (default: the default context)").option(
20441
21875
  "--all-contexts",
20442
21876
  "list every key across ALL your contexts (owner-only) \u2014 the tenant-wide view for the key limit"
20443
- );
21877
+ ).option("--tenant <tenant>", "tenant to list: 'test' or 'live' (default live; not combinable with --all-contexts)");
20444
21878
  addCommonAuthOptions(list).action(
20445
21879
  async (flags) => {
21880
+ if (flags.allContexts && flags.tenant !== void 0) {
21881
+ process.stderr.write(" \u2716 --tenant is not combinable with --all-contexts (which already spans both tenants).\n");
21882
+ process.exit(2);
21883
+ }
21884
+ const tenant = parseTenantFlag(flags.tenant);
20446
21885
  process.exit(
20447
21886
  await runKeyList(
20448
21887
  {
20449
21888
  ...toCommonOptions(flags),
20450
21889
  principal: flags.principal,
20451
21890
  context: flags.context,
20452
- allContexts: Boolean(flags.allContexts)
21891
+ allContexts: Boolean(flags.allContexts),
21892
+ tenant
20453
21893
  },
20454
21894
  deps
20455
21895
  )
20456
21896
  );
20457
21897
  }
20458
21898
  );
20459
- const get = key.command("get").description("Show one key\u2019s metadata (no secret).").argument("<keyId>").option("--context <c>", "context the key is bound to (default: the default context)");
20460
- addCommonAuthOptions(get).action(async (keyId, flags) => {
20461
- process.exit(await runKeyGet(keyId, { ...toCommonOptions(flags), context: flags.context }, deps));
20462
- });
20463
- const revoke = key.command("revoke").description("Revoke a key (soft-delete; ~5 min to take effect).").argument("<keyId>").option("--context <c>", "context the key is bound to (default: the default context)");
20464
- addAuthOptions(revoke).action(async (keyId, flags) => {
20465
- process.exit(await runKeyRevoke(keyId, { ...toCommonOptions(flags), context: flags.context }, deps));
20466
- });
21899
+ const get = key.command("get").description("Show one key\u2019s metadata (no secret).").argument("<keyId>").option("--context <c>", "context the key is bound to (default: the default context)").option("--tenant <tenant>", "tenant the key lives in: 'test' or 'live' (default live)");
21900
+ addCommonAuthOptions(get).action(
21901
+ async (keyId, flags) => {
21902
+ const tenant = parseTenantFlag(flags.tenant);
21903
+ process.exit(await runKeyGet(keyId, { ...toCommonOptions(flags), context: flags.context, tenant }, deps));
21904
+ }
21905
+ );
21906
+ const revoke = key.command("revoke").description("Revoke a key (soft-delete; ~5 min to take effect).").argument("<keyId>").option("--context <c>", "context the key is bound to (default: the default context)").option("--tenant <tenant>", "tenant the key lives in: 'test' or 'live' (default live)");
21907
+ addAuthOptions(revoke).action(
21908
+ async (keyId, flags) => {
21909
+ const tenant = parseTenantFlag(flags.tenant);
21910
+ process.exit(await runKeyRevoke(keyId, { ...toCommonOptions(flags), context: flags.context, tenant }, deps));
21911
+ }
21912
+ );
20467
21913
  const rotate = addKeyDestinationOptions(
20468
21914
  key.command("rotate").description("Revoke the matching active key and mint a fresh secret.").requiredOption("--principal <p>", "principal (usr_<id> or <id>)").requiredOption("--context <c>", "app context").option("--name <name>", "key name to rotate", DEFAULT_KEY_NAME).option(
20469
21915
  "--force-new",
20470
21916
  "mint a new key even though other active keys already exist for this principal/context under a different name (bypasses the default-name-mismatch guard)"
20471
- )
21917
+ ).option("--tenant <tenant>", "tenant the key lives in: 'test' or 'live' (default live)")
20472
21918
  );
20473
21919
  addAuthOptions(rotate).action(async (flags) => {
21920
+ const tenant = parseTenantFlag(flags.tenant);
20474
21921
  process.exit(
20475
21922
  await runKeyRotate({
20476
21923
  ...toCommonOptions(flags),
@@ -20478,6 +21925,7 @@ function registerKeyCommands(program, deps) {
20478
21925
  context: flags.context,
20479
21926
  name: flags.name,
20480
21927
  forceNew: flags.forceNew,
21928
+ tenant,
20481
21929
  ...toKeyDestination(flags)
20482
21930
  }, deps)
20483
21931
  );