@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.mjs CHANGED
@@ -4440,7 +4440,7 @@ var AuthClient = class {
4440
4440
  );
4441
4441
  }
4442
4442
  /**
4443
- * 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.
4443
+ * 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.
4444
4444
  *
4445
4445
  * @param {Vectros.CreateAccessProfileRequest} request
4446
4446
  * @param {AuthClient.RequestOptions} requestOptions - Request-specific configuration.
@@ -5541,7 +5541,7 @@ var AuthClient = class {
5541
5541
  );
5542
5542
  }
5543
5543
  /**
5544
- * 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.
5544
+ * 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.
5545
5545
  *
5546
5546
  * @param {Vectros.GetUsageRequest} request
5547
5547
  * @param {AuthClient.RequestOptions} requestOptions - Request-specific configuration.
@@ -5604,7 +5604,7 @@ var AuthClient = class {
5604
5604
  return handleNonStatusCodeError(_response.error, _response.rawResponse, "GET", "/v1/usage");
5605
5605
  }
5606
5606
  /**
5607
- * Retrieves a single registered issuer by issuerId. Requires a root API key or the bootstrap's provisioning capability.
5607
+ * Retrieves a single registered issuer by issuerId. Requires a root API key or the bootstrap's provisioning capability. A credential confined to one app context sees only an issuer registered in that context; naming one registered in another context returns 404, identically to a nonexistent issuerId. A root API key sees every context.
5608
5608
  *
5609
5609
  * @param {Vectros.GetIssuerRequest} request
5610
5610
  * @param {AuthClient.RequestOptions} requestOptions - Request-specific configuration.
@@ -5662,13 +5662,14 @@ var AuthClient = class {
5662
5662
  return handleNonStatusCodeError(_response.error, _response.rawResponse, "GET", "/v1/auth/issuers/{issuerId}");
5663
5663
  }
5664
5664
  /**
5665
- * 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.
5665
+ * Deregisters a trusted third-party IdP issuer. Requires a root API key or the bootstrap's provisioning capability. A credential confined to one app context may only deregister an issuer registered in that context; naming one registered in another context returns 404, identically to a nonexistent issuerId. A root API key may deregister any issuer. Refused if any user account was ever created or matched via this issuer (by a prior self-signup or accepted invite, through `POST /v1/auth/token/exchange`) — that access cannot be silently orphaned. Deactivate the affected users first if you intend to cut off their access, or register a replacement issuer before removing this one. An issuer that has never been used for an exchange (no bound users yet) can always be deregistered.
5666
5666
  *
5667
5667
  * @param {Vectros.DeleteIssuerRequest} request
5668
5668
  * @param {AuthClient.RequestOptions} requestOptions - Request-specific configuration.
5669
5669
  *
5670
5670
  * @throws {@link Vectros.ForbiddenError}
5671
5671
  * @throws {@link Vectros.NotFoundError}
5672
+ * @throws {@link Vectros.ConflictError}
5672
5673
  * @throws {@link Vectros.TooManyRequestsError}
5673
5674
  *
5674
5675
  * @example
@@ -5710,6 +5711,8 @@ var AuthClient = class {
5710
5711
  throw new ForbiddenError(_response.error.body, _response.rawResponse);
5711
5712
  case 404:
5712
5713
  throw new NotFoundError(_response.error.body, _response.rawResponse);
5714
+ case 409:
5715
+ throw new ConflictError(_response.error.body, _response.rawResponse);
5713
5716
  case 429:
5714
5717
  throw new TooManyRequestsError(_response.error.body, _response.rawResponse);
5715
5718
  default:
@@ -5728,7 +5731,7 @@ var AuthClient = class {
5728
5731
  );
5729
5732
  }
5730
5733
  /**
5731
- * Returns the issuers registered in your tenant. Requires a root API key or the bootstrap's provisioning capability. Returns a `{data, nextCursor}` envelope.
5734
+ * Returns the issuers registered in your tenant. Requires a root API key or the bootstrap's provisioning capability. A credential confined to one app context sees only the issuers registered in that context; a root API key sees every context. Returns a `{data, nextCursor}` envelope.
5732
5735
  *
5733
5736
  * @param {Vectros.ListIssuersRequest} request
5734
5737
  * @param {AuthClient.RequestOptions} requestOptions - Request-specific configuration.
@@ -5785,7 +5788,7 @@ var AuthClient = class {
5785
5788
  return handleNonStatusCodeError(_response.error, _response.rawResponse, "GET", "/v1/auth/issuers");
5786
5789
  }
5787
5790
  /**
5788
- * 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.
5791
+ * Registers a trusted third-party IdP issuer that BYO-IdP token exchange (`POST /v1/auth/token/exchange`) may accept a `subject_token` from. Requires a root API key or the CLI bootstrap's provisioning capability — never an ordinary partner-grantable scope. Idempotent by `issuerId` within your tenant; the `(issuer, audience)` pair must not already be registered by a different issuerId/tenant. An app context may have at most one active issuer — deregister the existing one first if you need to replace it. One issuer MAY serve several contexts today, each via its own registration row with a distinct `audience`.
5789
5792
  *
5790
5793
  * @param {Vectros.IssuerRequest} request
5791
5794
  * @param {AuthClient.RequestOptions} requestOptions - Request-specific configuration.
@@ -5896,7 +5899,7 @@ var AuthClient = class {
5896
5899
  return handleNonStatusCodeError(_response.error, _response.rawResponse, "GET", "/v1/ping");
5897
5900
  }
5898
5901
  /**
5899
- * 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.
5902
+ * 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.
5900
5903
  *
5901
5904
  * @param {Vectros.ListProfilesForPrincipalRequest} request
5902
5905
  * @param {AuthClient.RequestOptions} requestOptions - Request-specific configuration.
@@ -6175,7 +6178,7 @@ var AuthClient = class {
6175
6178
  return handleNonStatusCodeError(_response.error, _response.rawResponse, "POST", "/v1/users/invite/resend");
6176
6179
  }
6177
6180
  /**
6178
- * 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.
6181
+ * 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.
6179
6182
  *
6180
6183
  * @param {Vectros.TokenExchangeRequest} request
6181
6184
  * @param {AuthClient.RequestOptions} requestOptions - Request-specific configuration.
@@ -7717,6 +7720,7 @@ var IdentityClient = class {
7717
7720
  * @example
7718
7721
  * await client.identity.listEntities({
7719
7722
  * namespace: "team",
7723
+ * contextId: "myapp",
7720
7724
  * startFrom: "b3BhcXVlLWN1cnNvci1mcm9tLXRoZS1wcmV2aW91cy1wYWdl"
7721
7725
  * })
7722
7726
  */
