@vectros-ai/cli 0.15.0 → 0.16.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
@@ -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.
5603
5603
  *
5604
5604
  * @param {Vectros.GetIssuerRequest} request
5605
5605
  * @param {AuthClient.RequestOptions} requestOptions - Request-specific configuration.
@@ -5657,13 +5657,14 @@ 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
+ * 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
5661
  *
5662
5662
  * @param {Vectros.DeleteIssuerRequest} request
5663
5663
  * @param {AuthClient.RequestOptions} requestOptions - Request-specific configuration.
5664
5664
  *
5665
5665
  * @throws {@link Vectros.ForbiddenError}
5666
5666
  * @throws {@link Vectros.NotFoundError}
5667
+ * @throws {@link Vectros.ConflictError}
5667
5668
  * @throws {@link Vectros.TooManyRequestsError}
5668
5669
  *
5669
5670
  * @example
@@ -5705,6 +5706,8 @@ var AuthClient = class {
5705
5706
  throw new ForbiddenError(_response.error.body, _response.rawResponse);
5706
5707
  case 404:
5707
5708
  throw new NotFoundError(_response.error.body, _response.rawResponse);
5709
+ case 409:
5710
+ throw new ConflictError(_response.error.body, _response.rawResponse);
5708
5711
  case 429:
5709
5712
  throw new TooManyRequestsError(_response.error.body, _response.rawResponse);
5710
5713
  default:
@@ -5723,7 +5726,7 @@ var AuthClient = class {
5723
5726
  );
5724
5727
  }
5725
5728
  /**
5726
- * Returns the issuers registered in your tenant. Requires a root API key or the bootstrap's provisioning capability. Returns a `{data, nextCursor}` envelope.
5729
+ * Returns the issuers registered in your tenant. Requires a root API key or the bootstrap's provisioning capability. A credential confined to one app context sees only the issuers registered in that context; a root API key sees every context. Returns a `{data, nextCursor}` envelope.
5727
5730
  *
5728
5731
  * @param {Vectros.ListIssuersRequest} request
5729
5732
  * @param {AuthClient.RequestOptions} requestOptions - Request-specific configuration.
@@ -5780,7 +5783,7 @@ var AuthClient = class {
5780
5783
  return handleNonStatusCodeError(_response.error, _response.rawResponse, "GET", "/v1/auth/issuers");
5781
5784
  }
5782
5785
  /**
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.
5786
+ * Registers a trusted third-party IdP issuer that BYO-IdP token exchange (`POST /v1/auth/token/exchange`) may accept a `subject_token` from. Requires a root API key or the CLI bootstrap's provisioning capability — never an ordinary partner-grantable scope. Idempotent by `issuerId` within your tenant; the `(issuer, audience)` pair must not already be registered by a different issuerId/tenant. An app context may have at most one active issuer — deregister the existing one first if you need to replace it. One issuer MAY serve several contexts today, each via its own registration row with a distinct `audience`.
5784
5787
  *
5785
5788
  * @param {Vectros.IssuerRequest} request
5786
5789
  * @param {AuthClient.RequestOptions} requestOptions - Request-specific configuration.
@@ -5891,7 +5894,7 @@ var AuthClient = class {
5891
5894
  return handleNonStatusCodeError(_response.error, _response.rawResponse, "GET", "/v1/ping");
5892
5895
  }
5893
5896
  /**
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.
5897
+ * 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
5898
  *
5896
5899
  * @param {Vectros.ListProfilesForPrincipalRequest} request
5897
5900
  * @param {AuthClient.RequestOptions} requestOptions - Request-specific configuration.
@@ -6170,7 +6173,7 @@ var AuthClient = class {
6170
6173
  return handleNonStatusCodeError(_response.error, _response.rawResponse, "POST", "/v1/users/invite/resend");
6171
6174
  }
6172
6175
  /**
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.
6176
+ * 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
6177
  *
6175
6178
  * @param {Vectros.TokenExchangeRequest} request
6176
6179
  * @param {AuthClient.RequestOptions} requestOptions - Request-specific configuration.
@@ -7712,6 +7715,7 @@ var IdentityClient = class {
7712
7715
  * @example
7713
7716
  * await client.identity.listEntities({
7714
7717
  * namespace: "team",
7718
+ * contextId: "myapp",
7715
7719
  * startFrom: "b3BhcXVlLWN1cnNvci1mcm9tLXRoZS1wcmV2aW91cy1wYWdl"
7716
7720
  * })
7717
7721
  */