@@ -7726,6 +7730,7 @@ var IdentityClient = class {
7726
7730
  async __listEntities(request, requestOptions) {
7727
7731
  const {
7728
7732
  namespace,
7733
+ contextId,
7729
7734
  userId,
7730
7735
  externalId,
7731
7736
  scope,
@@ -7740,6 +7745,7 @@ var IdentityClient = class {
7740
7745
  limit
7741
7746
  } = request;
7742
7747
  const _queryParams = {
7748
+ contextId,
7743
7749
  userId,
7744
7750
  externalId,
7745
7751
  scope,
@@ -7798,6 +7804,7 @@ var IdentityClient = class {
7798
7804
  * @example
7799
7805
  * await client.identity.createEntity({
7800
7806
  * namespace: "team",
7807
+ * contextId: "myapp",
7801
7808
  * body: {
7802
7809
  * externalId: "team_eng_platform"
7803
7810
  * }
@@ -7807,9 +7814,10 @@ var IdentityClient = class {
7807
7814
  return HttpResponsePromise.fromPromise(this.__createEntity(request, requestOptions));
7808
7815
  }
7809
7816
  async __createEntity(request, requestOptions) {
7810
- const { namespace, upsert, body: _body } = request;
7817
+ const { namespace, upsert, contextId, body: _body } = request;
7811
7818
  const _queryParams = {
7812
- upsert
7819
+ upsert,
7820
+ contextId
7813
7821
  };
7814
7822
  const _authRequest = await this._options.authProvider.getAuthRequest();
7815
7823
  const _headers = mergeHeaders(
@@ -7866,14 +7874,18 @@ var IdentityClient = class {
7866
7874
  * @example
7867
7875
  * await client.identity.getEntity({
7868
7876
  * namespace: "team",
7869
- * id: "6ba7b810-9dad-11d1-80b4-00c04fd430c8"
7877
+ * id: "6ba7b810-9dad-11d1-80b4-00c04fd430c8",
7878
+ * contextId: "myapp"
7870
7879
  * })
7871
7880
  */
7872
7881
  getEntity(request, requestOptions) {
7873
7882
  return HttpResponsePromise.fromPromise(this.__getEntity(request, requestOptions));
7874
7883
  }
7875
7884
  async __getEntity(request, requestOptions) {
7876
- const { namespace, id } = request;
7885
+ const { namespace, id, contextId } = request;
7886
+ const _queryParams = {
7887
+ contextId
7888
+ };
7877
7889
  const _authRequest = await this._options.authProvider.getAuthRequest();
7878
7890
  const _headers = mergeHeaders(
7879
7891
  _authRequest.headers,
@@ -7887,7 +7899,7 @@ var IdentityClient = class {
7887
7899
  ),
7888
7900
  method: "GET",
7889
7901
  headers: _headers,
7890
- queryString: url_exports.queryBuilder().mergeAdditional(requestOptions?.queryParams).build(),
7902
+ queryString: url_exports.queryBuilder().addMany(_queryParams).mergeAdditional(requestOptions?.queryParams).build(),
7891
7903
  timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1e3,
7892
7904
  maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries,
7893
7905
  abortSignal: requestOptions?.abortSignal,
@@ -7924,6 +7936,7 @@ var IdentityClient = class {
7924
7936
  * await client.identity.updateEntity({
7925
7937
  * namespace: "team",
7926
7938
  * id: "id",
7939
+ * contextId: "myapp",
7927
7940
  * body: {
7928
7941
  * externalId: "team_eng_platform"
7929
7942
  * }
@@ -7933,7 +7946,10 @@ var IdentityClient = class {
7933
7946
  return HttpResponsePromise.fromPromise(this.__updateEntity(request, requestOptions));
7934
7947
  }
7935
7948
  async __updateEntity(request, requestOptions) {
7936
- const { namespace, id, body: _body } = request;
7949
+ const { namespace, id, contextId, body: _body } = request;
7950
+ const _queryParams = {
7951
+ contextId
7952
+ };
7937
7953
  const _authRequest = await this._options.authProvider.getAuthRequest();
7938
7954
  const _headers = mergeHeaders(
7939
7955
  _authRequest.headers,
@@ -7948,7 +7964,7 @@ var IdentityClient = class {
7948
7964
  method: "PUT",
7949
7965
  headers: _headers,
7950
7966
  contentType: "application/json",
7951
- queryString: url_exports.queryBuilder().mergeAdditional(requestOptions?.queryParams).build(),
7967
+ queryString: url_exports.queryBuilder().addMany(_queryParams).mergeAdditional(requestOptions?.queryParams).build(),
7952
7968
  requestType: "json",
7953
7969
  body: _body,
7954
7970
  timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1e3,
@@ -7988,14 +8004,18 @@ var IdentityClient = class {
7988
8004
  * @example
7989
8005
  * await client.identity.deleteEntity({
7990
8006
  * namespace: "team",
7991
- * id: "id"
8007
+ * id: "id",
8008
+ * contextId: "myapp"
7992
8009
  * })
7993
8010
  */
7994
8011
  deleteEntity(request, requestOptions) {
7995
8012
  return HttpResponsePromise.fromPromise(this.__deleteEntity(request, requestOptions));
7996
8013
  }
7997
8014
  async __deleteEntity(request, requestOptions) {
7998
- const { namespace, id } = request;
8015
+ const { namespace, id, contextId } = request;
8016
+ const _queryParams = {
8017
+ contextId
8018
+ };
7999
8019
  const _authRequest = await this._options.authProvider.getAuthRequest();
8000
8020
  const _headers = mergeHeaders(
8001
8021
  _authRequest.headers,
@@ -8009,7 +8029,7 @@ var IdentityClient = class {
8009
8029
  ),
8010
8030
  method: "DELETE",
8011
8031
  headers: _headers,
8012
- queryString: url_exports.queryBuilder().mergeAdditional(requestOptions?.queryParams).build(),
8032
+ queryString: url_exports.queryBuilder().addMany(_queryParams).mergeAdditional(requestOptions?.queryParams).build(),
8013
8033
  timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1e3,
8014
8034
  maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries,
8015
8035
  abortSignal: requestOptions?.abortSignal,
@@ -8052,6 +8072,7 @@ var IdentityClient = class {
8052
8072
  * @example
8053
8073
  * await client.identity.lookupEntities({
8054
8074
  * namespace: "team",
8075
+ * contextId: "myapp",
8055
8076
  * body: {
8056
8077
  * type: "person_v1",
8057
8078
  * field: "ssn"
@@ -8062,7 +8083,10 @@ var IdentityClient = class {
8062
8083
  return HttpResponsePromise.fromPromise(this.__lookupEntities(request, requestOptions));
8063
8084
  }
8064
8085
  async __lookupEntities(request, requestOptions) {
8065
- const { namespace, body: _body } = request;
8086
+ const { namespace, contextId, body: _body } = request;
8087
+ const _queryParams = {
8088
+ contextId
8089
+ };
8066
8090
  const _authRequest = await this._options.authProvider.getAuthRequest();
8067
8091
  const _headers = mergeHeaders(
8068
8092
  _authRequest.headers,
@@ -8077,7 +8101,7 @@ var IdentityClient = class {
8077
8101
  method: "POST",
8078
8102
  headers: _headers,
8079
8103
  contentType: "application/json",
8080
- queryString: url_exports.queryBuilder().mergeAdditional(requestOptions?.queryParams).build(),
8104
+ queryString: url_exports.queryBuilder().addMany(_queryParams).mergeAdditional(requestOptions?.queryParams).build(),
8081
8105
  requestType: "json",
8082
8106
  body: _body,
8083
8107
  timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1e3,
@@ -8121,15 +8145,17 @@ var IdentityClient = class {
8121
8145
  * @example
8122
8146
  * await client.identity.getEntityVersions({
8123
8147
  * namespace: "team",
8124
- * id: "id"
8148
+ * id: "id",
8149
+ * contextId: "myapp"
8125
8150
  * })
8126
8151
  */
8127
8152
  getEntityVersions(request, requestOptions) {
8128
8153
  return HttpResponsePromise.fromPromise(this.__getEntityVersions(request, requestOptions));
8129
8154
  }
8130
8155
  async __getEntityVersions(request, requestOptions) {
8131
- const { namespace, id, startFrom } = request;
8156
+ const { namespace, id, contextId, startFrom } = request;
8132
8157
  const _queryParams = {
8158
+ contextId,
8133
8159
  startFrom
8134
8160
  };
8135
8161
  const _authRequest = await this._options.authProvider.getAuthRequest();
@@ -8191,7 +8217,10 @@ var IdentityClient = class {
8191
8217
  return HttpResponsePromise.fromPromise(this.__getNamespace(request, requestOptions));
8192
8218
  }
8193
8219
  async __getNamespace(request, requestOptions) {
8194
- const { namespace } = request;
8220
+ const { namespace, contextId } = request;
8221
+ const _queryParams = {
8222
+ contextId
8223
+ };
8195
8224
  const _authRequest = await this._options.authProvider.getAuthRequest();
8196
8225
  const _headers = mergeHeaders(
8197
8226
  _authRequest.headers,
@@ -8205,7 +8234,7 @@ var IdentityClient = class {
8205
8234
  ),
8206
8235
  method: "GET",
8207
8236
  headers: _headers,
8208
- queryString: url_exports.queryBuilder().mergeAdditional(requestOptions?.queryParams).build(),
8237
+ queryString: url_exports.queryBuilder().addMany(_queryParams).mergeAdditional(requestOptions?.queryParams).build(),
8209
8238
  timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1e3,
8210
8239
  maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries,
8211
8240
  abortSignal: requestOptions?.abortSignal,
@@ -8230,7 +8259,7 @@ var IdentityClient = class {
8230
8259
  return handleNonStatusCodeError(_response.error, _response.rawResponse, "GET", "/v1/namespaces/{namespace}");
8231
8260
  }
8232
8261
  /**
8233
- * 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.
8262
+ * 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.
8234
8263
  *
8235
8264
  * @param {Vectros.UpdateNamespaceRequest} request
8236
8265
  * @param {IdentityClient.RequestOptions} requestOptions - Request-specific configuration.
@@ -8252,7 +8281,10 @@ var IdentityClient = class {
8252
8281
  return HttpResponsePromise.fromPromise(this.__updateNamespace(request, requestOptions));
8253
8282
  }
8254
8283
  async __updateNamespace(request, requestOptions) {
8255
- const { namespace, body: _body } = request;
8284
+ const { namespace, contextId, body: _body } = request;
8285
+ const _queryParams = {
8286
+ contextId
8287
+ };
8256
8288
  const _authRequest = await this._options.authProvider.getAuthRequest();
8257
8289
  const _headers = mergeHeaders(
8258
8290
  _authRequest.headers,
@@ -8267,7 +8299,7 @@ var IdentityClient = class {
8267
8299
  method: "PUT",
8268
8300
  headers: _headers,
8269
8301
  contentType: "application/json",
8270
- queryString: url_exports.queryBuilder().mergeAdditional(requestOptions?.queryParams).build(),
8302
+ queryString: url_exports.queryBuilder().addMany(_queryParams).mergeAdditional(requestOptions?.queryParams).build(),
8271
8303
  requestType: "json",
8272
8304
  body: _body,
8273
8305
  timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1e3,
@@ -8298,7 +8330,7 @@ var IdentityClient = class {
8298
8330
  return handleNonStatusCodeError(_response.error, _response.rawResponse, "PUT", "/v1/namespaces/{namespace}");
8299
8331
  }
8300
8332
  /**
8301
- * 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.
8333
+ * 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.
8302
8334
  *
8303
8335
  * @param {Vectros.DeleteNamespaceRequest} request
8304
8336
  * @param {IdentityClient.RequestOptions} requestOptions - Request-specific configuration.
@@ -8316,7 +8348,10 @@ var IdentityClient = class {
8316
8348
  return HttpResponsePromise.fromPromise(this.__deleteNamespace(request, requestOptions));
8317
8349
  }
8318
8350
  async __deleteNamespace(request, requestOptions) {
8319
- const { namespace } = request;
8351
+ const { namespace, contextId } = request;
8352
+ const _queryParams = {
8353
+ contextId
8354
+ };
8320
8355
  const _authRequest = await this._options.authProvider.getAuthRequest();
8321
8356
  const _headers = mergeHeaders(
8322
8357
  _authRequest.headers,
@@ -8330,7 +8365,7 @@ var IdentityClient = class {
8330
8365
  ),
8331
8366
  method: "DELETE",
8332
8367
  headers: _headers,
8333
- queryString: url_exports.queryBuilder().mergeAdditional(requestOptions?.queryParams).build(),
8368
+ queryString: url_exports.queryBuilder().addMany(_queryParams).mergeAdditional(requestOptions?.queryParams).build(),
8334
8369
  timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1e3,
8335
8370
  maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries,
8336
8371
  abortSignal: requestOptions?.abortSignal,
@@ -8359,7 +8394,7 @@ var IdentityClient = class {
8359
8394
  return handleNonStatusCodeError(_response.error, _response.rawResponse, "DELETE", "/v1/namespaces/{namespace}");
8360
8395
  }
8361
8396
  /**
8362
- * Returns the scope namespaces registered in your account, with the reserved built-ins `org` and `client` listed first. Returns a `{data, nextCursor}` envelope.
8397
+ * Returns the scope namespaces in your account, including `org` and `client`, which are provisioned for you when the account is created. Returns a `{data, nextCursor}` envelope.
8363
8398
  *
8364
8399
  * @param {Vectros.ListNamespacesRequest} request
8365
8400
  * @param {IdentityClient.RequestOptions} requestOptions - Request-specific configuration.
@@ -8371,10 +8406,11 @@ var IdentityClient = class {
8371
8406
  return HttpResponsePromise.fromPromise(this.__listNamespaces(request, requestOptions));
8372
8407
  }
8373
8408
  async __listNamespaces(request = {}, requestOptions) {
8374
- const { startFrom, limit } = request;
8409
+ const { startFrom, limit, contextId } = request;
8375
8410
  const _queryParams = {
8376
8411
  startFrom,
8377
- limit
8412
+ limit,
8413
+ contextId
8378
8414
  };
8379
8415
  const _authRequest = await this._options.authProvider.getAuthRequest();
8380
8416
  const _headers = mergeHeaders(
@@ -8409,9 +8445,9 @@ var IdentityClient = class {
8409
8445
  return handleNonStatusCodeError(_response.error, _response.rawResponse, "GET", "/v1/namespaces");
8410
8446
  }
8411
8447
  /**
8412
- * 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.
8448
+ * Registers a new scope namespace and declares whether its values resolve to identity entities (`entityBacked`). Also requires `specificityRank`, an explicit, account-unique position in the specificity order used to break recordType schema-resolution ties. Requires a root API key or the CLI bootstrap's provisioning capability — never an ordinary partner-grantable scope. `org` and `client` are provisioned when your account is created, so registering either returns the already-registered error.
8413
8449
  *
8414
- * @param {Vectros.NamespaceRequest} request
8450
+ * @param {Vectros.RegisterNamespaceRequest} request
8415
8451
  * @param {IdentityClient.RequestOptions} requestOptions - Request-specific configuration.
8416
8452
  *
8417
8453
  * @throws {@link Vectros.BadRequestError}
@@ -8420,14 +8456,20 @@ var IdentityClient = class {
8420
8456
  *
8421
8457
  * @example
8422
8458
  * await client.identity.registerNamespace({
8423
- * namespace: "team",
8424
- * specificityRank: 1500
8459
+ * body: {
8460
+ * namespace: "team",
8461
+ * specificityRank: 1500
8462
+ * }
8425
8463
  * })
8426
8464
  */
8427
8465
  registerNamespace(request, requestOptions) {
8428
8466
  return HttpResponsePromise.fromPromise(this.__registerNamespace(request, requestOptions));
8429
8467
  }
8430
8468
  async __registerNamespace(request, requestOptions) {
8469
+ const { contextId, body: _body } = request;
8470
+ const _queryParams = {
8471
+ contextId
8472
+ };
8431
8473
  const _authRequest = await this._options.authProvider.getAuthRequest();
8432
8474
  const _headers = mergeHeaders(
8433
8475
  _authRequest.headers,
@@ -8442,9 +8484,9 @@ var IdentityClient = class {
8442
8484
  method: "POST",
8443
8485
  headers: _headers,
8444
8486
  contentType: "application/json",
8445
- queryString: url_exports.queryBuilder().mergeAdditional(requestOptions?.queryParams).build(),
8487
+ queryString: url_exports.queryBuilder().addMany(_queryParams).mergeAdditional(requestOptions?.queryParams).build(),
8446
8488
  requestType: "json",
8447
- body: request,
8489
+ body: _body,
8448
8490
  timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1e3,
8449
8491
  maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries,
8450
8492
  abortSignal: requestOptions?.abortSignal,
@@ -8729,12 +8771,13 @@ var IdentityClient = class {
8729
8771
  return handleNonStatusCodeError(_response.error, _response.rawResponse, "PUT", "/v1/users/{id}");
8730
8772
  }
8731
8773
  /**
8732
- * 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.
8774
+ * Permanently deletes a user identity. This cannot be undone. If the user is a pending invitation, the associated access profile created for that invitation is also removed. Requires the `users:d` scope. Deleting your account's last OWNER is refused (409) — an account must always retain at least one owner.
8733
8775
  *
8734
8776
  * @param {Vectros.DeleteUserRequest} request
8735
8777
  * @param {IdentityClient.RequestOptions} requestOptions - Request-specific configuration.
8736
8778
  *
8737
8779
  * @throws {@link Vectros.NotFoundError}
8780
+ * @throws {@link Vectros.ConflictError}
8738
8781
  * @throws {@link Vectros.TooManyRequestsError}
8739
8782
  *
8740
8783
  * @example
@@ -8774,6 +8817,8 @@ var IdentityClient = class {
8774
8817
  switch (_response.error.statusCode) {
8775
8818
  case 404:
8776
8819
  throw new NotFoundError(_response.error.body, _response.rawResponse);
8820
+ case 409:
8821
+ throw new ConflictError(_response.error.body, _response.rawResponse);
8777
8822
  case 429:
8778
8823
  throw new TooManyRequestsError(_response.error.body, _response.rawResponse);
8779
8824
  default:
@@ -10856,6 +10901,35 @@ var BlueprintSchemaSchema = z.object({
10856
10901
  });
10857
10902
  }
10858
10903
  });
10904
+ var CAPABILITY_NAME_RE = /^[a-z][a-z0-9-]*$/;
10905
+ var GrantedCapabilitiesSchema = z.array(z.string().min(1)).optional().superRefine((caps, ctx) => {
10906
+ if (!caps) return;
10907
+ const seen = /* @__PURE__ */ new Set();
10908
+ caps.forEach((name, i) => {
10909
+ if (name === "") return;
10910
+ if (name === "*") {
10911
+ ctx.addIssue({
10912
+ code: z.ZodIssueCode.custom,
10913
+ path: [i],
10914
+ message: "'*' is not a capability \u2014 granted_capabilities has no wildcard, and a '*' in allowedActions confers zero capabilities. Name each capability explicitly."
10915
+ });
10916
+ } else if (!CAPABILITY_NAME_RE.test(name)) {
10917
+ ctx.addIssue({
10918
+ code: z.ZodIssueCode.custom,
10919
+ path: [i],
10920
+ 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)`
10921
+ });
10922
+ }
10923
+ if (seen.has(name)) {
10924
+ ctx.addIssue({
10925
+ code: z.ZodIssueCode.custom,
10926
+ path: [i],
10927
+ message: `'${name}' is declared more than once`
10928
+ });
10929
+ }
10930
+ seen.add(name);
10931
+ });
10932
+ });
10859
10933
  var BlueprintAccessProfileSchema = z.object({
10860
10934
  // Validated structurally here; the SCOPE GATE (in @vectros-ai/cli) is
10861
10935
  // what enforces the data-plane-only security boundary.
@@ -10887,11 +10961,31 @@ var BlueprintAccessProfileSchema = z.object({
10887
10961
  // below (`lintIdentityOverrides`, placeholder-aware — see that function's
10888
10962
  // docstring); this `.min(1)` is only the shape-level "non-blank" guard on
10889
10963
  // the value, not the grammar check.
10890
- identityOverrides: z.record(z.string().min(1)).optional()
10964
+ identityOverrides: z.record(z.string().min(1)).optional(),
10965
+ // Optional capabilities granted to the blueprint's own service-principal
10966
+ // profile. DECISION: allowed here, not role-clause-only. On the platform,
10967
+ // an access profile's scopes are themselves a list of clauses — the exact
10968
+ // same wire shape a role clause carries — and the loader already builds
10969
+ // this profile as ONE such clause. Refusing `capabilities` here while
10970
+ // allowing it on a role clause would be an asymmetry this FORMAT package
10971
+ // has no basis to invent: nothing on the platform side distinguishes the
10972
+ // two, and whether granting a capability to a blueprint's own service key
10973
+ // is sound POLICY is exactly the question the CLI's scope gate exists to
10974
+ // answer (a separate package, out of scope for this format layer) — the
10975
+ // same posture this package already takes with `allowedActions` above.
10976
+ //
10977
+ // Worth naming for whoever writes that gate: this profile is a long-lived,
10978
+ // non-human-attributable machine credential (typically stored in CI/CD
10979
+ // secrets), not an ephemeral human session — a materially different
10980
+ // blast-radius question from granting the same capability to a role that
10981
+ // a person is bound to. The gate should weigh the two separately rather
10982
+ // than reuse one policy for both.
10983
+ capabilities: GrantedCapabilitiesSchema
10891
10984
  }).strict();
10892
10985
  var BlueprintRoleClauseSchema = z.object({
10893
10986
  allowedActions: z.array(z.string().min(1)).min(1),
10894
- dataScope: z.record(z.array(z.union([z.string().min(1), z.null()]))).optional()
10987
+ dataScope: z.record(z.array(z.union([z.string().min(1), z.null()]))).optional(),
10988
+ capabilities: GrantedCapabilitiesSchema
10895
10989
  }).strict();
10896
10990
  var BlueprintRolesSchema = z.record(z.array(BlueprintRoleClauseSchema).min(1));
10897
10991
  var IDENTITY_NAMESPACE_RE = /^[a-z][a-z0-9_-]{1,31}$/;
@@ -10929,7 +11023,7 @@ var BlueprintServicePrincipalSchema = z.object({
10929
11023
  }).strict();
10930
11024
  var ISSUER_ID_RE = CONTEXT_ID_RE;
10931
11025
  var BlueprintIssuerSchema = z.object({
10932
- /** Short slug identifying this issuer within your tenant. Immutable once registered. */
11026
+ /** Short slug identifying this issuer within your account. Immutable once registered. */
10933
11027
  issuerId: z.string().regex(ISSUER_ID_RE, {
10934
11028
  message: "issuerId must be 3-31 chars, start with a lowercase letter, then lowercase letters/digits/dashes (e.g. 'auth0-prod')"
10935
11029
  }),
@@ -10944,7 +11038,12 @@ var BlueprintIssuerSchema = z.object({
10944
11038
  * this as an ordinary per-API/application default).
10945
11039
  */
10946
11040
  audience: z.string().min(1),
10947
- /** Which app context an exchanged token targets. Must be an existing app context. */
11041
+ /**
11042
+ * Which app context an exchanged token targets. **Must equal the blueprint's own
11043
+ * `contextId`** — an issuer is a trust anchor, so a blueprint may only attach one to the
11044
+ * context it actually provisions. One IdP account serving several contexts needs one entry
11045
+ * per context, each in that context's own blueprint.
11046
+ */
10948
11047
  contextId: z.string().regex(CONTEXT_ID_RE, {
10949
11048
  message: "contextId must be 3-31 chars, start with a lowercase letter, then lowercase letters/digits/dashes (e.g. 'casework')"
10950
11049
  }),
@@ -10971,6 +11070,151 @@ var BlueprintIssuerSchema = z.object({
10971
11070
  }).strict()
10972
11071
  ).optional()
10973
11072
  }).strict();
11073
+ var BlueprintNamespaceSchema = z.object({
11074
+ /** The namespace to register — same grammar as a `scope:<namespace>` key. */
11075
+ namespace: z.string().min(1),
11076
+ /**
11077
+ * When `true`, every value in this namespace must resolve to an existing
11078
+ * identity entity (create via `entities:c` at the in-context phase, or a
11079
+ * declared `identities:` entry). When `false`/omitted, values are
11080
+ * free-form strings validated by grammar only.
11081
+ */
11082
+ entityBacked: z.boolean().optional(),
11083
+ /**
11084
+ * This namespace's position in your account's specificity order (used to
11085
+ * break a tie when a caller holds two scope dimensions at once). Required
11086
+ * by the platform on registration; MUST be unique across every namespace
11087
+ * in the account, including `org` (1000) and `client` (2000) — pick a
11088
+ * value outside that pair, and outside any other namespace this same
11089
+ * account might register (its own, or another blueprint's).
11090
+ */
11091
+ specificityRank: z.number().int().min(0).max(1e6),
11092
+ /**
11093
+ * Optional membership backing — declares which record type + field hold
11094
+ * grants of THIS namespace's values. Supply together with
11095
+ * `membershipTargetField`, or omit both. Declaring this grants nobody
11096
+ * anything on its own: a role opts in explicitly with
11097
+ * `${{ member.scope.<namespace> }}` in its `dataScope`.
11098
+ */
11099
+ membershipRecordType: z.string().min(1).optional(),
11100
+ /** The field on `membershipRecordType` naming the user a grant is FOR. Required alongside `membershipRecordType`. */
11101
+ membershipTargetField: z.string().min(1).optional(),
11102
+ /**
11103
+ * The field on `membershipRecordType` naming a grant's LEVEL, so the same
11104
+ * user can hold different levels in different values of this namespace.
11105
+ * Supply together with `membershipLevels`, or omit both for plain
11106
+ * in-or-out membership.
11107
+ */
11108
+ membershipLevelField: z.string().min(1).optional(),
11109
+ /** The complete set of level labels this namespace allows. Required alongside `membershipLevelField`. */
11110
+ membershipLevels: z.array(z.string().min(1)).optional()
11111
+ }).strict();
11112
+ var MEMBERSHIP_FIELD_NAME_RE = /^[A-Za-z0-9_-]{1,64}$/;
11113
+ function lintNamespaces(value, ctx) {
11114
+ if (!value || typeof value !== "object" || !("namespaces" in value)) return;
11115
+ const namespaces = value.namespaces;
11116
+ if (!Array.isArray(namespaces)) return;
11117
+ const rawSchemas = value.schemas;
11118
+ const declaredSchemaTypes = new Set(
11119
+ (Array.isArray(rawSchemas) ? rawSchemas : []).map((s) => s && typeof s === "object" && "typeName" in s ? s.typeName : void 0).filter((t) => typeof t === "string")
11120
+ );
11121
+ const seenNames = /* @__PURE__ */ new Set();
11122
+ const seenRanks = /* @__PURE__ */ new Map();
11123
+ namespaces.forEach((entry, i) => {
11124
+ if (!entry || typeof entry !== "object") return;
11125
+ const ns = entry;
11126
+ const path4 = ["namespaces", i];
11127
+ if (typeof ns.namespace === "string") {
11128
+ const err = scopeNamespaceGrammarError(ns.namespace);
11129
+ if (err) ctx.addIssue({ code: z.ZodIssueCode.custom, path: [...path4, "namespace"], message: err });
11130
+ if (seenNames.has(ns.namespace)) {
11131
+ ctx.addIssue({
11132
+ code: z.ZodIssueCode.custom,
11133
+ path: [...path4, "namespace"],
11134
+ message: `'${ns.namespace}' is declared more than once in namespaces[]`
11135
+ });
11136
+ }
11137
+ seenNames.add(ns.namespace);
11138
+ }
11139
+ if (typeof ns.specificityRank === "number") {
11140
+ const prior = seenRanks.get(ns.specificityRank);
11141
+ if (prior !== void 0) {
11142
+ ctx.addIssue({
11143
+ code: z.ZodIssueCode.custom,
11144
+ path: [...path4, "specificityRank"],
11145
+ message: `specificityRank ${ns.specificityRank} is already used by namespaces[${prior}] \u2014 every namespace's rank must be unique`
11146
+ });
11147
+ }
11148
+ seenRanks.set(ns.specificityRank, i);
11149
+ }
11150
+ const hasRecordType = ns.membershipRecordType !== void 0;
11151
+ const hasTargetField = ns.membershipTargetField !== void 0;
11152
+ if (hasRecordType !== hasTargetField) {
11153
+ ctx.addIssue({
11154
+ code: z.ZodIssueCode.custom,
11155
+ path: [...path4, "membershipRecordType"],
11156
+ message: "membershipRecordType and membershipTargetField must be declared together, or neither"
11157
+ });
11158
+ }
11159
+ if (typeof ns.membershipRecordType === "string" && !declaredSchemaTypes.has(ns.membershipRecordType)) {
11160
+ ctx.addIssue({
11161
+ code: z.ZodIssueCode.custom,
11162
+ path: [...path4, "membershipRecordType"],
11163
+ 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.`
11164
+ });
11165
+ }
11166
+ for (const [field, value2] of [
11167
+ ["membershipRecordType", ns.membershipRecordType],
11168
+ ["membershipTargetField", ns.membershipTargetField],
11169
+ ["membershipLevelField", ns.membershipLevelField]
11170
+ ]) {
11171
+ if (typeof value2 === "string" && !MEMBERSHIP_FIELD_NAME_RE.test(value2)) {
11172
+ ctx.addIssue({
11173
+ code: z.ZodIssueCode.custom,
11174
+ path: [...path4, field],
11175
+ message: `${field} '${value2}' is invalid \u2014 must be 1-64 characters of letters, digits, underscores, or hyphens`
11176
+ });
11177
+ }
11178
+ }
11179
+ const hasLevelField = ns.membershipLevelField !== void 0;
11180
+ const hasLevels = Array.isArray(ns.membershipLevels) && ns.membershipLevels.length > 0;
11181
+ if (hasLevelField !== hasLevels) {
11182
+ ctx.addIssue({
11183
+ code: z.ZodIssueCode.custom,
11184
+ path: [...path4, "membershipLevelField"],
11185
+ message: "membershipLevelField and a non-empty membershipLevels must be declared together, or neither"
11186
+ });
11187
+ }
11188
+ if ((hasLevelField || hasLevels) && !hasRecordType) {
11189
+ ctx.addIssue({
11190
+ code: z.ZodIssueCode.custom,
11191
+ path: [...path4, "membershipLevelField"],
11192
+ message: "membershipLevelField/membershipLevels require membershipRecordType + membershipTargetField too"
11193
+ });
11194
+ }
11195
+ if (Array.isArray(ns.membershipLevels)) {
11196
+ const seenLevels = /* @__PURE__ */ new Set();
11197
+ for (const level of ns.membershipLevels) {
11198
+ if (typeof level !== "string") continue;
11199
+ const err = scopeNamespaceGrammarError(level);
11200
+ if (err) {
11201
+ ctx.addIssue({
11202
+ code: z.ZodIssueCode.custom,
11203
+ path: [...path4, "membershipLevels"],
11204
+ message: `level '${level}': ${err}`
11205
+ });
11206
+ } else if (seenLevels.has(level)) {
11207
+ ctx.addIssue({
11208
+ code: z.ZodIssueCode.custom,
11209
+ path: [...path4, "membershipLevels"],
11210
+ message: `membershipLevels contains '${level}' twice`
11211
+ });
11212
+ }
11213
+ seenLevels.add(level);
11214
+ }
11215
+ }
11216
+ });
11217
+ }
10974
11218
  var SeedCommonShape = {
10975
11219
  /** The schema typeName this seed instantiates (should match a declared schema). */
10976
11220
  typeName: z.string().min(1),
@@ -11011,7 +11255,7 @@ var BlueprintSeedRecordSchema = z.discriminatedUnion("surface", [
11011
11255
  RecordSeedSchema,
11012
11256
  DocumentSeedSchema
11013
11257
  ]);
11014
- var SELF_TOKEN_RE = /\$\{\{\s*(?:self|under)\.[^}]*?\s*\}\}/;
11258
+ var SELF_TOKEN_RE = /\$\{\{\s*(?:self|under|member)\.[^}]*?\s*\}\}/;
11015
11259
  function lintSelfTokenPlacement(value, ctx) {
11016
11260
  const walk = (node, path4, inRoleDataScope) => {
11017
11261
  if (typeof node === "string") {
@@ -11019,7 +11263,7 @@ function lintSelfTokenPlacement(value, ctx) {
11019
11263
  ctx.addIssue({
11020
11264
  code: z.ZodIssueCode.custom,
11021
11265
  path: path4,
11022
- message: "'${{ self.* }}' is a runtime per-principal placeholder \u2014 it is only valid inside a roles[].dataScope value"
11266
+ message: "'${{ self.* }}' / '${{ member.* }}' is a runtime placeholder \u2014 it is only valid inside a roles[].dataScope value"
11023
11267
  });
11024
11268
  }
11025
11269
  return;
@@ -11219,9 +11463,36 @@ function lintSeedSurfaces(value, ctx) {
11219
11463
  }
11220
11464
  });
11221
11465
  }
11466
+ function lintIdentitySurfaces(value, ctx) {
11467
+ const bp = value;
11468
+ if (!Array.isArray(bp.schemas)) return;
11469
+ bp.schemas.forEach((schema, i) => {
11470
+ if (!schema.allowedSurfaces?.includes("user")) return;
11471
+ ctx.addIssue({
11472
+ code: z.ZodIssueCode.custom,
11473
+ path: ["schemas", i, "allowedSurfaces"],
11474
+ message: `schema '${schema.typeName}': allowedSurfaces cannot include 'user' \u2014 a user-surfaced schema is account-global and requires a root API key, which no blueprint credential ever holds, so this would fail at apply with a 403. Model the data as a 'record' scoped to the user, or as an 'entity' schema (which a blueprint CAN provision, in its own app context).`
11475
+ });
11476
+ });
11477
+ }
11478
+ function lintIssuerContext(value, ctx) {
11479
+ const bp = value;
11480
+ if (!Array.isArray(bp.issuers)) return;
11481
+ bp.issuers.forEach((issuer, i) => {
11482
+ if (issuer.contextId === void 0 || issuer.contextId === bp.contextId) return;
11483
+ ctx.addIssue({
11484
+ code: z.ZodIssueCode.custom,
11485
+ path: ["issuers", i, "contextId"],
11486
+ message: `issuer '${issuer.issuerId}' targets app context '${issuer.contextId}', but this blueprint provisions '${bp.contextId}'. An issuer must target the blueprint's own context \u2014 everything else a blueprint creates does, and an issuer that points elsewhere attaches an identity provider to an app context the blueprint never mentions. Set it to '${bp.contextId}', or move this issuer into a blueprint for '${issuer.contextId}'.`
11487
+ });
11488
+ });
11489
+ }
11222
11490
  var BLUEPRINT_FIELD_PHASES = Object.freeze({
11223
11491
  servicePrincipal: "bootstrap",
11224
11492
  issuers: "bootstrap",
11493
+ // Registration requires the platform's provisioning:c capability, which
11494
+ // only the bootstrap credential carries — same reasoning as issuers.
11495
+ namespaces: "bootstrap",
11225
11496
  schemas: "in-context",
11226
11497
  accessProfile: "in-context",
11227
11498
  roles: "in-context",
@@ -11252,7 +11523,16 @@ var BlueprintSchema = z.object({
11252
11523
  * service-principal creation — NOT the in-context load (see
11253
11524
  * {@link BlueprintIssuerSchema}).
11254
11525
  */
11255
- issuers: z.array(BlueprintIssuerSchema).optional()
11526
+ issuers: z.array(BlueprintIssuerSchema).optional(),
11527
+ /**
11528
+ * Optional namespaces to register before anything else needs them — see
11529
+ * {@link BlueprintNamespaceSchema}. Applied in the BOOTSTRAP-token phase,
11530
+ * alongside app-context/service-principal creation and issuers, BEFORE
11531
+ * schemas/seed (an `entityBacked` namespace's entities, and an
11532
+ * `allowedSurfaces: ['entity']` schema, both need the registration to
11533
+ * already exist). `org`/`client` need no entry — they exist already.
11534
+ */
11535
+ namespaces: z.array(BlueprintNamespaceSchema).optional()
11256
11536
  }).strict().superRefine((bp, ctx) => {
11257
11537
  lintSelfTokenPlacement(bp, ctx);
11258
11538
  lintIdentityRefs(bp, ctx);
@@ -11260,6 +11540,9 @@ var BlueprintSchema = z.object({
11260
11540
  lintNamespacedScopeArrays(bp, ctx);
11261
11541
  lintDataScopeKeys(bp, ctx);
11262
11542
  lintSeedSurfaces(bp, ctx);
11543
+ lintIdentitySurfaces(bp, ctx);
11544
+ lintIssuerContext(bp, ctx);
11545
+ lintNamespaces(bp, ctx);
11263
11546
  });
11264
11547
  function formatIssuePath(path4) {
11265
11548
  let out = "";
@@ -11414,9 +11697,11 @@ function deriveSuffix(contextId) {
11414
11697
  var WHOLE_TOKEN_RE = /^\$\{\{\s*([A-Za-z_]\w*)\.([A-Za-z_]\w*)\s*\}\}$/;
11415
11698
  var TOKEN_SCAN_RE = /\$\$\{\{|\$\{\{([\s\S]*?)\}\}|\$\{\{/g;
11416
11699
  var INNER_REF_RE = /^([A-Za-z_]\w*)\.([A-Za-z_]\w*)$/;
11417
- var DEFERRED_NAMESPACES = /* @__PURE__ */ new Set(["self", "identities", "under"]);
11700
+ var DEFERRED_NAMESPACES = /* @__PURE__ */ new Set(["self", "identities", "under", "member"]);
11418
11701
  var DEFERRED_NS_ALTERNATION = [...DEFERRED_NAMESPACES].join("|");
11419
- var DEFERRED_INNER_RE = new RegExp(`^(?:${DEFERRED_NS_ALTERNATION})(?:\\.[A-Za-z_]\\w*)+$`);
11702
+ var DEFERRED_INNER_RE = new RegExp(
11703
+ `^(?:${DEFERRED_NS_ALTERNATION})(?:\\.[A-Za-z_]\\w*)+(?::[A-Za-z_]\\w*)?$`
11704
+ );
11420
11705
  function resolveRef(ns, name, ctx, path4) {
11421
11706
  if (ns === "inputs") {
11422
11707
  if (!(name in ctx.declared)) {
@@ -11715,9 +12000,11 @@ var taskManagement = {
11715
12000
  // Least-privilege profile — MUST pass the CLI scope gate.
11716
12001
  // r/c/u + search + schema discovery. NOT records:d (least privilege).
11717
12002
  accessProfile: {
11718
- allowedActions: ["records:r", "records:c", "records:u", "search:r", "schemas:r"]
11719
- // dataScope omitted → tenant-level shared tracker. Add a `scope:org`
11720
- // entry for per-org isolation.
12003
+ allowedActions: ["records:r", "records:c", "records:u", "search:r", "schemas:r"],
12004
+ // dataScope: {} (present, empty) is the deliberate marker for a
12005
+ // tenant-level shared tracker — every task visible to every holder of
12006
+ // this credential. Add a `scope:org` entry instead for per-org isolation.
12007
+ dataScope: {}
11721
12008
  },
11722
12009
  servicePrincipal: {
11723
12010
  externalId: "task-management",
@@ -11740,250 +12027,6 @@ var taskManagement = {
11740
12027
  ]
11741
12028
  };
11742
12029
  var task_management_default = taskManagement;
11743
- var codingAgentMemory = {
11744
- name: "coding-agent-memory",
11745
- version: "1.0.0",
11746
- description: "Persistent project memory for a coding agent \u2014 decisions, conventions, and gotchas that survive across sessions.",
11747
- contextId: "coding-memory",
11748
- contextName: "Coding Agent \u2014 Project Memory",
11749
- schemas: [
11750
- {
11751
- // A durable architectural/product decision: what was decided, and WHY.
11752
- // The rationale is the high-value, RAG-able body — "why did we do X?"
11753
- // is the question a cold-context agent most needs answered.
11754
- typeName: "decision",
11755
- displayName: "Decision",
11756
- indexMode: "HYBRID",
11757
- // keyword on titles + semantic on statement/rationale
11758
- fields: [
11759
- {
11760
- fieldId: "title",
11761
- fieldType: "string",
11762
- required: true,
11763
- searchable: true,
11764
- validation: { minLength: 1, maxLength: 200 },
11765
- renderHints: { label: "Title", widget: "text", order: 1, displayField: true }
11766
- },
11767
- {
11768
- fieldId: "statement",
11769
- fieldType: "string",
11770
- searchable: true,
11771
- description: "What was decided, in one or two sentences.",
11772
- renderHints: { label: "Statement", widget: "textarea", order: 2 }
11773
- },
11774
- {
11775
- fieldId: "rationale",
11776
- fieldType: "string",
11777
- searchable: true,
11778
- description: "Why \u2014 the trade-offs and the context. The most-recalled field.",
11779
- renderHints: { label: "Rationale", widget: "textarea", order: 3 }
11780
- },
11781
- {
11782
- fieldId: "status",
11783
- fieldType: "enum",
11784
- filterable: true,
11785
- enumValues: ["proposed", "active", "superseded"],
11786
- renderHints: { label: "Status", widget: "select", order: 4 }
11787
- },
11788
- {
11789
- fieldId: "area",
11790
- fieldType: "string",
11791
- filterable: true,
11792
- description: 'Subsystem / module the decision applies to (e.g. "auth", "search").',
11793
- renderHints: { label: "Area", widget: "text", order: 5 }
11794
- },
11795
- {
11796
- fieldId: "decidedOn",
11797
- fieldType: "date",
11798
- description: 'ISO-8601 date. Range-queryable \u2014 "decisions made this quarter".',
11799
- renderHints: { label: "Decided on", widget: "date", order: 6 }
11800
- }
11801
- // externalId is the record's FIRST-CLASS identifier (the dedup/upsert key + the
11802
- // value a `reference` resolves against) — it is NOT a payload field, so it is not
11803
- // declared here; the loader sends it top-level on the RecordRequest.
11804
- ],
11805
- // `externalId` is a first-class identifier with its own finder (exact
11806
- // get/upsert) — look it up directly, never redeclare it as a schema lookup.
11807
- // The choice below is MIGRATION-LOCKED once the schema is live:
11808
- // • `area`/`status` — EQUALITY (categorical): "list active decisions",
11809
- // "all decisions in the auth area". 2 of the 7 fast index slots.
11810
- // • `decidedOn` — RANGE: ordered `from`/`to`/`prefix` over the date.
11811
- lookupFields: ["area", "status", { fieldName: "decidedOn", rangeEnabled: true }]
11812
- },
11813
- {
11814
- // A coding convention the team follows — the agent reads these before it
11815
- // writes code so it matches the surrounding style instead of inventing one.
11816
- typeName: "convention",
11817
- displayName: "Convention",
11818
- indexMode: "HYBRID",
11819
- fields: [
11820
- {
11821
- fieldId: "name",
11822
- fieldType: "string",
11823
- required: true,
11824
- searchable: true,
11825
- validation: { minLength: 1, maxLength: 200 },
11826
- renderHints: { label: "Name", widget: "text", order: 1, displayField: true }
11827
- },
11828
- {
11829
- fieldId: "rule",
11830
- fieldType: "string",
11831
- searchable: true,
11832
- description: "The convention itself, stated as an imperative.",
11833
- renderHints: { label: "Rule", widget: "textarea", order: 2 }
11834
- },
11835
- {
11836
- fieldId: "area",
11837
- fieldType: "string",
11838
- filterable: true,
11839
- renderHints: { label: "Area", widget: "text", order: 3 }
11840
- },
11841
- {
11842
- // Parallels `decision.status` so a convention can be RETIRED rather than
11843
- // deleted (the blueprint omits records:d — memory is superseded, not lost).
11844
- fieldId: "status",
11845
- fieldType: "enum",
11846
- filterable: true,
11847
- enumValues: ["active", "retired"],
11848
- renderHints: { label: "Status", widget: "select", order: 4 }
11849
- },
11850
- {
11851
- // A typed REFERENCE to the decision that established this convention — a
11852
- // foreign-key link between record types (the knowledge-graph edge). The
11853
- // platform requires both the target type AND its surface; the value is the
11854
- // target decision's externalId (the default `targetField`). Provenance:
11855
- // "why does this convention exist? → open the decision behind it." Not
11856
- // searchable (a foreign-key id is search noise); declared as an equality
11857
- // lookup below so you can also enumerate "conventions from decision X".
11858
- fieldId: "establishedByDecision",
11859
- fieldType: "reference",
11860
- targetTypeName: "decision",
11861
- targetSurface: "record",
11862
- targetField: "externalId",
11863
- cardinality: "one",
11864
- renderHints: { label: "Established by (decision)", order: 5 }
11865
- },
11866
- {
11867
- fieldId: "adoptedOn",
11868
- fieldType: "date",
11869
- description: "ISO-8601 \u2014 when this convention was adopted. Range-queryable.",
11870
- renderHints: { label: "Adopted on", widget: "date", order: 6 }
11871
- }
11872
- // externalId: first-class identifier, not a payload field (see `decision`).
11873
- ],
11874
- // externalId has its own first-class finder for exact get — look it up
11875
- // directly, not as a schema lookup. Locked equality-vs-range choices:
11876
- // • `area`/`status` — EQUALITY: "active conventions for auth".
11877
- // • `establishedByDecision` — EQUALITY on the reference value: enumerate
11878
- // "conventions established by decision X" (a forward link query; the
11879
- // platform has no reverse-reference index on this surface).
11880
- // • `adoptedOn` — RANGE over the adoption date.
11881
- // 3 equality lookups (of 7 fast slots) + 1 range row.
11882
- lookupFields: ["area", "status", "establishedByDecision", { fieldName: "adoptedOn", rangeEnabled: true }]
11883
- },
11884
- {
11885
- // A gotcha / sharp edge: a symptom, its cause, and the fix. Saves the
11886
- // agent (and the human) from re-discovering the same trap.
11887
- typeName: "gotcha",
11888
- displayName: "Gotcha",
11889
- indexMode: "HYBRID",
11890
- fields: [
11891
- {
11892
- fieldId: "symptom",
11893
- fieldType: "string",
11894
- required: true,
11895
- searchable: true,
11896
- validation: { minLength: 1, maxLength: 500 },
11897
- renderHints: { label: "Symptom", widget: "textarea", order: 1, displayField: true }
11898
- },
11899
- {
11900
- fieldId: "cause",
11901
- fieldType: "string",
11902
- searchable: true,
11903
- renderHints: { label: "Cause", widget: "textarea", order: 2 }
11904
- },
11905
- {
11906
- fieldId: "fix",
11907
- fieldType: "string",
11908
- searchable: true,
11909
- renderHints: { label: "Fix", widget: "textarea", order: 3 }
11910
- },
11911
- {
11912
- fieldId: "area",
11913
- fieldType: "string",
11914
- filterable: true,
11915
- renderHints: { label: "Area", widget: "text", order: 4 }
11916
- },
11917
- {
11918
- // A gotcha can be RETIRED once the underlying trap is fixed for good —
11919
- // superseded, not deleted (the blueprint omits records:d).
11920
- fieldId: "status",
11921
- fieldType: "enum",
11922
- filterable: true,
11923
- enumValues: ["active", "retired"],
11924
- renderHints: { label: "Status", widget: "select", order: 5 }
11925
- },
11926
- {
11927
- fieldId: "discoveredOn",
11928
- fieldType: "date",
11929
- description: "ISO-8601 \u2014 when this gotcha was first hit. Range-queryable.",
11930
- renderHints: { label: "Discovered on", widget: "date", order: 6 }
11931
- }
11932
- // externalId: first-class identifier, not a payload field (see `decision`).
11933
- ],
11934
- // externalId has its own first-class finder for exact get — look it up
11935
- // directly, not as a schema lookup. `area`/`status` EQUALITY ("active
11936
- // gotchas in search"); `discoveredOn` RANGE over the discovery date.
11937
- lookupFields: ["area", "status", { fieldName: "discoveredOn", rangeEnabled: true }]
11938
- }
11939
- ],
11940
- // Least-privilege profile — MUST pass the CLI scope gate.
11941
- // r/c/u + search + schema discovery + inference:r (grounded "why did we do X?"
11942
- // recall over the captured rationale). NOT records:d: memory is superseded,
11943
- // not deleted, so the project's decision history stays auditable.
11944
- accessProfile: {
11945
- allowedActions: ["records:r", "records:c", "records:u", "search:r", "schemas:r", "inference:r"]
11946
- },
11947
- servicePrincipal: {
11948
- externalId: "coding-agent-memory",
11949
- displayName: "Coding Agent \u2014 Project Memory"
11950
- },
11951
- seed: [
11952
- {
11953
- // Seeded FIRST so the convention below resolves its reference to it — the
11954
- // loader creates seeds in array order, and a reference target must exist when
11955
- // the referencing record is written.
11956
- surface: "record",
11957
- typeName: "decision",
11958
- externalId: "seed-use-vectros-for-memory",
11959
- fields: {
11960
- title: "Use Vectros as the coding agent\u2019s project memory",
11961
- statement: "Persist decisions, conventions, and gotchas as Vectros records so they survive across agent sessions.",
11962
- 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.",
11963
- status: "active",
11964
- area: "getting-started",
11965
- decidedOn: "2026-06-14"
11966
- }
11967
- },
11968
- {
11969
- // Demonstrates a live typed link at bootstrap: this convention references the
11970
- // decision above by its externalId, so "open the decision behind this rule"
11971
- // works the moment the blueprint is applied.
11972
- surface: "record",
11973
- typeName: "convention",
11974
- externalId: "seed-record-the-why",
11975
- fields: {
11976
- name: "Record the why, not just the what",
11977
- 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.",
11978
- area: "getting-started",
11979
- status: "active",
11980
- establishedByDecision: "seed-use-vectros-for-memory",
11981
- adoptedOn: "2026-06-14"
11982
- }
11983
- }
11984
- ]
11985
- };
11986
- var coding_agent_memory_default = codingAgentMemory;
11987
12030
  var DATA_PLANE_ACTIONS = [
11988
12031
  "records:r",
11989
12032
  "records:c",
@@ -13110,7 +13153,11 @@ var agenticSdlc = {
13110
13153
  // mints for THIS blueprint's service principal (the MCP/API runtime). See
13111
13154
  // DATA_PLANE_ACTIONS above for the action set + rationale.
13112
13155
  accessProfile: {
13113
- allowedActions: DATA_PLANE_ACTIONS
13156
+ allowedActions: DATA_PLANE_ACTIONS,
13157
+ // dataScope: {} (present, empty) is the deliberate marker for tenant-wide
13158
+ // reach — this service key IS the runtime's own access to the whole KB,
13159
+ // not a multi-owner resource to isolate.
13160
+ dataScope: {}
13114
13161
  },
13115
13162
  // A reusable `editor` role for the HUMAN owner — DISTINCT from `accessProfile`
13116
13163
  // (which scopes only the service-principal key). `bootstrap` provisions this
@@ -13130,7 +13177,9 @@ var agenticSdlc = {
13130
13177
  // which includes members' private memory — the trusted-administrator tradeoff,
13131
13178
  // called out in the bundled guide. Scope it down if your deployment wants
13132
13179
  // curator access without private-memory visibility.
13133
- editor: [{ allowedActions: EDITOR_ACTIONS }],
13180
+ // dataScope: {} echoes the "deliberately UNSCOPED (whole-context)" call-out
13181
+ // above as an explicit marker, not just prose — see it for the tradeoff.
13182
+ editor: [{ allowedActions: EDITOR_ACTIONS, dataScope: {} }],
13134
13183
  // A team member (or their agent) — TWO memory tiers as two UNIONed clauses
13135
13184
  // (a record is accessible when ANY clause grants it; within one clause every
13136
13185
  // dataScope dimension must match):
@@ -13150,7 +13199,9 @@ var agenticSdlc = {
13150
13199
  // `vectros join agentic-sdlc --role member`; verify a binding with
13151
13200
  // `vectros access explain --principal me --context agentic-sdlc`.
13152
13201
  member: [
13153
- { allowedActions: MEMBER_SHARED_READ_ACTIONS },
13202
+ // dataScope: {} echoes clause 1's own "UNSCOPED, but safe" call-out above
13203
+ // as an explicit marker, not just prose.
13204
+ { allowedActions: MEMBER_SHARED_READ_ACTIONS, dataScope: {} },
13154
13205
  {
13155
13206
  // `inference:r` is IN this self-scoped clause (not only clause 1) so a
13156
13207
  // member's `rag_ask` can ground on its OWN private memory — the read
@@ -13265,7 +13316,11 @@ var secondBrain = {
13265
13316
  // (so the agent can `rag_ask` a grounded, cited question over your notes). No
13266
13317
  // records:d: notes are archived (status → 'archived'), never deleted.
13267
13318
  accessProfile: {
13268
- allowedActions: ["records:r", "records:c", "records:u", "search:r", "schemas:r", "inference:r"]
13319
+ allowedActions: ["records:r", "records:c", "records:u", "search:r", "schemas:r", "inference:r"],
13320
+ // dataScope: {} (present, empty) is the deliberate marker for tenant-wide
13321
+ // reach — this is a single personal-knowledge-base credential with one
13322
+ // holder, not a multi-owner shared resource to isolate.
13323
+ dataScope: {}
13269
13324
  },
13270
13325
  servicePrincipal: {
13271
13326
  externalId: "second-brain",
@@ -13397,7 +13452,11 @@ var clinicalIntake = {
13397
13452
  // reveal qualifier (the demo key literally cannot un-redact the PHI fields),
13398
13453
  // and NO inference:r (we never point a RAG capability at a PHI corpus here).
13399
13454
  accessProfile: {
13400
- allowedActions: ["records:r", "records:c", "records:u", "search:r", "schemas:r"]
13455
+ allowedActions: ["records:r", "records:c", "records:u", "search:r", "schemas:r"],
13456
+ // dataScope: {} (present, empty) is the deliberate marker for tenant-wide
13457
+ // reach, unchanged from this demo's original intent — a real clinical
13458
+ // deployment should scope this (e.g. `scope:org`) before handling real PHI.
13459
+ dataScope: {}
13401
13460
  },
13402
13461
  servicePrincipal: {
13403
13462
  externalId: "clinical-intake",
@@ -13426,7 +13485,6 @@ var clinicalIntake = {
13426
13485
  var clinical_intake_default = clinicalIntake;
13427
13486
  var BUNDLED_BLUEPRINTS = [
13428
13487
  task_management_default,
13429
- coding_agent_memory_default,
13430
13488
  agentic_sdlc_default,
13431
13489
  second_brain_default,
13432
13490
  clinical_intake_default
@@ -13725,18 +13783,17 @@ async function mintBridgeToken(opts) {
13725
13783
  ttlSeconds = DEFAULT_BRIDGE_TTL_SECONDS,
13726
13784
  tenant = "live",
13727
13785
  useScopedToken = false,
13786
+ contextId,
13728
13787
  fetchImpl = fetch
13729
13788
  } = opts;
13730
13789
  const base = apiBaseUrl.replace(/\/$/, "");
13731
13790
  const path4 = bridgeTokenPath(useScopedToken);
13732
- const url = `${base}${path4}?ttl=${encodeURIComponent(String(ttlSeconds))}&tenant=${encodeURIComponent(tenant)}`;
13733
- return fetchStToken(
13734
- url,
13735
- cognitoToken,
13736
- fetchImpl,
13737
- path4,
13738
- (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)` : ""
13739
- );
13791
+ let url = `${base}${path4}?ttl=${encodeURIComponent(String(ttlSeconds))}&tenant=${encodeURIComponent(tenant)}`;
13792
+ if (contextId) url += `&context=${encodeURIComponent(contextId)}`;
13793
+ return fetchStToken(url, cognitoToken, fetchImpl, path4, (status) => {
13794
+ if (status !== 404 || useScopedToken || contextId) return "";
13795
+ return ` (the dedicated ${CLI_BOOTSTRAP_TOKEN_PATH} endpoint may not be deployed yet \u2014 retry with --use-scoped-token for the fallback path)`;
13796
+ });
13740
13797
  }
13741
13798
  async function mintContextToken(opts) {
13742
13799
  const {
@@ -14119,8 +14176,29 @@ var KNOWN_CONTROL_PLANE_RESOURCES = [
14119
14176
  // control-plane, unlike per-namespace entity CRUD. (Cosmetic: any
14120
14177
  // non-data-plane resource hard-rejects whether or not it is listed here;
14121
14178
  // this list only sharpens the error message.)
14179
+ //
14180
+ // ⚠️ This bars `namespaces` as a SCOPE — no partner-authored role can ever
14181
+ // be granted it. It does NOT bar a blueprint's own top-level `namespaces[]`
14182
+ // FORMAT field (@vectros-ai/blueprints), which deliberately DOES write this
14183
+ // same registry — the same "DISCRETIONARY vs UNCONDITIONAL" distinction
14184
+ // {@link PARTIALLY_ADMITTED_OPS} draws for `entities`, applied here: the
14185
+ // write rides `provisioning:c` (non-grantable — no partner role can ever
14186
+ // hold it, the same bearer `keys`/`app-contexts` writes already require)
14187
+ // and the platform confines it to the blueprint's OWN context
14188
+ // unconditionally, never tenant-wide, so the reach is bounded to what the
14189
+ // blueprint already provisions, never the pack's say-so alone. See
14190
+ // `@vectros-ai/blueprints`' `namespaces[]` doc for the full design.
14122
14191
  "namespaces"
14123
14192
  ];
14193
+ var PARTIALLY_ADMITTED_OPS = /* @__PURE__ */ new Map([
14194
+ ["profiles", /* @__PURE__ */ new Set(["r", "u", "d"])]
14195
+ ]);
14196
+ var SELF_QUALIFIED_OPS = /* @__PURE__ */ new Map([
14197
+ ["profiles", /* @__PURE__ */ new Set(["c"])]
14198
+ ]);
14199
+ var UNQUALIFIABLE_WITH_ANY_TAIL = /* @__PURE__ */ new Map([
14200
+ ["profiles", /* @__PURE__ */ new Set(["r"])]
14201
+ ]);
14124
14202
  var VALID_OPS = /* @__PURE__ */ new Set(["c", "r", "u", "d"]);
14125
14203
  var DATA_PLANE_SET = new Set(DATA_PLANE_ALLOWLIST);
14126
14204
  var CONTROL_PLANE_SET = new Set(KNOWN_CONTROL_PLANE_RESOURCES);
@@ -14136,7 +14214,60 @@ ${summary}`
14136
14214
  this.name = "ScopeGateError";
14137
14215
  this.rejected = rejected;
14138
14216
  }
14139
- };
14217
+ };
14218
+ var GRANTED_CAPABILITIES_ALLOWLIST = ["member-lifecycle"];
14219
+ var GRANTED_CAPABILITIES_SET = new Set(GRANTED_CAPABILITIES_ALLOWLIST);
14220
+ var CAPABILITIES_HINT = GRANTED_CAPABILITIES_ALLOWLIST.join("/");
14221
+ var CapabilityGateError = class extends Error {
14222
+ rejected;
14223
+ constructor(rejected) {
14224
+ const summary = rejected.map((r) => ` \u2022 ${r.action}: ${r.reason}`).join("\n");
14225
+ super(
14226
+ `Capability gate rejected ${rejected.length} requested capability(ies); the bootstrap loader grants (${CAPABILITIES_HINT}) only and mints nothing on reject:
14227
+ ${summary}`
14228
+ );
14229
+ this.name = "CapabilityGateError";
14230
+ this.rejected = rejected;
14231
+ }
14232
+ };
14233
+ function evaluateCapabilityToken(name) {
14234
+ const token = name.trim();
14235
+ if (token.length === 0) {
14236
+ return { action: name, reason: "empty capability name" };
14237
+ }
14238
+ if (!GRANTED_CAPABILITIES_SET.has(token)) {
14239
+ return {
14240
+ action: name,
14241
+ 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`
14242
+ };
14243
+ }
14244
+ return null;
14245
+ }
14246
+ function evaluateCapabilityGate(capabilities) {
14247
+ const accepted = [];
14248
+ const rejected = [];
14249
+ for (const name of capabilities ?? []) {
14250
+ const rejection = evaluateCapabilityToken(name);
14251
+ if (rejection) {
14252
+ rejected.push(rejection);
14253
+ } else {
14254
+ accepted.push(name);
14255
+ }
14256
+ }
14257
+ return { ok: rejected.length === 0, accepted, rejected };
14258
+ }
14259
+ function assertCapabilityGate(capabilities) {
14260
+ const result = evaluateCapabilityGate(capabilities);
14261
+ if (!result.ok) {
14262
+ throw new CapabilityGateError(result.rejected);
14263
+ }
14264
+ }
14265
+ function firstInvalidOp(ops) {
14266
+ for (const ch of ops) {
14267
+ if (!VALID_OPS.has(ch)) return ch;
14268
+ }
14269
+ return void 0;
14270
+ }
14140
14271
  function evaluateScopeToken(action) {
14141
14272
  const token = action.trim();
14142
14273
  if (token.length === 0) {
@@ -14158,6 +14289,48 @@ function evaluateScopeToken(action) {
14158
14289
  };
14159
14290
  }
14160
14291
  if (!DATA_PLANE_SET.has(resource)) {
14292
+ const admittedOps = PARTIALLY_ADMITTED_OPS.get(resource);
14293
+ const selfOnlyOps = SELF_QUALIFIED_OPS.get(resource);
14294
+ if (admittedOps || selfOnlyOps) {
14295
+ const admittedHint = [.../* @__PURE__ */ new Set([...admittedOps ?? [], ...selfOnlyOps ?? []])].sort().join("/");
14296
+ if (ops === "*") {
14297
+ return {
14298
+ action,
14299
+ 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`
14300
+ };
14301
+ }
14302
+ const invalidOp2 = firstInvalidOp(ops);
14303
+ if (invalidOp2) {
14304
+ return {
14305
+ action,
14306
+ reason: `invalid operation character '${invalidOp2}' (valid operations are c, r, u, d, or '*')`
14307
+ };
14308
+ }
14309
+ const qualifier = parts.length > 2 ? parts.slice(2).join(":") : void 0;
14310
+ const isSelfQualified = qualifier === "self";
14311
+ const unqualifiableOps = UNQUALIFIABLE_WITH_ANY_TAIL.get(resource);
14312
+ for (const ch of ops) {
14313
+ if (qualifier !== void 0 && unqualifiableOps?.has(ch)) {
14314
+ return {
14315
+ action,
14316
+ 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).`
14317
+ };
14318
+ }
14319
+ if (admittedOps?.has(ch)) continue;
14320
+ if (selfOnlyOps?.has(ch)) {
14321
+ if (isSelfQualified) continue;
14322
+ return {
14323
+ action,
14324
+ reason: `requests control-plane scope '${resource}:${ch}' without the required ':self' qualifier. A bare or arbitrarily-qualified '${resource}:${ch}' is outside the narrow allowlist (${admittedHint}) admitted for this resource \u2014 only '${resource}:${ch}:self' is. That operation must otherwise be done deliberately in the developer portal, never via the bootstrap loader`
14325
+ };
14326
+ }
14327
+ return {
14328
+ action,
14329
+ 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`
14330
+ };
14331
+ }
14332
+ return null;
14333
+ }
14161
14334
  if (CONTROL_PLANE_SET.has(resource)) {
14162
14335
  return {
14163
14336
  action,
@@ -14172,13 +14345,12 @@ function evaluateScopeToken(action) {
14172
14345
  if (ops === "*") {
14173
14346
  return null;
14174
14347
  }
14175
- for (const ch of ops) {
14176
- if (!VALID_OPS.has(ch)) {
14177
- return {
14178
- action,
14179
- reason: `invalid operation character '${ch}' (valid operations are c, r, u, d, or '*')`
14180
- };
14181
- }
14348
+ const invalidOp = firstInvalidOp(ops);
14349
+ if (invalidOp) {
14350
+ return {
14351
+ action,
14352
+ reason: `invalid operation character '${invalidOp}' (valid operations are c, r, u, d, or '*')`
14353
+ };
14182
14354
  }
14183
14355
  return null;
14184
14356
  }
@@ -14201,6 +14373,16 @@ function assertScopeGate(allowedActions) {
14201
14373
  throw new ScopeGateError(result.rejected);
14202
14374
  }
14203
14375
  }
14376
+ function assertBlueprintGates(blueprint) {
14377
+ assertScopeGate(blueprint.accessProfile.allowedActions);
14378
+ assertCapabilityGate(blueprint.accessProfile.capabilities);
14379
+ for (const clauses of Object.values(blueprint.roles ?? {})) {
14380
+ for (const clause of clauses) {
14381
+ assertScopeGate(clause.allowedActions);
14382
+ assertCapabilityGate(clause.capabilities);
14383
+ }
14384
+ }
14385
+ }
14204
14386
 
14205
14387
  // src/machine-id.ts
14206
14388
  import { promises as fs } from "fs";
@@ -14305,6 +14487,68 @@ async function resolveApplyTimeIdentities(blueprint, client, log) {
14305
14487
  function actionOf(created) {
14306
14488
  return created ? "created" : "reconciled";
14307
14489
  }
14490
+ async function registerNamespaceIdempotently(client, contextIdForRequest, decl) {
14491
+ const body = {
14492
+ namespace: decl.namespace,
14493
+ entityBacked: decl.entityBacked,
14494
+ specificityRank: decl.specificityRank,
14495
+ membershipRecordType: decl.membershipRecordType,
14496
+ membershipTargetField: decl.membershipTargetField,
14497
+ membershipLevelField: decl.membershipLevelField,
14498
+ membershipLevels: decl.membershipLevels
14499
+ };
14500
+ try {
14501
+ await client.registerNamespace({ contextId: contextIdForRequest, body });
14502
+ return "created";
14503
+ } catch (err) {
14504
+ const info = extractServerError(err);
14505
+ if (info.statusCode !== 400) throw err;
14506
+ let existing;
14507
+ try {
14508
+ existing = await client.getNamespace({ namespace: decl.namespace, contextId: contextIdForRequest });
14509
+ } catch {
14510
+ throw err;
14511
+ }
14512
+ if (existing.contextId !== contextIdForRequest) throw err;
14513
+ const mismatch = describeNamespaceMismatch(decl, existing);
14514
+ if (mismatch) {
14515
+ throw new Error(
14516
+ `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.`
14517
+ );
14518
+ }
14519
+ return "reconciled";
14520
+ }
14521
+ }
14522
+ function describeNamespaceMismatch(decl, existing) {
14523
+ const mismatches = [];
14524
+ const declaredEntityBacked = decl.entityBacked ?? false;
14525
+ const existingEntityBacked = existing.entityBacked ?? false;
14526
+ if (declaredEntityBacked !== existingEntityBacked) {
14527
+ mismatches.push(
14528
+ `entityBacked: declared ${JSON.stringify(declaredEntityBacked)}, registered ${JSON.stringify(existingEntityBacked)}`
14529
+ );
14530
+ }
14531
+ const fields = [
14532
+ "specificityRank",
14533
+ "membershipRecordType",
14534
+ "membershipTargetField",
14535
+ "membershipLevelField"
14536
+ ];
14537
+ for (const field of fields) {
14538
+ const declared = decl[field];
14539
+ const existingValue = existing[field];
14540
+ if (declared === void 0 && (existingValue === void 0 || existingValue === null)) continue;
14541
+ if (declared === existingValue) continue;
14542
+ mismatches.push(`${field}: declared ${JSON.stringify(declared)}, registered ${JSON.stringify(existingValue)}`);
14543
+ }
14544
+ const declaredLevels = decl.membershipLevels;
14545
+ const existingLevels = existing.membershipLevels;
14546
+ const levelsDiffer = declaredLevels === void 0 ? existingLevels != null && existingLevels.length > 0 : JSON.stringify([...declaredLevels].sort()) !== JSON.stringify([...existingLevels ?? []].sort());
14547
+ if (levelsDiffer) {
14548
+ mismatches.push(`membershipLevels: declared ${JSON.stringify(declaredLevels)}, registered ${JSON.stringify(existingLevels)}`);
14549
+ }
14550
+ return mismatches.length > 0 ? mismatches.join("; ") : null;
14551
+ }
14308
14552
  var LostKeyError = class extends Error {
14309
14553
  constructor(keyName) {
14310
14554
  super(
@@ -14315,6 +14559,20 @@ var LostKeyError = class extends Error {
14315
14559
  }
14316
14560
  keyName;
14317
14561
  };
14562
+ var ExistingPrincipalNotConfirmedError = class extends Error {
14563
+ constructor(contextId, userId, externalId) {
14564
+ super(
14565
+ `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.`
14566
+ );
14567
+ this.contextId = contextId;
14568
+ this.userId = userId;
14569
+ this.externalId = externalId;
14570
+ this.name = "ExistingPrincipalNotConfirmedError";
14571
+ }
14572
+ contextId;
14573
+ userId;
14574
+ externalId;
14575
+ };
14318
14576
  function mapEnumValues(enumValues) {
14319
14577
  if (!enumValues) return void 0;
14320
14578
  return enumValues.map((value) => ({ value }));
@@ -14389,7 +14647,8 @@ function toCapabilities(capabilities) {
14389
14647
  function toRoleScopeClauses(clauses) {
14390
14648
  return clauses.map((c) => ({
14391
14649
  allowed_actions: c.allowedActions,
14392
- data_scope: c.dataScope
14650
+ data_scope: c.dataScope,
14651
+ granted_capabilities: c.capabilities
14393
14652
  }));
14394
14653
  }
14395
14654
  var UnresolvedIdentitiesError = class extends Error {
@@ -14409,11 +14668,18 @@ function assertNoUnresolvedIdentities(blueprint) {
14409
14668
  }
14410
14669
  }
14411
14670
  async function runLoader(client, input) {
14412
- const { blueprint, tenantId, machineId, rotate = false, skipSeed = false, readExistingKey, persistMintedKey, log } = input;
14413
- assertScopeGate(blueprint.accessProfile.allowedActions);
14414
- for (const clauses of Object.values(blueprint.roles ?? {})) {
14415
- for (const clause of clauses) assertScopeGate(clause.allowedActions);
14416
- }
14671
+ const {
14672
+ blueprint,
14673
+ tenantId,
14674
+ machineId,
14675
+ rotate = false,
14676
+ skipSeed = false,
14677
+ readExistingKey,
14678
+ persistMintedKey,
14679
+ confirmExistingPrincipal,
14680
+ log
14681
+ } = input;
14682
+ assertBlueprintGates(blueprint);
14417
14683
  assertNoUnresolvedIdentities(blueprint);
14418
14684
  const ctxResp = await client.auth.createAppContext({ body: { contextId: blueprint.contextId, name: contextNameOf(blueprint) } });
14419
14685
  const contextAction = actionOf(ctxResp.created);
@@ -14431,10 +14697,42 @@ async function runLoader(client, input) {
14431
14697
  if (!userId) {
14432
14698
  throw new Error("Service-principal create returned no user id; cannot bind AccessProfile/key.");
14433
14699
  }
14434
- log.debug({ component: "loader", externalId: blueprint.servicePrincipal.externalId, userId }, "service principal ensured");
14700
+ const userAction = actionOf(user.created);
14701
+ log.debug(
14702
+ { component: "loader", externalId: blueprint.servicePrincipal.externalId, userId, userAction },
14703
+ "service principal ensured"
14704
+ );
14705
+ const localKeyForContext = await readExistingKey(blueprint.contextId);
14706
+ const storedPrincipalId = localKeyForContext?.principalId;
14707
+ const localKeyNamesThisPrincipal = storedPrincipalId === `usr_${userId}` || storedPrincipalId === userId;
14708
+ if (userAction === "reconciled" && !localKeyNamesThisPrincipal) {
14709
+ const proceed = confirmExistingPrincipal ? await confirmExistingPrincipal({
14710
+ contextId: blueprint.contextId,
14711
+ userId,
14712
+ externalId: blueprint.servicePrincipal.externalId
14713
+ }) : true;
14714
+ if (!proceed) {
14715
+ throw new ExistingPrincipalNotConfirmedError(blueprint.contextId, userId, blueprint.servicePrincipal.externalId);
14716
+ }
14717
+ log.debug(
14718
+ { component: "loader", contextId: blueprint.contextId, userId },
14719
+ "existing-principal collision confirmed (or unwired) \u2014 proceeding with any further write to this principal"
14720
+ );
14721
+ }
14722
+ const needsContextPinnedBootstrap = (blueprint.issuers ?? []).length > 0 || (blueprint.namespaces ?? []).length > 0;
14723
+ const ctxBootstrapClient = needsContextPinnedBootstrap ? input.remintBootstrapForContext ? await input.remintBootstrapForContext(blueprint.contextId) : client : void 0;
14724
+ if (ctxBootstrapClient && input.remintBootstrapForContext) {
14725
+ log.debug({ component: "loader", contextId: blueprint.contextId }, "re-minted context-pinned bootstrap client");
14726
+ }
14727
+ const namespaceActions = [];
14728
+ for (const ns of blueprint.namespaces ?? []) {
14729
+ const action = await registerNamespaceIdempotently(ctxBootstrapClient.identity, blueprint.contextId, ns);
14730
+ namespaceActions.push({ namespace: ns.namespace, action });
14731
+ log.debug({ component: "loader", namespace: ns.namespace, contextId: blueprint.contextId, action }, "namespace ensured");
14732
+ }
14435
14733
  const issuerActions = [];
14436
14734
  for (const issuer of blueprint.issuers ?? []) {
14437
- const issuerResp = await client.auth.registerIssuer({
14735
+ const issuerResp = await ctxBootstrapClient.auth.registerIssuer({
14438
14736
  issuerId: issuer.issuerId,
14439
14737
  issuer: issuer.issuer,
14440
14738
  jwksUri: issuer.jwksUri,
@@ -14504,7 +14802,10 @@ async function runLoader(client, input) {
14504
14802
  // literal JSON null, which is exactly what the platform's scope
14505
14803
  // matcher tests via `allowedValues.contains(null)`. The cast bridges
14506
14804
  // to the SDK's loosely-typed Record<string, Record<string,unknown>>.
14507
- data_scope: blueprint.accessProfile.dataScope
14805
+ data_scope: blueprint.accessProfile.dataScope,
14806
+ // Already gate-validated (assertBlueprintGates, above this function's
14807
+ // call site) before this body is ever built.
14808
+ granted_capabilities: blueprint.accessProfile.capabilities
14508
14809
  }
14509
14810
  ],
14510
14811
  // Optional identity overrides — scope values the service key STAMPS onto
@@ -14562,7 +14863,7 @@ async function runLoader(client, input) {
14562
14863
  keyId = keyResp.keyId ?? "";
14563
14864
  rawKey = keyResp.rawKey;
14564
14865
  if (keyId) {
14565
- await persistMintedKey(blueprint.contextId, { keyId, rawKey, keyName, principalId: userId, revokedKeyId });
14866
+ await persistMintedKey(blueprint.contextId, { keyId, rawKey, keyName, principalId: `usr_${userId}`, revokedKeyId });
14566
14867
  }
14567
14868
  if (keyAction !== "rotated") keyAction = "created";
14568
14869
  } else {
@@ -14636,10 +14937,12 @@ async function runLoader(client, input) {
14636
14937
  keyName,
14637
14938
  keyAction,
14638
14939
  contextAction,
14940
+ userAction,
14639
14941
  profileAction,
14640
14942
  schemaActions,
14641
14943
  roleActions,
14642
14944
  issuerActions,
14945
+ namespaceActions,
14643
14946
  rawKey,
14644
14947
  keyId,
14645
14948
  revokedKeyId,
@@ -14658,6 +14961,15 @@ function buildPlan(blueprint, opts = {}) {
14658
14961
  kind: "principal",
14659
14962
  summary: `principal '${blueprint.servicePrincipal.externalId}' (SERVICE)`
14660
14963
  });
14964
+ for (const ns of blueprint.namespaces ?? []) {
14965
+ const ownerNote = `owned by '${blueprint.contextId}'`;
14966
+ const entityNote = ns.entityBacked ? ", entity-backed" : "";
14967
+ const membershipNote = ns.membershipRecordType ? `, membership: ${ns.membershipRecordType}.${ns.membershipTargetField}` + (ns.membershipLevelField ? ` (levels: ${ns.membershipLevels?.join("/")})` : "") : "";
14968
+ steps.push({
14969
+ kind: "namespace",
14970
+ summary: `namespace '${ns.namespace}' (${ownerNote}${entityNote}${membershipNote}) [${BLUEPRINT_FIELD_PHASES.namespaces}-token phase]`
14971
+ });
14972
+ }
14661
14973
  for (const iss of blueprint.issuers ?? []) {
14662
14974
  const selfSignup = iss.selfSignupPolicies?.length ? `, self-signup: ${iss.selfSignupPolicies.length} polic${iss.selfSignupPolicies.length === 1 ? "y" : "ies"}` : "";
14663
14975
  steps.push({
@@ -14680,15 +14992,18 @@ function buildPlan(blueprint, opts = {}) {
14680
14992
  }
14681
14993
  const actions = blueprint.accessProfile.allowedActions;
14682
14994
  const scopeNote = blueprint.accessProfile.dataScope && Object.keys(blueprint.accessProfile.dataScope).length ? ` dataScope: ${Object.keys(blueprint.accessProfile.dataScope).join(", ")}` : "";
14995
+ const capabilityNote = blueprint.accessProfile.capabilities?.length ? ` capabilities: [${blueprint.accessProfile.capabilities.join(", ")}]` : "";
14683
14996
  steps.push({
14684
14997
  kind: "profile",
14685
- summary: `profile scopes [${actions.join(", ")}] (data-plane \u2713)${scopeNote}`
14998
+ summary: `profile scopes [${actions.join(", ")}] (data-plane \u2713)${scopeNote}${capabilityNote}`
14686
14999
  });
14687
15000
  for (const [roleId, clauses] of Object.entries(blueprint.roles ?? {})) {
14688
15001
  const actionSet = [...new Set(clauses.flatMap((c) => c.allowedActions))];
15002
+ const capSet = [...new Set(clauses.flatMap((c) => c.capabilities ?? []))];
15003
+ const roleCapabilityNote = capSet.length ? `, capabilities [${capSet.join(", ")}]` : "";
14689
15004
  steps.push({
14690
15005
  kind: "role",
14691
- summary: `role '${roleId}' (${clauses.length} clause${clauses.length === 1 ? "" : "s"}, actions [${actionSet.join(", ")}])`
15006
+ summary: `role '${roleId}' (${clauses.length} clause${clauses.length === 1 ? "" : "s"}, actions [${actionSet.join(", ")}]${roleCapabilityNote})`
14692
15007
  });
14693
15008
  }
14694
15009
  steps.push({ kind: "key", summary: `key ${blueprint.name}-<machine> (mint 1 scoped ssk_*)` });
@@ -14715,6 +15030,13 @@ function renderPlan(blueprint, steps) {
14715
15030
  ` issuer is left unchanged, not reconciled to match.`
14716
15031
  );
14717
15032
  }
15033
+ if (steps.some((s) => s.kind === "namespace")) {
15034
+ legendLines.push(
15035
+ ` Exception: namespace registration is not idempotent \u2014 a re-apply matching`,
15036
+ ` an existing registration converges silently, but one that disagrees FAILS`,
15037
+ ` the apply rather than reconciling it.`
15038
+ );
15039
+ }
14718
15040
  const legend = legendLines.join("\n");
14719
15041
  const lines = steps.map((s) => ` + ${s.summary}`);
14720
15042
  return [header, legend, "", ...lines].join("\n");
@@ -14963,6 +15285,15 @@ var OsKeychainSecretStore = class {
14963
15285
  * against the real Windows store on every MR — the dev-machine probe is a point
14964
15286
  * measurement, not a regression gate. (Tracked internally; this file ships to
14965
15287
  * public npm, so no tracker number here.)
15288
+ *
15289
+ * ⚠️ THIS METHOD'S FAIL-CLOSED GUARANTEE IS AN UPSTREAM BEHAVIOURAL CONTRACT, NOT
15290
+ * SOMETHING THIS CODE ENFORCES. `@napi-rs/keyring` is pinned to an EXACT version
15291
+ * in `package.json` for exactly this reason: if a future release returned `[]`
15292
+ * from `findCredentials()` on a locked store instead of throwing, the `catch`
15293
+ * block above would never run — `!.some(...)` on an empty array reads as "gone",
15294
+ * `deleteSecret` returns `true`, and `remove`/`prune` drop the index row of a
15295
+ * still-live credential. A version bump of this dependency is a security review
15296
+ * of the paragraph above against the new release, not a routine dependency bump.
14966
15297
  */
14967
15298
  async deleteSecret(ref) {
14968
15299
  try {
@@ -15896,6 +16227,26 @@ The key itself WAS created server-side and is live \u2014 its id is ${entry.keyI
15896
16227
  const entry = await this.getActive();
15897
16228
  return entry ? this.readSecret(entry) : void 0;
15898
16229
  }
16230
+ /**
16231
+ * The ACTIVE entry's tenant (test/live), or undefined if there is no active
16232
+ * entry or its tenant cannot be determined — WITHOUT migrating its secret.
16233
+ *
16234
+ * Deliberately NOT built on {@link resolveActiveSecret}/{@link readSecret}:
16235
+ * both migrate the secret to this machine's preferred store as a side effect
16236
+ * of reading it, and a caller that only wants to CLASSIFY the active identity
16237
+ * (e.g. "is it safe to activate a different one over it?") must not silently
16238
+ * relocate — and take the cross-process index lock for — a credential it has
16239
+ * no other reason to touch. Same non-migrating shape {@link assess} already
16240
+ * uses per entry (`store.getSecret` directly, never `readSecret`).
16241
+ */
16242
+ async activeTenant() {
16243
+ const entry = await this.getActive();
16244
+ if (!entry) return void 0;
16245
+ const store = this.storeFor(entry);
16246
+ if (!store) return void 0;
16247
+ const secret = await store.getSecret(entry.secretRef);
16248
+ return secret !== void 0 ? keyTenant(secret) : void 0;
16249
+ }
15899
16250
  /**
15900
16251
  * Read one entry's secret, moving it to this machine's preferred store if it is
15901
16252
  * somewhere else.
@@ -16022,26 +16373,31 @@ import * as readline from "readline/promises";
16022
16373
  async function runKeyringList(opts, deps = {}) {
16023
16374
  try {
16024
16375
  const keyring = deps.keyring ?? await Keyring.open();
16025
- const entries = await keyring.list();
16026
- const active = await keyring.activeAlias();
16027
16376
  if (opts.json) {
16377
+ const entries = await keyring.list();
16378
+ const active = await keyring.activeAlias();
16028
16379
  process.stdout.write(`${formatJson({ active, entries })}
16029
16380
  `);
16030
16381
  return 0;
16031
16382
  }
16032
- if (entries.length === 0) {
16383
+ const { health } = await assessEntries(keyring);
16384
+ if (health.length === 0) {
16033
16385
  process.stdout.write("\n (no keyring entries yet \u2014 run `vectros join <context>` to add one)\n");
16034
16386
  return 0;
16035
16387
  }
16036
16388
  process.stdout.write(
16037
- "\n" + formatTable(entries, [
16038
- { header: "", get: (e) => e.alias === active ? "*" : "" },
16039
- { header: "alias", get: (e) => e.alias },
16040
- { header: "context", get: (e) => e.context },
16041
- { header: "principal", get: (e) => e.principalId },
16042
- { header: "role", get: (e) => e.role },
16043
- { header: "env", get: (e) => e.env },
16044
- { header: "key id", get: (e) => e.keyId }
16389
+ "\n" + formatTable(health, [
16390
+ { header: "", get: (h) => h.active ? "*" : "" },
16391
+ { header: "alias", get: (h) => h.entry.alias },
16392
+ { header: "context", get: (h) => h.entry.context },
16393
+ { header: "principal", get: (h) => h.entry.principalId },
16394
+ { header: "role", get: (h) => h.entry.role },
16395
+ // `tenant` (test/live) replaces the old `env` (staging/production) column
16396
+ // here: env is an internal-team concern an external developer's CLI never
16397
+ // sees, while tenant is the distinction every user routinely holds two
16398
+ // credentials side by side over.
16399
+ { header: "tenant", get: (h) => tenantCell(h, { upper: true }) },
16400
+ { header: "key id", get: (h) => h.entry.keyId }
16045
16401
  ]) + "\n"
16046
16402
  );
16047
16403
  return 0;
@@ -16223,6 +16579,10 @@ async function assessEntries(keyring) {
16223
16579
  }))
16224
16580
  };
16225
16581
  }
16582
+ function tenantCell(h, opts) {
16583
+ if (h.status !== "ok") return void 0;
16584
+ return h.tenant ?? (opts.upper ? "UNKNOWN" : "unknown");
16585
+ }
16226
16586
  function describeEntry(h) {
16227
16587
  return `${h.entry.alias} (${h.entry.context}, ${h.entry.keyId})`;
16228
16588
  }
@@ -16367,9 +16727,7 @@ async function runKeyringDoctor(opts, deps = {}) {
16367
16727
  // `unknown` can escalate; one that sees a confident wrong answer cannot.
16368
16728
  store: storeKindOf(h.entry) ?? "unknown",
16369
16729
  secret: h.status,
16370
- // `unknown` rather than absent: a consumer must be able to tell "not a
16371
- // live key" from "we could not read the shape of this one".
16372
- tenant: h.status === "ok" ? h.tenant ?? "unknown" : void 0
16730
+ tenant: tenantCell(h, { upper: false })
16373
16731
  })),
16374
16732
  liveKeys: liveKeys.map((h) => h.entry.alias),
16375
16733
  findings
@@ -16384,7 +16742,7 @@ async function runKeyringDoctor(opts, deps = {}) {
16384
16742
  { header: "", get: (h) => h.active ? "*" : "" },
16385
16743
  { header: "alias", get: (h) => h.entry.alias },
16386
16744
  { header: "context", get: (h) => h.entry.context },
16387
- { header: "tenant", get: (h) => h.status === "ok" ? h.tenant ?? "UNKNOWN" : void 0 },
16745
+ { header: "tenant", get: (h) => tenantCell(h, { upper: true }) },
16388
16746
  { header: "key id", get: (h) => h.entry.keyId },
16389
16747
  { header: "secret", get: (h) => h.status === "ok" ? "ok" : h.status.toUpperCase() }
16390
16748
  ]) + "\n"
@@ -16578,6 +16936,47 @@ function keyringAliasFor(alias, tenant) {
16578
16936
  }
16579
16937
  return tenant === "test" ? `${alias}.test` : alias;
16580
16938
  }
16939
+ function resolveActivateDecision(opts) {
16940
+ if (opts.explicitActivate !== void 0) return opts.explicitActivate;
16941
+ if (opts.tenant === "live") return true;
16942
+ return opts.currentActiveTenant !== "live";
16943
+ }
16944
+ async function determineActivateDecision(keyring, opts) {
16945
+ if (opts.explicitActivate !== void 0 || opts.tenant === "live") {
16946
+ return resolveActivateDecision(opts);
16947
+ }
16948
+ let currentActiveTenant;
16949
+ try {
16950
+ if (opts.hint && opts.hint.alias === await keyring.activeAlias()) {
16951
+ currentActiveTenant = keyTenant(opts.hint.secret);
16952
+ } else {
16953
+ currentActiveTenant = await keyring.activeTenant();
16954
+ }
16955
+ } catch {
16956
+ }
16957
+ return resolveActivateDecision({ ...opts, currentActiveTenant });
16958
+ }
16959
+ function tenantActivateMessage(opts) {
16960
+ if (opts.willActivate) {
16961
+ if (opts.tenant !== "test") return "";
16962
+ return `
16963
+ \u26A0 '${opts.alias}' is now your ACTIVE identity \u2014 a TEST tenant credential. Anything
16964
+ resolving a key with no explicit --alias (including the MCP server) follows it.
16965
+ `;
16966
+ }
16967
+ if (opts.explicitlyDeclined) {
16968
+ return `
16969
+ \u26A0 Stored '${opts.alias}' (${opts.tenant} tenant) without activating it \u2014 you passed --no-activate.
16970
+ Run \`vectros switch ${opts.alias}\` when you're ready to use it.
16971
+ `;
16972
+ }
16973
+ return `
16974
+ \u26A0 Stored '${opts.alias}' (test tenant) WITHOUT activating it \u2014 your currently-active
16975
+ identity is a LIVE credential, and switching it out from under you silently is
16976
+ exactly the mistake this check exists to prevent. Run \`vectros switch ${opts.alias}\`
16977
+ to use the test key, or re-run with --activate to do so automatically.
16978
+ `;
16979
+ }
16581
16980
  function nowIso() {
16582
16981
  return (/* @__PURE__ */ new Date()).toISOString();
16583
16982
  }
@@ -16674,10 +17073,7 @@ async function storeMintedKey(keyring, binding, secret, opts = {}) {
16674
17073
  // src/cli-bootstrap.ts
16675
17074
  function validateBlueprint(input) {
16676
17075
  const blueprint = parseBlueprint(input);
16677
- assertScopeGate(blueprint.accessProfile.allowedActions);
16678
- for (const clauses of Object.values(blueprint.roles ?? {})) {
16679
- for (const clause of clauses) assertScopeGate(clause.allowedActions);
16680
- }
17076
+ assertBlueprintGates(blueprint);
16681
17077
  return blueprint;
16682
17078
  }
16683
17079
  var BLANK_SLATE = {
@@ -16699,6 +17095,7 @@ function parseBootstrapArgs(argv) {
16699
17095
  yes: false,
16700
17096
  noMerge: false,
16701
17097
  rotate: false,
17098
+ confirmExistingPrincipal: false,
16702
17099
  useScopedToken: false,
16703
17100
  blankSlate: false,
16704
17101
  noSeed: false,
@@ -16750,6 +17147,9 @@ function parseBootstrapArgs(argv) {
16750
17147
  case "--rotate":
16751
17148
  args.rotate = true;
16752
17149
  break;
17150
+ case "--confirm-existing-principal":
17151
+ args.confirmExistingPrincipal = true;
17152
+ break;
16753
17153
  case "--use-scoped-token":
16754
17154
  args.useScopedToken = true;
16755
17155
  break;
@@ -16771,6 +17171,12 @@ function parseBootstrapArgs(argv) {
16771
17171
  case "--test":
16772
17172
  args.tenant = "test";
16773
17173
  break;
17174
+ case "--activate":
17175
+ args.activate = true;
17176
+ break;
17177
+ case "--no-activate":
17178
+ args.activate = false;
17179
+ break;
16774
17180
  case "--help":
16775
17181
  case "-h":
16776
17182
  args.help = true;
@@ -16788,6 +17194,26 @@ function seedDeclined(answer) {
16788
17194
  const a = answer.trim().toLowerCase();
16789
17195
  return a === "n" || a === "no";
16790
17196
  }
17197
+ function existingPrincipalConfirmed(answer) {
17198
+ const a = answer.trim().toLowerCase();
17199
+ return a === "y" || a === "yes";
17200
+ }
17201
+ function buildConfirmExistingPrincipal(args, prompt, isTty) {
17202
+ return async (details) => {
17203
+ if (!args.yes && isTty) {
17204
+ const ans = await prompt(
17205
+ `
17206
+ \u26A0 '${details.externalId}' already exists in your tenant (context '${details.contextId}') \u2014 this key will bind to that EXISTING identity, not a new one.
17207
+ Expected if you're re-applying a blueprint from a new machine, or setting up a second
17208
+ credential for an integration you already own. If you don't recognize this identity,
17209
+ do NOT proceed \u2014 this may be a name collision with something you don't control.
17210
+ Proceed and bind to the existing identity? [y/N]: `
17211
+ );
17212
+ return existingPrincipalConfirmed(ans);
17213
+ }
17214
+ return args.confirmExistingPrincipal;
17215
+ };
17216
+ }
16791
17217
  function seedSummaryLine(opts) {
16792
17218
  if (opts.noSeed) {
16793
17219
  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";
@@ -16824,16 +17250,28 @@ function issuerTally(items) {
16824
17250
  if (unchanged) seg.push(`${unchanged} already registered \u2014 unchanged (issuers cannot be updated once registered)`);
16825
17251
  return `${items.length} issuer${items.length === 1 ? "" : "s"} (${seg.join(", ")})`;
16826
17252
  }
17253
+ function namespaceTally(items) {
17254
+ if (items.length === 0) return null;
17255
+ const created = items.filter((i) => i.action === "created").length;
17256
+ const matched = items.length - created;
17257
+ const seg = [];
17258
+ if (created) seg.push(`${created} created`);
17259
+ if (matched) seg.push(`${matched} already registered \u2014 confirmed matching`);
17260
+ return `${items.length} namespace${items.length === 1 ? "" : "s"} (${seg.join(", ")})`;
17261
+ }
16827
17262
  function applyOutcomeSummary(result) {
16828
17263
  const parts = [
16829
17264
  reconcileTally(result.schemaActions, "schema"),
16830
17265
  reconcileTally(result.roleActions, "role"),
16831
17266
  issuerTally(result.issuerActions),
17267
+ namespaceTally(result.namespaceActions),
16832
17268
  `profile ${result.profileAction}`
16833
17269
  ].filter((p) => p !== null);
16834
17270
  const detail = parts.length ? ` \u2014 ${parts.join("; ")}` : "";
17271
+ const principalNote = result.userAction === "reconciled" ? ` \u26A0 service principal already existed in this tenant before this run (not newly created)
17272
+ ` : "";
16835
17273
  return ` \u2714 context '${result.contextId}' ${result.contextAction}${detail}
16836
- `;
17274
+ ${principalNote}`;
16837
17275
  }
16838
17276
  var BOOTSTRAP_HELP = `vectros bootstrap \u2014 provision a least-privilege MCP credential.
16839
17277
 
@@ -16841,7 +17279,7 @@ Usage:
16841
17279
  vectros bootstrap [--blueprint <name> | --blank] [--no-seed]
16842
17280
  [--set name=value ...] [--values <file>]
16843
17281
  [--tenant test|live | --test] [--client desktop|code]
16844
- [--yes] [--no-merge] [--rotate]
17282
+ [--yes] [--no-merge] [--rotate] [--confirm-existing-principal]
16845
17283
  [--token <t> | env VECTROS_BOOTSTRAP_TOKEN]
16846
17284
  [--base-url <url>] [--use-scoped-token]
16847
17285
 
@@ -16864,6 +17302,10 @@ Options:
16864
17302
  The tenant rides the key (ssk_test_* vs ssk_live_*), so
16865
17303
  the MCP/client config is otherwise identical.
16866
17304
  --test Shorthand for --tenant test.
17305
+ --activate Force the freshly-stored key to become your active identity, even
17306
+ if that would switch away from a currently-active LIVE credential
17307
+ (the one case --tenant test does NOT do this automatically).
17308
+ --no-activate Store the key without activating it, regardless of tenant.
16867
17309
  --base-url <url> Override the API base URL (beats VECTROS_API_BASE_URL).
16868
17310
  --client <desktop|code> MCP host to configure (default: desktop). 'desktop' merges
16869
17311
  claude_desktop_config.json (also works for Cursor/Cline);
@@ -16878,6 +17320,13 @@ Options:
16878
17320
  print the config snippet instead of merging it into your client's
16879
17321
  config file. Not a dry run; see 'blueprint plan' for that.
16880
17322
  --rotate Revoke + re-mint THIS machine's key.
17323
+ --confirm-existing-principal
17324
+ Required (with --yes, or with no TTY) to mint a NEW key bound to a
17325
+ service principal that already existed in your tenant before this run
17326
+ \u2014 e.g. a deliberate second machine for an integration you already own.
17327
+ Refused by default so an unexpected collision halts a CI run loudly
17328
+ instead of silently minting a credential bound to the wrong identity.
17329
+ Never needed for --rotate or an ordinary same-machine re-apply.
16881
17330
  --use-scoped-token Legacy fallback.
16882
17331
  NOTE: it mints a token WITHOUT the provisioning
16883
17332
  capability, so it cannot create a blueprint's
@@ -16965,6 +17414,22 @@ function buildRemintForContext(opts) {
16965
17414
  });
16966
17415
  };
16967
17416
  }
17417
+ function buildRemintBootstrapForContext(opts) {
17418
+ const mintBridge = opts.mintBridge ?? mintBridgeToken;
17419
+ return async (contextId) => {
17420
+ const ctxToken = await mintBridge({
17421
+ cognitoToken: opts.cognitoToken,
17422
+ apiBaseUrl: opts.apiBaseUrl,
17423
+ contextId,
17424
+ tenant: opts.tenant,
17425
+ useScopedToken: opts.useScopedToken
17426
+ });
17427
+ return new VectrosClient({
17428
+ token: ctxToken.token,
17429
+ environment: opts.apiBaseUrl
17430
+ });
17431
+ };
17432
+ }
16968
17433
  async function runBootstrapCli(argv, deps = {}) {
16969
17434
  const log = deps.log ?? createLogger2();
16970
17435
  let args;
@@ -16998,6 +17463,15 @@ ${BOOTSTRAP_HELP}
16998
17463
  \u2716 Scope gate refused blueprint '${blueprint.name}':
16999
17464
  `);
17000
17465
  for (const r of err.rejected) process.stderr.write(` \u2022 ${r.action}: ${r.reason}
17466
+ `);
17467
+ process.stderr.write(" Minting nothing.\n");
17468
+ return 2;
17469
+ }
17470
+ if (err instanceof CapabilityGateError) {
17471
+ process.stderr.write(`
17472
+ \u2716 Capability gate refused blueprint '${blueprint.name}':
17473
+ `);
17474
+ for (const r of err.rejected) process.stderr.write(` \u2022 ${r.action}: ${r.reason}
17001
17475
  `);
17002
17476
  process.stderr.write(" Minting nothing.\n");
17003
17477
  return 2;
@@ -17009,6 +17483,11 @@ ${BOOTSTRAP_HELP}
17009
17483
  Blueprint: ${blueprint.name}
17010
17484
  \u2714 Scope gate: [${gate.accepted.join(", ")}] \u2014 all data-plane \u2713
17011
17485
  `);
17486
+ if (blueprint.accessProfile.capabilities?.length) {
17487
+ const capGate = evaluateCapabilityGate(blueprint.accessProfile.capabilities);
17488
+ process.stdout.write(` \u2714 Capability gate: [${capGate.accepted.join(", ")}] \u2014 granted \u2713
17489
+ `);
17490
+ }
17012
17491
  let noSeed = args.noSeed;
17013
17492
  let seedDeclinedInteractively = false;
17014
17493
  if (shouldOfferSeedOptOut(args, blueprint, isTty)) {
@@ -17082,6 +17561,7 @@ ${renderPlan(blueprint, planSteps)}
17082
17561
  keyringOpenError = err;
17083
17562
  }
17084
17563
  let storedFreshInKeyring = false;
17564
+ const lastResolvedSecret = /* @__PURE__ */ new Map();
17085
17565
  const readExistingKey = async (contextId) => {
17086
17566
  if (!keyring) return void 0;
17087
17567
  const alias = keyringAliasFor(contextId, args.tenant);
@@ -17089,7 +17569,8 @@ ${renderPlan(blueprint, planSteps)}
17089
17569
  const existing = await keyring.get(alias);
17090
17570
  if (!existing?.keyId) return void 0;
17091
17571
  const rawKey = await keyring.resolveSecret(alias);
17092
- return rawKey ? { keyId: existing.keyId, rawKey } : void 0;
17572
+ lastResolvedSecret.set(alias, rawKey);
17573
+ return rawKey ? { keyId: existing.keyId, rawKey, principalId: existing.principalId } : void 0;
17093
17574
  } catch {
17094
17575
  return void 0;
17095
17576
  }
@@ -17108,6 +17589,11 @@ ${renderPlan(blueprint, planSteps)}
17108
17589
  const alias = keyringAliasFor(contextId, args.tenant);
17109
17590
  try {
17110
17591
  warnReplacingAlias(await keyring.get(alias), alias, record.revokedKeyId);
17592
+ const willActivate = await determineActivateDecision(keyring, {
17593
+ tenant: args.tenant,
17594
+ explicitActivate: args.activate,
17595
+ hint: lastResolvedSecret.has(alias) ? { alias, secret: lastResolvedSecret.get(alias) } : void 0
17596
+ });
17111
17597
  await storeMintedKey(
17112
17598
  keyring,
17113
17599
  {
@@ -17122,11 +17608,14 @@ ${renderPlan(blueprint, planSteps)}
17122
17608
  env: deriveEnvironment(baseUrl) ?? args.env
17123
17609
  },
17124
17610
  record.rawKey,
17125
- { activate: true, revokedKeyId: record.revokedKeyId }
17611
+ { activate: willActivate, revokedKeyId: record.revokedKeyId }
17126
17612
  );
17127
17613
  if (record.revokedKeyId) {
17128
17614
  await warnOtherAliasesNaming(keyring, record.revokedKeyId, alias);
17129
17615
  }
17616
+ process.stdout.write(
17617
+ tenantActivateMessage({ alias, tenant: args.tenant, willActivate, explicitlyDeclined: args.activate === false })
17618
+ );
17130
17619
  storedFreshInKeyring = true;
17131
17620
  } catch (err) {
17132
17621
  process.stderr.write(
@@ -17147,12 +17636,20 @@ ${renderPlan(blueprint, planSteps)}
17147
17636
  skipSeed: noSeed,
17148
17637
  readExistingKey,
17149
17638
  persistMintedKey,
17639
+ remintBootstrapForContext: buildRemintBootstrapForContext({
17640
+ cognitoToken,
17641
+ apiBaseUrl: baseUrl,
17642
+ tenant: args.tenant,
17643
+ useScopedToken: args.useScopedToken,
17644
+ mintBridge
17645
+ }),
17150
17646
  remintForContext: buildRemintForContext({
17151
17647
  cognitoToken,
17152
17648
  apiBaseUrl: baseUrl,
17153
17649
  tenant: args.tenant,
17154
17650
  mintContext
17155
17651
  }),
17652
+ confirmExistingPrincipal: buildConfirmExistingPrincipal(args, promptCtx.prompt, isTty),
17156
17653
  log
17157
17654
  });
17158
17655
  process.stdout.write(
@@ -17230,6 +17727,18 @@ ${codeAddHint}`);
17230
17727
  `);
17231
17728
  return 3;
17232
17729
  }
17730
+ if (err instanceof ExistingPrincipalNotConfirmedError) {
17731
+ process.stderr.write(
17732
+ `
17733
+ \u2716 ${err.message}
17734
+
17735
+ If this is expected (a re-apply, or a deliberate second machine for an integration you already own),
17736
+ re-run with --confirm-existing-principal, or confirm interactively.
17737
+ If you don't recognize this identity, investigate the blueprint's servicePrincipal.externalId before re-running.
17738
+ `
17739
+ );
17740
+ return 3;
17741
+ }
17233
17742
  if (err instanceof BlueprintIdentityError || err instanceof UnresolvedIdentitiesError) {
17234
17743
  process.stderr.write(`
17235
17744
  \u2716 ${err.message}
@@ -17450,6 +17959,10 @@ function lintBlueprint(bp) {
17450
17959
  }
17451
17960
  }
17452
17961
  }
17962
+ {
17963
+ const reached = ownerScopedReachWithoutDataScope(bp.accessProfile.allowedActions, ds);
17964
+ if (reached.length > 0) findings.push(unconstrainedReachFinding("accessProfile", reached));
17965
+ }
17453
17966
  for (const roleId of Object.keys(bp.roles ?? {})) {
17454
17967
  if (!ROLE_ID_RE.test(roleId)) {
17455
17968
  findings.push({
@@ -17462,6 +17975,10 @@ function lintBlueprint(bp) {
17462
17975
  for (const [roleId, clauses] of Object.entries(bp.roles ?? {})) {
17463
17976
  clauses.forEach((clause, i) => {
17464
17977
  const cds = clause.dataScope;
17978
+ const reached = ownerScopedReachWithoutDataScope(clause.allowedActions, cds);
17979
+ if (reached.length > 0) {
17980
+ findings.push(unconstrainedReachFinding(`role '${roleId}' clause ${i}`, reached));
17981
+ }
17465
17982
  if (!cds) return;
17466
17983
  for (const [key, values] of Object.entries(cds)) {
17467
17984
  for (const value of values) {
@@ -17514,6 +18031,25 @@ function placeholderFinding(where, key, value) {
17514
18031
  message: `${where}: dataScope.${key} value '${value}' is not a placeholder the platform recognizes \u2014 it will be rejected at apply. Valid: \${{ any }}, \${{ self.userId }}, \${{ self.scope.<namespace> }}, \${{ under.self.userId }}, \${{ under.self.scope.<namespace> }}. Spelling is exact and lowercase; use a plain value for a literal owner id.`
17515
18032
  };
17516
18033
  }
18034
+ var OWNER_SCOPED_RESOURCES = /* @__PURE__ */ new Set(["records", "documents", "entities"]);
18035
+ var UNCONSTRAINED_REACH_OPS = /* @__PURE__ */ new Set(["r", "u", "d"]);
18036
+ function ownerScopedReachWithoutDataScope(allowedActions, dataScope) {
18037
+ const reached = /* @__PURE__ */ new Set();
18038
+ for (const action of allowedActions) {
18039
+ const [resource, ops] = action.split(":", 2);
18040
+ if (!resource || !ops || !OWNER_SCOPED_RESOURCES.has(resource)) continue;
18041
+ if (ops === "*" || [...ops].some((op) => UNCONSTRAINED_REACH_OPS.has(op))) reached.add(resource);
18042
+ }
18043
+ if (dataScope !== void 0) return [];
18044
+ return [...reached].sort();
18045
+ }
18046
+ function unconstrainedReachFinding(where, resources) {
18047
+ return {
18048
+ severity: "warning",
18049
+ rule: "datascope-absent-unconstrained-reach",
18050
+ message: `${where}: no dataScope at all, but allowedActions grants r/u/d on ${resources.join("/")} \u2014 this reaches EVERY row of EVERY owner on ${resources.length > 1 ? "those resources" : "that resource"}, not just "your own org" or any other value you may have intended. If that's deliberate (a genuinely shared/tenant-wide resource), you can ignore this; otherwise add a dataScope entry.`
18051
+ };
18052
+ }
17517
18053
  function lintPasses(findings) {
17518
18054
  return !findings.some((f) => f.severity === "error");
17519
18055
  }
@@ -17552,8 +18088,12 @@ schemas:
17552
18088
  lookupFields: [title] # extra O(1) exact-match lookup indexes (max 10)
17553
18089
 
17554
18090
  accessProfile:
17555
- # SCOPE GATE: data-plane only (records/schemas/search/documents/folders/inference/entities).
17556
- # Anything else (keys/profiles/users/admin/namespaces/*) is HARD-REJECTED \u2014 by design, no override.
18091
+ # SCOPE GATE: data-plane only (records/schemas/search/documents/folders/inference/entities),
18092
+ # PLUS profiles:r/u/d (bounded by the platform to redistributing access you already have,
18093
+ # never granting more) and profiles:c:self (create a profile for yourself only \u2014 the
18094
+ # platform resolves "self" from the caller, not from anything you declare here). A bare
18095
+ # profiles:c, or profiles:c:usr_<id> naming another principal, is HARD-REJECTED \u2014 by
18096
+ # design, no override. Same for keys/users/admin/namespaces/*.
17557
18097
  # Ops: c|r|u|d, combinable (records:cru). Prefer the narrowest set (least privilege).
17558
18098
  allowedActions: [records:r, records:c, records:u, search:r, schemas:r]
17559
18099
  # dataScope binds the key to specific owners. Include null to ALSO see
@@ -17561,6 +18101,17 @@ accessProfile:
17561
18101
  # owners' records, not shared/seed data. Keys are userId (the principal) and
17562
18102
  # namespaced scopes (scope:org, scope:client, ...):
17563
18103
  # dataScope: { "scope:org": [<org-uuid>, null] }
18104
+ # Left empty ({}) below \u2014 the explicit marker for "deliberately tenant-wide,
18105
+ # every record visible" ('blueprint validate' warns on a MISSING dataScope
18106
+ # key, but not on this one). This is a SCAFFOLD PLACEHOLDER, not a
18107
+ # recommendation: it also grants records:c/u tenant-wide, so before this
18108
+ # profile touches real data, replace it with a real scope (the example
18109
+ # above) unless every owner genuinely sharing every record is what you want.
18110
+ dataScope: {}
18111
+ # capabilities is a SEPARATE gated dimension from allowedActions \u2014 named platform
18112
+ # effects a resource:ops verb can't express. Only 'member-lifecycle' is admitted
18113
+ # today; every other name is HARD-REJECTED, same posture as the scope gate above.
18114
+ # capabilities: [member-lifecycle]
17564
18115
 
17565
18116
  servicePrincipal:
17566
18117
  externalId: ${name}-sp # stable; the principal the profile + key bind to
@@ -17680,15 +18231,19 @@ async function resolveAndValidate(arg, verb, supplied) {
17680
18231
  return null;
17681
18232
  }
17682
18233
  try {
17683
- assertScopeGate(blueprint.accessProfile.allowedActions);
17684
- for (const clauses of Object.values(blueprint.roles ?? {})) {
17685
- for (const clause of clauses) assertScopeGate(clause.allowedActions);
17686
- }
18234
+ assertBlueprintGates(blueprint);
17687
18235
  } catch (err) {
17688
18236
  if (err instanceof ScopeGateError) {
17689
18237
  process.stderr.write(` \u2716 Scope gate rejected '${blueprint.name}':
17690
18238
  `);
17691
18239
  for (const r of err.rejected) process.stderr.write(` \u2022 ${r.action}: ${r.reason}
18240
+ `);
18241
+ return null;
18242
+ }
18243
+ if (err instanceof CapabilityGateError) {
18244
+ process.stderr.write(` \u2716 Capability gate rejected '${blueprint.name}':
18245
+ `);
18246
+ for (const r of err.rejected) process.stderr.write(` \u2022 ${r.action}: ${r.reason}
17692
18247
  `);
17693
18248
  return null;
17694
18249
  }
@@ -17720,9 +18275,10 @@ async function runValidate(args) {
17720
18275
  const blueprint = await resolveAndValidate(args.file, "validate", supplied);
17721
18276
  if (!blueprint) return 2;
17722
18277
  const gate = evaluateScopeGate(blueprint.accessProfile.allowedActions);
18278
+ const capGate = evaluateCapabilityGate(blueprint.accessProfile.capabilities);
17723
18279
  const warnCount = lintBlueprint(blueprint).length;
17724
18280
  process.stdout.write(
17725
- ` \u2714 '${blueprint.name}' is valid \u2014 structural \u2713, scope gate \u2713 [${gate.accepted.join(", ")}], lint \u2713` + (warnCount ? ` (${warnCount} warning${warnCount === 1 ? "" : "s"})` : "") + "\n"
18281
+ ` \u2714 '${blueprint.name}' is valid \u2014 structural \u2713, scope gate \u2713 [${gate.accepted.join(", ")}]` + (capGate.accepted.length ? `, capability gate \u2713 [${capGate.accepted.join(", ")}]` : "") + `, lint \u2713` + (warnCount ? ` (${warnCount} warning${warnCount === 1 ? "" : "s"})` : "") + "\n"
17726
18282
  );
17727
18283
  return 0;
17728
18284
  }
@@ -18053,7 +18609,14 @@ async function resolveKeyDestination(opts, deps) {
18053
18609
  `);
18054
18610
  return 2;
18055
18611
  }
18056
- const alias = opts.alias ?? opts.context;
18612
+ let alias;
18613
+ try {
18614
+ alias = keyringAliasFor(opts.alias ?? opts.context, opts.tenant ?? "live");
18615
+ } catch (err) {
18616
+ process.stderr.write(` \u2716 ${formatError(err)}
18617
+ `);
18618
+ return 2;
18619
+ }
18057
18620
  if (opts.activate === false && await keyring.activeAlias() === alias) {
18058
18621
  process.stderr.write(
18059
18622
  ` \u2716 --no-activate cannot be used on '${alias}': it is your ACTIVE identity, and storing
@@ -18074,6 +18637,8 @@ async function storeAndReport(dest, key, auth, opts) {
18074
18637
  return 1;
18075
18638
  }
18076
18639
  const principalId = opts.principalId;
18640
+ const tenant = keyTenant(key.rawKey) ?? "test";
18641
+ const willActivate = await determineActivateDecision(dest.keyring, { tenant, explicitActivate: opts.activate });
18077
18642
  let entry;
18078
18643
  try {
18079
18644
  warnReplacingAlias(await dest.keyring.get(dest.alias), dest.alias, opts.revokedKeyId);
@@ -18088,7 +18653,7 @@ async function storeAndReport(dest, key, auth, opts) {
18088
18653
  env: auth.env
18089
18654
  },
18090
18655
  key.rawKey,
18091
- { activate: opts.activate !== false, revokedKeyId: opts.revokedKeyId }
18656
+ { activate: willActivate, revokedKeyId: opts.revokedKeyId }
18092
18657
  ));
18093
18658
  } catch (err) {
18094
18659
  process.stderr.write(
@@ -18100,7 +18665,6 @@ async function storeAndReport(dest, key, auth, opts) {
18100
18665
  );
18101
18666
  return 1;
18102
18667
  }
18103
- const active = opts.activate !== false;
18104
18668
  process.stdout.write(
18105
18669
  `
18106
18670
  \u2714 Stored the new key in your ${describeEntryStoreNoun(entry)} \u2014 it was not printed.
@@ -18110,9 +18674,19 @@ async function storeAndReport(dest, key, auth, opts) {
18110
18674
  ["principal", entry.principalId || void 0],
18111
18675
  ["key id", entry.keyId],
18112
18676
  ["env", entry.env],
18113
- ["active", active ? "yes" : "no"]
18114
- ]) + "\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.
18115
- `)
18677
+ ["active", willActivate ? "yes" : "no"]
18678
+ ]) + "\n\n" + // The plain "now active" note fires only on the activated path. When it
18679
+ // did NOT activate, tenantActivateMessage below is the ONLY explanation —
18680
+ // it already says to run `switch`, AND (unlike a bare "Not active") says
18681
+ // WHY: an explicit --no-activate vs. this call's tenant-safety default.
18682
+ // Saying both here would print two different "run switch" instructions
18683
+ // for one outcome.
18684
+ (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({
18685
+ alias: entry.alias,
18686
+ tenant,
18687
+ willActivate,
18688
+ explicitlyDeclined: opts.activate === false
18689
+ })
18116
18690
  );
18117
18691
  return 0;
18118
18692
  }
@@ -19646,6 +20220,19 @@ async function runAssertions(client, bootstrapClient, deps, blueprint, tenantId,
19646
20220
  assertions.push({ name: `issuer:${issuerId}`, ok: false, detail: `getIssuer(${issuerId}) failed: ${errMsg(e)}` });
19647
20221
  }
19648
20222
  }
20223
+ for (const { namespace } of result.namespaceActions) {
20224
+ try {
20225
+ const ns = await bootstrapClient.identity.getNamespace({ namespace, contextId: blueprint.contextId });
20226
+ const ok = ns.namespace === namespace && ns.contextId === blueprint.contextId;
20227
+ assertions.push({
20228
+ name: `namespace:${namespace}`,
20229
+ ok,
20230
+ 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}'`
20231
+ });
20232
+ } catch (e) {
20233
+ assertions.push({ name: `namespace:${namespace}`, ok: false, detail: `getNamespace(${namespace}) failed: ${errMsg(e)}` });
20234
+ }
20235
+ }
19649
20236
  return assertions;
19650
20237
  }
19651
20238
  async function runTeardown(client, bootstrapClient, blueprint, machineId, snapshot, result) {
@@ -19758,10 +20345,18 @@ async function runTeardown(client, bootstrapClient, blueprint, machineId, snapsh
19758
20345
  steps.push({ step: `delete issuer ${issuerId}`, ok: false, error: errMsg(e) });
19759
20346
  }
19760
20347
  }
20348
+ for (const { namespace } of result.namespaceActions) {
20349
+ steps.push({
20350
+ step: `delete namespace ${namespace}`,
20351
+ ok: true,
20352
+ skipped: true,
20353
+ error: "namespace deletion requires a root API key (sk_*) \u2014 blueprint-test never holds one; the registration is left in place"
20354
+ });
20355
+ }
19761
20356
  return steps;
19762
20357
  }
19763
20358
  async function runBlueprintHarness(client, input, deps) {
19764
- const { tenantId, machineId, keep = false, log, remintForContext } = input;
20359
+ const { tenantId, machineId, keep = false, log, remintForContext, remintBootstrapForContext } = input;
19765
20360
  const readExistingKey = async () => void 0;
19766
20361
  const persistMintedKey = async () => {
19767
20362
  };
@@ -19771,6 +20366,9 @@ async function runBlueprintHarness(client, input, deps) {
19771
20366
  log
19772
20367
  );
19773
20368
  const remint = remintForContext ?? (async () => client);
20369
+ const remintBootstrap = remintBootstrapForContext ?? (async () => client);
20370
+ const hasIssuers = (blueprint.issuers ?? []).length > 0;
20371
+ const hasNamespaces = (blueprint.namespaces ?? []).length > 0;
19774
20372
  let contextSnapshot;
19775
20373
  if (remintForContext) {
19776
20374
  try {
@@ -19786,7 +20384,18 @@ async function runBlueprintHarness(client, input, deps) {
19786
20384
  } else {
19787
20385
  contextSnapshot = await captureSnapshot(client, blueprint);
19788
20386
  }
19789
- const issuerSnapshot = await captureIssuerSnapshot(client);
20387
+ let issuerSnapshot;
20388
+ if (!hasIssuers) {
20389
+ issuerSnapshot = { existingIssuerIds: /* @__PURE__ */ new Set(), failed: false };
20390
+ } else if (remintBootstrapForContext) {
20391
+ try {
20392
+ issuerSnapshot = await captureIssuerSnapshot(await remintBootstrapForContext(blueprint.contextId));
20393
+ } catch (e) {
20394
+ issuerSnapshot = { existingIssuerIds: /* @__PURE__ */ new Set(), failed: !isNotFound(e) };
20395
+ }
20396
+ } else {
20397
+ issuerSnapshot = await captureIssuerSnapshot(client);
20398
+ }
19790
20399
  const snapshot = {
19791
20400
  ...contextSnapshot,
19792
20401
  existingIssuerIds: issuerSnapshot.existingIssuerIds,
@@ -19796,9 +20405,25 @@ async function runBlueprintHarness(client, input, deps) {
19796
20405
  let assertions = [];
19797
20406
  let applyError;
19798
20407
  try {
19799
- result = await runLoader(client, { blueprint, tenantId, machineId, readExistingKey, persistMintedKey, log, remintForContext });
20408
+ result = await runLoader(client, {
20409
+ blueprint,
20410
+ tenantId,
20411
+ machineId,
20412
+ readExistingKey,
20413
+ persistMintedKey,
20414
+ log,
20415
+ remintForContext,
20416
+ remintBootstrapForContext
20417
+ });
19800
20418
  log.debug({ component: "harness", contextId: result.contextId, keyName: result.keyName }, "applied");
19801
- assertions = await runAssertions(await remint(blueprint.contextId), client, deps, blueprint, tenantId, result);
20419
+ assertions = await runAssertions(
20420
+ await remint(blueprint.contextId),
20421
+ hasIssuers || hasNamespaces ? await remintBootstrap(blueprint.contextId) : client,
20422
+ deps,
20423
+ blueprint,
20424
+ tenantId,
20425
+ result
20426
+ );
19802
20427
  } catch (e) {
19803
20428
  applyError = errMsg(e);
19804
20429
  log.warn({ component: "harness", err: applyError }, "apply phase failed");
@@ -19810,7 +20435,14 @@ async function runBlueprintHarness(client, input, deps) {
19810
20435
  tdClient = await remint(blueprint.contextId);
19811
20436
  } catch {
19812
20437
  }
19813
- teardown = await runTeardown(tdClient, client, blueprint, machineId, snapshot, result);
20438
+ let tdBootstrapClient = client;
20439
+ if (hasIssuers) {
20440
+ try {
20441
+ tdBootstrapClient = await remintBootstrap(blueprint.contextId);
20442
+ } catch {
20443
+ }
20444
+ }
20445
+ teardown = await runTeardown(tdClient, tdBootstrapClient, blueprint, machineId, snapshot, result);
19814
20446
  }
19815
20447
  const passed = applyError === void 0 && assertions.length > 0 && assertions.every((a) => a.ok);
19816
20448
  return {
@@ -19884,6 +20516,13 @@ async function runBlueprintTest(fileOrName, opts, deps = {}) {
19884
20516
  process.stderr.write(` \u2716 Scope gate refused blueprint:
19885
20517
  `);
19886
20518
  for (const r of err.rejected) process.stderr.write(` \u2022 ${r.action}: ${r.reason}
20519
+ `);
20520
+ return 2;
20521
+ }
20522
+ if (err instanceof CapabilityGateError) {
20523
+ process.stderr.write(` \u2716 Capability gate refused blueprint:
20524
+ `);
20525
+ for (const r of err.rejected) process.stderr.write(` \u2022 ${r.action}: ${r.reason}
19887
20526
  `);
19888
20527
  return 2;
19889
20528
  }
@@ -19919,9 +20558,22 @@ async function runBlueprintTest(fileOrName, opts, deps = {}) {
19919
20558
  environment: auth.baseUrl
19920
20559
  });
19921
20560
  };
20561
+ const remintBootstrapForContext = async (contextId) => {
20562
+ const ctxToken = await mintBridgeToken({
20563
+ cognitoToken: auth.cognitoToken,
20564
+ apiBaseUrl: auth.baseUrl,
20565
+ contextId,
20566
+ tenant: auth.tenant,
20567
+ useScopedToken: opts.useScopedToken
20568
+ });
20569
+ return new VectrosClient({
20570
+ token: ctxToken.token,
20571
+ environment: auth.baseUrl
20572
+ });
20573
+ };
19922
20574
  const report = await runBlueprintHarness(
19923
20575
  client,
19924
- { blueprint, tenantId, machineId, keep: opts.keep, log, remintForContext },
20576
+ { blueprint, tenantId, machineId, keep: opts.keep, log, remintForContext, remintBootstrapForContext },
19925
20577
  { pingWithKey }
19926
20578
  );
19927
20579
  if (opts.json) {
@@ -20002,9 +20654,10 @@ async function runJoin(opts, deps = {}) {
20002
20654
  return 2;
20003
20655
  }
20004
20656
  const context = opts.contextArg;
20657
+ const tenant = opts.tenant ?? "live";
20005
20658
  let alias;
20006
20659
  try {
20007
- alias = keyringAliasFor(opts.alias ?? context, opts.tenant ?? "live");
20660
+ alias = keyringAliasFor(opts.alias ?? context, tenant);
20008
20661
  } catch (err) {
20009
20662
  process.stderr.write(` \u2716 ${formatError(err)}
20010
20663
  `);
@@ -20039,8 +20692,23 @@ async function runJoin(opts, deps = {}) {
20039
20692
  const heldSecret = existingEntry ? await keyring.resolveSecret(alias) : void 0;
20040
20693
  const canReuse = existingEntry && heldSecret && existingEntry.context === context && existingEntry.principalId === principalId && existingEntry.env === auth.env;
20041
20694
  if (canReuse && existingEntry) {
20042
- await keyring.setActive(alias);
20043
- emitJoined(alias, context, principalId, opts.role, existingEntry.keyId, true);
20695
+ const willActivate2 = await determineActivateDecision(keyring, {
20696
+ tenant,
20697
+ explicitActivate: opts.activate,
20698
+ hint: { alias, secret: heldSecret }
20699
+ });
20700
+ if (willActivate2) await keyring.setActive(alias);
20701
+ emitJoined({
20702
+ alias,
20703
+ context,
20704
+ principalId,
20705
+ role: opts.role,
20706
+ keyId: existingEntry.keyId,
20707
+ reused: true,
20708
+ tenant,
20709
+ willActivate: willActivate2,
20710
+ explicitlyDeclined: opts.activate === false
20711
+ });
20044
20712
  return 0;
20045
20713
  }
20046
20714
  warnReplacingAlias(existingEntry, alias);
@@ -20052,13 +20720,24 @@ async function runJoin(opts, deps = {}) {
20052
20720
  );
20053
20721
  }
20054
20722
  if (revokedKeyId) await warnOtherAliasesNaming(keyring, revokedKeyId, alias);
20723
+ const willActivate = await determineActivateDecision(keyring, { tenant, explicitActivate: opts.activate });
20055
20724
  const { entry } = await storeMintedKey(
20056
20725
  keyring,
20057
20726
  { alias, context, principalId, role: opts.role, keyId: key.keyId, env: auth.env },
20058
20727
  key.rawKey,
20059
- { revokedKeyId }
20728
+ { activate: willActivate, revokedKeyId }
20060
20729
  );
20061
- emitJoined(alias, context, principalId, opts.role, entry.keyId, false);
20730
+ emitJoined({
20731
+ alias,
20732
+ context,
20733
+ principalId,
20734
+ role: opts.role,
20735
+ keyId: entry.keyId,
20736
+ reused: false,
20737
+ tenant,
20738
+ willActivate,
20739
+ explicitlyDeclined: opts.activate === false
20740
+ });
20062
20741
  return 0;
20063
20742
  } catch (err) {
20064
20743
  process.stderr.write(formatCliError(err, { action: "join failed" }));
@@ -20066,27 +20745,34 @@ async function runJoin(opts, deps = {}) {
20066
20745
  }
20067
20746
  });
20068
20747
  }
20069
- function emitJoined(alias, context, principalId, role, keyId, reused) {
20070
- process.stdout.write(
20071
- `
20072
- \u2714 Joined '${context}' as ${principalId}` + (reused ? " (existing key reused)" : "") + "\n" + formatKeyValues([
20073
- ["alias", alias],
20074
- ["context", context],
20075
- ["principal", principalId],
20076
- ["role", role],
20077
- ["key id", keyId]
20078
- ]) + `
20748
+ function emitJoined(opts) {
20749
+ const header = `
20750
+ \u2714 Joined '${opts.context}' as ${opts.principalId}` + (opts.reused ? " (existing key reused)" : "") + "\n" + formatKeyValues([
20751
+ ["alias", opts.alias],
20752
+ ["context", opts.context],
20753
+ ["principal", opts.principalId],
20754
+ ["role", opts.role],
20755
+ ["key id", opts.keyId]
20756
+ ]);
20757
+ const activationNote = opts.willActivate ? `
20079
20758
 
20080
- Now active. Use \`vectros switch ${alias}\` to return to it, \`vectros keyring show --format env\` to export the key for the MCP server.
20081
- `
20759
+ Now active. Use \`vectros switch ${opts.alias}\` to return to it, \`vectros keyring show --format env\` to export the key for the MCP server.
20760
+ ` : "";
20761
+ process.stdout.write(
20762
+ header + activationNote + tenantActivateMessage({
20763
+ alias: opts.alias,
20764
+ tenant: opts.tenant,
20765
+ willActivate: opts.willActivate,
20766
+ explicitlyDeclined: opts.explicitlyDeclined
20767
+ })
20082
20768
  );
20083
20769
  }
20084
20770
 
20085
20771
  // src/build-info.ts
20086
20772
  var BUILD_INFO = {
20087
- cli: true ? "0.15.0" : "dev",
20088
- sdk: true ? "0.39.0" : "dev",
20089
- blueprints: true ? "0.12.0" : "dev"
20773
+ cli: true ? "0.16.0" : "dev",
20774
+ sdk: true ? "0.40.0" : "dev",
20775
+ blueprints: true ? "0.13.0" : "dev"
20090
20776
  };
20091
20777
  function formatBuildInfo() {
20092
20778
  return `vectros ${BUILD_INFO.cli} (sdk ${BUILD_INFO.sdk}, blueprints ${BUILD_INFO.blueprints})`;
@@ -20110,6 +20796,14 @@ function addCommonAuthOptions(cmd) {
20110
20796
  "emit machine-readable JSON instead of the human-readable view"
20111
20797
  );
20112
20798
  }
20799
+ function parseTenantFlag(raw) {
20800
+ if (raw !== void 0 && raw !== "test" && raw !== "live") {
20801
+ process.stderr.write(` \u2716 --tenant must be 'test' or 'live' (got '${raw}')
20802
+ `);
20803
+ process.exit(2);
20804
+ }
20805
+ return raw;
20806
+ }
20113
20807
  function toCommonOptions(flags) {
20114
20808
  return {
20115
20809
  env: flags.env,
@@ -20163,7 +20857,10 @@ function buildProgram(deps = {}) {
20163
20857
  return program;
20164
20858
  }
20165
20859
  function registerJoinCommand(program, deps) {
20166
- 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)");
20860
+ const join3 = program.command("join").description("Enroll a principal in a context (grant + key) and keep the key in the keyring.").argument("<context>", "the app context to join").option("--role <roleId>", "reference a Role to grant (XOR --actions)").option("--actions <csv>", "inline single-clause actions to grant, comma-separated (XOR --role)").option("--as <principal>", "who to enroll: 'me' (default) or an explicit 'usr_<id>'", "me").option("--scope <ns:value>", "identity override: a scope the member acts under (repeatable, e.g. org:<id>)", collect, []).option("--alias <name>", "keyring entry name (defaults to the context id)").option("--tenant <tenant>", "target tenant: 'test' or 'live' (default live)").option(
20861
+ "--activate",
20862
+ "force the freshly-stored key to become your active identity, even over a currently-active LIVE credential (the one case --tenant test does NOT do automatically)"
20863
+ ).option("--no-activate", "store the key without activating it, regardless of tenant");
20167
20864
  addAuthOptions(join3).action(
20168
20865
  async (contextArg, flags) => {
20169
20866
  if (flags.tenant !== void 0 && flags.tenant !== "test" && flags.tenant !== "live") {
@@ -20181,7 +20878,8 @@ function registerJoinCommand(program, deps) {
20181
20878
  as: flags.as,
20182
20879
  scope: flags.scope,
20183
20880
  alias: flags.alias,
20184
- tenant: flags.tenant
20881
+ tenant: flags.tenant,
20882
+ activate: flags.activate
20185
20883
  },
20186
20884
  deps
20187
20885
  )
@@ -20428,9 +21126,10 @@ function toKeyDestination(flags) {
20428
21126
  function registerKeyCommands(program, deps) {
20429
21127
  const key = program.command("key").description("Manage scoped API keys (ssk_*) \u2014 issue, list, revoke, rotate.");
20430
21128
  const issue = addKeyDestinationOptions(
20431
- 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)")
21129
+ 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)")
20432
21130
  );
20433
21131
  addAuthOptions(issue).action(async (flags) => {
21132
+ const tenant = parseTenantFlag(flags.tenant);
20434
21133
  process.exit(
20435
21134
  await runKeyIssue({
20436
21135
  ...toCommonOptions(flags),
@@ -20438,6 +21137,7 @@ function registerKeyCommands(program, deps) {
20438
21137
  context: flags.context,
20439
21138
  name: flags.name,
20440
21139
  label: flags.label,
21140
+ tenant,
20441
21141
  ...toKeyDestination(flags)
20442
21142
  }, deps)
20443
21143
  );
@@ -20445,37 +21145,50 @@ function registerKeyCommands(program, deps) {
20445
21145
  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(
20446
21146
  "--all-contexts",
20447
21147
  "list every key across ALL your contexts (owner-only) \u2014 the tenant-wide view for the key limit"
20448
- );
21148
+ ).option("--tenant <tenant>", "tenant to list: 'test' or 'live' (default live; not combinable with --all-contexts)");
20449
21149
  addCommonAuthOptions(list).action(
20450
21150
  async (flags) => {
21151
+ if (flags.allContexts && flags.tenant !== void 0) {
21152
+ process.stderr.write(" \u2716 --tenant is not combinable with --all-contexts (which already spans both tenants).\n");
21153
+ process.exit(2);
21154
+ }
21155
+ const tenant = parseTenantFlag(flags.tenant);
20451
21156
  process.exit(
20452
21157
  await runKeyList(
20453
21158
  {
20454
21159
  ...toCommonOptions(flags),
20455
21160
  principal: flags.principal,
20456
21161
  context: flags.context,
20457
- allContexts: Boolean(flags.allContexts)
21162
+ allContexts: Boolean(flags.allContexts),
21163
+ tenant
20458
21164
  },
20459
21165
  deps
20460
21166
  )
20461
21167
  );
20462
21168
  }
20463
21169
  );
20464
- 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)");
20465
- addCommonAuthOptions(get).action(async (keyId, flags) => {
20466
- process.exit(await runKeyGet(keyId, { ...toCommonOptions(flags), context: flags.context }, deps));
20467
- });
20468
- 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)");
20469
- addAuthOptions(revoke).action(async (keyId, flags) => {
20470
- process.exit(await runKeyRevoke(keyId, { ...toCommonOptions(flags), context: flags.context }, deps));
20471
- });
21170
+ 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)");
21171
+ addCommonAuthOptions(get).action(
21172
+ async (keyId, flags) => {
21173
+ const tenant = parseTenantFlag(flags.tenant);
21174
+ process.exit(await runKeyGet(keyId, { ...toCommonOptions(flags), context: flags.context, tenant }, deps));
21175
+ }
21176
+ );
21177
+ 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)");
21178
+ addAuthOptions(revoke).action(
21179
+ async (keyId, flags) => {
21180
+ const tenant = parseTenantFlag(flags.tenant);
21181
+ process.exit(await runKeyRevoke(keyId, { ...toCommonOptions(flags), context: flags.context, tenant }, deps));
21182
+ }
21183
+ );
20472
21184
  const rotate = addKeyDestinationOptions(
20473
21185
  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(
20474
21186
  "--force-new",
20475
21187
  "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)"
20476
- )
21188
+ ).option("--tenant <tenant>", "tenant the key lives in: 'test' or 'live' (default live)")
20477
21189
  );
20478
21190
  addAuthOptions(rotate).action(async (flags) => {
21191
+ const tenant = parseTenantFlag(flags.tenant);
20479
21192
  process.exit(
20480
21193
  await runKeyRotate({
20481
21194
  ...toCommonOptions(flags),
@@ -20483,6 +21196,7 @@ function registerKeyCommands(program, deps) {
20483
21196
  context: flags.context,
20484
21197
  name: flags.name,
20485
21198
  forceNew: flags.forceNew,
21199
+ tenant,
20486
21200
  ...toKeyDestination(flags)
20487
21201
  }, deps)
20488
21202
  );