@@ -7721,6 +7725,7 @@ var IdentityClient = class {
7721
7725
  async __listEntities(request, requestOptions) {
7722
7726
  const {
7723
7727
  namespace,
7728
+ contextId,
7724
7729
  userId,
7725
7730
  externalId,
7726
7731
  scope,
@@ -7735,6 +7740,7 @@ var IdentityClient = class {
7735
7740
  limit
7736
7741
  } = request;
7737
7742
  const _queryParams = {
7743
+ contextId,
7738
7744
  userId,
7739
7745
  externalId,
7740
7746
  scope,
@@ -7793,6 +7799,7 @@ var IdentityClient = class {
7793
7799
  * @example
7794
7800
  * await client.identity.createEntity({
7795
7801
  * namespace: "team",
7802
+ * contextId: "myapp",
7796
7803
  * body: {
7797
7804
  * externalId: "team_eng_platform"
7798
7805
  * }
@@ -7802,9 +7809,10 @@ var IdentityClient = class {
7802
7809
  return HttpResponsePromise.fromPromise(this.__createEntity(request, requestOptions));
7803
7810
  }
7804
7811
  async __createEntity(request, requestOptions) {
7805
- const { namespace, upsert, body: _body } = request;
7812
+ const { namespace, upsert, contextId, body: _body } = request;
7806
7813
  const _queryParams = {
7807
- upsert
7814
+ upsert,
7815
+ contextId
7808
7816
  };
7809
7817
  const _authRequest = await this._options.authProvider.getAuthRequest();
7810
7818
  const _headers = mergeHeaders(
@@ -7861,14 +7869,18 @@ var IdentityClient = class {
7861
7869
  * @example
7862
7870
  * await client.identity.getEntity({
7863
7871
  * namespace: "team",
7864
- * id: "6ba7b810-9dad-11d1-80b4-00c04fd430c8"
7872
+ * id: "6ba7b810-9dad-11d1-80b4-00c04fd430c8",
7873
+ * contextId: "myapp"
7865
7874
  * })
7866
7875
  */
7867
7876
  getEntity(request, requestOptions) {
7868
7877
  return HttpResponsePromise.fromPromise(this.__getEntity(request, requestOptions));
7869
7878
  }
7870
7879
  async __getEntity(request, requestOptions) {
7871
- const { namespace, id } = request;
7880
+ const { namespace, id, contextId } = request;
7881
+ const _queryParams = {
7882
+ contextId
7883
+ };
7872
7884
  const _authRequest = await this._options.authProvider.getAuthRequest();
7873
7885
  const _headers = mergeHeaders(
7874
7886
  _authRequest.headers,
@@ -7882,7 +7894,7 @@ var IdentityClient = class {
7882
7894
  ),
7883
7895
  method: "GET",
7884
7896
  headers: _headers,
7885
- queryString: url_exports.queryBuilder().mergeAdditional(requestOptions?.queryParams).build(),
7897
+ queryString: url_exports.queryBuilder().addMany(_queryParams).mergeAdditional(requestOptions?.queryParams).build(),
7886
7898
  timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1e3,
7887
7899
  maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries,
7888
7900
  abortSignal: requestOptions?.abortSignal,
@@ -7919,6 +7931,7 @@ var IdentityClient = class {
7919
7931
  * await client.identity.updateEntity({
7920
7932
  * namespace: "team",
7921
7933
  * id: "id",
7934
+ * contextId: "myapp",
7922
7935
  * body: {
7923
7936
  * externalId: "team_eng_platform"
7924
7937
  * }
@@ -7928,7 +7941,10 @@ var IdentityClient = class {
7928
7941
  return HttpResponsePromise.fromPromise(this.__updateEntity(request, requestOptions));
7929
7942
  }
7930
7943
  async __updateEntity(request, requestOptions) {
7931
- const { namespace, id, body: _body } = request;
7944
+ const { namespace, id, contextId, body: _body } = request;
7945
+ const _queryParams = {
7946
+ contextId
7947
+ };
7932
7948
  const _authRequest = await this._options.authProvider.getAuthRequest();
7933
7949
  const _headers = mergeHeaders(
7934
7950
  _authRequest.headers,
@@ -7943,7 +7959,7 @@ var IdentityClient = class {
7943
7959
  method: "PUT",
7944
7960
  headers: _headers,
7945
7961
  contentType: "application/json",
7946
- queryString: url_exports.queryBuilder().mergeAdditional(requestOptions?.queryParams).build(),
7962
+ queryString: url_exports.queryBuilder().addMany(_queryParams).mergeAdditional(requestOptions?.queryParams).build(),
7947
7963
  requestType: "json",
7948
7964
  body: _body,
7949
7965
  timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1e3,
@@ -7983,14 +7999,18 @@ var IdentityClient = class {
7983
7999
  * @example
7984
8000
  * await client.identity.deleteEntity({
7985
8001
  * namespace: "team",
7986
- * id: "id"
8002
+ * id: "id",
8003
+ * contextId: "myapp"
7987
8004
  * })
7988
8005
  */
7989
8006
  deleteEntity(request, requestOptions) {
7990
8007
  return HttpResponsePromise.fromPromise(this.__deleteEntity(request, requestOptions));
7991
8008
  }
7992
8009
  async __deleteEntity(request, requestOptions) {
7993
- const { namespace, id } = request;
8010
+ const { namespace, id, contextId } = request;
8011
+ const _queryParams = {
8012
+ contextId
8013
+ };
7994
8014
  const _authRequest = await this._options.authProvider.getAuthRequest();
7995
8015
  const _headers = mergeHeaders(
7996
8016
  _authRequest.headers,
@@ -8004,7 +8024,7 @@ var IdentityClient = class {
8004
8024
  ),
8005
8025
  method: "DELETE",
8006
8026
  headers: _headers,
8007
- queryString: url_exports.queryBuilder().mergeAdditional(requestOptions?.queryParams).build(),
8027
+ queryString: url_exports.queryBuilder().addMany(_queryParams).mergeAdditional(requestOptions?.queryParams).build(),
8008
8028
  timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1e3,
8009
8029
  maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries,
8010
8030
  abortSignal: requestOptions?.abortSignal,
@@ -8047,6 +8067,7 @@ var IdentityClient = class {
8047
8067
  * @example
8048
8068
  * await client.identity.lookupEntities({
8049
8069
  * namespace: "team",
8070
+ * contextId: "myapp",
8050
8071
  * body: {
8051
8072
  * type: "person_v1",
8052
8073
  * field: "ssn"
@@ -8057,7 +8078,10 @@ var IdentityClient = class {
8057
8078
  return HttpResponsePromise.fromPromise(this.__lookupEntities(request, requestOptions));
8058
8079
  }
8059
8080
  async __lookupEntities(request, requestOptions) {
8060
- const { namespace, body: _body } = request;
8081
+ const { namespace, contextId, body: _body } = request;
8082
+ const _queryParams = {
8083
+ contextId
8084
+ };
8061
8085
  const _authRequest = await this._options.authProvider.getAuthRequest();
8062
8086
  const _headers = mergeHeaders(
8063
8087
  _authRequest.headers,
@@ -8072,7 +8096,7 @@ var IdentityClient = class {
8072
8096
  method: "POST",
8073
8097
  headers: _headers,
8074
8098
  contentType: "application/json",
8075
- queryString: url_exports.queryBuilder().mergeAdditional(requestOptions?.queryParams).build(),
8099
+ queryString: url_exports.queryBuilder().addMany(_queryParams).mergeAdditional(requestOptions?.queryParams).build(),
8076
8100
  requestType: "json",
8077
8101
  body: _body,
8078
8102
  timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1e3,
@@ -8116,15 +8140,17 @@ var IdentityClient = class {
8116
8140
  * @example
8117
8141
  * await client.identity.getEntityVersions({
8118
8142
  * namespace: "team",
8119
- * id: "id"
8143
+ * id: "id",
8144
+ * contextId: "myapp"
8120
8145
  * })
8121
8146
  */
8122
8147
  getEntityVersions(request, requestOptions) {
8123
8148
  return HttpResponsePromise.fromPromise(this.__getEntityVersions(request, requestOptions));
8124
8149
  }
8125
8150
  async __getEntityVersions(request, requestOptions) {
8126
- const { namespace, id, startFrom } = request;
8151
+ const { namespace, id, contextId, startFrom } = request;
8127
8152
  const _queryParams = {
8153
+ contextId,
8128
8154
  startFrom
8129
8155
  };
8130
8156
  const _authRequest = await this._options.authProvider.getAuthRequest();
@@ -8186,7 +8212,10 @@ var IdentityClient = class {
8186
8212
  return HttpResponsePromise.fromPromise(this.__getNamespace(request, requestOptions));
8187
8213
  }
8188
8214
  async __getNamespace(request, requestOptions) {
8189
- const { namespace } = request;
8215
+ const { namespace, contextId } = request;
8216
+ const _queryParams = {
8217
+ contextId
8218
+ };
8190
8219
  const _authRequest = await this._options.authProvider.getAuthRequest();
8191
8220
  const _headers = mergeHeaders(
8192
8221
  _authRequest.headers,
@@ -8200,7 +8229,7 @@ var IdentityClient = class {
8200
8229
  ),
8201
8230
  method: "GET",
8202
8231
  headers: _headers,
8203
- queryString: url_exports.queryBuilder().mergeAdditional(requestOptions?.queryParams).build(),
8232
+ queryString: url_exports.queryBuilder().addMany(_queryParams).mergeAdditional(requestOptions?.queryParams).build(),
8204
8233
  timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1e3,
8205
8234
  maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries,
8206
8235
  abortSignal: requestOptions?.abortSignal,
@@ -8225,7 +8254,7 @@ var IdentityClient = class {
8225
8254
  return handleNonStatusCodeError(_response.error, _response.rawResponse, "GET", "/v1/namespaces/{namespace}");
8226
8255
  }
8227
8256
  /**
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.
8257
+ * 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
8258
  *
8230
8259
  * @param {Vectros.UpdateNamespaceRequest} request
8231
8260
  * @param {IdentityClient.RequestOptions} requestOptions - Request-specific configuration.
@@ -8247,7 +8276,10 @@ var IdentityClient = class {
8247
8276
  return HttpResponsePromise.fromPromise(this.__updateNamespace(request, requestOptions));
8248
8277
  }
8249
8278
  async __updateNamespace(request, requestOptions) {
8250
- const { namespace, body: _body } = request;
8279
+ const { namespace, contextId, body: _body } = request;
8280
+ const _queryParams = {
8281
+ contextId
8282
+ };
8251
8283
  const _authRequest = await this._options.authProvider.getAuthRequest();
8252
8284
  const _headers = mergeHeaders(
8253
8285
  _authRequest.headers,
@@ -8262,7 +8294,7 @@ var IdentityClient = class {
8262
8294
  method: "PUT",
8263
8295
  headers: _headers,
8264
8296
  contentType: "application/json",
8265
- queryString: url_exports.queryBuilder().mergeAdditional(requestOptions?.queryParams).build(),
8297
+ queryString: url_exports.queryBuilder().addMany(_queryParams).mergeAdditional(requestOptions?.queryParams).build(),
8266
8298
  requestType: "json",
8267
8299
  body: _body,
8268
8300
  timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1e3,
@@ -8293,7 +8325,7 @@ var IdentityClient = class {
8293
8325
  return handleNonStatusCodeError(_response.error, _response.rawResponse, "PUT", "/v1/namespaces/{namespace}");
8294
8326
  }
8295
8327
  /**
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.
8328
+ * 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
8329
  *
8298
8330
  * @param {Vectros.DeleteNamespaceRequest} request
8299
8331
  * @param {IdentityClient.RequestOptions} requestOptions - Request-specific configuration.
@@ -8311,7 +8343,10 @@ var IdentityClient = class {
8311
8343
  return HttpResponsePromise.fromPromise(this.__deleteNamespace(request, requestOptions));
8312
8344
  }
8313
8345
  async __deleteNamespace(request, requestOptions) {
8314
- const { namespace } = request;
8346
+ const { namespace, contextId } = request;
8347
+ const _queryParams = {
8348
+ contextId
8349
+ };
8315
8350
  const _authRequest = await this._options.authProvider.getAuthRequest();
8316
8351
  const _headers = mergeHeaders(
8317
8352
  _authRequest.headers,
@@ -8325,7 +8360,7 @@ var IdentityClient = class {
8325
8360
  ),
8326
8361
  method: "DELETE",
8327
8362
  headers: _headers,
8328
- queryString: url_exports.queryBuilder().mergeAdditional(requestOptions?.queryParams).build(),
8363
+ queryString: url_exports.queryBuilder().addMany(_queryParams).mergeAdditional(requestOptions?.queryParams).build(),
8329
8364
  timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1e3,
8330
8365
  maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries,
8331
8366
  abortSignal: requestOptions?.abortSignal,
@@ -8354,7 +8389,7 @@ var IdentityClient = class {
8354
8389
  return handleNonStatusCodeError(_response.error, _response.rawResponse, "DELETE", "/v1/namespaces/{namespace}");
8355
8390
  }
8356
8391
  /**
8357
- * Returns the scope namespaces registered in your account, with the reserved built-ins `org` and `client` listed first. Returns a `{data, nextCursor}` envelope.
8392
+ * Returns the scope namespaces in your account, including `org` and `client`, which are provisioned for you when the account is created. Returns a `{data, nextCursor}` envelope.
8358
8393
  *
8359
8394
  * @param {Vectros.ListNamespacesRequest} request
8360
8395
  * @param {IdentityClient.RequestOptions} requestOptions - Request-specific configuration.
@@ -8366,10 +8401,11 @@ var IdentityClient = class {
8366
8401
  return HttpResponsePromise.fromPromise(this.__listNamespaces(request, requestOptions));
8367
8402
  }
8368
8403
  async __listNamespaces(request = {}, requestOptions) {
8369
- const { startFrom, limit } = request;
8404
+ const { startFrom, limit, contextId } = request;
8370
8405
  const _queryParams = {
8371
8406
  startFrom,
8372
- limit
8407
+ limit,
8408
+ contextId
8373
8409
  };
8374
8410
  const _authRequest = await this._options.authProvider.getAuthRequest();
8375
8411
  const _headers = mergeHeaders(
@@ -8404,9 +8440,9 @@ var IdentityClient = class {
8404
8440
  return handleNonStatusCodeError(_response.error, _response.rawResponse, "GET", "/v1/namespaces");
8405
8441
  }
8406
8442
  /**
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.
8443
+ * Registers a new scope namespace and declares whether its values resolve to identity entities (`entityBacked`). Also requires `specificityRank`, an explicit, account-unique position in the specificity order used to break recordType schema-resolution ties. Requires a root API key or the CLI bootstrap's provisioning capability — never an ordinary partner-grantable scope. `org` and `client` are provisioned when your account is created, so registering either returns the already-registered error.
8408
8444
  *
8409
- * @param {Vectros.NamespaceRequest} request
8445
+ * @param {Vectros.RegisterNamespaceRequest} request
8410
8446
  * @param {IdentityClient.RequestOptions} requestOptions - Request-specific configuration.
8411
8447
  *
8412
8448
  * @throws {@link Vectros.BadRequestError}
@@ -8415,14 +8451,20 @@ var IdentityClient = class {
8415
8451
  *
8416
8452
  * @example
8417
8453
  * await client.identity.registerNamespace({
8418
- * namespace: "team",
8419
- * specificityRank: 1500
8454
+ * body: {
8455
+ * namespace: "team",
8456
+ * specificityRank: 1500
8457
+ * }
8420
8458
  * })
8421
8459
  */
8422
8460
  registerNamespace(request, requestOptions) {
8423
8461
  return HttpResponsePromise.fromPromise(this.__registerNamespace(request, requestOptions));
8424
8462
  }
8425
8463
  async __registerNamespace(request, requestOptions) {
8464
+ const { contextId, body: _body } = request;
8465
+ const _queryParams = {
8466
+ contextId
8467
+ };
8426
8468
  const _authRequest = await this._options.authProvider.getAuthRequest();
8427
8469
  const _headers = mergeHeaders(
8428
8470
  _authRequest.headers,
@@ -8437,9 +8479,9 @@ var IdentityClient = class {
8437
8479
  method: "POST",
8438
8480
  headers: _headers,
8439
8481
  contentType: "application/json",
8440
- queryString: url_exports.queryBuilder().mergeAdditional(requestOptions?.queryParams).build(),
8482
+ queryString: url_exports.queryBuilder().addMany(_queryParams).mergeAdditional(requestOptions?.queryParams).build(),
8441
8483
  requestType: "json",
8442
- body: request,
8484
+ body: _body,
8443
8485
  timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1e3,
8444
8486
  maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries,
8445
8487
  abortSignal: requestOptions?.abortSignal,
@@ -8724,12 +8766,13 @@ var IdentityClient = class {
8724
8766
  return handleNonStatusCodeError(_response.error, _response.rawResponse, "PUT", "/v1/users/{id}");
8725
8767
  }
8726
8768
  /**
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.
8769
+ * Permanently deletes a user identity. This cannot be undone. If the user is a pending invitation, the associated access profile created for that invitation is also removed. Requires the `users:d` scope. Deleting your account's last OWNER is refused (409) — an account must always retain at least one owner.
8728
8770
  *
8729
8771
  * @param {Vectros.DeleteUserRequest} request
8730
8772
  * @param {IdentityClient.RequestOptions} requestOptions - Request-specific configuration.
8731
8773
  *
8732
8774
  * @throws {@link Vectros.NotFoundError}
8775
+ * @throws {@link Vectros.ConflictError}
8733
8776
  * @throws {@link Vectros.TooManyRequestsError}
8734
8777
  *
8735
8778
  * @example
@@ -8769,6 +8812,8 @@ var IdentityClient = class {
8769
8812
  switch (_response.error.statusCode) {
8770
8813
  case 404:
8771
8814
  throw new NotFoundError(_response.error.body, _response.rawResponse);
8815
+ case 409:
8816
+ throw new ConflictError(_response.error.body, _response.rawResponse);
8772
8817
  case 429:
8773
8818
  throw new TooManyRequestsError(_response.error.body, _response.rawResponse);
8774
8819
  default:
@@ -10851,6 +10896,35 @@ var BlueprintSchemaSchema = import_zod.z.object({
10851
10896
  });
10852
10897
  }
10853
10898
  });
10899
+ var CAPABILITY_NAME_RE = /^[a-z][a-z0-9-]*$/;
10900
+ var GrantedCapabilitiesSchema = import_zod.z.array(import_zod.z.string().min(1)).optional().superRefine((caps, ctx) => {
10901
+ if (!caps) return;
10902
+ const seen = /* @__PURE__ */ new Set();
10903
+ caps.forEach((name, i) => {
10904
+ if (name === "") return;
10905
+ if (name === "*") {
10906
+ ctx.addIssue({
10907
+ code: import_zod.z.ZodIssueCode.custom,
10908
+ path: [i],
10909
+ message: "'*' is not a capability \u2014 granted_capabilities has no wildcard, and a '*' in allowedActions confers zero capabilities. Name each capability explicitly."
10910
+ });
10911
+ } else if (!CAPABILITY_NAME_RE.test(name)) {
10912
+ ctx.addIssue({
10913
+ code: import_zod.z.ZodIssueCode.custom,
10914
+ path: [i],
10915
+ 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)`
10916
+ });
10917
+ }
10918
+ if (seen.has(name)) {
10919
+ ctx.addIssue({
10920
+ code: import_zod.z.ZodIssueCode.custom,
10921
+ path: [i],
10922
+ message: `'${name}' is declared more than once`
10923
+ });
10924
+ }
10925
+ seen.add(name);
10926
+ });
10927
+ });
10854
10928
  var BlueprintAccessProfileSchema = import_zod.z.object({
10855
10929
  // Validated structurally here; the SCOPE GATE (in @vectros-ai/cli) is
10856
10930
  // what enforces the data-plane-only security boundary.
@@ -10882,11 +10956,31 @@ var BlueprintAccessProfileSchema = import_zod.z.object({
10882
10956
  // below (`lintIdentityOverrides`, placeholder-aware — see that function's
10883
10957
  // docstring); this `.min(1)` is only the shape-level "non-blank" guard on
10884
10958
  // the value, not the grammar check.
10885
- identityOverrides: import_zod.z.record(import_zod.z.string().min(1)).optional()
10959
+ identityOverrides: import_zod.z.record(import_zod.z.string().min(1)).optional(),
10960
+ // Optional capabilities granted to the blueprint's own service-principal
10961
+ // profile. DECISION: allowed here, not role-clause-only. On the platform,
10962
+ // an access profile's scopes are themselves a list of clauses — the exact
10963
+ // same wire shape a role clause carries — and the loader already builds
10964
+ // this profile as ONE such clause. Refusing `capabilities` here while
10965
+ // allowing it on a role clause would be an asymmetry this FORMAT package
10966
+ // has no basis to invent: nothing on the platform side distinguishes the
10967
+ // two, and whether granting a capability to a blueprint's own service key
10968
+ // is sound POLICY is exactly the question the CLI's scope gate exists to
10969
+ // answer (a separate package, out of scope for this format layer) — the
10970
+ // same posture this package already takes with `allowedActions` above.
10971
+ //
10972
+ // Worth naming for whoever writes that gate: this profile is a long-lived,
10973
+ // non-human-attributable machine credential (typically stored in CI/CD
10974
+ // secrets), not an ephemeral human session — a materially different
10975
+ // blast-radius question from granting the same capability to a role that
10976
+ // a person is bound to. The gate should weigh the two separately rather
10977
+ // than reuse one policy for both.
10978
+ capabilities: GrantedCapabilitiesSchema
10886
10979
  }).strict();
10887
10980
  var BlueprintRoleClauseSchema = import_zod.z.object({
10888
10981
  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()
10982
+ dataScope: import_zod.z.record(import_zod.z.array(import_zod.z.union([import_zod.z.string().min(1), import_zod.z.null()]))).optional(),
10983
+ capabilities: GrantedCapabilitiesSchema
10890
10984
  }).strict();
10891
10985
  var BlueprintRolesSchema = import_zod.z.record(import_zod.z.array(BlueprintRoleClauseSchema).min(1));
10892
10986
  var IDENTITY_NAMESPACE_RE = /^[a-z][a-z0-9_-]{1,31}$/;
@@ -10924,7 +11018,7 @@ var BlueprintServicePrincipalSchema = import_zod.z.object({
10924
11018
  }).strict();
10925
11019
  var ISSUER_ID_RE = CONTEXT_ID_RE;
10926
11020
  var BlueprintIssuerSchema = import_zod.z.object({
10927
- /** Short slug identifying this issuer within your tenant. Immutable once registered. */
11021
+ /** Short slug identifying this issuer within your account. Immutable once registered. */
10928
11022
  issuerId: import_zod.z.string().regex(ISSUER_ID_RE, {
10929
11023
  message: "issuerId must be 3-31 chars, start with a lowercase letter, then lowercase letters/digits/dashes (e.g. 'auth0-prod')"
10930
11024
  }),
@@ -10939,7 +11033,12 @@ var BlueprintIssuerSchema = import_zod.z.object({
10939
11033
  * this as an ordinary per-API/application default).
10940
11034
  */
10941
11035
  audience: import_zod.z.string().min(1),
10942
- /** Which app context an exchanged token targets. Must be an existing app context. */
11036
+ /**
11037
+ * Which app context an exchanged token targets. **Must equal the blueprint's own
11038
+ * `contextId`** — an issuer is a trust anchor, so a blueprint may only attach one to the
11039
+ * context it actually provisions. One IdP account serving several contexts needs one entry
11040
+ * per context, each in that context's own blueprint.
11041
+ */
10943
11042
  contextId: import_zod.z.string().regex(CONTEXT_ID_RE, {
10944
11043
  message: "contextId must be 3-31 chars, start with a lowercase letter, then lowercase letters/digits/dashes (e.g. 'casework')"
10945
11044
  }),
@@ -10966,6 +11065,151 @@ var BlueprintIssuerSchema = import_zod.z.object({
10966
11065
  }).strict()
10967
11066
  ).optional()
10968
11067
  }).strict();
11068
+ var BlueprintNamespaceSchema = import_zod.z.object({
11069
+ /** The namespace to register — same grammar as a `scope:<namespace>` key. */
11070
+ namespace: import_zod.z.string().min(1),
11071
+ /**
11072
+ * When `true`, every value in this namespace must resolve to an existing
11073
+ * identity entity (create via `entities:c` at the in-context phase, or a
11074
+ * declared `identities:` entry). When `false`/omitted, values are
11075
+ * free-form strings validated by grammar only.
11076
+ */
11077
+ entityBacked: import_zod.z.boolean().optional(),
11078
+ /**
11079
+ * This namespace's position in your account's specificity order (used to
11080
+ * break a tie when a caller holds two scope dimensions at once). Required
11081
+ * by the platform on registration; MUST be unique across every namespace
11082
+ * in the account, including `org` (1000) and `client` (2000) — pick a
11083
+ * value outside that pair, and outside any other namespace this same
11084
+ * account might register (its own, or another blueprint's).
11085
+ */
11086
+ specificityRank: import_zod.z.number().int().min(0).max(1e6),
11087
+ /**
11088
+ * Optional membership backing — declares which record type + field hold
11089
+ * grants of THIS namespace's values. Supply together with
11090
+ * `membershipTargetField`, or omit both. Declaring this grants nobody
11091
+ * anything on its own: a role opts in explicitly with
11092
+ * `${{ member.scope.<namespace> }}` in its `dataScope`.
11093
+ */
11094
+ membershipRecordType: import_zod.z.string().min(1).optional(),
11095
+ /** The field on `membershipRecordType` naming the user a grant is FOR. Required alongside `membershipRecordType`. */
11096
+ membershipTargetField: import_zod.z.string().min(1).optional(),
11097
+ /**
11098
+ * The field on `membershipRecordType` naming a grant's LEVEL, so the same
11099
+ * user can hold different levels in different values of this namespace.
11100
+ * Supply together with `membershipLevels`, or omit both for plain
11101
+ * in-or-out membership.
11102
+ */
11103
+ membershipLevelField: import_zod.z.string().min(1).optional(),
11104
+ /** The complete set of level labels this namespace allows. Required alongside `membershipLevelField`. */
11105
+ membershipLevels: import_zod.z.array(import_zod.z.string().min(1)).optional()
11106
+ }).strict();
11107
+ var MEMBERSHIP_FIELD_NAME_RE = /^[A-Za-z0-9_-]{1,64}$/;
11108
+ function lintNamespaces(value, ctx) {
11109
+ if (!value || typeof value !== "object" || !("namespaces" in value)) return;
11110
+ const namespaces = value.namespaces;
11111
+ if (!Array.isArray(namespaces)) return;
11112
+ const rawSchemas = value.schemas;
11113
+ const declaredSchemaTypes = new Set(
11114
+ (Array.isArray(rawSchemas) ? rawSchemas : []).map((s) => s && typeof s === "object" && "typeName" in s ? s.typeName : void 0).filter((t) => typeof t === "string")
11115
+ );
11116
+ const seenNames = /* @__PURE__ */ new Set();
11117
+ const seenRanks = /* @__PURE__ */ new Map();
11118
+ namespaces.forEach((entry, i) => {
11119
+ if (!entry || typeof entry !== "object") return;
11120
+ const ns = entry;
11121
+ const path4 = ["namespaces", i];
11122
+ if (typeof ns.namespace === "string") {
11123
+ const err = scopeNamespaceGrammarError(ns.namespace);
11124
+ if (err) ctx.addIssue({ code: import_zod.z.ZodIssueCode.custom, path: [...path4, "namespace"], message: err });
11125
+ if (seenNames.has(ns.namespace)) {
11126
+ ctx.addIssue({
11127
+ code: import_zod.z.ZodIssueCode.custom,
11128
+ path: [...path4, "namespace"],
11129
+ message: `'${ns.namespace}' is declared more than once in namespaces[]`
11130
+ });
11131
+ }
11132
+ seenNames.add(ns.namespace);
11133
+ }
11134
+ if (typeof ns.specificityRank === "number") {
11135
+ const prior = seenRanks.get(ns.specificityRank);
11136
+ if (prior !== void 0) {
11137
+ ctx.addIssue({
11138
+ code: import_zod.z.ZodIssueCode.custom,
11139
+ path: [...path4, "specificityRank"],
11140
+ message: `specificityRank ${ns.specificityRank} is already used by namespaces[${prior}] \u2014 every namespace's rank must be unique`
11141
+ });
11142
+ }
11143
+ seenRanks.set(ns.specificityRank, i);
11144
+ }
11145
+ const hasRecordType = ns.membershipRecordType !== void 0;
11146
+ const hasTargetField = ns.membershipTargetField !== void 0;
11147
+ if (hasRecordType !== hasTargetField) {
11148
+ ctx.addIssue({
11149
+ code: import_zod.z.ZodIssueCode.custom,
11150
+ path: [...path4, "membershipRecordType"],
11151
+ message: "membershipRecordType and membershipTargetField must be declared together, or neither"
11152
+ });
11153
+ }
11154
+ if (typeof ns.membershipRecordType === "string" && !declaredSchemaTypes.has(ns.membershipRecordType)) {
11155
+ ctx.addIssue({
11156
+ code: import_zod.z.ZodIssueCode.custom,
11157
+ path: [...path4, "membershipRecordType"],
11158
+ 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.`
11159
+ });
11160
+ }
11161
+ for (const [field, value2] of [
11162
+ ["membershipRecordType", ns.membershipRecordType],
11163
+ ["membershipTargetField", ns.membershipTargetField],
11164
+ ["membershipLevelField", ns.membershipLevelField]
11165
+ ]) {
11166
+ if (typeof value2 === "string" && !MEMBERSHIP_FIELD_NAME_RE.test(value2)) {
11167
+ ctx.addIssue({
11168
+ code: import_zod.z.ZodIssueCode.custom,
11169
+ path: [...path4, field],
11170
+ message: `${field} '${value2}' is invalid \u2014 must be 1-64 characters of letters, digits, underscores, or hyphens`
11171
+ });
11172
+ }
11173
+ }
11174
+ const hasLevelField = ns.membershipLevelField !== void 0;
11175
+ const hasLevels = Array.isArray(ns.membershipLevels) && ns.membershipLevels.length > 0;
11176
+ if (hasLevelField !== hasLevels) {
11177
+ ctx.addIssue({
11178
+ code: import_zod.z.ZodIssueCode.custom,
11179
+ path: [...path4, "membershipLevelField"],
11180
+ message: "membershipLevelField and a non-empty membershipLevels must be declared together, or neither"
11181
+ });
11182
+ }
11183
+ if ((hasLevelField || hasLevels) && !hasRecordType) {
11184
+ ctx.addIssue({
11185
+ code: import_zod.z.ZodIssueCode.custom,
11186
+ path: [...path4, "membershipLevelField"],
11187
+ message: "membershipLevelField/membershipLevels require membershipRecordType + membershipTargetField too"
11188
+ });
11189
+ }
11190
+ if (Array.isArray(ns.membershipLevels)) {
11191
+ const seenLevels = /* @__PURE__ */ new Set();
11192
+ for (const level of ns.membershipLevels) {
11193
+ if (typeof level !== "string") continue;
11194
+ const err = scopeNamespaceGrammarError(level);
11195
+ if (err) {
11196
+ ctx.addIssue({
11197
+ code: import_zod.z.ZodIssueCode.custom,
11198
+ path: [...path4, "membershipLevels"],
11199
+ message: `level '${level}': ${err}`
11200
+ });
11201
+ } else if (seenLevels.has(level)) {
11202
+ ctx.addIssue({
11203
+ code: import_zod.z.ZodIssueCode.custom,
11204
+ path: [...path4, "membershipLevels"],
11205
+ message: `membershipLevels contains '${level}' twice`
11206
+ });
11207
+ }
11208
+ seenLevels.add(level);
11209
+ }
11210
+ }
11211
+ });
11212
+ }
10969
11213
  var SeedCommonShape = {
10970
11214
  /** The schema typeName this seed instantiates (should match a declared schema). */
10971
11215
  typeName: import_zod.z.string().min(1),
@@ -11006,7 +11250,7 @@ var BlueprintSeedRecordSchema = import_zod.z.discriminatedUnion("surface", [
11006
11250
  RecordSeedSchema,
11007
11251
  DocumentSeedSchema
11008
11252
  ]);
11009
- var SELF_TOKEN_RE = /\$\{\{\s*(?:self|under)\.[^}]*?\s*\}\}/;
11253
+ var SELF_TOKEN_RE = /\$\{\{\s*(?:self|under|member)\.[^}]*?\s*\}\}/;
11010
11254
  function lintSelfTokenPlacement(value, ctx) {
11011
11255
  const walk = (node, path4, inRoleDataScope) => {
11012
11256
  if (typeof node === "string") {
@@ -11014,7 +11258,7 @@ function lintSelfTokenPlacement(value, ctx) {
11014
11258
  ctx.addIssue({
11015
11259
  code: import_zod.z.ZodIssueCode.custom,
11016
11260
  path: path4,
11017
- message: "'${{ self.* }}' is a runtime per-principal placeholder \u2014 it is only valid inside a roles[].dataScope value"
11261
+ message: "'${{ self.* }}' / '${{ member.* }}' is a runtime placeholder \u2014 it is only valid inside a roles[].dataScope value"
11018
11262
  });
11019
11263
  }
11020
11264
  return;
@@ -11214,9 +11458,36 @@ function lintSeedSurfaces(value, ctx) {
11214
11458
  }
11215
11459
  });
11216
11460
  }
11461
+ function lintIdentitySurfaces(value, ctx) {
11462
+ const bp = value;
11463
+ if (!Array.isArray(bp.schemas)) return;
11464
+ bp.schemas.forEach((schema, i) => {
11465
+ if (!schema.allowedSurfaces?.includes("user")) return;
11466
+ ctx.addIssue({
11467
+ code: import_zod.z.ZodIssueCode.custom,
11468
+ path: ["schemas", i, "allowedSurfaces"],
11469
+ message: `schema '${schema.typeName}': allowedSurfaces cannot include 'user' \u2014 a user-surfaced schema is account-global and requires a root API key, which no blueprint credential ever holds, so this would fail at apply with a 403. Model the data as a 'record' scoped to the user, or as an 'entity' schema (which a blueprint CAN provision, in its own app context).`
11470
+ });
11471
+ });
11472
+ }
11473
+ function lintIssuerContext(value, ctx) {
11474
+ const bp = value;
11475
+ if (!Array.isArray(bp.issuers)) return;
11476
+ bp.issuers.forEach((issuer, i) => {
11477
+ if (issuer.contextId === void 0 || issuer.contextId === bp.contextId) return;
11478
+ ctx.addIssue({
11479
+ code: import_zod.z.ZodIssueCode.custom,
11480
+ path: ["issuers", i, "contextId"],
11481
+ message: `issuer '${issuer.issuerId}' targets app context '${issuer.contextId}', but this blueprint provisions '${bp.contextId}'. An issuer must target the blueprint's own context \u2014 everything else a blueprint creates does, and an issuer that points elsewhere attaches an identity provider to an app context the blueprint never mentions. Set it to '${bp.contextId}', or move this issuer into a blueprint for '${issuer.contextId}'.`
11482
+ });
11483
+ });
11484
+ }
11217
11485
  var BLUEPRINT_FIELD_PHASES = Object.freeze({
11218
11486
  servicePrincipal: "bootstrap",
11219
11487
  issuers: "bootstrap",
11488
+ // Registration requires the platform's provisioning:c capability, which
11489
+ // only the bootstrap credential carries — same reasoning as issuers.
11490
+ namespaces: "bootstrap",
11220
11491
  schemas: "in-context",
11221
11492
  accessProfile: "in-context",
11222
11493
  roles: "in-context",
@@ -11247,7 +11518,16 @@ var BlueprintSchema = import_zod.z.object({
11247
11518
  * service-principal creation — NOT the in-context load (see
11248
11519
  * {@link BlueprintIssuerSchema}).
11249
11520
  */
11250
- issuers: import_zod.z.array(BlueprintIssuerSchema).optional()
11521
+ issuers: import_zod.z.array(BlueprintIssuerSchema).optional(),
11522
+ /**
11523
+ * Optional namespaces to register before anything else needs them — see
11524
+ * {@link BlueprintNamespaceSchema}. Applied in the BOOTSTRAP-token phase,
11525
+ * alongside app-context/service-principal creation and issuers, BEFORE
11526
+ * schemas/seed (an `entityBacked` namespace's entities, and an
11527
+ * `allowedSurfaces: ['entity']` schema, both need the registration to
11528
+ * already exist). `org`/`client` need no entry — they exist already.
11529
+ */
11530
+ namespaces: import_zod.z.array(BlueprintNamespaceSchema).optional()
11251
11531
  }).strict().superRefine((bp, ctx) => {
11252
11532
  lintSelfTokenPlacement(bp, ctx);
11253
11533
  lintIdentityRefs(bp, ctx);
@@ -11255,6 +11535,9 @@ var BlueprintSchema = import_zod.z.object({
11255
11535
  lintNamespacedScopeArrays(bp, ctx);
11256
11536
  lintDataScopeKeys(bp, ctx);
11257
11537
  lintSeedSurfaces(bp, ctx);
11538
+ lintIdentitySurfaces(bp, ctx);
11539
+ lintIssuerContext(bp, ctx);
11540
+ lintNamespaces(bp, ctx);
11258
11541
  });
11259
11542
  function formatIssuePath(path4) {
11260
11543
  let out = "";
@@ -11409,9 +11692,11 @@ function deriveSuffix(contextId) {
11409
11692
  var WHOLE_TOKEN_RE = /^\$\{\{\s*([A-Za-z_]\w*)\.([A-Za-z_]\w*)\s*\}\}$/;
11410
11693
  var TOKEN_SCAN_RE = /\$\$\{\{|\$\{\{([\s\S]*?)\}\}|\$\{\{/g;
11411
11694
  var INNER_REF_RE = /^([A-Za-z_]\w*)\.([A-Za-z_]\w*)$/;
11412
- var DEFERRED_NAMESPACES = /* @__PURE__ */ new Set(["self", "identities", "under"]);
11695
+ var DEFERRED_NAMESPACES = /* @__PURE__ */ new Set(["self", "identities", "under", "member"]);
11413
11696
  var DEFERRED_NS_ALTERNATION = [...DEFERRED_NAMESPACES].join("|");
11414
- var DEFERRED_INNER_RE = new RegExp(`^(?:${DEFERRED_NS_ALTERNATION})(?:\\.[A-Za-z_]\\w*)+$`);
11697
+ var DEFERRED_INNER_RE = new RegExp(
11698
+ `^(?:${DEFERRED_NS_ALTERNATION})(?:\\.[A-Za-z_]\\w*)+(?::[A-Za-z_]\\w*)?$`
11699
+ );
11415
11700
  function resolveRef(ns, name, ctx, path4) {
11416
11701
  if (ns === "inputs") {
11417
11702
  if (!(name in ctx.declared)) {
@@ -11710,9 +11995,11 @@ var taskManagement = {
11710
11995
  // Least-privilege profile — MUST pass the CLI scope gate.
11711
11996
  // r/c/u + search + schema discovery. NOT records:d (least privilege).
11712
11997
  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.
11998
+ allowedActions: ["records:r", "records:c", "records:u", "search:r", "schemas:r"],
11999
+ // dataScope: {} (present, empty) is the deliberate marker for a
12000
+ // tenant-level shared tracker — every task visible to every holder of
12001
+ // this credential. Add a `scope:org` entry instead for per-org isolation.
12002
+ dataScope: {}
11716
12003
  },
11717
12004
  servicePrincipal: {
11718
12005
  externalId: "task-management",
@@ -11735,250 +12022,6 @@ var taskManagement = {
11735
12022
  ]
11736
12023
  };
11737
12024
  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",
11917
- filterable: true,
11918
- enumValues: ["active", "retired"],
11919
- renderHints: { label: "Status", widget: "select", order: 5 }
11920
- },
11921
- {
11922
- fieldId: "discoveredOn",
11923
- fieldType: "date",
11924
- description: "ISO-8601 \u2014 when this gotcha was first hit. Range-queryable.",
11925
- renderHints: { label: "Discovered on", widget: "date", order: 6 }
11926
- }
11927
- // externalId: first-class identifier, not a payload field (see `decision`).
11928
- ],
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 }]
11933
- }
11934
- ],
11935
- // 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.
11939
- accessProfile: {
11940
- allowedActions: ["records:r", "records:c", "records:u", "search:r", "schemas:r", "inference:r"]
11941
- },
11942
- servicePrincipal: {
11943
- externalId: "coding-agent-memory",
11944
- displayName: "Coding Agent \u2014 Project Memory"
11945
- },
11946
- seed: [
11947
- {
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
- surface: "record",
11968
- typeName: "convention",
11969
- externalId: "seed-record-the-why",
11970
- 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"
11977
- }
11978
- }
11979
- ]
11980
- };
11981
- var coding_agent_memory_default = codingAgentMemory;
11982
12025
  var DATA_PLANE_ACTIONS = [
11983
12026
  "records:r",
11984
12027
  "records:c",
@@ -13105,7 +13148,11 @@ var agenticSdlc = {
13105
13148
  // mints for THIS blueprint's service principal (the MCP/API runtime). See
13106
13149
  // DATA_PLANE_ACTIONS above for the action set + rationale.
13107
13150
  accessProfile: {
13108
- allowedActions: DATA_PLANE_ACTIONS
13151
+ allowedActions: DATA_PLANE_ACTIONS,
13152
+ // dataScope: {} (present, empty) is the deliberate marker for tenant-wide
13153
+ // reach — this service key IS the runtime's own access to the whole KB,
13154
+ // not a multi-owner resource to isolate.
13155
+ dataScope: {}
13109
13156
  },
13110
13157
  // A reusable `editor` role for the HUMAN owner — DISTINCT from `accessProfile`
13111
13158
  // (which scopes only the service-principal key). `bootstrap` provisions this
@@ -13125,7 +13172,9 @@ var agenticSdlc = {
13125
13172
  // which includes members' private memory — the trusted-administrator tradeoff,
13126
13173
  // called out in the bundled guide. Scope it down if your deployment wants
13127
13174
  // curator access without private-memory visibility.
13128
- editor: [{ allowedActions: EDITOR_ACTIONS }],
13175
+ // dataScope: {} echoes the "deliberately UNSCOPED (whole-context)" call-out
13176
+ // above as an explicit marker, not just prose — see it for the tradeoff.
13177
+ editor: [{ allowedActions: EDITOR_ACTIONS, dataScope: {} }],
13129
13178
  // A team member (or their agent) — TWO memory tiers as two UNIONed clauses
13130
13179
  // (a record is accessible when ANY clause grants it; within one clause every
13131
13180
  // dataScope dimension must match):
@@ -13145,7 +13194,9 @@ var agenticSdlc = {
13145
13194
  // `vectros join agentic-sdlc --role member`; verify a binding with
13146
13195
  // `vectros access explain --principal me --context agentic-sdlc`.
13147
13196
  member: [
13148
- { allowedActions: MEMBER_SHARED_READ_ACTIONS },
13197
+ // dataScope: {} echoes clause 1's own "UNSCOPED, but safe" call-out above
13198
+ // as an explicit marker, not just prose.
13199
+ { allowedActions: MEMBER_SHARED_READ_ACTIONS, dataScope: {} },
13149
13200
  {
13150
13201
  // `inference:r` is IN this self-scoped clause (not only clause 1) so a
13151
13202
  // member's `rag_ask` can ground on its OWN private memory — the read
@@ -13260,7 +13311,11 @@ var secondBrain = {
13260
13311
  // (so the agent can `rag_ask` a grounded, cited question over your notes). No
13261
13312
  // records:d: notes are archived (status → 'archived'), never deleted.
13262
13313
  accessProfile: {
13263
- allowedActions: ["records:r", "records:c", "records:u", "search:r", "schemas:r", "inference:r"]
13314
+ allowedActions: ["records:r", "records:c", "records:u", "search:r", "schemas:r", "inference:r"],
13315
+ // dataScope: {} (present, empty) is the deliberate marker for tenant-wide
13316
+ // reach — this is a single personal-knowledge-base credential with one
13317
+ // holder, not a multi-owner shared resource to isolate.
13318
+ dataScope: {}
13264
13319
  },
13265
13320
  servicePrincipal: {
13266
13321
  externalId: "second-brain",
@@ -13392,7 +13447,11 @@ var clinicalIntake = {
13392
13447
  // reveal qualifier (the demo key literally cannot un-redact the PHI fields),
13393
13448
  // and NO inference:r (we never point a RAG capability at a PHI corpus here).
13394
13449
  accessProfile: {
13395
- allowedActions: ["records:r", "records:c", "records:u", "search:r", "schemas:r"]
13450
+ allowedActions: ["records:r", "records:c", "records:u", "search:r", "schemas:r"],
13451
+ // dataScope: {} (present, empty) is the deliberate marker for tenant-wide
13452
+ // reach, unchanged from this demo's original intent — a real clinical
13453
+ // deployment should scope this (e.g. `scope:org`) before handling real PHI.
13454
+ dataScope: {}
13396
13455
  },
13397
13456
  servicePrincipal: {
13398
13457
  externalId: "clinical-intake",
@@ -13421,7 +13480,6 @@ var clinicalIntake = {
13421
13480
  var clinical_intake_default = clinicalIntake;
13422
13481
  var BUNDLED_BLUEPRINTS = [
13423
13482
  task_management_default,
13424
- coding_agent_memory_default,
13425
13483
  agentic_sdlc_default,
13426
13484
  second_brain_default,
13427
13485
  clinical_intake_default
@@ -13720,18 +13778,17 @@ async function mintBridgeToken(opts) {
13720
13778
  ttlSeconds = DEFAULT_BRIDGE_TTL_SECONDS,
13721
13779
  tenant = "live",
13722
13780
  useScopedToken = false,
13781
+ contextId,
13723
13782
  fetchImpl = fetch
13724
13783
  } = opts;
13725
13784
  const base = apiBaseUrl.replace(/\/$/, "");
13726
13785
  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
- );
13786
+ let url = `${base}${path4}?ttl=${encodeURIComponent(String(ttlSeconds))}&tenant=${encodeURIComponent(tenant)}`;
13787
+ if (contextId) url += `&context=${encodeURIComponent(contextId)}`;
13788
+ return fetchStToken(url, cognitoToken, fetchImpl, path4, (status) => {
13789
+ if (status !== 404 || useScopedToken || contextId) return "";
13790
+ return ` (the dedicated ${CLI_BOOTSTRAP_TOKEN_PATH} endpoint may not be deployed yet \u2014 retry with --use-scoped-token for the fallback path)`;
13791
+ });
13735
13792
  }
13736
13793
  async function mintContextToken(opts) {
13737
13794
  const {
@@ -14114,8 +14171,29 @@ var KNOWN_CONTROL_PLANE_RESOURCES = [
14114
14171
  // control-plane, unlike per-namespace entity CRUD. (Cosmetic: any
14115
14172
  // non-data-plane resource hard-rejects whether or not it is listed here;
14116
14173
  // this list only sharpens the error message.)
14174
+ //
14175
+ // ⚠️ This bars `namespaces` as a SCOPE — no partner-authored role can ever
14176
+ // be granted it. It does NOT bar a blueprint's own top-level `namespaces[]`
14177
+ // FORMAT field (@vectros-ai/blueprints), which deliberately DOES write this
14178
+ // same registry — the same "DISCRETIONARY vs UNCONDITIONAL" distinction
14179
+ // {@link PARTIALLY_ADMITTED_OPS} draws for `entities`, applied here: the
14180
+ // write rides `provisioning:c` (non-grantable — no partner role can ever
14181
+ // hold it, the same bearer `keys`/`app-contexts` writes already require)
14182
+ // and the platform confines it to the blueprint's OWN context
14183
+ // unconditionally, never tenant-wide, so the reach is bounded to what the
14184
+ // blueprint already provisions, never the pack's say-so alone. See
14185
+ // `@vectros-ai/blueprints`' `namespaces[]` doc for the full design.
14117
14186
  "namespaces"
14118
14187
  ];
14188
+ var PARTIALLY_ADMITTED_OPS = /* @__PURE__ */ new Map([
14189
+ ["profiles", /* @__PURE__ */ new Set(["r", "u", "d"])]
14190
+ ]);
14191
+ var SELF_QUALIFIED_OPS = /* @__PURE__ */ new Map([
14192
+ ["profiles", /* @__PURE__ */ new Set(["c"])]
14193
+ ]);
14194
+ var UNQUALIFIABLE_WITH_ANY_TAIL = /* @__PURE__ */ new Map([
14195
+ ["profiles", /* @__PURE__ */ new Set(["r"])]
14196
+ ]);
14119
14197
  var VALID_OPS = /* @__PURE__ */ new Set(["c", "r", "u", "d"]);
14120
14198
  var DATA_PLANE_SET = new Set(DATA_PLANE_ALLOWLIST);
14121
14199
  var CONTROL_PLANE_SET = new Set(KNOWN_CONTROL_PLANE_RESOURCES);
@@ -14131,7 +14209,60 @@ ${summary}`
14131
14209
  this.name = "ScopeGateError";
14132
14210
  this.rejected = rejected;
14133
14211
  }
14134
- };
14212
+ };
14213
+ var GRANTED_CAPABILITIES_ALLOWLIST = ["member-lifecycle"];
14214
+ var GRANTED_CAPABILITIES_SET = new Set(GRANTED_CAPABILITIES_ALLOWLIST);
14215
+ var CAPABILITIES_HINT = GRANTED_CAPABILITIES_ALLOWLIST.join("/");
14216
+ var CapabilityGateError = class extends Error {
14217
+ rejected;
14218
+ constructor(rejected) {
14219
+ const summary = rejected.map((r) => ` \u2022 ${r.action}: ${r.reason}`).join("\n");
14220
+ super(
14221
+ `Capability gate rejected ${rejected.length} requested capability(ies); the bootstrap loader grants (${CAPABILITIES_HINT}) only and mints nothing on reject:
14222
+ ${summary}`
14223
+ );
14224
+ this.name = "CapabilityGateError";
14225
+ this.rejected = rejected;
14226
+ }
14227
+ };
14228
+ function evaluateCapabilityToken(name) {
14229
+ const token = name.trim();
14230
+ if (token.length === 0) {
14231
+ return { action: name, reason: "empty capability name" };
14232
+ }
14233
+ if (!GRANTED_CAPABILITIES_SET.has(token)) {
14234
+ return {
14235
+ action: name,
14236
+ 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`
14237
+ };
14238
+ }
14239
+ return null;
14240
+ }
14241
+ function evaluateCapabilityGate(capabilities) {
14242
+ const accepted = [];
14243
+ const rejected = [];
14244
+ for (const name of capabilities ?? []) {
14245
+ const rejection = evaluateCapabilityToken(name);
14246
+ if (rejection) {
14247
+ rejected.push(rejection);
14248
+ } else {
14249
+ accepted.push(name);
14250
+ }
14251
+ }
14252
+ return { ok: rejected.length === 0, accepted, rejected };
14253
+ }
14254
+ function assertCapabilityGate(capabilities) {
14255
+ const result = evaluateCapabilityGate(capabilities);
14256
+ if (!result.ok) {
14257
+ throw new CapabilityGateError(result.rejected);
14258
+ }
14259
+ }
14260
+ function firstInvalidOp(ops) {
14261
+ for (const ch of ops) {
14262
+ if (!VALID_OPS.has(ch)) return ch;
14263
+ }
14264
+ return void 0;
14265
+ }
14135
14266
  function evaluateScopeToken(action) {
14136
14267
  const token = action.trim();
14137
14268
  if (token.length === 0) {
@@ -14153,6 +14284,48 @@ function evaluateScopeToken(action) {
14153
14284
  };
14154
14285
  }
14155
14286
  if (!DATA_PLANE_SET.has(resource)) {
14287
+ const admittedOps = PARTIALLY_ADMITTED_OPS.get(resource);
14288
+ const selfOnlyOps = SELF_QUALIFIED_OPS.get(resource);
14289
+ if (admittedOps || selfOnlyOps) {
14290
+ const admittedHint = [.../* @__PURE__ */ new Set([...admittedOps ?? [], ...selfOnlyOps ?? []])].sort().join("/");
14291
+ if (ops === "*") {
14292
+ return {
14293
+ action,
14294
+ 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`
14295
+ };
14296
+ }
14297
+ const invalidOp2 = firstInvalidOp(ops);
14298
+ if (invalidOp2) {
14299
+ return {
14300
+ action,
14301
+ reason: `invalid operation character '${invalidOp2}' (valid operations are c, r, u, d, or '*')`
14302
+ };
14303
+ }
14304
+ const qualifier = parts.length > 2 ? parts.slice(2).join(":") : void 0;
14305
+ const isSelfQualified = qualifier === "self";
14306
+ const unqualifiableOps = UNQUALIFIABLE_WITH_ANY_TAIL.get(resource);
14307
+ for (const ch of ops) {
14308
+ if (qualifier !== void 0 && unqualifiableOps?.has(ch)) {
14309
+ return {
14310
+ action,
14311
+ 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).`
14312
+ };
14313
+ }
14314
+ if (admittedOps?.has(ch)) continue;
14315
+ if (selfOnlyOps?.has(ch)) {
14316
+ if (isSelfQualified) continue;
14317
+ return {
14318
+ action,
14319
+ reason: `requests control-plane scope '${resource}:${ch}' without the required ':self' qualifier. A bare or arbitrarily-qualified '${resource}:${ch}' is outside the narrow allowlist (${admittedHint}) admitted for this resource \u2014 only '${resource}:${ch}:self' is. That operation must otherwise be done deliberately in the developer portal, never via the bootstrap loader`
14320
+ };
14321
+ }
14322
+ return {
14323
+ action,
14324
+ 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`
14325
+ };
14326
+ }
14327
+ return null;
14328
+ }
14156
14329
  if (CONTROL_PLANE_SET.has(resource)) {
14157
14330
  return {
14158
14331
  action,
@@ -14167,13 +14340,12 @@ function evaluateScopeToken(action) {
14167
14340
  if (ops === "*") {
14168
14341
  return null;
14169
14342
  }
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
- }
14343
+ const invalidOp = firstInvalidOp(ops);
14344
+ if (invalidOp) {
14345
+ return {
14346
+ action,
14347
+ reason: `invalid operation character '${invalidOp}' (valid operations are c, r, u, d, or '*')`
14348
+ };
14177
14349
  }
14178
14350
  return null;
14179
14351
  }
@@ -14196,6 +14368,16 @@ function assertScopeGate(allowedActions) {
14196
14368
  throw new ScopeGateError(result.rejected);
14197
14369
  }
14198
14370
  }
14371
+ function assertBlueprintGates(blueprint) {
14372
+ assertScopeGate(blueprint.accessProfile.allowedActions);
14373
+ assertCapabilityGate(blueprint.accessProfile.capabilities);
14374
+ for (const clauses of Object.values(blueprint.roles ?? {})) {
14375
+ for (const clause of clauses) {
14376
+ assertScopeGate(clause.allowedActions);
14377
+ assertCapabilityGate(clause.capabilities);
14378
+ }
14379
+ }
14380
+ }
14199
14381
 
14200
14382
  // src/machine-id.ts
14201
14383
  var import_node_fs = require("fs");
@@ -14300,6 +14482,68 @@ async function resolveApplyTimeIdentities(blueprint, client, log) {
14300
14482
  function actionOf(created) {
14301
14483
  return created ? "created" : "reconciled";
14302
14484
  }
14485
+ async function registerNamespaceIdempotently(client, contextIdForRequest, decl) {
14486
+ const body = {
14487
+ namespace: decl.namespace,
14488
+ entityBacked: decl.entityBacked,
14489
+ specificityRank: decl.specificityRank,
14490
+ membershipRecordType: decl.membershipRecordType,
14491
+ membershipTargetField: decl.membershipTargetField,
14492
+ membershipLevelField: decl.membershipLevelField,
14493
+ membershipLevels: decl.membershipLevels
14494
+ };
14495
+ try {
14496
+ await client.registerNamespace({ contextId: contextIdForRequest, body });
14497
+ return "created";
14498
+ } catch (err) {
14499
+ const info = extractServerError(err);
14500
+ if (info.statusCode !== 400) throw err;
14501
+ let existing;
14502
+ try {
14503
+ existing = await client.getNamespace({ namespace: decl.namespace, contextId: contextIdForRequest });
14504
+ } catch {
14505
+ throw err;
14506
+ }
14507
+ if (existing.contextId !== contextIdForRequest) throw err;
14508
+ const mismatch = describeNamespaceMismatch(decl, existing);
14509
+ if (mismatch) {
14510
+ throw new Error(
14511
+ `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.`
14512
+ );
14513
+ }
14514
+ return "reconciled";
14515
+ }
14516
+ }
14517
+ function describeNamespaceMismatch(decl, existing) {
14518
+ const mismatches = [];
14519
+ const declaredEntityBacked = decl.entityBacked ?? false;
14520
+ const existingEntityBacked = existing.entityBacked ?? false;
14521
+ if (declaredEntityBacked !== existingEntityBacked) {
14522
+ mismatches.push(
14523
+ `entityBacked: declared ${JSON.stringify(declaredEntityBacked)}, registered ${JSON.stringify(existingEntityBacked)}`
14524
+ );
14525
+ }
14526
+ const fields = [
14527
+ "specificityRank",
14528
+ "membershipRecordType",
14529
+ "membershipTargetField",
14530
+ "membershipLevelField"
14531
+ ];
14532
+ for (const field of fields) {
14533
+ const declared = decl[field];
14534
+ const existingValue = existing[field];
14535
+ if (declared === void 0 && (existingValue === void 0 || existingValue === null)) continue;
14536
+ if (declared === existingValue) continue;
14537
+ mismatches.push(`${field}: declared ${JSON.stringify(declared)}, registered ${JSON.stringify(existingValue)}`);
14538
+ }
14539
+ const declaredLevels = decl.membershipLevels;
14540
+ const existingLevels = existing.membershipLevels;
14541
+ const levelsDiffer = declaredLevels === void 0 ? existingLevels != null && existingLevels.length > 0 : JSON.stringify([...declaredLevels].sort()) !== JSON.stringify([...existingLevels ?? []].sort());
14542
+ if (levelsDiffer) {
14543
+ mismatches.push(`membershipLevels: declared ${JSON.stringify(declaredLevels)}, registered ${JSON.stringify(existingLevels)}`);
14544
+ }
14545
+ return mismatches.length > 0 ? mismatches.join("; ") : null;
14546
+ }
14303
14547
  var LostKeyError = class extends Error {
14304
14548
  constructor(keyName) {
14305
14549
  super(
@@ -14310,6 +14554,20 @@ var LostKeyError = class extends Error {
14310
14554
  }
14311
14555
  keyName;
14312
14556
  };
14557
+ var ExistingPrincipalNotConfirmedError = class extends Error {
14558
+ constructor(contextId, userId, externalId) {
14559
+ super(
14560
+ `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.`
14561
+ );
14562
+ this.contextId = contextId;
14563
+ this.userId = userId;
14564
+ this.externalId = externalId;
14565
+ this.name = "ExistingPrincipalNotConfirmedError";
14566
+ }
14567
+ contextId;
14568
+ userId;
14569
+ externalId;
14570
+ };
14313
14571
  function mapEnumValues(enumValues) {
14314
14572
  if (!enumValues) return void 0;
14315
14573
  return enumValues.map((value) => ({ value }));
@@ -14384,7 +14642,8 @@ function toCapabilities(capabilities) {
14384
14642
  function toRoleScopeClauses(clauses) {
14385
14643
  return clauses.map((c) => ({
14386
14644
  allowed_actions: c.allowedActions,
14387
- data_scope: c.dataScope
14645
+ data_scope: c.dataScope,
14646
+ granted_capabilities: c.capabilities
14388
14647
  }));
14389
14648
  }
14390
14649
  var UnresolvedIdentitiesError = class extends Error {
@@ -14404,11 +14663,18 @@ function assertNoUnresolvedIdentities(blueprint) {
14404
14663
  }
14405
14664
  }
14406
14665
  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);
14411
- }
14666
+ const {
14667
+ blueprint,
14668
+ tenantId,
14669
+ machineId,
14670
+ rotate = false,
14671
+ skipSeed = false,
14672
+ readExistingKey,
14673
+ persistMintedKey,
14674
+ confirmExistingPrincipal,
14675
+ log
14676
+ } = input;
14677
+ assertBlueprintGates(blueprint);
14412
14678
  assertNoUnresolvedIdentities(blueprint);
14413
14679
  const ctxResp = await client.auth.createAppContext({ body: { contextId: blueprint.contextId, name: contextNameOf(blueprint) } });
14414
14680
  const contextAction = actionOf(ctxResp.created);
@@ -14426,10 +14692,42 @@ async function runLoader(client, input) {
14426
14692
  if (!userId) {
14427
14693
  throw new Error("Service-principal create returned no user id; cannot bind AccessProfile/key.");
14428
14694
  }
14429
- log.debug({ component: "loader", externalId: blueprint.servicePrincipal.externalId, userId }, "service principal ensured");
14695
+ const userAction = actionOf(user.created);
14696
+ log.debug(
14697
+ { component: "loader", externalId: blueprint.servicePrincipal.externalId, userId, userAction },
14698
+ "service principal ensured"
14699
+ );
14700
+ const localKeyForContext = await readExistingKey(blueprint.contextId);
14701
+ const storedPrincipalId = localKeyForContext?.principalId;
14702
+ const localKeyNamesThisPrincipal = storedPrincipalId === `usr_${userId}` || storedPrincipalId === userId;
14703
+ if (userAction === "reconciled" && !localKeyNamesThisPrincipal) {
14704
+ const proceed = confirmExistingPrincipal ? await confirmExistingPrincipal({
14705
+ contextId: blueprint.contextId,
14706
+ userId,
14707
+ externalId: blueprint.servicePrincipal.externalId
14708
+ }) : true;
14709
+ if (!proceed) {
14710
+ throw new ExistingPrincipalNotConfirmedError(blueprint.contextId, userId, blueprint.servicePrincipal.externalId);
14711
+ }
14712
+ log.debug(
14713
+ { component: "loader", contextId: blueprint.contextId, userId },
14714
+ "existing-principal collision confirmed (or unwired) \u2014 proceeding with any further write to this principal"
14715
+ );
14716
+ }
14717
+ const needsContextPinnedBootstrap = (blueprint.issuers ?? []).length > 0 || (blueprint.namespaces ?? []).length > 0;
14718
+ const ctxBootstrapClient = needsContextPinnedBootstrap ? input.remintBootstrapForContext ? await input.remintBootstrapForContext(blueprint.contextId) : client : void 0;
14719
+ if (ctxBootstrapClient && input.remintBootstrapForContext) {
14720
+ log.debug({ component: "loader", contextId: blueprint.contextId }, "re-minted context-pinned bootstrap client");
14721
+ }
14722
+ const namespaceActions = [];
14723
+ for (const ns of blueprint.namespaces ?? []) {
14724
+ const action = await registerNamespaceIdempotently(ctxBootstrapClient.identity, blueprint.contextId, ns);
14725
+ namespaceActions.push({ namespace: ns.namespace, action });
14726
+ log.debug({ component: "loader", namespace: ns.namespace, contextId: blueprint.contextId, action }, "namespace ensured");
14727
+ }
14430
14728
  const issuerActions = [];
14431
14729
  for (const issuer of blueprint.issuers ?? []) {
14432
- const issuerResp = await client.auth.registerIssuer({
14730
+ const issuerResp = await ctxBootstrapClient.auth.registerIssuer({
14433
14731
  issuerId: issuer.issuerId,
14434
14732
  issuer: issuer.issuer,
14435
14733
  jwksUri: issuer.jwksUri,
@@ -14499,7 +14797,10 @@ async function runLoader(client, input) {
14499
14797
  // literal JSON null, which is exactly what the platform's scope
14500
14798
  // matcher tests via `allowedValues.contains(null)`. The cast bridges
14501
14799
  // to the SDK's loosely-typed Record<string, Record<string,unknown>>.
14502
- data_scope: blueprint.accessProfile.dataScope
14800
+ data_scope: blueprint.accessProfile.dataScope,
14801
+ // Already gate-validated (assertBlueprintGates, above this function's
14802
+ // call site) before this body is ever built.
14803
+ granted_capabilities: blueprint.accessProfile.capabilities
14503
14804
  }
14504
14805
  ],
14505
14806
  // Optional identity overrides — scope values the service key STAMPS onto
@@ -14557,7 +14858,7 @@ async function runLoader(client, input) {
14557
14858
  keyId = keyResp.keyId ?? "";
14558
14859
  rawKey = keyResp.rawKey;
14559
14860
  if (keyId) {
14560
- await persistMintedKey(blueprint.contextId, { keyId, rawKey, keyName, principalId: userId, revokedKeyId });
14861
+ await persistMintedKey(blueprint.contextId, { keyId, rawKey, keyName, principalId: `usr_${userId}`, revokedKeyId });
14561
14862
  }
14562
14863
  if (keyAction !== "rotated") keyAction = "created";
14563
14864
  } else {
@@ -14631,10 +14932,12 @@ async function runLoader(client, input) {
14631
14932
  keyName,
14632
14933
  keyAction,
14633
14934
  contextAction,
14935
+ userAction,
14634
14936
  profileAction,
14635
14937
  schemaActions,
14636
14938
  roleActions,
14637
14939
  issuerActions,
14940
+ namespaceActions,
14638
14941
  rawKey,
14639
14942
  keyId,
14640
14943
  revokedKeyId,
@@ -14653,6 +14956,15 @@ function buildPlan(blueprint, opts = {}) {
14653
14956
  kind: "principal",
14654
14957
  summary: `principal '${blueprint.servicePrincipal.externalId}' (SERVICE)`
14655
14958
  });
14959
+ for (const ns of blueprint.namespaces ?? []) {
14960
+ const ownerNote = `owned by '${blueprint.contextId}'`;
14961
+ const entityNote = ns.entityBacked ? ", entity-backed" : "";
14962
+ const membershipNote = ns.membershipRecordType ? `, membership: ${ns.membershipRecordType}.${ns.membershipTargetField}` + (ns.membershipLevelField ? ` (levels: ${ns.membershipLevels?.join("/")})` : "") : "";
14963
+ steps.push({
14964
+ kind: "namespace",
14965
+ summary: `namespace '${ns.namespace}' (${ownerNote}${entityNote}${membershipNote}) [${BLUEPRINT_FIELD_PHASES.namespaces}-token phase]`
14966
+ });
14967
+ }
14656
14968
  for (const iss of blueprint.issuers ?? []) {
14657
14969
  const selfSignup = iss.selfSignupPolicies?.length ? `, self-signup: ${iss.selfSignupPolicies.length} polic${iss.selfSignupPolicies.length === 1 ? "y" : "ies"}` : "";
14658
14970
  steps.push({
@@ -14675,15 +14987,18 @@ function buildPlan(blueprint, opts = {}) {
14675
14987
  }
14676
14988
  const actions = blueprint.accessProfile.allowedActions;
14677
14989
  const scopeNote = blueprint.accessProfile.dataScope && Object.keys(blueprint.accessProfile.dataScope).length ? ` dataScope: ${Object.keys(blueprint.accessProfile.dataScope).join(", ")}` : "";
14990
+ const capabilityNote = blueprint.accessProfile.capabilities?.length ? ` capabilities: [${blueprint.accessProfile.capabilities.join(", ")}]` : "";
14678
14991
  steps.push({
14679
14992
  kind: "profile",
14680
- summary: `profile scopes [${actions.join(", ")}] (data-plane \u2713)${scopeNote}`
14993
+ summary: `profile scopes [${actions.join(", ")}] (data-plane \u2713)${scopeNote}${capabilityNote}`
14681
14994
  });
14682
14995
  for (const [roleId, clauses] of Object.entries(blueprint.roles ?? {})) {
14683
14996
  const actionSet = [...new Set(clauses.flatMap((c) => c.allowedActions))];
14997
+ const capSet = [...new Set(clauses.flatMap((c) => c.capabilities ?? []))];
14998
+ const roleCapabilityNote = capSet.length ? `, capabilities [${capSet.join(", ")}]` : "";
14684
14999
  steps.push({
14685
15000
  kind: "role",
14686
- summary: `role '${roleId}' (${clauses.length} clause${clauses.length === 1 ? "" : "s"}, actions [${actionSet.join(", ")}])`
15001
+ summary: `role '${roleId}' (${clauses.length} clause${clauses.length === 1 ? "" : "s"}, actions [${actionSet.join(", ")}]${roleCapabilityNote})`
14687
15002
  });
14688
15003
  }
14689
15004
  steps.push({ kind: "key", summary: `key ${blueprint.name}-<machine> (mint 1 scoped ssk_*)` });
@@ -14710,6 +15025,13 @@ function renderPlan(blueprint, steps) {
14710
15025
  ` issuer is left unchanged, not reconciled to match.`
14711
15026
  );
14712
15027
  }
15028
+ if (steps.some((s) => s.kind === "namespace")) {
15029
+ legendLines.push(
15030
+ ` Exception: namespace registration is not idempotent \u2014 a re-apply matching`,
15031
+ ` an existing registration converges silently, but one that disagrees FAILS`,
15032
+ ` the apply rather than reconciling it.`
15033
+ );
15034
+ }
14713
15035
  const legend = legendLines.join("\n");
14714
15036
  const lines = steps.map((s) => ` + ${s.summary}`);
14715
15037
  return [header, legend, "", ...lines].join("\n");
@@ -14958,6 +15280,15 @@ var OsKeychainSecretStore = class {
14958
15280
  * against the real Windows store on every MR — the dev-machine probe is a point
14959
15281
  * measurement, not a regression gate. (Tracked internally; this file ships to
14960
15282
  * public npm, so no tracker number here.)
15283
+ *
15284
+ * ⚠️ THIS METHOD'S FAIL-CLOSED GUARANTEE IS AN UPSTREAM BEHAVIOURAL CONTRACT, NOT
15285
+ * SOMETHING THIS CODE ENFORCES. `@napi-rs/keyring` is pinned to an EXACT version
15286
+ * in `package.json` for exactly this reason: if a future release returned `[]`
15287
+ * from `findCredentials()` on a locked store instead of throwing, the `catch`
15288
+ * block above would never run — `!.some(...)` on an empty array reads as "gone",
15289
+ * `deleteSecret` returns `true`, and `remove`/`prune` drop the index row of a
15290
+ * still-live credential. A version bump of this dependency is a security review
15291
+ * of the paragraph above against the new release, not a routine dependency bump.
14961
15292
  */
14962
15293
  async deleteSecret(ref) {
14963
15294
  try {
@@ -15891,6 +16222,26 @@ The key itself WAS created server-side and is live \u2014 its id is ${entry.keyI
15891
16222
  const entry = await this.getActive();
15892
16223
  return entry ? this.readSecret(entry) : void 0;
15893
16224
  }
16225
+ /**
16226
+ * The ACTIVE entry's tenant (test/live), or undefined if there is no active
16227
+ * entry or its tenant cannot be determined — WITHOUT migrating its secret.
16228
+ *
16229
+ * Deliberately NOT built on {@link resolveActiveSecret}/{@link readSecret}:
16230
+ * both migrate the secret to this machine's preferred store as a side effect
16231
+ * of reading it, and a caller that only wants to CLASSIFY the active identity
16232
+ * (e.g. "is it safe to activate a different one over it?") must not silently
16233
+ * relocate — and take the cross-process index lock for — a credential it has
16234
+ * no other reason to touch. Same non-migrating shape {@link assess} already
16235
+ * uses per entry (`store.getSecret` directly, never `readSecret`).
16236
+ */
16237
+ async activeTenant() {
16238
+ const entry = await this.getActive();
16239
+ if (!entry) return void 0;
16240
+ const store = this.storeFor(entry);
16241
+ if (!store) return void 0;
16242
+ const secret = await store.getSecret(entry.secretRef);
16243
+ return secret !== void 0 ? keyTenant(secret) : void 0;
16244
+ }
15894
16245
  /**
15895
16246
  * Read one entry's secret, moving it to this machine's preferred store if it is
15896
16247
  * somewhere else.
@@ -16017,26 +16368,31 @@ var readline = __toESM(require("readline/promises"));
16017
16368
  async function runKeyringList(opts, deps = {}) {
16018
16369
  try {
16019
16370
  const keyring = deps.keyring ?? await Keyring.open();
16020
- const entries = await keyring.list();
16021
- const active = await keyring.activeAlias();
16022
16371
  if (opts.json) {
16372
+ const entries = await keyring.list();
16373
+ const active = await keyring.activeAlias();
16023
16374
  process.stdout.write(`${formatJson({ active, entries })}
16024
16375
  `);
16025
16376
  return 0;
16026
16377
  }
16027
- if (entries.length === 0) {
16378
+ const { health } = await assessEntries(keyring);
16379
+ if (health.length === 0) {
16028
16380
  process.stdout.write("\n (no keyring entries yet \u2014 run `vectros join <context>` to add one)\n");
16029
16381
  return 0;
16030
16382
  }
16031
16383
  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 }
16384
+ "\n" + formatTable(health, [
16385
+ { header: "", get: (h) => h.active ? "*" : "" },
16386
+ { header: "alias", get: (h) => h.entry.alias },
16387
+ { header: "context", get: (h) => h.entry.context },
16388
+ { header: "principal", get: (h) => h.entry.principalId },
16389
+ { header: "role", get: (h) => h.entry.role },
16390
+ // `tenant` (test/live) replaces the old `env` (staging/production) column
16391
+ // here: env is an internal-team concern an external developer's CLI never
16392
+ // sees, while tenant is the distinction every user routinely holds two
16393
+ // credentials side by side over.
16394
+ { header: "tenant", get: (h) => tenantCell(h, { upper: true }) },
16395
+ { header: "key id", get: (h) => h.entry.keyId }
16040
16396
  ]) + "\n"
16041
16397
  );
16042
16398
  return 0;
@@ -16218,6 +16574,10 @@ async function assessEntries(keyring) {
16218
16574
  }))
16219
16575
  };
16220
16576
  }
16577
+ function tenantCell(h, opts) {
16578
+ if (h.status !== "ok") return void 0;
16579
+ return h.tenant ?? (opts.upper ? "UNKNOWN" : "unknown");
16580
+ }
16221
16581
  function describeEntry(h) {
16222
16582
  return `${h.entry.alias} (${h.entry.context}, ${h.entry.keyId})`;
16223
16583
  }
@@ -16362,9 +16722,7 @@ async function runKeyringDoctor(opts, deps = {}) {
16362
16722
  // `unknown` can escalate; one that sees a confident wrong answer cannot.
16363
16723
  store: storeKindOf(h.entry) ?? "unknown",
16364
16724
  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
16725
+ tenant: tenantCell(h, { upper: false })
16368
16726
  })),
16369
16727
  liveKeys: liveKeys.map((h) => h.entry.alias),
16370
16728
  findings
@@ -16379,7 +16737,7 @@ async function runKeyringDoctor(opts, deps = {}) {
16379
16737
  { header: "", get: (h) => h.active ? "*" : "" },
16380
16738
  { header: "alias", get: (h) => h.entry.alias },
16381
16739
  { header: "context", get: (h) => h.entry.context },
16382
- { header: "tenant", get: (h) => h.status === "ok" ? h.tenant ?? "UNKNOWN" : void 0 },
16740
+ { header: "tenant", get: (h) => tenantCell(h, { upper: true }) },
16383
16741
  { header: "key id", get: (h) => h.entry.keyId },
16384
16742
  { header: "secret", get: (h) => h.status === "ok" ? "ok" : h.status.toUpperCase() }
16385
16743
  ]) + "\n"
@@ -16573,6 +16931,47 @@ function keyringAliasFor(alias, tenant) {
16573
16931
  }
16574
16932
  return tenant === "test" ? `${alias}.test` : alias;
16575
16933
  }
16934
+ function resolveActivateDecision(opts) {
16935
+ if (opts.explicitActivate !== void 0) return opts.explicitActivate;
16936
+ if (opts.tenant === "live") return true;
16937
+ return opts.currentActiveTenant !== "live";
16938
+ }
16939
+ async function determineActivateDecision(keyring, opts) {
16940
+ if (opts.explicitActivate !== void 0 || opts.tenant === "live") {
16941
+ return resolveActivateDecision(opts);
16942
+ }
16943
+ let currentActiveTenant;
16944
+ try {
16945
+ if (opts.hint && opts.hint.alias === await keyring.activeAlias()) {
16946
+ currentActiveTenant = keyTenant(opts.hint.secret);
16947
+ } else {
16948
+ currentActiveTenant = await keyring.activeTenant();
16949
+ }
16950
+ } catch {
16951
+ }
16952
+ return resolveActivateDecision({ ...opts, currentActiveTenant });
16953
+ }
16954
+ function tenantActivateMessage(opts) {
16955
+ if (opts.willActivate) {
16956
+ if (opts.tenant !== "test") return "";
16957
+ return `
16958
+ \u26A0 '${opts.alias}' is now your ACTIVE identity \u2014 a TEST tenant credential. Anything
16959
+ resolving a key with no explicit --alias (including the MCP server) follows it.
16960
+ `;
16961
+ }
16962
+ if (opts.explicitlyDeclined) {
16963
+ return `
16964
+ \u26A0 Stored '${opts.alias}' (${opts.tenant} tenant) without activating it \u2014 you passed --no-activate.
16965
+ Run \`vectros switch ${opts.alias}\` when you're ready to use it.
16966
+ `;
16967
+ }
16968
+ return `
16969
+ \u26A0 Stored '${opts.alias}' (test tenant) WITHOUT activating it \u2014 your currently-active
16970
+ identity is a LIVE credential, and switching it out from under you silently is
16971
+ exactly the mistake this check exists to prevent. Run \`vectros switch ${opts.alias}\`
16972
+ to use the test key, or re-run with --activate to do so automatically.
16973
+ `;
16974
+ }
16576
16975
  function nowIso() {
16577
16976
  return (/* @__PURE__ */ new Date()).toISOString();
16578
16977
  }
@@ -16669,10 +17068,7 @@ async function storeMintedKey(keyring, binding, secret, opts = {}) {
16669
17068
  // src/cli-bootstrap.ts
16670
17069
  function validateBlueprint(input) {
16671
17070
  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
- }
17071
+ assertBlueprintGates(blueprint);
16676
17072
  return blueprint;
16677
17073
  }
16678
17074
  var BLANK_SLATE = {
@@ -16694,6 +17090,7 @@ function parseBootstrapArgs(argv) {
16694
17090
  yes: false,
16695
17091
  noMerge: false,
16696
17092
  rotate: false,
17093
+ confirmExistingPrincipal: false,
16697
17094
  useScopedToken: false,
16698
17095
  blankSlate: false,
16699
17096
  noSeed: false,
@@ -16745,6 +17142,9 @@ function parseBootstrapArgs(argv) {
16745
17142
  case "--rotate":
16746
17143
  args.rotate = true;
16747
17144
  break;
17145
+ case "--confirm-existing-principal":
17146
+ args.confirmExistingPrincipal = true;
17147
+ break;
16748
17148
  case "--use-scoped-token":
16749
17149
  args.useScopedToken = true;
16750
17150
  break;
@@ -16766,6 +17166,12 @@ function parseBootstrapArgs(argv) {
16766
17166
  case "--test":
16767
17167
  args.tenant = "test";
16768
17168
  break;
17169
+ case "--activate":
17170
+ args.activate = true;
17171
+ break;
17172
+ case "--no-activate":
17173
+ args.activate = false;
17174
+ break;
16769
17175
  case "--help":
16770
17176
  case "-h":
16771
17177
  args.help = true;
@@ -16783,6 +17189,26 @@ function seedDeclined(answer) {
16783
17189
  const a = answer.trim().toLowerCase();
16784
17190
  return a === "n" || a === "no";
16785
17191
  }
17192
+ function existingPrincipalConfirmed(answer) {
17193
+ const a = answer.trim().toLowerCase();
17194
+ return a === "y" || a === "yes";
17195
+ }
17196
+ function buildConfirmExistingPrincipal(args, prompt, isTty) {
17197
+ return async (details) => {
17198
+ if (!args.yes && isTty) {
17199
+ const ans = await prompt(
17200
+ `
17201
+ \u26A0 '${details.externalId}' already exists in your tenant (context '${details.contextId}') \u2014 this key will bind to that EXISTING identity, not a new one.
17202
+ Expected if you're re-applying a blueprint from a new machine, or setting up a second
17203
+ credential for an integration you already own. If you don't recognize this identity,
17204
+ do NOT proceed \u2014 this may be a name collision with something you don't control.
17205
+ Proceed and bind to the existing identity? [y/N]: `
17206
+ );
17207
+ return existingPrincipalConfirmed(ans);
17208
+ }
17209
+ return args.confirmExistingPrincipal;
17210
+ };
17211
+ }
16786
17212
  function seedSummaryLine(opts) {
16787
17213
  if (opts.noSeed) {
16788
17214
  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";
@@ -16819,16 +17245,28 @@ function issuerTally(items) {
16819
17245
  if (unchanged) seg.push(`${unchanged} already registered \u2014 unchanged (issuers cannot be updated once registered)`);
16820
17246
  return `${items.length} issuer${items.length === 1 ? "" : "s"} (${seg.join(", ")})`;
16821
17247
  }
17248
+ function namespaceTally(items) {
17249
+ if (items.length === 0) return null;
17250
+ const created = items.filter((i) => i.action === "created").length;
17251
+ const matched = items.length - created;
17252
+ const seg = [];
17253
+ if (created) seg.push(`${created} created`);
17254
+ if (matched) seg.push(`${matched} already registered \u2014 confirmed matching`);
17255
+ return `${items.length} namespace${items.length === 1 ? "" : "s"} (${seg.join(", ")})`;
17256
+ }
16822
17257
  function applyOutcomeSummary(result) {
16823
17258
  const parts = [
16824
17259
  reconcileTally(result.schemaActions, "schema"),
16825
17260
  reconcileTally(result.roleActions, "role"),
16826
17261
  issuerTally(result.issuerActions),
17262
+ namespaceTally(result.namespaceActions),
16827
17263
  `profile ${result.profileAction}`
16828
17264
  ].filter((p) => p !== null);
16829
17265
  const detail = parts.length ? ` \u2014 ${parts.join("; ")}` : "";
17266
+ const principalNote = result.userAction === "reconciled" ? ` \u26A0 service principal already existed in this tenant before this run (not newly created)
17267
+ ` : "";
16830
17268
  return ` \u2714 context '${result.contextId}' ${result.contextAction}${detail}
16831
- `;
17269
+ ${principalNote}`;
16832
17270
  }
16833
17271
  var BOOTSTRAP_HELP = `vectros bootstrap \u2014 provision a least-privilege MCP credential.
16834
17272
 
@@ -16836,7 +17274,7 @@ Usage:
16836
17274
  vectros bootstrap [--blueprint <name> | --blank] [--no-seed]
16837
17275
  [--set name=value ...] [--values <file>]
16838
17276
  [--tenant test|live | --test] [--client desktop|code]
16839
- [--yes] [--no-merge] [--rotate]
17277
+ [--yes] [--no-merge] [--rotate] [--confirm-existing-principal]
16840
17278
  [--token <t> | env VECTROS_BOOTSTRAP_TOKEN]
16841
17279
  [--base-url <url>] [--use-scoped-token]
16842
17280
 
@@ -16859,6 +17297,10 @@ Options:
16859
17297
  The tenant rides the key (ssk_test_* vs ssk_live_*), so
16860
17298
  the MCP/client config is otherwise identical.
16861
17299
  --test Shorthand for --tenant test.
17300
+ --activate Force the freshly-stored key to become your active identity, even
17301
+ if that would switch away from a currently-active LIVE credential
17302
+ (the one case --tenant test does NOT do this automatically).
17303
+ --no-activate Store the key without activating it, regardless of tenant.
16862
17304
  --base-url <url> Override the API base URL (beats VECTROS_API_BASE_URL).
16863
17305
  --client <desktop|code> MCP host to configure (default: desktop). 'desktop' merges
16864
17306
  claude_desktop_config.json (also works for Cursor/Cline);
@@ -16873,6 +17315,13 @@ Options:
16873
17315
  print the config snippet instead of merging it into your client's
16874
17316
  config file. Not a dry run; see 'blueprint plan' for that.
16875
17317
  --rotate Revoke + re-mint THIS machine's key.
17318
+ --confirm-existing-principal
17319
+ Required (with --yes, or with no TTY) to mint a NEW key bound to a
17320
+ service principal that already existed in your tenant before this run
17321
+ \u2014 e.g. a deliberate second machine for an integration you already own.
17322
+ Refused by default so an unexpected collision halts a CI run loudly
17323
+ instead of silently minting a credential bound to the wrong identity.
17324
+ Never needed for --rotate or an ordinary same-machine re-apply.
16876
17325
  --use-scoped-token Legacy fallback.
16877
17326
  NOTE: it mints a token WITHOUT the provisioning
16878
17327
  capability, so it cannot create a blueprint's
@@ -16960,6 +17409,22 @@ function buildRemintForContext(opts) {
16960
17409
  });
16961
17410
  };
16962
17411
  }
17412
+ function buildRemintBootstrapForContext(opts) {
17413
+ const mintBridge = opts.mintBridge ?? mintBridgeToken;
17414
+ return async (contextId) => {
17415
+ const ctxToken = await mintBridge({
17416
+ cognitoToken: opts.cognitoToken,
17417
+ apiBaseUrl: opts.apiBaseUrl,
17418
+ contextId,
17419
+ tenant: opts.tenant,
17420
+ useScopedToken: opts.useScopedToken
17421
+ });
17422
+ return new VectrosClient({
17423
+ token: ctxToken.token,
17424
+ environment: opts.apiBaseUrl
17425
+ });
17426
+ };
17427
+ }
16963
17428
  async function runBootstrapCli(argv, deps = {}) {
16964
17429
  const log = deps.log ?? createLogger2();
16965
17430
  let args;
@@ -16993,6 +17458,15 @@ ${BOOTSTRAP_HELP}
16993
17458
  \u2716 Scope gate refused blueprint '${blueprint.name}':
16994
17459
  `);
16995
17460
  for (const r of err.rejected) process.stderr.write(` \u2022 ${r.action}: ${r.reason}
17461
+ `);
17462
+ process.stderr.write(" Minting nothing.\n");
17463
+ return 2;
17464
+ }
17465
+ if (err instanceof CapabilityGateError) {
17466
+ process.stderr.write(`
17467
+ \u2716 Capability gate refused blueprint '${blueprint.name}':
17468
+ `);
17469
+ for (const r of err.rejected) process.stderr.write(` \u2022 ${r.action}: ${r.reason}
16996
17470
  `);
16997
17471
  process.stderr.write(" Minting nothing.\n");
16998
17472
  return 2;
@@ -17004,6 +17478,11 @@ ${BOOTSTRAP_HELP}
17004
17478
  Blueprint: ${blueprint.name}
17005
17479
  \u2714 Scope gate: [${gate.accepted.join(", ")}] \u2014 all data-plane \u2713
17006
17480
  `);
17481
+ if (blueprint.accessProfile.capabilities?.length) {
17482
+ const capGate = evaluateCapabilityGate(blueprint.accessProfile.capabilities);
17483
+ process.stdout.write(` \u2714 Capability gate: [${capGate.accepted.join(", ")}] \u2014 granted \u2713
17484
+ `);
17485
+ }
17007
17486
  let noSeed = args.noSeed;
17008
17487
  let seedDeclinedInteractively = false;
17009
17488
  if (shouldOfferSeedOptOut(args, blueprint, isTty)) {
@@ -17077,6 +17556,7 @@ ${renderPlan(blueprint, planSteps)}
17077
17556
  keyringOpenError = err;
17078
17557
  }
17079
17558
  let storedFreshInKeyring = false;
17559
+ const lastResolvedSecret = /* @__PURE__ */ new Map();
17080
17560
  const readExistingKey = async (contextId) => {
17081
17561
  if (!keyring) return void 0;
17082
17562
  const alias = keyringAliasFor(contextId, args.tenant);
@@ -17084,7 +17564,8 @@ ${renderPlan(blueprint, planSteps)}
17084
17564
  const existing = await keyring.get(alias);
17085
17565
  if (!existing?.keyId) return void 0;
17086
17566
  const rawKey = await keyring.resolveSecret(alias);
17087
- return rawKey ? { keyId: existing.keyId, rawKey } : void 0;
17567
+ lastResolvedSecret.set(alias, rawKey);
17568
+ return rawKey ? { keyId: existing.keyId, rawKey, principalId: existing.principalId } : void 0;
17088
17569
  } catch {
17089
17570
  return void 0;
17090
17571
  }
@@ -17103,6 +17584,11 @@ ${renderPlan(blueprint, planSteps)}
17103
17584
  const alias = keyringAliasFor(contextId, args.tenant);
17104
17585
  try {
17105
17586
  warnReplacingAlias(await keyring.get(alias), alias, record.revokedKeyId);
17587
+ const willActivate = await determineActivateDecision(keyring, {
17588
+ tenant: args.tenant,
17589
+ explicitActivate: args.activate,
17590
+ hint: lastResolvedSecret.has(alias) ? { alias, secret: lastResolvedSecret.get(alias) } : void 0
17591
+ });
17106
17592
  await storeMintedKey(
17107
17593
  keyring,
17108
17594
  {
@@ -17117,11 +17603,14 @@ ${renderPlan(blueprint, planSteps)}
17117
17603
  env: deriveEnvironment(baseUrl) ?? args.env
17118
17604
  },
17119
17605
  record.rawKey,
17120
- { activate: true, revokedKeyId: record.revokedKeyId }
17606
+ { activate: willActivate, revokedKeyId: record.revokedKeyId }
17121
17607
  );
17122
17608
  if (record.revokedKeyId) {
17123
17609
  await warnOtherAliasesNaming(keyring, record.revokedKeyId, alias);
17124
17610
  }
17611
+ process.stdout.write(
17612
+ tenantActivateMessage({ alias, tenant: args.tenant, willActivate, explicitlyDeclined: args.activate === false })
17613
+ );
17125
17614
  storedFreshInKeyring = true;
17126
17615
  } catch (err) {
17127
17616
  process.stderr.write(
@@ -17142,12 +17631,20 @@ ${renderPlan(blueprint, planSteps)}
17142
17631
  skipSeed: noSeed,
17143
17632
  readExistingKey,
17144
17633
  persistMintedKey,
17634
+ remintBootstrapForContext: buildRemintBootstrapForContext({
17635
+ cognitoToken,
17636
+ apiBaseUrl: baseUrl,
17637
+ tenant: args.tenant,
17638
+ useScopedToken: args.useScopedToken,
17639
+ mintBridge
17640
+ }),
17145
17641
  remintForContext: buildRemintForContext({
17146
17642
  cognitoToken,
17147
17643
  apiBaseUrl: baseUrl,
17148
17644
  tenant: args.tenant,
17149
17645
  mintContext
17150
17646
  }),
17647
+ confirmExistingPrincipal: buildConfirmExistingPrincipal(args, promptCtx.prompt, isTty),
17151
17648
  log
17152
17649
  });
17153
17650
  process.stdout.write(
@@ -17225,6 +17722,18 @@ ${codeAddHint}`);
17225
17722
  `);
17226
17723
  return 3;
17227
17724
  }
17725
+ if (err instanceof ExistingPrincipalNotConfirmedError) {
17726
+ process.stderr.write(
17727
+ `
17728
+ \u2716 ${err.message}
17729
+
17730
+ If this is expected (a re-apply, or a deliberate second machine for an integration you already own),
17731
+ re-run with --confirm-existing-principal, or confirm interactively.
17732
+ If you don't recognize this identity, investigate the blueprint's servicePrincipal.externalId before re-running.
17733
+ `
17734
+ );
17735
+ return 3;
17736
+ }
17228
17737
  if (err instanceof BlueprintIdentityError || err instanceof UnresolvedIdentitiesError) {
17229
17738
  process.stderr.write(`
17230
17739
  \u2716 ${err.message}
@@ -17445,6 +17954,10 @@ function lintBlueprint(bp) {
17445
17954
  }
17446
17955
  }
17447
17956
  }
17957
+ {
17958
+ const reached = ownerScopedReachWithoutDataScope(bp.accessProfile.allowedActions, ds);
17959
+ if (reached.length > 0) findings.push(unconstrainedReachFinding("accessProfile", reached));
17960
+ }
17448
17961
  for (const roleId of Object.keys(bp.roles ?? {})) {
17449
17962
  if (!ROLE_ID_RE.test(roleId)) {
17450
17963
  findings.push({
@@ -17457,6 +17970,10 @@ function lintBlueprint(bp) {
17457
17970
  for (const [roleId, clauses] of Object.entries(bp.roles ?? {})) {
17458
17971
  clauses.forEach((clause, i) => {
17459
17972
  const cds = clause.dataScope;
17973
+ const reached = ownerScopedReachWithoutDataScope(clause.allowedActions, cds);
17974
+ if (reached.length > 0) {
17975
+ findings.push(unconstrainedReachFinding(`role '${roleId}' clause ${i}`, reached));
17976
+ }
17460
17977
  if (!cds) return;
17461
17978
  for (const [key, values] of Object.entries(cds)) {
17462
17979
  for (const value of values) {
@@ -17509,6 +18026,25 @@ function placeholderFinding(where, key, value) {
17509
18026
  message: `${where}: dataScope.${key} value '${value}' is not a placeholder the platform recognizes \u2014 it will be rejected at apply. Valid: \${{ any }}, \${{ self.userId }}, \${{ self.scope.<namespace> }}, \${{ under.self.userId }}, \${{ under.self.scope.<namespace> }}. Spelling is exact and lowercase; use a plain value for a literal owner id.`
17510
18027
  };
17511
18028
  }
18029
+ var OWNER_SCOPED_RESOURCES = /* @__PURE__ */ new Set(["records", "documents", "entities"]);
18030
+ var UNCONSTRAINED_REACH_OPS = /* @__PURE__ */ new Set(["r", "u", "d"]);
18031
+ function ownerScopedReachWithoutDataScope(allowedActions, dataScope) {
18032
+ const reached = /* @__PURE__ */ new Set();
18033
+ for (const action of allowedActions) {
18034
+ const [resource, ops] = action.split(":", 2);
18035
+ if (!resource || !ops || !OWNER_SCOPED_RESOURCES.has(resource)) continue;
18036
+ if (ops === "*" || [...ops].some((op) => UNCONSTRAINED_REACH_OPS.has(op))) reached.add(resource);
18037
+ }
18038
+ if (dataScope !== void 0) return [];
18039
+ return [...reached].sort();
18040
+ }
18041
+ function unconstrainedReachFinding(where, resources) {
18042
+ return {
18043
+ severity: "warning",
18044
+ rule: "datascope-absent-unconstrained-reach",
18045
+ message: `${where}: no dataScope at all, but allowedActions grants r/u/d on ${resources.join("/")} \u2014 this reaches EVERY row of EVERY owner on ${resources.length > 1 ? "those resources" : "that resource"}, not just "your own org" or any other value you may have intended. If that's deliberate (a genuinely shared/tenant-wide resource), you can ignore this; otherwise add a dataScope entry.`
18046
+ };
18047
+ }
17512
18048
  function lintPasses(findings) {
17513
18049
  return !findings.some((f) => f.severity === "error");
17514
18050
  }
@@ -17547,8 +18083,12 @@ schemas:
17547
18083
  lookupFields: [title] # extra O(1) exact-match lookup indexes (max 10)
17548
18084
 
17549
18085
  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.
18086
+ # SCOPE GATE: data-plane only (records/schemas/search/documents/folders/inference/entities),
18087
+ # PLUS profiles:r/u/d (bounded by the platform to redistributing access you already have,
18088
+ # never granting more) and profiles:c:self (create a profile for yourself only \u2014 the
18089
+ # platform resolves "self" from the caller, not from anything you declare here). A bare
18090
+ # profiles:c, or profiles:c:usr_<id> naming another principal, is HARD-REJECTED \u2014 by
18091
+ # design, no override. Same for keys/users/admin/namespaces/*.
17552
18092
  # Ops: c|r|u|d, combinable (records:cru). Prefer the narrowest set (least privilege).
17553
18093
  allowedActions: [records:r, records:c, records:u, search:r, schemas:r]
17554
18094
  # dataScope binds the key to specific owners. Include null to ALSO see
@@ -17556,6 +18096,17 @@ accessProfile:
17556
18096
  # owners' records, not shared/seed data. Keys are userId (the principal) and
17557
18097
  # namespaced scopes (scope:org, scope:client, ...):
17558
18098
  # dataScope: { "scope:org": [<org-uuid>, null] }
18099
+ # Left empty ({}) below \u2014 the explicit marker for "deliberately tenant-wide,
18100
+ # every record visible" ('blueprint validate' warns on a MISSING dataScope
18101
+ # key, but not on this one). This is a SCAFFOLD PLACEHOLDER, not a
18102
+ # recommendation: it also grants records:c/u tenant-wide, so before this
18103
+ # profile touches real data, replace it with a real scope (the example
18104
+ # above) unless every owner genuinely sharing every record is what you want.
18105
+ dataScope: {}
18106
+ # capabilities is a SEPARATE gated dimension from allowedActions \u2014 named platform
18107
+ # effects a resource:ops verb can't express. Only 'member-lifecycle' is admitted
18108
+ # today; every other name is HARD-REJECTED, same posture as the scope gate above.
18109
+ # capabilities: [member-lifecycle]
17559
18110
 
17560
18111
  servicePrincipal:
17561
18112
  externalId: ${name}-sp # stable; the principal the profile + key bind to
@@ -17675,15 +18226,19 @@ async function resolveAndValidate(arg, verb, supplied) {
17675
18226
  return null;
17676
18227
  }
17677
18228
  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
- }
18229
+ assertBlueprintGates(blueprint);
17682
18230
  } catch (err) {
17683
18231
  if (err instanceof ScopeGateError) {
17684
18232
  process.stderr.write(` \u2716 Scope gate rejected '${blueprint.name}':
17685
18233
  `);
17686
18234
  for (const r of err.rejected) process.stderr.write(` \u2022 ${r.action}: ${r.reason}
18235
+ `);
18236
+ return null;
18237
+ }
18238
+ if (err instanceof CapabilityGateError) {
18239
+ process.stderr.write(` \u2716 Capability gate rejected '${blueprint.name}':
18240
+ `);
18241
+ for (const r of err.rejected) process.stderr.write(` \u2022 ${r.action}: ${r.reason}
17687
18242
  `);
17688
18243
  return null;
17689
18244
  }
@@ -17715,9 +18270,10 @@ async function runValidate(args) {
17715
18270
  const blueprint = await resolveAndValidate(args.file, "validate", supplied);
17716
18271
  if (!blueprint) return 2;
17717
18272
  const gate = evaluateScopeGate(blueprint.accessProfile.allowedActions);
18273
+ const capGate = evaluateCapabilityGate(blueprint.accessProfile.capabilities);
17718
18274
  const warnCount = lintBlueprint(blueprint).length;
17719
18275
  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"
18276
+ ` \u2714 '${blueprint.name}' is valid \u2014 structural \u2713, scope gate \u2713 [${gate.accepted.join(", ")}]` + (capGate.accepted.length ? `, capability gate \u2713 [${capGate.accepted.join(", ")}]` : "") + `, lint \u2713` + (warnCount ? ` (${warnCount} warning${warnCount === 1 ? "" : "s"})` : "") + "\n"
17721
18277
  );
17722
18278
  return 0;
17723
18279
  }
@@ -18048,7 +18604,14 @@ async function resolveKeyDestination(opts, deps) {
18048
18604
  `);
18049
18605
  return 2;
18050
18606
  }
18051
- const alias = opts.alias ?? opts.context;
18607
+ let alias;
18608
+ try {
18609
+ alias = keyringAliasFor(opts.alias ?? opts.context, opts.tenant ?? "live");
18610
+ } catch (err) {
18611
+ process.stderr.write(` \u2716 ${formatError(err)}
18612
+ `);
18613
+ return 2;
18614
+ }
18052
18615
  if (opts.activate === false && await keyring.activeAlias() === alias) {
18053
18616
  process.stderr.write(
18054
18617
  ` \u2716 --no-activate cannot be used on '${alias}': it is your ACTIVE identity, and storing
@@ -18069,6 +18632,8 @@ async function storeAndReport(dest, key, auth, opts) {
18069
18632
  return 1;
18070
18633
  }
18071
18634
  const principalId = opts.principalId;
18635
+ const tenant = keyTenant(key.rawKey) ?? "test";
18636
+ const willActivate = await determineActivateDecision(dest.keyring, { tenant, explicitActivate: opts.activate });
18072
18637
  let entry;
18073
18638
  try {
18074
18639
  warnReplacingAlias(await dest.keyring.get(dest.alias), dest.alias, opts.revokedKeyId);
@@ -18083,7 +18648,7 @@ async function storeAndReport(dest, key, auth, opts) {
18083
18648
  env: auth.env
18084
18649
  },
18085
18650
  key.rawKey,
18086
- { activate: opts.activate !== false, revokedKeyId: opts.revokedKeyId }
18651
+ { activate: willActivate, revokedKeyId: opts.revokedKeyId }
18087
18652
  ));
18088
18653
  } catch (err) {
18089
18654
  process.stderr.write(
@@ -18095,7 +18660,6 @@ async function storeAndReport(dest, key, auth, opts) {
18095
18660
  );
18096
18661
  return 1;
18097
18662
  }
18098
- const active = opts.activate !== false;
18099
18663
  process.stdout.write(
18100
18664
  `
18101
18665
  \u2714 Stored the new key in your ${describeEntryStoreNoun(entry)} \u2014 it was not printed.
@@ -18105,9 +18669,19 @@ async function storeAndReport(dest, key, auth, opts) {
18105
18669
  ["principal", entry.principalId || void 0],
18106
18670
  ["key id", entry.keyId],
18107
18671
  ["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
- `)
18672
+ ["active", willActivate ? "yes" : "no"]
18673
+ ]) + "\n\n" + // The plain "now active" note fires only on the activated path. When it
18674
+ // did NOT activate, tenantActivateMessage below is the ONLY explanation —
18675
+ // it already says to run `switch`, AND (unlike a bare "Not active") says
18676
+ // WHY: an explicit --no-activate vs. this call's tenant-safety default.
18677
+ // Saying both here would print two different "run switch" instructions
18678
+ // for one outcome.
18679
+ (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({
18680
+ alias: entry.alias,
18681
+ tenant,
18682
+ willActivate,
18683
+ explicitlyDeclined: opts.activate === false
18684
+ })
18111
18685
  );
18112
18686
  return 0;
18113
18687
  }
@@ -19641,6 +20215,19 @@ async function runAssertions(client, bootstrapClient, deps, blueprint, tenantId,
19641
20215
  assertions.push({ name: `issuer:${issuerId}`, ok: false, detail: `getIssuer(${issuerId}) failed: ${errMsg(e)}` });
19642
20216
  }
19643
20217
  }
20218
+ for (const { namespace } of result.namespaceActions) {
20219
+ try {
20220
+ const ns = await bootstrapClient.identity.getNamespace({ namespace, contextId: blueprint.contextId });
20221
+ const ok = ns.namespace === namespace && ns.contextId === blueprint.contextId;
20222
+ assertions.push({
20223
+ name: `namespace:${namespace}`,
20224
+ ok,
20225
+ 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}'`
20226
+ });
20227
+ } catch (e) {
20228
+ assertions.push({ name: `namespace:${namespace}`, ok: false, detail: `getNamespace(${namespace}) failed: ${errMsg(e)}` });
20229
+ }
20230
+ }
19644
20231
  return assertions;
19645
20232
  }
19646
20233
  async function runTeardown(client, bootstrapClient, blueprint, machineId, snapshot, result) {
@@ -19753,10 +20340,18 @@ async function runTeardown(client, bootstrapClient, blueprint, machineId, snapsh
19753
20340
  steps.push({ step: `delete issuer ${issuerId}`, ok: false, error: errMsg(e) });
19754
20341
  }
19755
20342
  }
20343
+ for (const { namespace } of result.namespaceActions) {
20344
+ steps.push({
20345
+ step: `delete namespace ${namespace}`,
20346
+ ok: true,
20347
+ skipped: true,
20348
+ error: "namespace deletion requires a root API key (sk_*) \u2014 blueprint-test never holds one; the registration is left in place"
20349
+ });
20350
+ }
19756
20351
  return steps;
19757
20352
  }
19758
20353
  async function runBlueprintHarness(client, input, deps) {
19759
- const { tenantId, machineId, keep = false, log, remintForContext } = input;
20354
+ const { tenantId, machineId, keep = false, log, remintForContext, remintBootstrapForContext } = input;
19760
20355
  const readExistingKey = async () => void 0;
19761
20356
  const persistMintedKey = async () => {
19762
20357
  };
@@ -19766,6 +20361,9 @@ async function runBlueprintHarness(client, input, deps) {
19766
20361
  log
19767
20362
  );
19768
20363
  const remint = remintForContext ?? (async () => client);
20364
+ const remintBootstrap = remintBootstrapForContext ?? (async () => client);
20365
+ const hasIssuers = (blueprint.issuers ?? []).length > 0;
20366
+ const hasNamespaces = (blueprint.namespaces ?? []).length > 0;
19769
20367
  let contextSnapshot;
19770
20368
  if (remintForContext) {
19771
20369
  try {
@@ -19781,7 +20379,18 @@ async function runBlueprintHarness(client, input, deps) {
19781
20379
  } else {
19782
20380
  contextSnapshot = await captureSnapshot(client, blueprint);
19783
20381
  }
19784
- const issuerSnapshot = await captureIssuerSnapshot(client);
20382
+ let issuerSnapshot;
20383
+ if (!hasIssuers) {
20384
+ issuerSnapshot = { existingIssuerIds: /* @__PURE__ */ new Set(), failed: false };
20385
+ } else if (remintBootstrapForContext) {
20386
+ try {
20387
+ issuerSnapshot = await captureIssuerSnapshot(await remintBootstrapForContext(blueprint.contextId));
20388
+ } catch (e) {
20389
+ issuerSnapshot = { existingIssuerIds: /* @__PURE__ */ new Set(), failed: !isNotFound(e) };
20390
+ }
20391
+ } else {
20392
+ issuerSnapshot = await captureIssuerSnapshot(client);
20393
+ }
19785
20394
  const snapshot = {
19786
20395
  ...contextSnapshot,
19787
20396
  existingIssuerIds: issuerSnapshot.existingIssuerIds,
@@ -19791,9 +20400,25 @@ async function runBlueprintHarness(client, input, deps) {
19791
20400
  let assertions = [];
19792
20401
  let applyError;
19793
20402
  try {
19794
- result = await runLoader(client, { blueprint, tenantId, machineId, readExistingKey, persistMintedKey, log, remintForContext });
20403
+ result = await runLoader(client, {
20404
+ blueprint,
20405
+ tenantId,
20406
+ machineId,
20407
+ readExistingKey,
20408
+ persistMintedKey,
20409
+ log,
20410
+ remintForContext,
20411
+ remintBootstrapForContext
20412
+ });
19795
20413
  log.debug({ component: "harness", contextId: result.contextId, keyName: result.keyName }, "applied");
19796
- assertions = await runAssertions(await remint(blueprint.contextId), client, deps, blueprint, tenantId, result);
20414
+ assertions = await runAssertions(
20415
+ await remint(blueprint.contextId),
20416
+ hasIssuers || hasNamespaces ? await remintBootstrap(blueprint.contextId) : client,
20417
+ deps,
20418
+ blueprint,
20419
+ tenantId,
20420
+ result
20421
+ );
19797
20422
  } catch (e) {
19798
20423
  applyError = errMsg(e);
19799
20424
  log.warn({ component: "harness", err: applyError }, "apply phase failed");
@@ -19805,7 +20430,14 @@ async function runBlueprintHarness(client, input, deps) {
19805
20430
  tdClient = await remint(blueprint.contextId);
19806
20431
  } catch {
19807
20432
  }
19808
- teardown = await runTeardown(tdClient, client, blueprint, machineId, snapshot, result);
20433
+ let tdBootstrapClient = client;
20434
+ if (hasIssuers) {
20435
+ try {
20436
+ tdBootstrapClient = await remintBootstrap(blueprint.contextId);
20437
+ } catch {
20438
+ }
20439
+ }
20440
+ teardown = await runTeardown(tdClient, tdBootstrapClient, blueprint, machineId, snapshot, result);
19809
20441
  }
19810
20442
  const passed = applyError === void 0 && assertions.length > 0 && assertions.every((a) => a.ok);
19811
20443
  return {
@@ -19879,6 +20511,13 @@ async function runBlueprintTest(fileOrName, opts, deps = {}) {
19879
20511
  process.stderr.write(` \u2716 Scope gate refused blueprint:
19880
20512
  `);
19881
20513
  for (const r of err.rejected) process.stderr.write(` \u2022 ${r.action}: ${r.reason}
20514
+ `);
20515
+ return 2;
20516
+ }
20517
+ if (err instanceof CapabilityGateError) {
20518
+ process.stderr.write(` \u2716 Capability gate refused blueprint:
20519
+ `);
20520
+ for (const r of err.rejected) process.stderr.write(` \u2022 ${r.action}: ${r.reason}
19882
20521
  `);
19883
20522
  return 2;
19884
20523
  }
@@ -19914,9 +20553,22 @@ async function runBlueprintTest(fileOrName, opts, deps = {}) {
19914
20553
  environment: auth.baseUrl
19915
20554
  });
19916
20555
  };
20556
+ const remintBootstrapForContext = async (contextId) => {
20557
+ const ctxToken = await mintBridgeToken({
20558
+ cognitoToken: auth.cognitoToken,
20559
+ apiBaseUrl: auth.baseUrl,
20560
+ contextId,
20561
+ tenant: auth.tenant,
20562
+ useScopedToken: opts.useScopedToken
20563
+ });
20564
+ return new VectrosClient({
20565
+ token: ctxToken.token,
20566
+ environment: auth.baseUrl
20567
+ });
20568
+ };
19917
20569
  const report = await runBlueprintHarness(
19918
20570
  client,
19919
- { blueprint, tenantId, machineId, keep: opts.keep, log, remintForContext },
20571
+ { blueprint, tenantId, machineId, keep: opts.keep, log, remintForContext, remintBootstrapForContext },
19920
20572
  { pingWithKey }
19921
20573
  );
19922
20574
  if (opts.json) {
@@ -19997,9 +20649,10 @@ async function runJoin(opts, deps = {}) {
19997
20649
  return 2;
19998
20650
  }
19999
20651
  const context = opts.contextArg;
20652
+ const tenant = opts.tenant ?? "live";
20000
20653
  let alias;
20001
20654
  try {
20002
- alias = keyringAliasFor(opts.alias ?? context, opts.tenant ?? "live");
20655
+ alias = keyringAliasFor(opts.alias ?? context, tenant);
20003
20656
  } catch (err) {
20004
20657
  process.stderr.write(` \u2716 ${formatError(err)}
20005
20658
  `);
@@ -20034,8 +20687,23 @@ async function runJoin(opts, deps = {}) {
20034
20687
  const heldSecret = existingEntry ? await keyring.resolveSecret(alias) : void 0;
20035
20688
  const canReuse = existingEntry && heldSecret && existingEntry.context === context && existingEntry.principalId === principalId && existingEntry.env === auth.env;
20036
20689
  if (canReuse && existingEntry) {
20037
- await keyring.setActive(alias);
20038
- emitJoined(alias, context, principalId, opts.role, existingEntry.keyId, true);
20690
+ const willActivate2 = await determineActivateDecision(keyring, {
20691
+ tenant,
20692
+ explicitActivate: opts.activate,
20693
+ hint: { alias, secret: heldSecret }
20694
+ });
20695
+ if (willActivate2) await keyring.setActive(alias);
20696
+ emitJoined({
20697
+ alias,
20698
+ context,
20699
+ principalId,
20700
+ role: opts.role,
20701
+ keyId: existingEntry.keyId,
20702
+ reused: true,
20703
+ tenant,
20704
+ willActivate: willActivate2,
20705
+ explicitlyDeclined: opts.activate === false
20706
+ });
20039
20707
  return 0;
20040
20708
  }
20041
20709
  warnReplacingAlias(existingEntry, alias);
@@ -20047,13 +20715,24 @@ async function runJoin(opts, deps = {}) {
20047
20715
  );
20048
20716
  }
20049
20717
  if (revokedKeyId) await warnOtherAliasesNaming(keyring, revokedKeyId, alias);
20718
+ const willActivate = await determineActivateDecision(keyring, { tenant, explicitActivate: opts.activate });
20050
20719
  const { entry } = await storeMintedKey(
20051
20720
  keyring,
20052
20721
  { alias, context, principalId, role: opts.role, keyId: key.keyId, env: auth.env },
20053
20722
  key.rawKey,
20054
- { revokedKeyId }
20723
+ { activate: willActivate, revokedKeyId }
20055
20724
  );
20056
- emitJoined(alias, context, principalId, opts.role, entry.keyId, false);
20725
+ emitJoined({
20726
+ alias,
20727
+ context,
20728
+ principalId,
20729
+ role: opts.role,
20730
+ keyId: entry.keyId,
20731
+ reused: false,
20732
+ tenant,
20733
+ willActivate,
20734
+ explicitlyDeclined: opts.activate === false
20735
+ });
20057
20736
  return 0;
20058
20737
  } catch (err) {
20059
20738
  process.stderr.write(formatCliError(err, { action: "join failed" }));
@@ -20061,27 +20740,34 @@ async function runJoin(opts, deps = {}) {
20061
20740
  }
20062
20741
  });
20063
20742
  }
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
- ]) + `
20743
+ function emitJoined(opts) {
20744
+ const header = `
20745
+ \u2714 Joined '${opts.context}' as ${opts.principalId}` + (opts.reused ? " (existing key reused)" : "") + "\n" + formatKeyValues([
20746
+ ["alias", opts.alias],
20747
+ ["context", opts.context],
20748
+ ["principal", opts.principalId],
20749
+ ["role", opts.role],
20750
+ ["key id", opts.keyId]
20751
+ ]);
20752
+ const activationNote = opts.willActivate ? `
20074
20753
 
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
- `
20754
+ Now active. Use \`vectros switch ${opts.alias}\` to return to it, \`vectros keyring show --format env\` to export the key for the MCP server.
20755
+ ` : "";
20756
+ process.stdout.write(
20757
+ header + activationNote + tenantActivateMessage({
20758
+ alias: opts.alias,
20759
+ tenant: opts.tenant,
20760
+ willActivate: opts.willActivate,
20761
+ explicitlyDeclined: opts.explicitlyDeclined
20762
+ })
20077
20763
  );
20078
20764
  }
20079
20765
 
20080
20766
  // src/build-info.ts
20081
20767
  var BUILD_INFO = {
20082
- cli: true ? "0.15.0" : "dev",
20083
- sdk: true ? "0.39.0" : "dev",
20084
- blueprints: true ? "0.12.0" : "dev"
20768
+ cli: true ? "0.16.0" : "dev",
20769
+ sdk: true ? "0.40.0" : "dev",
20770
+ blueprints: true ? "0.13.0" : "dev"
20085
20771
  };
20086
20772
  function formatBuildInfo() {
20087
20773
  return `vectros ${BUILD_INFO.cli} (sdk ${BUILD_INFO.sdk}, blueprints ${BUILD_INFO.blueprints})`;
@@ -20105,6 +20791,14 @@ function addCommonAuthOptions(cmd) {
20105
20791
  "emit machine-readable JSON instead of the human-readable view"
20106
20792
  );
20107
20793
  }
20794
+ function parseTenantFlag(raw) {
20795
+ if (raw !== void 0 && raw !== "test" && raw !== "live") {
20796
+ process.stderr.write(` \u2716 --tenant must be 'test' or 'live' (got '${raw}')
20797
+ `);
20798
+ process.exit(2);
20799
+ }
20800
+ return raw;
20801
+ }
20108
20802
  function toCommonOptions(flags) {
20109
20803
  return {
20110
20804
  env: flags.env,
@@ -20158,7 +20852,10 @@ function buildProgram(deps = {}) {
20158
20852
  return program;
20159
20853
  }
20160
20854
  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)");
20855
+ const join3 = program.command("join").description("Enroll a principal in a context (grant + key) and keep the key in the keyring.").argument("<context>", "the app context to join").option("--role <roleId>", "reference a Role to grant (XOR --actions)").option("--actions <csv>", "inline single-clause actions to grant, comma-separated (XOR --role)").option("--as <principal>", "who to enroll: 'me' (default) or an explicit 'usr_<id>'", "me").option("--scope <ns:value>", "identity override: a scope the member acts under (repeatable, e.g. org:<id>)", collect, []).option("--alias <name>", "keyring entry name (defaults to the context id)").option("--tenant <tenant>", "target tenant: 'test' or 'live' (default live)").option(
20856
+ "--activate",
20857
+ "force the freshly-stored key to become your active identity, even over a currently-active LIVE credential (the one case --tenant test does NOT do automatically)"
20858
+ ).option("--no-activate", "store the key without activating it, regardless of tenant");
20162
20859
  addAuthOptions(join3).action(
20163
20860
  async (contextArg, flags) => {
20164
20861
  if (flags.tenant !== void 0 && flags.tenant !== "test" && flags.tenant !== "live") {
@@ -20176,7 +20873,8 @@ function registerJoinCommand(program, deps) {
20176
20873
  as: flags.as,
20177
20874
  scope: flags.scope,
20178
20875
  alias: flags.alias,
20179
- tenant: flags.tenant
20876
+ tenant: flags.tenant,
20877
+ activate: flags.activate
20180
20878
  },
20181
20879
  deps
20182
20880
  )
@@ -20423,9 +21121,10 @@ function toKeyDestination(flags) {
20423
21121
  function registerKeyCommands(program, deps) {
20424
21122
  const key = program.command("key").description("Manage scoped API keys (ssk_*) \u2014 issue, list, revoke, rotate.");
20425
21123
  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)")
21124
+ 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
21125
  );
20428
21126
  addAuthOptions(issue).action(async (flags) => {
21127
+ const tenant = parseTenantFlag(flags.tenant);
20429
21128
  process.exit(
20430
21129
  await runKeyIssue({
20431
21130
  ...toCommonOptions(flags),
@@ -20433,6 +21132,7 @@ function registerKeyCommands(program, deps) {
20433
21132
  context: flags.context,
20434
21133
  name: flags.name,
20435
21134
  label: flags.label,
21135
+ tenant,
20436
21136
  ...toKeyDestination(flags)
20437
21137
  }, deps)
20438
21138
  );
@@ -20440,37 +21140,50 @@ function registerKeyCommands(program, deps) {
20440
21140
  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
21141
  "--all-contexts",
20442
21142
  "list every key across ALL your contexts (owner-only) \u2014 the tenant-wide view for the key limit"
20443
- );
21143
+ ).option("--tenant <tenant>", "tenant to list: 'test' or 'live' (default live; not combinable with --all-contexts)");
20444
21144
  addCommonAuthOptions(list).action(
20445
21145
  async (flags) => {
21146
+ if (flags.allContexts && flags.tenant !== void 0) {
21147
+ process.stderr.write(" \u2716 --tenant is not combinable with --all-contexts (which already spans both tenants).\n");
21148
+ process.exit(2);
21149
+ }
21150
+ const tenant = parseTenantFlag(flags.tenant);
20446
21151
  process.exit(
20447
21152
  await runKeyList(
20448
21153
  {
20449
21154
  ...toCommonOptions(flags),
20450
21155
  principal: flags.principal,
20451
21156
  context: flags.context,
20452
- allContexts: Boolean(flags.allContexts)
21157
+ allContexts: Boolean(flags.allContexts),
21158
+ tenant
20453
21159
  },
20454
21160
  deps
20455
21161
  )
20456
21162
  );
20457
21163
  }
20458
21164
  );
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
- });
21165
+ 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)");
21166
+ addCommonAuthOptions(get).action(
21167
+ async (keyId, flags) => {
21168
+ const tenant = parseTenantFlag(flags.tenant);
21169
+ process.exit(await runKeyGet(keyId, { ...toCommonOptions(flags), context: flags.context, tenant }, deps));
21170
+ }
21171
+ );
21172
+ 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)");
21173
+ addAuthOptions(revoke).action(
21174
+ async (keyId, flags) => {
21175
+ const tenant = parseTenantFlag(flags.tenant);
21176
+ process.exit(await runKeyRevoke(keyId, { ...toCommonOptions(flags), context: flags.context, tenant }, deps));
21177
+ }
21178
+ );
20467
21179
  const rotate = addKeyDestinationOptions(
20468
21180
  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
21181
  "--force-new",
20470
21182
  "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
- )
21183
+ ).option("--tenant <tenant>", "tenant the key lives in: 'test' or 'live' (default live)")
20472
21184
  );
20473
21185
  addAuthOptions(rotate).action(async (flags) => {
21186
+ const tenant = parseTenantFlag(flags.tenant);
20474
21187
  process.exit(
20475
21188
  await runKeyRotate({
20476
21189
  ...toCommonOptions(flags),
@@ -20478,6 +21191,7 @@ function registerKeyCommands(program, deps) {
20478
21191
  context: flags.context,
20479
21192
  name: flags.name,
20480
21193
  forceNew: flags.forceNew,
21194
+ tenant,
20481
21195
  ...toKeyDestination(flags)
20482
21196
  }, deps)
20483
21197
  );