@vectros-ai/cli 0.14.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
@@ -1680,6 +1680,7 @@ __export(api_exports, {
1680
1680
  TooManyRequestsError: () => TooManyRequestsError,
1681
1681
  TruncationWarningEvent: () => TruncationWarningEvent,
1682
1682
  UnauthorizedError: () => UnauthorizedError,
1683
+ UserExistsResponse: () => UserExistsResponse,
1683
1684
  UserRequest: () => UserRequest,
1684
1685
  UserResponse: () => UserResponse,
1685
1686
  auth: () => auth_exports,
@@ -2356,6 +2357,14 @@ var TruncationWarningEvent;
2356
2357
  ContextWindowBudget: "context_window_budget"
2357
2358
  };
2358
2359
  })(TruncationWarningEvent || (TruncationWarningEvent = {}));
2360
+ var UserExistsResponse;
2361
+ ((UserExistsResponse2) => {
2362
+ UserExistsResponse2.Status = {
2363
+ Active: "ACTIVE",
2364
+ Suspended: "SUSPENDED",
2365
+ Pending: "PENDING"
2366
+ };
2367
+ })(UserExistsResponse || (UserExistsResponse = {}));
2359
2368
  var UserRequest;
2360
2369
  ((UserRequest2) => {
2361
2370
  UserRequest2.Status = {
@@ -4099,7 +4108,7 @@ var AuthClient = class {
4099
4108
  return handleNonStatusCodeError(_response.error, _response.rawResponse, "GET", "/v1/admin/keys/scoped");
4100
4109
  }
4101
4110
  /**
4102
- * Creates a scoped API key (an `ssk_*` secret) that inherits its permissions from an existing access profile in your account. The call is idempotent on the combination of tenant, context, user, and key name: re-issuing the same request returns the existing key WITHOUT re-disclosing its raw secret. The raw key is returned ONLY in this response — store it securely, as it cannot be retrieved again. Requires the `keys:c` scope.
4111
+ * Creates a scoped API key (an `ssk_*` secret) that inherits its permissions from an existing access profile in your account. The call is idempotent on the combination of tenant, context, user, and key name: re-issuing the same request returns the existing key WITHOUT re-disclosing its raw secret. The raw key is returned ONLY in this response — store it securely, as it cannot be retrieved again. Requires the `keys:c` scope. If you use a scoped credential, `keys:c` alone is not sufficient: because the minted key is durably bound to the profile you name, the profile's effective scopes may not exceed your own, and you may only mint against a profile whose `identityOverrides` values your own identity holds. A root API key (`sk_`) is exempt from both bounds.
4103
4112
  *
4104
4113
  * @param {Vectros.CreateScopedKeyRequest} request
4105
4114
  * @param {AuthClient.RequestOptions} requestOptions - Request-specific configuration.
@@ -4431,7 +4440,7 @@ var AuthClient = class {
4431
4440
  );
4432
4441
  }
4433
4442
  /**
4434
- * 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.
4435
4444
  *
4436
4445
  * @param {Vectros.CreateAccessProfileRequest} request
4437
4446
  * @param {AuthClient.RequestOptions} requestOptions - Request-specific configuration.
@@ -4849,11 +4858,12 @@ var AuthClient = class {
4849
4858
  );
4850
4859
  }
4851
4860
  /**
4852
- * Updates an access profile. This is a partial update: any field you omit (or send as null) keeps its existing value. A profile must reference either inline `scopes` or a `roleId`, never both — so setting `scopes` clears any `roleId`, and setting `roleId` clears any inline `scopes`. The `contextId` and `principalId` are immutable. Status changes (for example active to suspended) take effect within about five minutes. If you use a scoped credential, the profile's effective scopes may not exceed your own, and its `identityOverrides` are bounded twice: you may only set a value your own identity holds, and you may only change or clear a value the profile already holds if that value is yours as well. Repointing or clearing another principal's established identity therefore returns 403. A root API key (`sk_`) is exempt. Requires the `profiles:u` scope.
4861
+ * Updates an access profile. This is a partial update: any field you omit (or send as null) keeps its existing value. A profile must reference either inline `scopes` or a `roleId`, never both — so setting `scopes` clears any `roleId`, and setting `roleId` clears any inline `scopes`. The `contextId` and `principalId` are immutable. Status changes (for example active to suspended) take effect within about five minutes. If you use a scoped credential, the profile's effective scopes may not exceed your own, and its `identityOverrides` are bounded twice: you may only set a value your own identity holds, and you may only change or clear a value the profile already holds if that value is yours as well. Repointing or clearing another principal's established identity therefore returns 403. A root API key (`sk_`) is exempt. If you set `roleId`, it must reference a role that already exists in this context. Requires the `profiles:u` scope.
4853
4862
  *
4854
4863
  * @param {Vectros.UpdateAccessProfileRequest} request
4855
4864
  * @param {AuthClient.RequestOptions} requestOptions - Request-specific configuration.
4856
4865
  *
4866
+ * @throws {@link Vectros.BadRequestError}
4857
4867
  * @throws {@link Vectros.ForbiddenError}
4858
4868
  * @throws {@link Vectros.NotFoundError}
4859
4869
  * @throws {@link Vectros.TooManyRequestsError}
@@ -4900,6 +4910,8 @@ var AuthClient = class {
4900
4910
  }
4901
4911
  if (_response.error.reason === "status-code") {
4902
4912
  switch (_response.error.statusCode) {
4913
+ case 400:
4914
+ throw new BadRequestError(_response.error.body, _response.rawResponse);
4903
4915
  case 403:
4904
4916
  throw new ForbiddenError(_response.error.body, _response.rawResponse);
4905
4917
  case 404:
@@ -5529,7 +5541,7 @@ var AuthClient = class {
5529
5541
  );
5530
5542
  }
5531
5543
  /**
5532
- * 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.
5533
5545
  *
5534
5546
  * @param {Vectros.GetUsageRequest} request
5535
5547
  * @param {AuthClient.RequestOptions} requestOptions - Request-specific configuration.
@@ -5591,6 +5603,257 @@ var AuthClient = class {
5591
5603
  }
5592
5604
  return handleNonStatusCodeError(_response.error, _response.rawResponse, "GET", "/v1/usage");
5593
5605
  }
5606
+ /**
5607
+ * Retrieves a single registered issuer by issuerId. Requires a root API key or the bootstrap's provisioning capability. A credential confined to one app context sees only an issuer registered in that context; naming one registered in another context returns 404, identically to a nonexistent issuerId. A root API key sees every context.
5608
+ *
5609
+ * @param {Vectros.GetIssuerRequest} request
5610
+ * @param {AuthClient.RequestOptions} requestOptions - Request-specific configuration.
5611
+ *
5612
+ * @throws {@link Vectros.ForbiddenError}
5613
+ * @throws {@link Vectros.NotFoundError}
5614
+ *
5615
+ * @example
5616
+ * await client.auth.getIssuer({
5617
+ * issuerId: "auth0-prod"
5618
+ * })
5619
+ */
5620
+ getIssuer(request, requestOptions) {
5621
+ return HttpResponsePromise.fromPromise(this.__getIssuer(request, requestOptions));
5622
+ }
5623
+ async __getIssuer(request, requestOptions) {
5624
+ const { issuerId } = request;
5625
+ const _authRequest = await this._options.authProvider.getAuthRequest();
5626
+ const _headers = mergeHeaders(
5627
+ _authRequest.headers,
5628
+ this._options?.headers,
5629
+ requestOptions?.headers
5630
+ );
5631
+ const _response = await fetcher({
5632
+ url: url_exports.join(
5633
+ await Supplier.get(this._options.baseUrl) ?? await Supplier.get(this._options.environment),
5634
+ `v1/auth/issuers/${url_exports.encodePathParam(issuerId)}`
5635
+ ),
5636
+ method: "GET",
5637
+ headers: _headers,
5638
+ queryString: url_exports.queryBuilder().mergeAdditional(requestOptions?.queryParams).build(),
5639
+ timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1e3,
5640
+ maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries,
5641
+ abortSignal: requestOptions?.abortSignal,
5642
+ fetchFn: this._options?.fetch,
5643
+ logging: this._options.logging
5644
+ });
5645
+ if (_response.ok) {
5646
+ return { data: _response.body, rawResponse: _response.rawResponse };
5647
+ }
5648
+ if (_response.error.reason === "status-code") {
5649
+ switch (_response.error.statusCode) {
5650
+ case 403:
5651
+ throw new ForbiddenError(_response.error.body, _response.rawResponse);
5652
+ case 404:
5653
+ throw new NotFoundError(_response.error.body, _response.rawResponse);
5654
+ default:
5655
+ throw new VectrosError({
5656
+ statusCode: _response.error.statusCode,
5657
+ body: _response.error.body,
5658
+ rawResponse: _response.rawResponse
5659
+ });
5660
+ }
5661
+ }
5662
+ return handleNonStatusCodeError(_response.error, _response.rawResponse, "GET", "/v1/auth/issuers/{issuerId}");
5663
+ }
5664
+ /**
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
+ *
5667
+ * @param {Vectros.DeleteIssuerRequest} request
5668
+ * @param {AuthClient.RequestOptions} requestOptions - Request-specific configuration.
5669
+ *
5670
+ * @throws {@link Vectros.ForbiddenError}
5671
+ * @throws {@link Vectros.NotFoundError}
5672
+ * @throws {@link Vectros.ConflictError}
5673
+ * @throws {@link Vectros.TooManyRequestsError}
5674
+ *
5675
+ * @example
5676
+ * await client.auth.deleteIssuer({
5677
+ * issuerId: "auth0-prod"
5678
+ * })
5679
+ */
5680
+ deleteIssuer(request, requestOptions) {
5681
+ return HttpResponsePromise.fromPromise(this.__deleteIssuer(request, requestOptions));
5682
+ }
5683
+ async __deleteIssuer(request, requestOptions) {
5684
+ const { issuerId } = request;
5685
+ const _authRequest = await this._options.authProvider.getAuthRequest();
5686
+ const _headers = mergeHeaders(
5687
+ _authRequest.headers,
5688
+ this._options?.headers,
5689
+ requestOptions?.headers
5690
+ );
5691
+ const _response = await fetcher({
5692
+ url: url_exports.join(
5693
+ await Supplier.get(this._options.baseUrl) ?? await Supplier.get(this._options.environment),
5694
+ `v1/auth/issuers/${url_exports.encodePathParam(issuerId)}`
5695
+ ),
5696
+ method: "DELETE",
5697
+ headers: _headers,
5698
+ queryString: url_exports.queryBuilder().mergeAdditional(requestOptions?.queryParams).build(),
5699
+ timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1e3,
5700
+ maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries,
5701
+ abortSignal: requestOptions?.abortSignal,
5702
+ fetchFn: this._options?.fetch,
5703
+ logging: this._options.logging
5704
+ });
5705
+ if (_response.ok) {
5706
+ return { data: void 0, rawResponse: _response.rawResponse };
5707
+ }
5708
+ if (_response.error.reason === "status-code") {
5709
+ switch (_response.error.statusCode) {
5710
+ case 403:
5711
+ throw new ForbiddenError(_response.error.body, _response.rawResponse);
5712
+ case 404:
5713
+ throw new NotFoundError(_response.error.body, _response.rawResponse);
5714
+ case 409:
5715
+ throw new ConflictError(_response.error.body, _response.rawResponse);
5716
+ case 429:
5717
+ throw new TooManyRequestsError(_response.error.body, _response.rawResponse);
5718
+ default:
5719
+ throw new VectrosError({
5720
+ statusCode: _response.error.statusCode,
5721
+ body: _response.error.body,
5722
+ rawResponse: _response.rawResponse
5723
+ });
5724
+ }
5725
+ }
5726
+ return handleNonStatusCodeError(
5727
+ _response.error,
5728
+ _response.rawResponse,
5729
+ "DELETE",
5730
+ "/v1/auth/issuers/{issuerId}"
5731
+ );
5732
+ }
5733
+ /**
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.
5735
+ *
5736
+ * @param {Vectros.ListIssuersRequest} request
5737
+ * @param {AuthClient.RequestOptions} requestOptions - Request-specific configuration.
5738
+ *
5739
+ * @throws {@link Vectros.ForbiddenError}
5740
+ *
5741
+ * @example
5742
+ * await client.auth.listIssuers()
5743
+ */
5744
+ listIssuers(request = {}, requestOptions) {
5745
+ return HttpResponsePromise.fromPromise(this.__listIssuers(request, requestOptions));
5746
+ }
5747
+ async __listIssuers(request = {}, requestOptions) {
5748
+ const { startFrom, limit } = request;
5749
+ const _queryParams = {
5750
+ startFrom,
5751
+ limit
5752
+ };
5753
+ const _authRequest = await this._options.authProvider.getAuthRequest();
5754
+ const _headers = mergeHeaders(
5755
+ _authRequest.headers,
5756
+ this._options?.headers,
5757
+ requestOptions?.headers
5758
+ );
5759
+ const _response = await fetcher({
5760
+ url: url_exports.join(
5761
+ await Supplier.get(this._options.baseUrl) ?? await Supplier.get(this._options.environment),
5762
+ "v1/auth/issuers"
5763
+ ),
5764
+ method: "GET",
5765
+ headers: _headers,
5766
+ queryString: url_exports.queryBuilder().addMany(_queryParams).mergeAdditional(requestOptions?.queryParams).build(),
5767
+ timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1e3,
5768
+ maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries,
5769
+ abortSignal: requestOptions?.abortSignal,
5770
+ fetchFn: this._options?.fetch,
5771
+ logging: this._options.logging
5772
+ });
5773
+ if (_response.ok) {
5774
+ return { data: _response.body, rawResponse: _response.rawResponse };
5775
+ }
5776
+ if (_response.error.reason === "status-code") {
5777
+ switch (_response.error.statusCode) {
5778
+ case 403:
5779
+ throw new ForbiddenError(_response.error.body, _response.rawResponse);
5780
+ default:
5781
+ throw new VectrosError({
5782
+ statusCode: _response.error.statusCode,
5783
+ body: _response.error.body,
5784
+ rawResponse: _response.rawResponse
5785
+ });
5786
+ }
5787
+ }
5788
+ return handleNonStatusCodeError(_response.error, _response.rawResponse, "GET", "/v1/auth/issuers");
5789
+ }
5790
+ /**
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`.
5792
+ *
5793
+ * @param {Vectros.IssuerRequest} request
5794
+ * @param {AuthClient.RequestOptions} requestOptions - Request-specific configuration.
5795
+ *
5796
+ * @throws {@link Vectros.BadRequestError}
5797
+ * @throws {@link Vectros.ForbiddenError}
5798
+ * @throws {@link Vectros.TooManyRequestsError}
5799
+ *
5800
+ * @example
5801
+ * await client.auth.registerIssuer({
5802
+ * issuerId: "auth0-prod",
5803
+ * issuer: "https://your-tenant.us.auth0.com/",
5804
+ * jwksUri: "https://your-tenant.us.auth0.com/.well-known/jwks.json",
5805
+ * audience: "https://api.your-app.example.com",
5806
+ * contextId: "casework"
5807
+ * })
5808
+ */
5809
+ registerIssuer(request, requestOptions) {
5810
+ return HttpResponsePromise.fromPromise(this.__registerIssuer(request, requestOptions));
5811
+ }
5812
+ async __registerIssuer(request, requestOptions) {
5813
+ const _authRequest = await this._options.authProvider.getAuthRequest();
5814
+ const _headers = mergeHeaders(
5815
+ _authRequest.headers,
5816
+ this._options?.headers,
5817
+ requestOptions?.headers
5818
+ );
5819
+ const _response = await fetcher({
5820
+ url: url_exports.join(
5821
+ await Supplier.get(this._options.baseUrl) ?? await Supplier.get(this._options.environment),
5822
+ "v1/auth/issuers"
5823
+ ),
5824
+ method: "POST",
5825
+ headers: _headers,
5826
+ contentType: "application/json",
5827
+ queryString: url_exports.queryBuilder().mergeAdditional(requestOptions?.queryParams).build(),
5828
+ requestType: "json",
5829
+ body: request,
5830
+ timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1e3,
5831
+ maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries,
5832
+ abortSignal: requestOptions?.abortSignal,
5833
+ fetchFn: this._options?.fetch,
5834
+ logging: this._options.logging
5835
+ });
5836
+ if (_response.ok) {
5837
+ return { data: _response.body, rawResponse: _response.rawResponse };
5838
+ }
5839
+ if (_response.error.reason === "status-code") {
5840
+ switch (_response.error.statusCode) {
5841
+ case 400:
5842
+ throw new BadRequestError(_response.error.body, _response.rawResponse);
5843
+ case 403:
5844
+ throw new ForbiddenError(_response.error.body, _response.rawResponse);
5845
+ case 429:
5846
+ throw new TooManyRequestsError(_response.error.body, _response.rawResponse);
5847
+ default:
5848
+ throw new VectrosError({
5849
+ statusCode: _response.error.statusCode,
5850
+ body: _response.error.body,
5851
+ rawResponse: _response.rawResponse
5852
+ });
5853
+ }
5854
+ }
5855
+ return handleNonStatusCodeError(_response.error, _response.rawResponse, "POST", "/v1/auth/issuers");
5856
+ }
5594
5857
  /**
5595
5858
  * Returns the identity bound to your credential — your account, principal type, key id, and scope details — so you can confirm who you are authenticated as and that the credential is valid. MCP clients use this to render "signed in as ..." in a chat UI without a separate identity endpoint.
5596
5859
  *
@@ -5636,7 +5899,7 @@ var AuthClient = class {
5636
5899
  return handleNonStatusCodeError(_response.error, _response.rawResponse, "GET", "/v1/ping");
5637
5900
  }
5638
5901
  /**
5639
- * 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.
5640
5903
  *
5641
5904
  * @param {Vectros.ListProfilesForPrincipalRequest} request
5642
5905
  * @param {AuthClient.RequestOptions} requestOptions - Request-specific configuration.
@@ -5914,40 +6177,34 @@ var AuthClient = class {
5914
6177
  }
5915
6178
  return handleNonStatusCodeError(_response.error, _response.rawResponse, "POST", "/v1/users/invite/resend");
5916
6179
  }
5917
- };
5918
- var ComplianceClient = class {
5919
- constructor(options) {
5920
- this._options = normalizeClientOptionsWithAuth(options);
5921
- }
5922
6180
  /**
5923
- * Submits a right-to-erasure request for a single end-subject (a user, or an identity entity in any namespace). Erasure removes exactly the data the subject solely owns across the declared contexts, plus the subject's identity and lookup rows. It never touches another account's data and never cascades into another subject's data. The request is asynchronous: it returns 202 with a `requestId`; poll `GET /v1/erasure-requests/{id}` until the job completes to obtain the completion certificate. Requires a root API key — a scoped credential is rejected with 403.
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.
5924
6182
  *
5925
- * @param {Vectros.ErasureRequest} request
5926
- * @param {ComplianceClient.RequestOptions} requestOptions - Request-specific configuration.
6183
+ * @param {Vectros.TokenExchangeRequest} request
6184
+ * @param {AuthClient.RequestOptions} requestOptions - Request-specific configuration.
5927
6185
  *
6186
+ * @throws {@link Vectros.BadRequestError}
6187
+ * @throws {@link Vectros.UnauthorizedError}
5928
6188
  * @throws {@link Vectros.ForbiddenError}
6189
+ * @throws {@link Vectros.NotFoundError}
5929
6190
  * @throws {@link Vectros.TooManyRequestsError}
5930
- * @throws {@link Vectros.NotImplementedError}
5931
6191
  *
5932
6192
  * @example
5933
- * await client.compliance.createErasureRequest({
5934
- * subjectType: "user"
6193
+ * await client.auth.exchangeToken({
6194
+ * grant_type: "urn:ietf:params:oauth:grant-type:token-exchange",
6195
+ * subject_token: "subject_token",
6196
+ * subject_token_type: "urn:ietf:params:oauth:token-type:jwt"
5935
6197
  * })
5936
6198
  */
5937
- createErasureRequest(request, requestOptions) {
5938
- return HttpResponsePromise.fromPromise(this.__createErasureRequest(request, requestOptions));
6199
+ exchangeToken(request, requestOptions) {
6200
+ return HttpResponsePromise.fromPromise(this.__exchangeToken(request, requestOptions));
5939
6201
  }
5940
- async __createErasureRequest(request, requestOptions) {
5941
- const _authRequest = await this._options.authProvider.getAuthRequest();
5942
- const _headers = mergeHeaders(
5943
- _authRequest.headers,
5944
- this._options?.headers,
5945
- requestOptions?.headers
5946
- );
6202
+ async __exchangeToken(request, requestOptions) {
6203
+ const _headers = mergeHeaders(this._options?.headers, requestOptions?.headers);
5947
6204
  const _response = await fetcher({
5948
6205
  url: url_exports.join(
5949
6206
  await Supplier.get(this._options.baseUrl) ?? await Supplier.get(this._options.environment),
5950
- "v1/erasure-requests"
6207
+ "v1/auth/token/exchange"
5951
6208
  ),
5952
6209
  method: "POST",
5953
6210
  headers: _headers,
@@ -5966,12 +6223,16 @@ var ComplianceClient = class {
5966
6223
  }
5967
6224
  if (_response.error.reason === "status-code") {
5968
6225
  switch (_response.error.statusCode) {
6226
+ case 400:
6227
+ throw new BadRequestError(_response.error.body, _response.rawResponse);
6228
+ case 401:
6229
+ throw new UnauthorizedError(_response.error.body, _response.rawResponse);
5969
6230
  case 403:
5970
6231
  throw new ForbiddenError(_response.error.body, _response.rawResponse);
6232
+ case 404:
6233
+ throw new NotFoundError(_response.error.body, _response.rawResponse);
5971
6234
  case 429:
5972
6235
  throw new TooManyRequestsError(_response.error.body, _response.rawResponse);
5973
- case 501:
5974
- throw new NotImplementedError(_response.error.body, _response.rawResponse);
5975
6236
  default:
5976
6237
  throw new VectrosError({
5977
6238
  statusCode: _response.error.statusCode,
@@ -5980,10 +6241,78 @@ var ComplianceClient = class {
5980
6241
  });
5981
6242
  }
5982
6243
  }
5983
- return handleNonStatusCodeError(_response.error, _response.rawResponse, "POST", "/v1/erasure-requests");
6244
+ return handleNonStatusCodeError(_response.error, _response.rawResponse, "POST", "/v1/auth/token/exchange");
6245
+ }
6246
+ };
6247
+ var ComplianceClient = class {
6248
+ constructor(options) {
6249
+ this._options = normalizeClientOptionsWithAuth(options);
5984
6250
  }
5985
6251
  /**
5986
- * Polls an erasure request by id. While the job is still running this returns its status only; once it completes, the response also includes the verifiable completion certificate (which contexts were swept, per-context deletion counts, and reports of dangling references and shared rows that were left intact). Requires a root API key.
6252
+ * Submits a right-to-erasure request for a single end-subject (a user, or an identity entity in any namespace). Erasure removes exactly the data the subject solely owns across the declared contexts, plus the subject's identity and lookup rows. It never touches another account's data and never cascades into another subject's data. The request is asynchronous: it returns 202 with a `requestId`; poll `GET /v1/erasure-requests/{id}` until the job completes to obtain the completion certificate. Requires a root API key — a scoped credential is rejected with 403.
6253
+ *
6254
+ * @param {Vectros.ErasureRequest} request
6255
+ * @param {ComplianceClient.RequestOptions} requestOptions - Request-specific configuration.
6256
+ *
6257
+ * @throws {@link Vectros.ForbiddenError}
6258
+ * @throws {@link Vectros.TooManyRequestsError}
6259
+ * @throws {@link Vectros.NotImplementedError}
6260
+ *
6261
+ * @example
6262
+ * await client.compliance.createErasureRequest({
6263
+ * subjectType: "user"
6264
+ * })
6265
+ */
6266
+ createErasureRequest(request, requestOptions) {
6267
+ return HttpResponsePromise.fromPromise(this.__createErasureRequest(request, requestOptions));
6268
+ }
6269
+ async __createErasureRequest(request, requestOptions) {
6270
+ const _authRequest = await this._options.authProvider.getAuthRequest();
6271
+ const _headers = mergeHeaders(
6272
+ _authRequest.headers,
6273
+ this._options?.headers,
6274
+ requestOptions?.headers
6275
+ );
6276
+ const _response = await fetcher({
6277
+ url: url_exports.join(
6278
+ await Supplier.get(this._options.baseUrl) ?? await Supplier.get(this._options.environment),
6279
+ "v1/erasure-requests"
6280
+ ),
6281
+ method: "POST",
6282
+ headers: _headers,
6283
+ contentType: "application/json",
6284
+ queryString: url_exports.queryBuilder().mergeAdditional(requestOptions?.queryParams).build(),
6285
+ requestType: "json",
6286
+ body: request,
6287
+ timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1e3,
6288
+ maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries,
6289
+ abortSignal: requestOptions?.abortSignal,
6290
+ fetchFn: this._options?.fetch,
6291
+ logging: this._options.logging
6292
+ });
6293
+ if (_response.ok) {
6294
+ return { data: _response.body, rawResponse: _response.rawResponse };
6295
+ }
6296
+ if (_response.error.reason === "status-code") {
6297
+ switch (_response.error.statusCode) {
6298
+ case 403:
6299
+ throw new ForbiddenError(_response.error.body, _response.rawResponse);
6300
+ case 429:
6301
+ throw new TooManyRequestsError(_response.error.body, _response.rawResponse);
6302
+ case 501:
6303
+ throw new NotImplementedError(_response.error.body, _response.rawResponse);
6304
+ default:
6305
+ throw new VectrosError({
6306
+ statusCode: _response.error.statusCode,
6307
+ body: _response.error.body,
6308
+ rawResponse: _response.rawResponse
6309
+ });
6310
+ }
6311
+ }
6312
+ return handleNonStatusCodeError(_response.error, _response.rawResponse, "POST", "/v1/erasure-requests");
6313
+ }
6314
+ /**
6315
+ * Polls an erasure request by id. While the job is still running this returns its status only; once it completes, the response also includes the verifiable completion certificate (which contexts were swept, per-context deletion counts, and reports of dangling references and shared rows that were left intact). Requires a root API key.
5987
6316
  *
5988
6317
  * @param {Vectros.GetErasureRequestRequest} request
5989
6318
  * @param {ComplianceClient.RequestOptions} requestOptions - Request-specific configuration.
@@ -7391,6 +7720,7 @@ var IdentityClient = class {
7391
7720
  * @example
7392
7721
  * await client.identity.listEntities({
7393
7722
  * namespace: "team",
7723
+ * contextId: "myapp",
7394
7724
  * startFrom: "b3BhcXVlLWN1cnNvci1mcm9tLXRoZS1wcmV2aW91cy1wYWdl"
7395
7725
  * })
7396
7726
  */
@@ -7400,6 +7730,7 @@ var IdentityClient = class {
7400
7730
  async __listEntities(request, requestOptions) {
7401
7731
  const {
7402
7732
  namespace,
7733
+ contextId,
7403
7734
  userId,
7404
7735
  externalId,
7405
7736
  scope,
@@ -7414,6 +7745,7 @@ var IdentityClient = class {
7414
7745
  limit
7415
7746
  } = request;
7416
7747
  const _queryParams = {
7748
+ contextId,
7417
7749
  userId,
7418
7750
  externalId,
7419
7751
  scope,
@@ -7472,6 +7804,7 @@ var IdentityClient = class {
7472
7804
  * @example
7473
7805
  * await client.identity.createEntity({
7474
7806
  * namespace: "team",
7807
+ * contextId: "myapp",
7475
7808
  * body: {
7476
7809
  * externalId: "team_eng_platform"
7477
7810
  * }
@@ -7481,9 +7814,10 @@ var IdentityClient = class {
7481
7814
  return HttpResponsePromise.fromPromise(this.__createEntity(request, requestOptions));
7482
7815
  }
7483
7816
  async __createEntity(request, requestOptions) {
7484
- const { namespace, upsert, body: _body } = request;
7817
+ const { namespace, upsert, contextId, body: _body } = request;
7485
7818
  const _queryParams = {
7486
- upsert
7819
+ upsert,
7820
+ contextId
7487
7821
  };
7488
7822
  const _authRequest = await this._options.authProvider.getAuthRequest();
7489
7823
  const _headers = mergeHeaders(
@@ -7540,14 +7874,18 @@ var IdentityClient = class {
7540
7874
  * @example
7541
7875
  * await client.identity.getEntity({
7542
7876
  * namespace: "team",
7543
- * id: "6ba7b810-9dad-11d1-80b4-00c04fd430c8"
7877
+ * id: "6ba7b810-9dad-11d1-80b4-00c04fd430c8",
7878
+ * contextId: "myapp"
7544
7879
  * })
7545
7880
  */
7546
7881
  getEntity(request, requestOptions) {
7547
7882
  return HttpResponsePromise.fromPromise(this.__getEntity(request, requestOptions));
7548
7883
  }
7549
7884
  async __getEntity(request, requestOptions) {
7550
- const { namespace, id } = request;
7885
+ const { namespace, id, contextId } = request;
7886
+ const _queryParams = {
7887
+ contextId
7888
+ };
7551
7889
  const _authRequest = await this._options.authProvider.getAuthRequest();
7552
7890
  const _headers = mergeHeaders(
7553
7891
  _authRequest.headers,
@@ -7561,7 +7899,7 @@ var IdentityClient = class {
7561
7899
  ),
7562
7900
  method: "GET",
7563
7901
  headers: _headers,
7564
- queryString: url_exports.queryBuilder().mergeAdditional(requestOptions?.queryParams).build(),
7902
+ queryString: url_exports.queryBuilder().addMany(_queryParams).mergeAdditional(requestOptions?.queryParams).build(),
7565
7903
  timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1e3,
7566
7904
  maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries,
7567
7905
  abortSignal: requestOptions?.abortSignal,
@@ -7598,6 +7936,7 @@ var IdentityClient = class {
7598
7936
  * await client.identity.updateEntity({
7599
7937
  * namespace: "team",
7600
7938
  * id: "id",
7939
+ * contextId: "myapp",
7601
7940
  * body: {
7602
7941
  * externalId: "team_eng_platform"
7603
7942
  * }
@@ -7607,7 +7946,10 @@ var IdentityClient = class {
7607
7946
  return HttpResponsePromise.fromPromise(this.__updateEntity(request, requestOptions));
7608
7947
  }
7609
7948
  async __updateEntity(request, requestOptions) {
7610
- const { namespace, id, body: _body } = request;
7949
+ const { namespace, id, contextId, body: _body } = request;
7950
+ const _queryParams = {
7951
+ contextId
7952
+ };
7611
7953
  const _authRequest = await this._options.authProvider.getAuthRequest();
7612
7954
  const _headers = mergeHeaders(
7613
7955
  _authRequest.headers,
@@ -7622,7 +7964,7 @@ var IdentityClient = class {
7622
7964
  method: "PUT",
7623
7965
  headers: _headers,
7624
7966
  contentType: "application/json",
7625
- queryString: url_exports.queryBuilder().mergeAdditional(requestOptions?.queryParams).build(),
7967
+ queryString: url_exports.queryBuilder().addMany(_queryParams).mergeAdditional(requestOptions?.queryParams).build(),
7626
7968
  requestType: "json",
7627
7969
  body: _body,
7628
7970
  timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1e3,
@@ -7662,14 +8004,18 @@ var IdentityClient = class {
7662
8004
  * @example
7663
8005
  * await client.identity.deleteEntity({
7664
8006
  * namespace: "team",
7665
- * id: "id"
8007
+ * id: "id",
8008
+ * contextId: "myapp"
7666
8009
  * })
7667
8010
  */
7668
8011
  deleteEntity(request, requestOptions) {
7669
8012
  return HttpResponsePromise.fromPromise(this.__deleteEntity(request, requestOptions));
7670
8013
  }
7671
8014
  async __deleteEntity(request, requestOptions) {
7672
- const { namespace, id } = request;
8015
+ const { namespace, id, contextId } = request;
8016
+ const _queryParams = {
8017
+ contextId
8018
+ };
7673
8019
  const _authRequest = await this._options.authProvider.getAuthRequest();
7674
8020
  const _headers = mergeHeaders(
7675
8021
  _authRequest.headers,
@@ -7683,7 +8029,7 @@ var IdentityClient = class {
7683
8029
  ),
7684
8030
  method: "DELETE",
7685
8031
  headers: _headers,
7686
- queryString: url_exports.queryBuilder().mergeAdditional(requestOptions?.queryParams).build(),
8032
+ queryString: url_exports.queryBuilder().addMany(_queryParams).mergeAdditional(requestOptions?.queryParams).build(),
7687
8033
  timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1e3,
7688
8034
  maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries,
7689
8035
  abortSignal: requestOptions?.abortSignal,
@@ -7726,6 +8072,7 @@ var IdentityClient = class {
7726
8072
  * @example
7727
8073
  * await client.identity.lookupEntities({
7728
8074
  * namespace: "team",
8075
+ * contextId: "myapp",
7729
8076
  * body: {
7730
8077
  * type: "person_v1",
7731
8078
  * field: "ssn"
@@ -7736,7 +8083,10 @@ var IdentityClient = class {
7736
8083
  return HttpResponsePromise.fromPromise(this.__lookupEntities(request, requestOptions));
7737
8084
  }
7738
8085
  async __lookupEntities(request, requestOptions) {
7739
- const { namespace, body: _body } = request;
8086
+ const { namespace, contextId, body: _body } = request;
8087
+ const _queryParams = {
8088
+ contextId
8089
+ };
7740
8090
  const _authRequest = await this._options.authProvider.getAuthRequest();
7741
8091
  const _headers = mergeHeaders(
7742
8092
  _authRequest.headers,
@@ -7751,7 +8101,7 @@ var IdentityClient = class {
7751
8101
  method: "POST",
7752
8102
  headers: _headers,
7753
8103
  contentType: "application/json",
7754
- queryString: url_exports.queryBuilder().mergeAdditional(requestOptions?.queryParams).build(),
8104
+ queryString: url_exports.queryBuilder().addMany(_queryParams).mergeAdditional(requestOptions?.queryParams).build(),
7755
8105
  requestType: "json",
7756
8106
  body: _body,
7757
8107
  timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1e3,
@@ -7795,15 +8145,17 @@ var IdentityClient = class {
7795
8145
  * @example
7796
8146
  * await client.identity.getEntityVersions({
7797
8147
  * namespace: "team",
7798
- * id: "id"
8148
+ * id: "id",
8149
+ * contextId: "myapp"
7799
8150
  * })
7800
8151
  */
7801
8152
  getEntityVersions(request, requestOptions) {
7802
8153
  return HttpResponsePromise.fromPromise(this.__getEntityVersions(request, requestOptions));
7803
8154
  }
7804
8155
  async __getEntityVersions(request, requestOptions) {
7805
- const { namespace, id, startFrom } = request;
8156
+ const { namespace, id, contextId, startFrom } = request;
7806
8157
  const _queryParams = {
8158
+ contextId,
7807
8159
  startFrom
7808
8160
  };
7809
8161
  const _authRequest = await this._options.authProvider.getAuthRequest();
@@ -7865,7 +8217,10 @@ var IdentityClient = class {
7865
8217
  return HttpResponsePromise.fromPromise(this.__getNamespace(request, requestOptions));
7866
8218
  }
7867
8219
  async __getNamespace(request, requestOptions) {
7868
- const { namespace } = request;
8220
+ const { namespace, contextId } = request;
8221
+ const _queryParams = {
8222
+ contextId
8223
+ };
7869
8224
  const _authRequest = await this._options.authProvider.getAuthRequest();
7870
8225
  const _headers = mergeHeaders(
7871
8226
  _authRequest.headers,
@@ -7879,7 +8234,7 @@ var IdentityClient = class {
7879
8234
  ),
7880
8235
  method: "GET",
7881
8236
  headers: _headers,
7882
- queryString: url_exports.queryBuilder().mergeAdditional(requestOptions?.queryParams).build(),
8237
+ queryString: url_exports.queryBuilder().addMany(_queryParams).mergeAdditional(requestOptions?.queryParams).build(),
7883
8238
  timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1e3,
7884
8239
  maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries,
7885
8240
  abortSignal: requestOptions?.abortSignal,
@@ -7904,7 +8259,7 @@ var IdentityClient = class {
7904
8259
  return handleNonStatusCodeError(_response.error, _response.rawResponse, "GET", "/v1/namespaces/{namespace}");
7905
8260
  }
7906
8261
  /**
7907
- * 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.
7908
8263
  *
7909
8264
  * @param {Vectros.UpdateNamespaceRequest} request
7910
8265
  * @param {IdentityClient.RequestOptions} requestOptions - Request-specific configuration.
@@ -7926,7 +8281,10 @@ var IdentityClient = class {
7926
8281
  return HttpResponsePromise.fromPromise(this.__updateNamespace(request, requestOptions));
7927
8282
  }
7928
8283
  async __updateNamespace(request, requestOptions) {
7929
- const { namespace, body: _body } = request;
8284
+ const { namespace, contextId, body: _body } = request;
8285
+ const _queryParams = {
8286
+ contextId
8287
+ };
7930
8288
  const _authRequest = await this._options.authProvider.getAuthRequest();
7931
8289
  const _headers = mergeHeaders(
7932
8290
  _authRequest.headers,
@@ -7941,7 +8299,7 @@ var IdentityClient = class {
7941
8299
  method: "PUT",
7942
8300
  headers: _headers,
7943
8301
  contentType: "application/json",
7944
- queryString: url_exports.queryBuilder().mergeAdditional(requestOptions?.queryParams).build(),
8302
+ queryString: url_exports.queryBuilder().addMany(_queryParams).mergeAdditional(requestOptions?.queryParams).build(),
7945
8303
  requestType: "json",
7946
8304
  body: _body,
7947
8305
  timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1e3,
@@ -7972,7 +8330,7 @@ var IdentityClient = class {
7972
8330
  return handleNonStatusCodeError(_response.error, _response.rawResponse, "PUT", "/v1/namespaces/{namespace}");
7973
8331
  }
7974
8332
  /**
7975
- * 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.
7976
8334
  *
7977
8335
  * @param {Vectros.DeleteNamespaceRequest} request
7978
8336
  * @param {IdentityClient.RequestOptions} requestOptions - Request-specific configuration.
@@ -7990,7 +8348,10 @@ var IdentityClient = class {
7990
8348
  return HttpResponsePromise.fromPromise(this.__deleteNamespace(request, requestOptions));
7991
8349
  }
7992
8350
  async __deleteNamespace(request, requestOptions) {
7993
- const { namespace } = request;
8351
+ const { namespace, contextId } = request;
8352
+ const _queryParams = {
8353
+ contextId
8354
+ };
7994
8355
  const _authRequest = await this._options.authProvider.getAuthRequest();
7995
8356
  const _headers = mergeHeaders(
7996
8357
  _authRequest.headers,
@@ -8004,7 +8365,7 @@ var IdentityClient = class {
8004
8365
  ),
8005
8366
  method: "DELETE",
8006
8367
  headers: _headers,
8007
- queryString: url_exports.queryBuilder().mergeAdditional(requestOptions?.queryParams).build(),
8368
+ queryString: url_exports.queryBuilder().addMany(_queryParams).mergeAdditional(requestOptions?.queryParams).build(),
8008
8369
  timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1e3,
8009
8370
  maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries,
8010
8371
  abortSignal: requestOptions?.abortSignal,
@@ -8033,7 +8394,7 @@ var IdentityClient = class {
8033
8394
  return handleNonStatusCodeError(_response.error, _response.rawResponse, "DELETE", "/v1/namespaces/{namespace}");
8034
8395
  }
8035
8396
  /**
8036
- * 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.
8037
8398
  *
8038
8399
  * @param {Vectros.ListNamespacesRequest} request
8039
8400
  * @param {IdentityClient.RequestOptions} requestOptions - Request-specific configuration.
@@ -8045,10 +8406,11 @@ var IdentityClient = class {
8045
8406
  return HttpResponsePromise.fromPromise(this.__listNamespaces(request, requestOptions));
8046
8407
  }
8047
8408
  async __listNamespaces(request = {}, requestOptions) {
8048
- const { startFrom, limit } = request;
8409
+ const { startFrom, limit, contextId } = request;
8049
8410
  const _queryParams = {
8050
8411
  startFrom,
8051
- limit
8412
+ limit,
8413
+ contextId
8052
8414
  };
8053
8415
  const _authRequest = await this._options.authProvider.getAuthRequest();
8054
8416
  const _headers = mergeHeaders(
@@ -8083,9 +8445,9 @@ var IdentityClient = class {
8083
8445
  return handleNonStatusCodeError(_response.error, _response.rawResponse, "GET", "/v1/namespaces");
8084
8446
  }
8085
8447
  /**
8086
- * 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.
8087
8449
  *
8088
- * @param {Vectros.NamespaceRequest} request
8450
+ * @param {Vectros.RegisterNamespaceRequest} request
8089
8451
  * @param {IdentityClient.RequestOptions} requestOptions - Request-specific configuration.
8090
8452
  *
8091
8453
  * @throws {@link Vectros.BadRequestError}
@@ -8094,14 +8456,20 @@ var IdentityClient = class {
8094
8456
  *
8095
8457
  * @example
8096
8458
  * await client.identity.registerNamespace({
8097
- * namespace: "team",
8098
- * specificityRank: 1500
8459
+ * body: {
8460
+ * namespace: "team",
8461
+ * specificityRank: 1500
8462
+ * }
8099
8463
  * })
8100
8464
  */
8101
8465
  registerNamespace(request, requestOptions) {
8102
8466
  return HttpResponsePromise.fromPromise(this.__registerNamespace(request, requestOptions));
8103
8467
  }
8104
8468
  async __registerNamespace(request, requestOptions) {
8469
+ const { contextId, body: _body } = request;
8470
+ const _queryParams = {
8471
+ contextId
8472
+ };
8105
8473
  const _authRequest = await this._options.authProvider.getAuthRequest();
8106
8474
  const _headers = mergeHeaders(
8107
8475
  _authRequest.headers,
@@ -8116,9 +8484,9 @@ var IdentityClient = class {
8116
8484
  method: "POST",
8117
8485
  headers: _headers,
8118
8486
  contentType: "application/json",
8119
- queryString: url_exports.queryBuilder().mergeAdditional(requestOptions?.queryParams).build(),
8487
+ queryString: url_exports.queryBuilder().addMany(_queryParams).mergeAdditional(requestOptions?.queryParams).build(),
8120
8488
  requestType: "json",
8121
- body: request,
8489
+ body: _body,
8122
8490
  timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1e3,
8123
8491
  maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries,
8124
8492
  abortSignal: requestOptions?.abortSignal,
@@ -8403,12 +8771,13 @@ var IdentityClient = class {
8403
8771
  return handleNonStatusCodeError(_response.error, _response.rawResponse, "PUT", "/v1/users/{id}");
8404
8772
  }
8405
8773
  /**
8406
- * 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.
8407
8775
  *
8408
8776
  * @param {Vectros.DeleteUserRequest} request
8409
8777
  * @param {IdentityClient.RequestOptions} requestOptions - Request-specific configuration.
8410
8778
  *
8411
8779
  * @throws {@link Vectros.NotFoundError}
8780
+ * @throws {@link Vectros.ConflictError}
8412
8781
  * @throws {@link Vectros.TooManyRequestsError}
8413
8782
  *
8414
8783
  * @example
@@ -8448,6 +8817,8 @@ var IdentityClient = class {
8448
8817
  switch (_response.error.statusCode) {
8449
8818
  case 404:
8450
8819
  throw new NotFoundError(_response.error.body, _response.rawResponse);
8820
+ case 409:
8821
+ throw new ConflictError(_response.error.body, _response.rawResponse);
8451
8822
  case 429:
8452
8823
  throw new TooManyRequestsError(_response.error.body, _response.rawResponse);
8453
8824
  default:
@@ -8521,6 +8892,69 @@ var IdentityClient = class {
8521
8892
  }
8522
8893
  return handleNonStatusCodeError(_response.error, _response.rawResponse, "POST", "/v1/users/lookup");
8523
8894
  }
8895
+ /**
8896
+ * Answers "does a user with this email hold an ACTIVE access profile in this app context" — a narrow existence check, not a general lookup. `exists` is false for a member whose access to this context has been suspended, not only for a member who was never granted it. The answer is scoped to the `contextId` you supply: it does not reveal whether the email exists elsewhere in your tenant or account, only whether it belongs to an active member of the named context. Returns `{exists, userId, status}` — never the full user record — so a caller asking "does X exist" cannot receive that user's payload/schema binding/etc. as a side effect. Useful for resolving an email you were handed (for example, by an org-admin adding an existing member to another org) to a `userId`, without paging through the full context membership. Requires the `users:r` scope.
8897
+ *
8898
+ * @param {Vectros.UserExistsByEmailRequest} request
8899
+ * @param {IdentityClient.RequestOptions} requestOptions - Request-specific configuration.
8900
+ *
8901
+ * @throws {@link Vectros.BadRequestError}
8902
+ * @throws {@link Vectros.ForbiddenError}
8903
+ *
8904
+ * @example
8905
+ * await client.identity.userExistsByEmail({
8906
+ * email: "user@example.com",
8907
+ * contextId: "myapp"
8908
+ * })
8909
+ */
8910
+ userExistsByEmail(request = {}, requestOptions) {
8911
+ return HttpResponsePromise.fromPromise(this.__userExistsByEmail(request, requestOptions));
8912
+ }
8913
+ async __userExistsByEmail(request = {}, requestOptions) {
8914
+ const { email, contextId } = request;
8915
+ const _queryParams = {
8916
+ email,
8917
+ contextId
8918
+ };
8919
+ const _authRequest = await this._options.authProvider.getAuthRequest();
8920
+ const _headers = mergeHeaders(
8921
+ _authRequest.headers,
8922
+ this._options?.headers,
8923
+ requestOptions?.headers
8924
+ );
8925
+ const _response = await fetcher({
8926
+ url: url_exports.join(
8927
+ await Supplier.get(this._options.baseUrl) ?? await Supplier.get(this._options.environment),
8928
+ "v1/users/exists-by-email"
8929
+ ),
8930
+ method: "GET",
8931
+ headers: _headers,
8932
+ queryString: url_exports.queryBuilder().addMany(_queryParams).mergeAdditional(requestOptions?.queryParams).build(),
8933
+ timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1e3,
8934
+ maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries,
8935
+ abortSignal: requestOptions?.abortSignal,
8936
+ fetchFn: this._options?.fetch,
8937
+ logging: this._options.logging
8938
+ });
8939
+ if (_response.ok) {
8940
+ return { data: _response.body, rawResponse: _response.rawResponse };
8941
+ }
8942
+ if (_response.error.reason === "status-code") {
8943
+ switch (_response.error.statusCode) {
8944
+ case 400:
8945
+ throw new BadRequestError(_response.error.body, _response.rawResponse);
8946
+ case 403:
8947
+ throw new ForbiddenError(_response.error.body, _response.rawResponse);
8948
+ default:
8949
+ throw new VectrosError({
8950
+ statusCode: _response.error.statusCode,
8951
+ body: _response.error.body,
8952
+ rawResponse: _response.rawResponse
8953
+ });
8954
+ }
8955
+ }
8956
+ return handleNonStatusCodeError(_response.error, _response.rawResponse, "GET", "/v1/users/exists-by-email");
8957
+ }
8524
8958
  /**
8525
8959
  * Returns the audit trail of changes to a user, most recent first. Identity history is always recorded and always available. Sensitive field values are redacted in every historical version. Returns a page in the `{data, nextCursor}` envelope. Requires the `users:r` scope.
8526
8960
  *
@@ -10238,10 +10672,11 @@ var logging;
10238
10672
  logging2.ConsoleLogger = ConsoleLogger;
10239
10673
  })(logging || (logging = {}));
10240
10674
 
10241
- // node_modules/@vectros-ai/blueprints/dist/index.mjs
10675
+ // ../blueprints/dist/index.mjs
10242
10676
  import { z } from "zod";
10243
10677
  import { z as z2 } from "zod";
10244
10678
  var CONTEXT_ID_RE = /^[a-z][a-z0-9-]{2,30}$/;
10679
+ var MAX_SCOPE_DIMENSIONS = 2;
10245
10680
  var ValidationRulesSchema = z.object({
10246
10681
  required: z.boolean().optional(),
10247
10682
  minLength: z.number().int().optional(),
@@ -10436,11 +10871,14 @@ var BlueprintSchemaSchema = z.object({
10436
10871
  // Schema-level ownership defaults, matching `SchemaRequest`: the principal
10437
10872
  // `userId` plus `scopes` — namespaced parent edges as `<namespace>:<value>`
10438
10873
  // (`org:...`, `client:...`, or a namespace you registered), at most two
10439
- // namespaces. The flat `orgId`/`clientId` fields fold into `scopes`.
10874
+ // namespaces. The flat `orgId`/`clientId` fields fold into `scopes`. Each
10875
+ // entry's namespace + value are validated below
10876
+ // (`lintNamespacedScopeArrays`) — this `.max(MAX_SCOPE_DIMENSIONS)` is
10877
+ // only the shape-level cap, not the grammar check.
10440
10878
  // With a scoped token these must be consistent with the profile's dataScope
10441
10879
  // (a cross-consistency lint is deferred to the lint slice).
10442
10880
  userId: z.string().min(1).optional(),
10443
- scopes: z.array(z.string().min(1)).max(2).optional(),
10881
+ scopes: z.array(z.string().min(1)).max(MAX_SCOPE_DIMENSIONS).optional(),
10444
10882
  // The id of an existing schema this one is a CUSTOMIZATION of, when a schema
10445
10883
  // named `typeName` already exists in this context — required in that case,
10446
10884
  // and must be omitted when this create is the first schema under that name
@@ -10463,6 +10901,35 @@ var BlueprintSchemaSchema = z.object({
10463
10901
  });
10464
10902
  }
10465
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
+ });
10466
10933
  var BlueprintAccessProfileSchema = z.object({
10467
10934
  // Validated structurally here; the SCOPE GATE (in @vectros-ai/cli) is
10468
10935
  // what enforces the data-plane-only security boundary.
@@ -10478,7 +10945,9 @@ var BlueprintAccessProfileSchema = z.object({
10478
10945
  // ...) — the flat `orgId`/`clientId` keys are gone. e.g.
10479
10946
  // `{ "scope:org": ["org_x", null] }` = "org_x's records AND tenant-shared
10480
10947
  // records". Omitting null restricts to the listed owners ONLY (the key will
10481
- // NOT see tenant-level/seed records).
10948
+ // NOT see tenant-level/seed records). Every KEY is validated below
10949
+ // (`lintDataScopeKeys`) — `userId` or a grammar-valid `scope:<ns>`, no
10950
+ // other shape.
10482
10951
  dataScope: z.record(z.array(z.union([z.string().min(1), z.null()]))).optional(),
10483
10952
  // Optional identity overrides for the service principal's profile — scope
10484
10953
  // values its key STAMPS onto everything it writes (a credential can only
@@ -10487,11 +10956,36 @@ var BlueprintAccessProfileSchema = z.object({
10487
10956
  // `orgId`/`clientId` keys are gone); values may be `${{ identities.* }}`
10488
10957
  // tokens (substituted at apply time). e.g. `{ "scope:org": '${{ identities.team }}' }`
10489
10958
  // makes every record the service key writes org/team-owned.
10490
- identityOverrides: z.record(z.string().min(1)).optional()
10959
+ //
10960
+ // Every KEY, the ≤2-dimension cap, and every LITERAL value are validated
10961
+ // below (`lintIdentityOverrides`, placeholder-aware — see that function's
10962
+ // docstring); this `.min(1)` is only the shape-level "non-blank" guard on
10963
+ // the value, not the grammar check.
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
10491
10984
  }).strict();
10492
10985
  var BlueprintRoleClauseSchema = z.object({
10493
10986
  allowedActions: z.array(z.string().min(1)).min(1),
10494
- 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
10495
10989
  }).strict();
10496
10990
  var BlueprintRolesSchema = z.record(z.array(BlueprintRoleClauseSchema).min(1));
10497
10991
  var IDENTITY_NAMESPACE_RE = /^[a-z][a-z0-9_-]{1,31}$/;
@@ -10516,57 +11010,260 @@ var IdentityDeclSchema = z.object({
10516
11010
  displayName: z.string().min(1).optional(),
10517
11011
  metadata: z.record(z.unknown()).optional()
10518
11012
  }).strict();
10519
- var IdentitiesDeclSchema = z.record(IdentityDeclSchema);
11013
+ var IDENTITY_NAME_RE = /^[A-Za-z_]\w*$/;
11014
+ var IdentitiesDeclSchema = z.record(
11015
+ z.string().regex(IDENTITY_NAME_RE, {
11016
+ message: "identity name must match letters/digits/underscore, not starting with a digit (e.g. 'demoOrg', not 'demo-org') \u2014 '${{ identities.<name> }}' can never reference a name outside this grammar"
11017
+ }),
11018
+ IdentityDeclSchema
11019
+ );
10520
11020
  var BlueprintServicePrincipalSchema = z.object({
10521
11021
  externalId: z.string().min(1),
10522
11022
  displayName: z.string().min(1)
10523
11023
  }).strict();
10524
- var SeedCommonShape = {
10525
- /** The schema typeName this seed instantiates (should match a declared schema). */
10526
- typeName: z.string().min(1),
11024
+ var ISSUER_ID_RE = CONTEXT_ID_RE;
11025
+ var BlueprintIssuerSchema = z.object({
11026
+ /** Short slug identifying this issuer within your account. Immutable once registered. */
11027
+ issuerId: z.string().regex(ISSUER_ID_RE, {
11028
+ message: "issuerId must be 3-31 chars, start with a lowercase letter, then lowercase letters/digits/dashes (e.g. 'auth0-prod')"
11029
+ }),
11030
+ /** The IdP's `iss` claim value, exactly as it appears in tokens it issues. */
11031
+ issuer: z.string().min(1),
11032
+ /** The IdP's remote JWKS endpoint, used to verify presented tokens' signatures. */
11033
+ jwksUri: z.string().min(1),
10527
11034
  /**
10528
- * Stable, caller-supplied id — the loader's idempotency key AND the value other
10529
- * seeds resolve a `reference` against (across surfaces: a record seed may
10530
- * reference a document seed by its externalId, and vice versa).
11035
+ * The `aud` claim value a presented subject_token must carry. Must be globally
11036
+ * unique in combination with `issuer` — use a distinct audience per
11037
+ * environment/context sharing one IdP account (most OIDC providers support
11038
+ * this as an ordinary per-API/application default).
10531
11039
  */
10532
- externalId: z.string().min(1),
11040
+ audience: z.string().min(1),
10533
11041
  /**
10534
- * Optional scope ownership for the seeded item, as `namespace:value` entries
10535
- * (≤2, e.g. `['org:${{ identities.team }}']`; tokens substitute at apply
10536
- * time). `[]` = a private, user-owned item. Omitted = the seeding
10537
- * credential's full identity (the default).
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.
10538
11046
  */
10539
- scopes: z.array(z.string()).optional()
10540
- };
10541
- var RecordSeedSchema = z.object({
10542
- surface: z.literal("record"),
10543
- ...SeedCommonShape,
10544
- /** The record payload, validated against the bound schema. */
10545
- fields: z.record(z.unknown())
11047
+ contextId: z.string().regex(CONTEXT_ID_RE, {
11048
+ message: "contextId must be 3-31 chars, start with a lowercase letter, then lowercase letters/digits/dashes (e.g. 'casework')"
11049
+ }),
11050
+ /** The claim carrying the subject identifier. Platform defaults to 'sub' when omitted. */
11051
+ subClaim: z.string().min(1).optional(),
11052
+ /** The claim carrying the subject's email (first-login invite matching). Platform defaults to 'email' when omitted. */
11053
+ emailClaim: z.string().min(1).optional(),
11054
+ /**
11055
+ * Opt-in self-service signup: a list of {signupType, roleId} pairs. When a first-time exchange
11056
+ * caller presents no invite token but names a signupType matching one of these (or omits it and
11057
+ * exactly one entry exists), a brand-new user is created and bound to that entry's role, no
11058
+ * invite required. Every entry must be something you're willing to grant to ANY caller who can
11059
+ * present a token from this issuer — a role carrying elevated (provisioning or wildcard) scope
11060
+ * is rejected. Omit entirely to leave self-signup disabled (the default).
11061
+ */
11062
+ selfSignupPolicies: z.array(
11063
+ z.object({
11064
+ signupType: z.string().regex(ISSUER_ID_RE, {
11065
+ message: "signupType must be 3-31 chars, start with a lowercase letter, then lowercase letters/digits/dashes (e.g. 'practitioner')"
11066
+ }),
11067
+ roleId: z.string().regex(ISSUER_ID_RE, {
11068
+ message: "roleId must be 3-31 chars, start with a lowercase letter, then lowercase letters/digits/dashes (e.g. 'practitioner-member')"
11069
+ })
11070
+ }).strict()
11071
+ ).optional()
10546
11072
  }).strict();
10547
- var DocumentSeedSchema = z.object({
10548
- surface: z.literal("document"),
10549
- ...SeedCommonShape,
10550
- /** Human-readable document title — REQUIRED by the text-ingest path. */
10551
- title: z.string().min(1),
10552
- /** Raw text content to ingest + index — REQUIRED and non-empty (the platform rejects a blank ingest). */
10553
- text: z.string().min(1),
10554
- /** Optional structured payload bound to the schema (the document's `fields`). */
10555
- fields: z.record(z.unknown()).optional()
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()
10556
11111
  }).strict();
10557
- var BlueprintSeedRecordSchema = z.discriminatedUnion("surface", [
10558
- RecordSeedSchema,
10559
- DocumentSeedSchema
10560
- ]);
10561
- var SELF_TOKEN_RE = /\$\{\{\s*self\.[A-Za-z_]\w*\s*\}\}/;
10562
- function lintSelfTokenPlacement(value, ctx) {
10563
- const walk = (node, path4, inRoleDataScope) => {
10564
- if (typeof node === "string") {
10565
- if (!inRoleDataScope && SELF_TOKEN_RE.test(node)) {
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
+ }
11218
+ var SeedCommonShape = {
11219
+ /** The schema typeName this seed instantiates (should match a declared schema). */
11220
+ typeName: z.string().min(1),
11221
+ /**
11222
+ * Stable, caller-supplied id — the loader's idempotency key AND the value other
11223
+ * seeds resolve a `reference` against (across surfaces: a record seed may
11224
+ * reference a document seed by its externalId, and vice versa).
11225
+ */
11226
+ externalId: z.string().min(1),
11227
+ /**
11228
+ * Optional scope ownership for the seeded item, as `namespace:value` entries
11229
+ * (≤2, e.g. `['org:${{ identities.team }}']`; tokens substitute at apply
11230
+ * time). `[]` = a private, user-owned item. Omitted = the seeding
11231
+ * credential's full identity (the default). Each entry's namespace + value
11232
+ * are validated below (`lintNamespacedScopeArrays`) — this
11233
+ * `.max(MAX_SCOPE_DIMENSIONS)` is only the shape-level cap, not the grammar
11234
+ * check.
11235
+ */
11236
+ scopes: z.array(z.string()).max(MAX_SCOPE_DIMENSIONS).optional()
11237
+ };
11238
+ var RecordSeedSchema = z.object({
11239
+ surface: z.literal("record"),
11240
+ ...SeedCommonShape,
11241
+ /** The record payload, validated against the bound schema. */
11242
+ fields: z.record(z.unknown())
11243
+ }).strict();
11244
+ var DocumentSeedSchema = z.object({
11245
+ surface: z.literal("document"),
11246
+ ...SeedCommonShape,
11247
+ /** Human-readable document title — REQUIRED by the text-ingest path. */
11248
+ title: z.string().min(1),
11249
+ /** Raw text content to ingest + index — REQUIRED and non-empty (the platform rejects a blank ingest). */
11250
+ text: z.string().min(1),
11251
+ /** Optional structured payload bound to the schema (the document's `fields`). */
11252
+ fields: z.record(z.unknown()).optional()
11253
+ }).strict();
11254
+ var BlueprintSeedRecordSchema = z.discriminatedUnion("surface", [
11255
+ RecordSeedSchema,
11256
+ DocumentSeedSchema
11257
+ ]);
11258
+ var SELF_TOKEN_RE = /\$\{\{\s*(?:self|under|member)\.[^}]*?\s*\}\}/;
11259
+ function lintSelfTokenPlacement(value, ctx) {
11260
+ const walk = (node, path4, inRoleDataScope) => {
11261
+ if (typeof node === "string") {
11262
+ if (!inRoleDataScope && SELF_TOKEN_RE.test(node)) {
10566
11263
  ctx.addIssue({
10567
11264
  code: z.ZodIssueCode.custom,
10568
11265
  path: path4,
10569
- 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"
10570
11267
  });
10571
11268
  }
10572
11269
  return;
@@ -10585,12 +11282,20 @@ function lintSelfTokenPlacement(value, ctx) {
10585
11282
  walk(value, [], false);
10586
11283
  }
10587
11284
  var IDENTITY_REF_RE = /\$\{\{\s*identities\.([A-Za-z_]\w*)\s*\}\}/g;
10588
- function lintIdentityRefsDeclared(value, ctx) {
11285
+ var IDENTITY_REF_LOOSE_RE = /\$\{\{\s*identities\.([^}]*?)\s*\}\}/g;
11286
+ function lintIdentityRefs(value, ctx) {
10589
11287
  const declared = new Set(
10590
11288
  value && typeof value === "object" && "identities" in value && value.identities ? Object.keys(value.identities) : []
10591
11289
  );
10592
11290
  const walk = (node, path4) => {
10593
11291
  if (typeof node === "string") {
11292
+ for (const m of node.matchAll(IDENTITY_REF_LOOSE_RE)) {
11293
+ const captured = m[1];
11294
+ if (IDENTITY_NAME_RE.test(captured)) continue;
11295
+ const dotIndex = captured.indexOf(".");
11296
+ const message = dotIndex === -1 ? `'\${{ identities.${captured} }}' is not a valid identities reference \u2014 the name must match letters/digits/underscore, not starting with a digit` : `'\${{ identities.${captured} }}' is not valid \u2014 property access ('.${captured.slice(dotIndex + 1)}') is not supported; only the whole identity id can be referenced, as '\${{ identities.${captured.slice(0, dotIndex)} }}'`;
11297
+ ctx.addIssue({ code: z.ZodIssueCode.custom, path: path4, message });
11298
+ }
10594
11299
  for (const m of node.matchAll(IDENTITY_REF_RE)) {
10595
11300
  if (!declared.has(m[1])) {
10596
11301
  ctx.addIssue({
@@ -10613,6 +11318,134 @@ function lintIdentityRefsDeclared(value, ctx) {
10613
11318
  };
10614
11319
  walk(value, []);
10615
11320
  }
11321
+ var SCOPE_VALUE_RE = /^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$/;
11322
+ var SCOPE_NAMESPACE_RE = /^[a-z][a-z0-9_-]{1,31}$/;
11323
+ var FORBIDDEN_SCOPE_NAMESPACES = /* @__PURE__ */ new Set([
11324
+ "user",
11325
+ "record",
11326
+ "document",
11327
+ "entity",
11328
+ "self",
11329
+ "tenant",
11330
+ "context",
11331
+ "scope",
11332
+ "versions",
11333
+ "lookup"
11334
+ ]);
11335
+ function scopeNamespaceGrammarError(namespace) {
11336
+ if (namespace === "") return "scope namespace must not be blank";
11337
+ if (FORBIDDEN_SCOPE_NAMESPACES.has(namespace)) {
11338
+ return `'${namespace}' is a reserved namespace and cannot be used as an ownership scope`;
11339
+ }
11340
+ if (!SCOPE_NAMESPACE_RE.test(namespace)) {
11341
+ return `'${namespace}' is not a valid namespace \u2014 2-32 chars, lowercase letter first, then lowercase letters, digits, '_' or '-'`;
11342
+ }
11343
+ return null;
11344
+ }
11345
+ var IDENTITY_PLACEHOLDER_WHOLE_RE = new RegExp(`^${IDENTITY_REF_LOOSE_RE.source}$`);
11346
+ function lintIdentityOverrides(value, ctx) {
11347
+ const overrides = value.accessProfile?.identityOverrides;
11348
+ if (!overrides) return;
11349
+ const entries = Object.entries(overrides);
11350
+ if (entries.length > MAX_SCOPE_DIMENSIONS) {
11351
+ ctx.addIssue({
11352
+ code: z.ZodIssueCode.custom,
11353
+ path: ["accessProfile", "identityOverrides"],
11354
+ message: `identityOverrides names ${entries.length} scope dimensions \u2014 a record carries at most ${MAX_SCOPE_DIMENSIONS} scope values, so an identity may stamp at most ${MAX_SCOPE_DIMENSIONS}.`
11355
+ });
11356
+ }
11357
+ for (const [key, v] of entries) {
11358
+ const ns = key.startsWith("scope:") ? key.slice("scope:".length) : null;
11359
+ const nsError = ns === null ? null : scopeNamespaceGrammarError(ns);
11360
+ if (ns === null || nsError !== null) {
11361
+ ctx.addIssue({
11362
+ code: z.ZodIssueCode.custom,
11363
+ path: ["accessProfile", "identityOverrides", key],
11364
+ message: ns === null ? `'${key}' is not allowed \u2014 identityOverrides keys must be 'scope:org', 'scope:client', or an open 'scope:<namespace>' key (the bare 'orgId'/'clientId' spellings are retired)` : `identityOverrides['${key}'] ${nsError}`
11365
+ });
11366
+ }
11367
+ if (typeof v !== "string") continue;
11368
+ if (IDENTITY_PLACEHOLDER_WHOLE_RE.test(v)) continue;
11369
+ if (!SCOPE_VALUE_RE.test(v)) {
11370
+ ctx.addIssue({
11371
+ code: z.ZodIssueCode.custom,
11372
+ path: ["accessProfile", "identityOverrides", key],
11373
+ message: `'${v}' is not a valid scope value \u2014 1-128 characters, starting with a letter or digit and continuing with letters, digits, '_' or '-'. A value may not contain ':', '$', '{', or '}' (the one exception is the documented '\${{ identities.<name> }}' substitution token).`
11374
+ });
11375
+ }
11376
+ }
11377
+ }
11378
+ function lintScopeEntries(entries, path4, ctx) {
11379
+ if (!entries) return;
11380
+ const seenValueByNamespace = /* @__PURE__ */ new Map();
11381
+ for (let i = 0; i < entries.length; i++) {
11382
+ const raw = entries[i];
11383
+ if (typeof raw !== "string") continue;
11384
+ const sep = raw.indexOf(":");
11385
+ if (sep <= 0 || sep === raw.length - 1) {
11386
+ ctx.addIssue({
11387
+ code: z.ZodIssueCode.custom,
11388
+ path: [...path4, i],
11389
+ message: `'${raw}' must be '<namespace>:<value>' (e.g. 'org:6ba7...' or 'team:${"${{ identities.team }}"}')`
11390
+ });
11391
+ continue;
11392
+ }
11393
+ const namespace = raw.slice(0, sep);
11394
+ const val = raw.slice(sep + 1);
11395
+ const nsError = scopeNamespaceGrammarError(namespace);
11396
+ if (nsError !== null) {
11397
+ ctx.addIssue({ code: z.ZodIssueCode.custom, path: [...path4, i], message: `'${raw}' ${nsError}` });
11398
+ } else {
11399
+ const priorValue = seenValueByNamespace.get(namespace);
11400
+ if (priorValue !== void 0 && priorValue !== val) {
11401
+ ctx.addIssue({
11402
+ code: z.ZodIssueCode.custom,
11403
+ path: [...path4, i],
11404
+ message: `'${namespace}' appears more than once with different values ('${priorValue}' and '${val}') \u2014 an item carries at most one value per namespace`
11405
+ });
11406
+ }
11407
+ seenValueByNamespace.set(namespace, val);
11408
+ }
11409
+ if (IDENTITY_PLACEHOLDER_WHOLE_RE.test(val)) continue;
11410
+ if (!SCOPE_VALUE_RE.test(val)) {
11411
+ ctx.addIssue({
11412
+ code: z.ZodIssueCode.custom,
11413
+ path: [...path4, i],
11414
+ message: `'${raw}' has an invalid value \u2014 1-128 characters, starting with a letter or digit and continuing with letters, digits, '_' or '-'. A value may not contain ':', '$', '{', or '}' (the one exception is the documented '\${{ identities.<name> }}' substitution token).`
11415
+ });
11416
+ }
11417
+ }
11418
+ }
11419
+ function lintNamespacedScopeArrays(value, ctx) {
11420
+ const bp = value;
11421
+ (bp.schemas ?? []).forEach((s, i) => lintScopeEntries(s.scopes, ["schemas", i, "scopes"], ctx));
11422
+ (bp.seed ?? []).forEach((s, i) => lintScopeEntries(s.scopes, ["seed", i, "scopes"], ctx));
11423
+ }
11424
+ function lintDataScopeKey(key, path4, ctx) {
11425
+ if (key === "userId") return;
11426
+ const ns = key.startsWith("scope:") ? key.slice("scope:".length) : null;
11427
+ const nsError = ns === null ? null : scopeNamespaceGrammarError(ns);
11428
+ if (ns === null || nsError !== null) {
11429
+ ctx.addIssue({
11430
+ code: z.ZodIssueCode.custom,
11431
+ path: path4,
11432
+ message: ns === null ? `'${key}' is not allowed \u2014 dataScope keys must be 'userId' or a namespaced 'scope:<namespace>' key (the bare 'orgId'/'clientId' spellings are retired)` : `dataScope['${key}'] ${nsError}`
11433
+ });
11434
+ }
11435
+ }
11436
+ function lintDataScopeKeys(value, ctx) {
11437
+ const bp = value;
11438
+ for (const key of Object.keys(bp.accessProfile?.dataScope ?? {})) {
11439
+ lintDataScopeKey(key, ["accessProfile", "dataScope", key], ctx);
11440
+ }
11441
+ for (const [roleId, clauses] of Object.entries(bp.roles ?? {})) {
11442
+ clauses.forEach((clause, i) => {
11443
+ for (const key of Object.keys(clause.dataScope ?? {})) {
11444
+ lintDataScopeKey(key, ["roles", roleId, i, "dataScope", key], ctx);
11445
+ }
11446
+ });
11447
+ }
11448
+ }
10616
11449
  function lintSeedSurfaces(value, ctx) {
10617
11450
  const bp = value;
10618
11451
  if (!Array.isArray(bp.seed)) return;
@@ -10630,6 +11463,41 @@ function lintSeedSurfaces(value, ctx) {
10630
11463
  }
10631
11464
  });
10632
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
+ }
11490
+ var BLUEPRINT_FIELD_PHASES = Object.freeze({
11491
+ servicePrincipal: "bootstrap",
11492
+ issuers: "bootstrap",
11493
+ // Registration requires the platform's provisioning:c capability, which
11494
+ // only the bootstrap credential carries — same reasoning as issuers.
11495
+ namespaces: "bootstrap",
11496
+ schemas: "in-context",
11497
+ accessProfile: "in-context",
11498
+ roles: "in-context",
11499
+ seed: "in-context"
11500
+ });
10633
11501
  var BlueprintSchema = z.object({
10634
11502
  /** Stable blueprint id (the `--blueprint <name>` selector + idempotency key). */
10635
11503
  name: z.string().min(1),
@@ -10648,11 +11516,33 @@ var BlueprintSchema = z.object({
10648
11516
  /** Optional multi-clause roles, bound to principals via `access grant --role`. */
10649
11517
  roles: BlueprintRolesSchema.optional(),
10650
11518
  /** Optional principals ensured-exist at apply; referenced via ${{ identities.* }}. */
10651
- identities: IdentitiesDeclSchema.optional()
11519
+ identities: IdentitiesDeclSchema.optional(),
11520
+ /**
11521
+ * Optional trusted third-party IdP issuers to register (BYO-IdP token
11522
+ * exchange). Applied in the BOOTSTRAP-token phase, alongside app-context/
11523
+ * service-principal creation — NOT the in-context load (see
11524
+ * {@link BlueprintIssuerSchema}).
11525
+ */
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()
10652
11536
  }).strict().superRefine((bp, ctx) => {
10653
11537
  lintSelfTokenPlacement(bp, ctx);
10654
- lintIdentityRefsDeclared(bp, ctx);
11538
+ lintIdentityRefs(bp, ctx);
11539
+ lintIdentityOverrides(bp, ctx);
11540
+ lintNamespacedScopeArrays(bp, ctx);
11541
+ lintDataScopeKeys(bp, ctx);
10655
11542
  lintSeedSurfaces(bp, ctx);
11543
+ lintIdentitySurfaces(bp, ctx);
11544
+ lintIssuerContext(bp, ctx);
11545
+ lintNamespaces(bp, ctx);
10656
11546
  });
10657
11547
  function formatIssuePath(path4) {
10658
11548
  let out = "";
@@ -10807,7 +11697,11 @@ function deriveSuffix(contextId) {
10807
11697
  var WHOLE_TOKEN_RE = /^\$\{\{\s*([A-Za-z_]\w*)\.([A-Za-z_]\w*)\s*\}\}$/;
10808
11698
  var TOKEN_SCAN_RE = /\$\$\{\{|\$\{\{([\s\S]*?)\}\}|\$\{\{/g;
10809
11699
  var INNER_REF_RE = /^([A-Za-z_]\w*)\.([A-Za-z_]\w*)$/;
10810
- var DEFERRED_NAMESPACES = /* @__PURE__ */ new Set(["self", "identities"]);
11700
+ var DEFERRED_NAMESPACES = /* @__PURE__ */ new Set(["self", "identities", "under", "member"]);
11701
+ var DEFERRED_NS_ALTERNATION = [...DEFERRED_NAMESPACES].join("|");
11702
+ var DEFERRED_INNER_RE = new RegExp(
11703
+ `^(?:${DEFERRED_NS_ALTERNATION})(?:\\.[A-Za-z_]\\w*)+(?::[A-Za-z_]\\w*)?$`
11704
+ );
10811
11705
  function resolveRef(ns, name, ctx, path4) {
10812
11706
  if (ns === "inputs") {
10813
11707
  if (!(name in ctx.declared)) {
@@ -10865,14 +11759,17 @@ function interpolate(str, ctx, path4) {
10865
11759
  });
10866
11760
  } else {
10867
11761
  const inner = (m[1] ?? "").trim();
11762
+ if (DEFERRED_INNER_RE.test(inner)) {
11763
+ out += m[0];
11764
+ last = TOKEN_SCAN_RE.lastIndex;
11765
+ continue;
11766
+ }
10868
11767
  const ref = inner.match(INNER_REF_RE);
10869
11768
  if (!ref) {
10870
11769
  ctx.issues.push({
10871
11770
  path: path4,
10872
11771
  message: `malformed reference '\${{ ${inner} }}' \u2014 expected '\${{ namespace.name }}' (escape a literal with '$\${{')`
10873
11772
  });
10874
- } else if (DEFERRED_NAMESPACES.has(ref[1])) {
10875
- out += m[0];
10876
11773
  } else {
10877
11774
  const v = resolveRef(ref[1], ref[2], ctx, path4);
10878
11775
  if (v !== void 0) out += String(v);
@@ -11054,324 +11951,82 @@ var taskManagement = {
11054
11951
  // Equality, NOT range: the values are an ordinal vocabulary but they sort
11055
11952
  // LEXICALLY (high < low < medium < urgent), not by severity — a range
11056
11953
  // index here would be permanently wrong. Filter by exact priority instead.
11057
- renderHints: { label: "Priority", widget: "select", order: 4, section: "Tracking" }
11058
- },
11059
- {
11060
- fieldId: "assignee",
11061
- fieldType: "string",
11062
- filterable: true,
11063
- description: "userId or display name.",
11064
- renderHints: { label: "Assignee", widget: "text", order: 5, section: "Tracking" }
11065
- },
11066
- {
11067
- fieldId: "project",
11068
- fieldType: "string",
11069
- filterable: true,
11070
- description: "Grouping key for cross-session continuity.",
11071
- renderHints: { label: "Project", widget: "text", order: 6, section: "Tracking" }
11072
- },
11073
- {
11074
- fieldId: "dueDate",
11075
- fieldType: "date",
11076
- description: "ISO-8601. Range-queryable (see lookupFields) \u2014 list tasks due in a window.",
11077
- renderHints: { label: "Due date", widget: "date", order: 7, section: "Tracking" }
11078
- },
11079
- {
11080
- fieldId: "tags",
11081
- fieldType: "array",
11082
- filterable: true,
11083
- renderHints: { label: "Tags", order: 8, section: "Tracking" }
11084
- }
11085
- // externalId is the record's FIRST-CLASS identifier (the dedup/lookup key + the
11086
- // value a `reference` resolves against) — not a payload field; the loader sends
11087
- // it top-level on the RecordRequest.
11088
- ],
11089
- // Two lookup shapes, chosen deliberately because the equality-vs-range choice
11090
- // per field is MIGRATION-LOCKED once the schema is live (you cannot flip a
11091
- // field slot↔range later, even by removing and re-adding it):
11092
- // • `project` — EQUALITY (a grouping key, not ordered): enumerate one
11093
- // project's tasks directly, no search. Each equality lookup uses 1 of the
11094
- // schema's 7 fast index slots.
11095
- // • `dueDate` — RANGE: ordered `from`/`to`/`prefix` queries ("tasks due this
11096
- // week", "due in 2026-06"). Range lookups use a relationship row, not a
11097
- // slot, and are billed at the range rate. ISO-8601 sorts chronologically,
11098
- // so the order is correct.
11099
- // `externalId` has a built-in first-class finder and must NOT be redeclared here.
11100
- lookupFields: ["project", { fieldName: "dueDate", rangeEnabled: true }]
11101
- }
11102
- ],
11103
- // Least-privilege profile — MUST pass the CLI scope gate.
11104
- // r/c/u + search + schema discovery. NOT records:d (least privilege).
11105
- accessProfile: {
11106
- allowedActions: ["records:r", "records:c", "records:u", "search:r", "schemas:r"]
11107
- // dataScope omitted → tenant-level shared tracker. Add a `scope:org`
11108
- // entry for per-org isolation.
11109
- },
11110
- servicePrincipal: {
11111
- externalId: "task-management",
11112
- displayName: "Task Management"
11113
- },
11114
- seed: [
11115
- {
11116
- surface: "record",
11117
- typeName: "task",
11118
- externalId: "seed-welcome",
11119
- fields: {
11120
- title: "Welcome to your Vectros task tracker",
11121
- description: "Created by the bootstrap loader. Ask your agent to add tasks.",
11122
- status: "todo",
11123
- priority: "low",
11124
- project: "getting-started",
11125
- dueDate: "2026-06-30"
11126
- }
11127
- }
11128
- ]
11129
- };
11130
- var task_management_default = taskManagement;
11131
- var codingAgentMemory = {
11132
- name: "coding-agent-memory",
11133
- version: "1.0.0",
11134
- description: "Persistent project memory for a coding agent \u2014 decisions, conventions, and gotchas that survive across sessions.",
11135
- contextId: "coding-memory",
11136
- contextName: "Coding Agent \u2014 Project Memory",
11137
- schemas: [
11138
- {
11139
- // A durable architectural/product decision: what was decided, and WHY.
11140
- // The rationale is the high-value, RAG-able body — "why did we do X?"
11141
- // is the question a cold-context agent most needs answered.
11142
- typeName: "decision",
11143
- displayName: "Decision",
11144
- indexMode: "HYBRID",
11145
- // keyword on titles + semantic on statement/rationale
11146
- fields: [
11147
- {
11148
- fieldId: "title",
11149
- fieldType: "string",
11150
- required: true,
11151
- searchable: true,
11152
- validation: { minLength: 1, maxLength: 200 },
11153
- renderHints: { label: "Title", widget: "text", order: 1, displayField: true }
11154
- },
11155
- {
11156
- fieldId: "statement",
11157
- fieldType: "string",
11158
- searchable: true,
11159
- description: "What was decided, in one or two sentences.",
11160
- renderHints: { label: "Statement", widget: "textarea", order: 2 }
11161
- },
11162
- {
11163
- fieldId: "rationale",
11164
- fieldType: "string",
11165
- searchable: true,
11166
- description: "Why \u2014 the trade-offs and the context. The most-recalled field.",
11167
- renderHints: { label: "Rationale", widget: "textarea", order: 3 }
11168
- },
11169
- {
11170
- fieldId: "status",
11171
- fieldType: "enum",
11172
- filterable: true,
11173
- enumValues: ["proposed", "active", "superseded"],
11174
- renderHints: { label: "Status", widget: "select", order: 4 }
11175
- },
11176
- {
11177
- fieldId: "area",
11178
- fieldType: "string",
11179
- filterable: true,
11180
- description: 'Subsystem / module the decision applies to (e.g. "auth", "search").',
11181
- renderHints: { label: "Area", widget: "text", order: 5 }
11182
- },
11183
- {
11184
- fieldId: "decidedOn",
11185
- fieldType: "date",
11186
- description: 'ISO-8601 date. Range-queryable \u2014 "decisions made this quarter".',
11187
- renderHints: { label: "Decided on", widget: "date", order: 6 }
11188
- }
11189
- // externalId is the record's FIRST-CLASS identifier (the dedup/upsert key + the
11190
- // value a `reference` resolves against) — it is NOT a payload field, so it is not
11191
- // declared here; the loader sends it top-level on the RecordRequest.
11192
- ],
11193
- // `externalId` is a first-class identifier with its own finder (exact
11194
- // get/upsert) — look it up directly, never redeclare it as a schema lookup.
11195
- // The choice below is MIGRATION-LOCKED once the schema is live:
11196
- // • `area`/`status` — EQUALITY (categorical): "list active decisions",
11197
- // "all decisions in the auth area". 2 of the 7 fast index slots.
11198
- // • `decidedOn` — RANGE: ordered `from`/`to`/`prefix` over the date.
11199
- lookupFields: ["area", "status", { fieldName: "decidedOn", rangeEnabled: true }]
11200
- },
11201
- {
11202
- // A coding convention the team follows — the agent reads these before it
11203
- // writes code so it matches the surrounding style instead of inventing one.
11204
- typeName: "convention",
11205
- displayName: "Convention",
11206
- indexMode: "HYBRID",
11207
- fields: [
11208
- {
11209
- fieldId: "name",
11210
- fieldType: "string",
11211
- required: true,
11212
- searchable: true,
11213
- validation: { minLength: 1, maxLength: 200 },
11214
- renderHints: { label: "Name", widget: "text", order: 1, displayField: true }
11215
- },
11216
- {
11217
- fieldId: "rule",
11218
- fieldType: "string",
11219
- searchable: true,
11220
- description: "The convention itself, stated as an imperative.",
11221
- renderHints: { label: "Rule", widget: "textarea", order: 2 }
11222
- },
11223
- {
11224
- fieldId: "area",
11225
- fieldType: "string",
11226
- filterable: true,
11227
- renderHints: { label: "Area", widget: "text", order: 3 }
11228
- },
11229
- {
11230
- // Parallels `decision.status` so a convention can be RETIRED rather than
11231
- // deleted (the blueprint omits records:d — memory is superseded, not lost).
11232
- fieldId: "status",
11233
- fieldType: "enum",
11234
- filterable: true,
11235
- enumValues: ["active", "retired"],
11236
- renderHints: { label: "Status", widget: "select", order: 4 }
11237
- },
11238
- {
11239
- // A typed REFERENCE to the decision that established this convention — a
11240
- // foreign-key link between record types (the knowledge-graph edge). The
11241
- // platform requires both the target type AND its surface; the value is the
11242
- // target decision's externalId (the default `targetField`). Provenance:
11243
- // "why does this convention exist? → open the decision behind it." Not
11244
- // searchable (a foreign-key id is search noise); declared as an equality
11245
- // lookup below so you can also enumerate "conventions from decision X".
11246
- fieldId: "establishedByDecision",
11247
- fieldType: "reference",
11248
- targetTypeName: "decision",
11249
- targetSurface: "record",
11250
- targetField: "externalId",
11251
- cardinality: "one",
11252
- renderHints: { label: "Established by (decision)", order: 5 }
11253
- },
11254
- {
11255
- fieldId: "adoptedOn",
11256
- fieldType: "date",
11257
- description: "ISO-8601 \u2014 when this convention was adopted. Range-queryable.",
11258
- renderHints: { label: "Adopted on", widget: "date", order: 6 }
11259
- }
11260
- // externalId: first-class identifier, not a payload field (see `decision`).
11261
- ],
11262
- // externalId has its own first-class finder for exact get — look it up
11263
- // directly, not as a schema lookup. Locked equality-vs-range choices:
11264
- // • `area`/`status` — EQUALITY: "active conventions for auth".
11265
- // • `establishedByDecision` — EQUALITY on the reference value: enumerate
11266
- // "conventions established by decision X" (a forward link query; the
11267
- // platform has no reverse-reference index on this surface).
11268
- // • `adoptedOn` — RANGE over the adoption date.
11269
- // 3 equality lookups (of 7 fast slots) + 1 range row.
11270
- lookupFields: ["area", "status", "establishedByDecision", { fieldName: "adoptedOn", rangeEnabled: true }]
11271
- },
11272
- {
11273
- // A gotcha / sharp edge: a symptom, its cause, and the fix. Saves the
11274
- // agent (and the human) from re-discovering the same trap.
11275
- typeName: "gotcha",
11276
- displayName: "Gotcha",
11277
- indexMode: "HYBRID",
11278
- fields: [
11279
- {
11280
- fieldId: "symptom",
11281
- fieldType: "string",
11282
- required: true,
11283
- searchable: true,
11284
- validation: { minLength: 1, maxLength: 500 },
11285
- renderHints: { label: "Symptom", widget: "textarea", order: 1, displayField: true }
11286
- },
11287
- {
11288
- fieldId: "cause",
11289
- fieldType: "string",
11290
- searchable: true,
11291
- renderHints: { label: "Cause", widget: "textarea", order: 2 }
11954
+ renderHints: { label: "Priority", widget: "select", order: 4, section: "Tracking" }
11292
11955
  },
11293
11956
  {
11294
- fieldId: "fix",
11957
+ fieldId: "assignee",
11295
11958
  fieldType: "string",
11296
- searchable: true,
11297
- renderHints: { label: "Fix", widget: "textarea", order: 3 }
11959
+ filterable: true,
11960
+ description: "userId or display name.",
11961
+ renderHints: { label: "Assignee", widget: "text", order: 5, section: "Tracking" }
11298
11962
  },
11299
11963
  {
11300
- fieldId: "area",
11964
+ fieldId: "project",
11301
11965
  fieldType: "string",
11302
11966
  filterable: true,
11303
- renderHints: { label: "Area", widget: "text", order: 4 }
11967
+ description: "Grouping key for cross-session continuity.",
11968
+ renderHints: { label: "Project", widget: "text", order: 6, section: "Tracking" }
11304
11969
  },
11305
11970
  {
11306
- // A gotcha can be RETIRED once the underlying trap is fixed for good —
11307
- // superseded, not deleted (the blueprint omits records:d).
11308
- fieldId: "status",
11309
- fieldType: "enum",
11310
- filterable: true,
11311
- enumValues: ["active", "retired"],
11312
- renderHints: { label: "Status", widget: "select", order: 5 }
11971
+ fieldId: "dueDate",
11972
+ fieldType: "date",
11973
+ description: "ISO-8601. Range-queryable (see lookupFields) \u2014 list tasks due in a window.",
11974
+ renderHints: { label: "Due date", widget: "date", order: 7, section: "Tracking" }
11313
11975
  },
11314
11976
  {
11315
- fieldId: "discoveredOn",
11316
- fieldType: "date",
11317
- description: "ISO-8601 \u2014 when this gotcha was first hit. Range-queryable.",
11318
- renderHints: { label: "Discovered on", widget: "date", order: 6 }
11977
+ fieldId: "tags",
11978
+ fieldType: "array",
11979
+ filterable: true,
11980
+ renderHints: { label: "Tags", order: 8, section: "Tracking" }
11319
11981
  }
11320
- // externalId: first-class identifier, not a payload field (see `decision`).
11982
+ // externalId is the record's FIRST-CLASS identifier (the dedup/lookup key + the
11983
+ // value a `reference` resolves against) — not a payload field; the loader sends
11984
+ // it top-level on the RecordRequest.
11321
11985
  ],
11322
- // externalId has its own first-class finder for exact get — look it up
11323
- // directly, not as a schema lookup. `area`/`status` EQUALITY ("active
11324
- // gotchas in search"); `discoveredOn` RANGE over the discovery date.
11325
- lookupFields: ["area", "status", { fieldName: "discoveredOn", rangeEnabled: true }]
11986
+ // Two lookup shapes, chosen deliberately because the equality-vs-range choice
11987
+ // per field is MIGRATION-LOCKED once the schema is live (you cannot flip a
11988
+ // field slot↔range later, even by removing and re-adding it):
11989
+ // • `project` — EQUALITY (a grouping key, not ordered): enumerate one
11990
+ // project's tasks directly, no search. Each equality lookup uses 1 of the
11991
+ // schema's 7 fast index slots.
11992
+ // • `dueDate` — RANGE: ordered `from`/`to`/`prefix` queries ("tasks due this
11993
+ // week", "due in 2026-06"). Range lookups use a relationship row, not a
11994
+ // slot, and are billed at the range rate. ISO-8601 sorts chronologically,
11995
+ // so the order is correct.
11996
+ // `externalId` has a built-in first-class finder and must NOT be redeclared here.
11997
+ lookupFields: ["project", { fieldName: "dueDate", rangeEnabled: true }]
11326
11998
  }
11327
11999
  ],
11328
12000
  // Least-privilege profile — MUST pass the CLI scope gate.
11329
- // r/c/u + search + schema discovery + inference:r (grounded "why did we do X?"
11330
- // recall over the captured rationale). NOT records:d: memory is superseded,
11331
- // not deleted, so the project's decision history stays auditable.
12001
+ // r/c/u + search + schema discovery. NOT records:d (least privilege).
11332
12002
  accessProfile: {
11333
- allowedActions: ["records:r", "records:c", "records:u", "search:r", "schemas:r", "inference:r"]
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: {}
11334
12008
  },
11335
12009
  servicePrincipal: {
11336
- externalId: "coding-agent-memory",
11337
- displayName: "Coding Agent \u2014 Project Memory"
12010
+ externalId: "task-management",
12011
+ displayName: "Task Management"
11338
12012
  },
11339
12013
  seed: [
11340
12014
  {
11341
- // Seeded FIRST so the convention below resolves its reference to it — the
11342
- // loader creates seeds in array order, and a reference target must exist when
11343
- // the referencing record is written.
11344
- surface: "record",
11345
- typeName: "decision",
11346
- externalId: "seed-use-vectros-for-memory",
11347
- fields: {
11348
- title: "Use Vectros as the coding agent\u2019s project memory",
11349
- statement: "Persist decisions, conventions, and gotchas as Vectros records so they survive across agent sessions.",
11350
- 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.",
11351
- status: "active",
11352
- area: "getting-started",
11353
- decidedOn: "2026-06-14"
11354
- }
11355
- },
11356
- {
11357
- // Demonstrates a live typed link at bootstrap: this convention references the
11358
- // decision above by its externalId, so "open the decision behind this rule"
11359
- // works the moment the blueprint is applied.
11360
12015
  surface: "record",
11361
- typeName: "convention",
11362
- externalId: "seed-record-the-why",
12016
+ typeName: "task",
12017
+ externalId: "seed-welcome",
11363
12018
  fields: {
11364
- name: "Record the why, not just the what",
11365
- 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.",
11366
- area: "getting-started",
11367
- status: "active",
11368
- establishedByDecision: "seed-use-vectros-for-memory",
11369
- adoptedOn: "2026-06-14"
12019
+ title: "Welcome to your Vectros task tracker",
12020
+ description: "Created by the bootstrap loader. Ask your agent to add tasks.",
12021
+ status: "todo",
12022
+ priority: "low",
12023
+ project: "getting-started",
12024
+ dueDate: "2026-06-30"
11370
12025
  }
11371
12026
  }
11372
12027
  ]
11373
12028
  };
11374
- var coding_agent_memory_default = codingAgentMemory;
12029
+ var task_management_default = taskManagement;
11375
12030
  var DATA_PLANE_ACTIONS = [
11376
12031
  "records:r",
11377
12032
  "records:c",
@@ -11399,7 +12054,7 @@ var MEMBER_SHARED_READ_ACTIONS = [
11399
12054
  ];
11400
12055
  var agenticSdlc = {
11401
12056
  name: "agentic-sdlc",
11402
- version: "1.6.0",
12057
+ version: "1.9.0",
11403
12058
  description: "A whole-SDLC system of record for an AI development team \u2014 decisions, designs, references, runbooks, post-mortems (as documents) plus controls, conventions, gotchas, and a glossary (as records), cross-linked and recalled by meaning.",
11404
12059
  contextId: "agentic-sdlc",
11405
12060
  contextName: "Agentic SDLC Knowledge Base",
@@ -12265,13 +12920,244 @@ var agenticSdlc = {
12265
12920
  { fieldName: "updatedOn", rangeEnabled: true },
12266
12921
  { fieldName: "priority", rangeEnabled: true }
12267
12922
  ]
12923
+ },
12924
+ {
12925
+ // A PROPOSED memory, waiting on a verification step — the staging area in
12926
+ // front of the `memory` type above. An agent that distills its own working
12927
+ // sessions produces claims faster than it can check them, and an unchecked
12928
+ // claim is worse than no claim: it is a plausible sentence that will be
12929
+ // recalled as fact. So a proposal lands here, an agent later verifies it,
12930
+ // and only a verified one is written to `memory`.
12931
+ //
12932
+ // ⚠ indexMode NONE IS THE LOAD-BEARING DECISION, not a tuning choice.
12933
+ // Store-only means these records are never indexed, so a search or a
12934
+ // grounded answer CANNOT return them under any query — the separation
12935
+ // between unverified proposals and trusted knowledge is enforced by the
12936
+ // platform rather than by every caller remembering to filter. Make this
12937
+ // HYBRID and the corpus of unchecked claims starts competing with the
12938
+ // curated KB for retrieval slots, which is the one failure this type
12939
+ // exists to prevent. They stay reachable by id and by the two lookups
12940
+ // below, which is all a disposition workflow needs.
12941
+ //
12942
+ // Note the consequence for the fields: `filterable` narrows SEARCH, and
12943
+ // nothing here is searchable, so no field declares it. Query paths are the
12944
+ // lookups, deliberately — a flag that does nothing would just read as
12945
+ // capability that isn't there.
12946
+ typeName: "candidate",
12947
+ displayName: "Memory candidate",
12948
+ indexMode: "NONE",
12949
+ fields: [
12950
+ {
12951
+ fieldId: "title",
12952
+ fieldType: "string",
12953
+ required: true,
12954
+ validation: { minLength: 1, maxLength: 200 },
12955
+ description: "One line naming the claim \u2014 what a reviewer reads first.",
12956
+ renderHints: { label: "Title", widget: "text", order: 1, displayField: true }
12957
+ },
12958
+ {
12959
+ fieldId: "body",
12960
+ fieldType: "string",
12961
+ description: "The proposed fact in full, as the distiller stated it \u2014 the thing to verify.",
12962
+ renderHints: { label: "Body", widget: "textarea", order: 2 }
12963
+ },
12964
+ {
12965
+ // Mirrors `memory.kind` exactly, so a verified candidate maps 1:1 onto
12966
+ // the memory it becomes with no vocabulary translation in between.
12967
+ fieldId: "kind",
12968
+ fieldType: "enum",
12969
+ enumValues: ["user", "feedback", "project", "reference", "observation"],
12970
+ description: "Which flavour of memory this would become \u2014 the same vocabulary as `memory.kind`.",
12971
+ renderHints: { label: "Kind", widget: "select", order: 3 }
12972
+ },
12973
+ {
12974
+ // The distiller's ROUTING suggestion, and the one field here with no
12975
+ // counterpart on `memory` — because it stops existing the moment the
12976
+ // question it asks is answered. A verified claim either becomes a
12977
+ // memory record or becomes a paragraph in a document; this is the
12978
+ // proposer's guess about which, and the verifier is free to overrule
12979
+ // it (the verdict lands in `disposition`, not here).
12980
+ fieldId: "dest",
12981
+ fieldType: "enum",
12982
+ enumValues: ["memory", "doc"],
12983
+ description: "Where the proposer thinks this belongs if it survives review \u2014 a suggestion, not the verdict.",
12984
+ renderHints: { label: "Suggested destination", widget: "select", order: 4 }
12985
+ },
12986
+ {
12987
+ // `area`, `tags` and `sourceRef` below mirror `memory`'s exactly, for
12988
+ // the same reason `kind` does: a verified candidate is COPIED into a
12989
+ // memory, and any field that exists on only one side has to be
12990
+ // re-derived or dropped at exactly the moment a human is deciding
12991
+ // whether the claim is true.
12992
+ fieldId: "area",
12993
+ fieldType: "string",
12994
+ description: 'Subsystem/topic the claim applies to (e.g. "auth", "search") \u2014 the shared area vocabulary.',
12995
+ renderHints: { label: "Area", widget: "text", order: 5 }
12996
+ },
12997
+ {
12998
+ fieldId: "tags",
12999
+ fieldType: "array",
13000
+ description: "Freeform labels carried over from the proposal.",
13001
+ renderHints: { label: "Tags", order: 6 }
13002
+ },
13003
+ {
13004
+ // Provenance the VERIFIER needs most: checking a claim starts with the
13005
+ // file or issue it is about. A candidate whose sourceRef was dropped
13006
+ // in transit costs the reviewer the search that the proposer had
13007
+ // already done.
13008
+ fieldId: "sourceRef",
13009
+ fieldType: "string",
13010
+ description: "Provenance: the file path / issue / URL the claim is about \u2014 where a verifier starts.",
13011
+ renderHints: { label: "Source ref", widget: "text", order: 7 }
13012
+ },
13013
+ {
13014
+ fieldId: "sessionId",
13015
+ fieldType: "string",
13016
+ description: "The conversation that proposed it \u2014 your runtime's session identifier.",
13017
+ renderHints: { label: "Session", widget: "text", order: 8 }
13018
+ },
13019
+ {
13020
+ // Also the stable ordering key: a display ordinal is derived by sorting
13021
+ // on this, so proposals keep the numbering a reviewer saw. Nothing is
13022
+ // ever hard-deleted here, which is what keeps that ordering stable.
13023
+ fieldId: "proposedAt",
13024
+ fieldType: "date",
13025
+ description: "ISO-8601 \u2014 when it was proposed. Also the stable ordering key for display.",
13026
+ renderHints: { label: "Proposed at", widget: "date", order: 9 }
13027
+ },
13028
+ {
13029
+ // THE VERDICT, and deliberately a plain payload field rather than the
13030
+ // record's own ACTIVE/ARCHIVED status. Settling a candidate is
13031
+ // REVERSIBLE — a wrong dismissal has to be undoable — and a write-side
13032
+ // visibility flip is the wrong tool for a reversible state: it is
13033
+ // asynchronous, it moves the record out from under readers, and it
13034
+ // leaves the index status of a once-indexed row stranded. A filterable
13035
+ // payload field flips synchronously and reads the same for everyone.
13036
+ fieldId: "disposition",
13037
+ fieldType: "enum",
13038
+ enumValues: ["pending", "stored", "documented", "ignored"],
13039
+ description: "The verdict: still pending, written to memory, captured in a doc, or dismissed.",
13040
+ renderHints: { label: "Disposition", widget: "select", order: 10 }
13041
+ },
13042
+ {
13043
+ fieldId: "ref",
13044
+ fieldType: "string",
13045
+ description: "The citation given when settling it \u2014 the record id, document path, or reason.",
13046
+ renderHints: { label: "Ref", widget: "text", order: 11 }
13047
+ },
13048
+ {
13049
+ // What the settling step actually CHECKED, which is not always what the
13050
+ // agent typed. Keeping both makes a bad citation auditable after the
13051
+ // fact instead of indistinguishable from a good one.
13052
+ fieldId: "resolved",
13053
+ fieldType: "string",
13054
+ description: "What was verified when it was settled \u2014 kept alongside `ref`, which is what was claimed.",
13055
+ renderHints: { label: "Resolved", widget: "text", order: 12 }
13056
+ },
13057
+ {
13058
+ fieldId: "origin",
13059
+ fieldType: "enum",
13060
+ enumValues: ["session", "recovered"],
13061
+ description: "Proposed by a live session, or recovered later from one that ended without settling.",
13062
+ renderHints: { label: "Origin", widget: "select", order: 13 }
13063
+ },
13064
+ {
13065
+ fieldId: "reopenedWhy",
13066
+ fieldType: "string",
13067
+ description: "Why a settled candidate was reopened \u2014 the audit trail for undoing a verdict.",
13068
+ renderHints: { label: "Reopened why", widget: "textarea", order: 14 }
13069
+ },
13070
+ {
13071
+ // The forward half of a correction: this candidate restates an earlier
13072
+ // one whose claim was wrong. Written at create, so it costs nothing.
13073
+ fieldId: "revises",
13074
+ fieldType: "reference",
13075
+ targetTypeName: "candidate",
13076
+ targetSurface: "record",
13077
+ targetField: "externalId",
13078
+ cardinality: "one",
13079
+ description: "The candidate this one corrects (by externalId).",
13080
+ renderHints: { label: "Revises", order: 15 }
13081
+ },
13082
+ {
13083
+ // The reverse half, and NOT redundant: "is this still open?" has to be
13084
+ // answerable without asking whether some OTHER record points here, and
13085
+ // there is no reverse-reference read. So the superseded candidate is
13086
+ // marked directly.
13087
+ //
13088
+ // Kept SEPARATE from `disposition` on purpose. Superseded and dismissed
13089
+ // are different facts: a dismissal may be undone, while resurrecting a
13090
+ // claim that a later run already corrected would re-offer the version
13091
+ // known to be wrong. Folding both into one enum makes that distinction
13092
+ // unrepresentable exactly when it matters.
13093
+ fieldId: "supersededBy",
13094
+ fieldType: "reference",
13095
+ targetTypeName: "candidate",
13096
+ targetSurface: "record",
13097
+ targetField: "externalId",
13098
+ cardinality: "one",
13099
+ description: "The candidate that corrected this one (by externalId) \u2014 set on the older record.",
13100
+ renderHints: { label: "Superseded by", order: 16 }
13101
+ }
13102
+ ],
13103
+ // FOUR lookups, because this type is asked three genuinely different
13104
+ // questions and one of them is a conjunction:
13105
+ //
13106
+ // `disposition` — "what is waiting ANYWHERE?" The cross-session
13107
+ // review queue. Low cardinality, so its
13108
+ // partitions are large; that is inherent to the
13109
+ // question and the pending set is small.
13110
+ // `sessionId` — "everything belonging to this conversation",
13111
+ // settled candidates included.
13112
+ // (sessionId, disposition) — "what is still waiting IN this conversation".
13113
+ // `proposedAt` (range) — worked by age: "what has been sitting
13114
+ // longest", "what came in this week". A range
13115
+ // read, not a filter over everything ever
13116
+ // proposed.
13117
+ //
13118
+ // The composite earns its slot because that third question is the HOTTEST
13119
+ // read in the system — it is what an agent is shown as it works, so it runs
13120
+ // far more often than the queue-wide scan does. Without it the answer is the
13121
+ // more selective single lookup plus a filter in the caller.
13122
+ //
13123
+ // `sessionId` LEADS, and the order is not cosmetic: the leading field
13124
+ // becomes the partition key. Leading with `disposition` would sort every
13125
+ // candidate ever proposed into four partitions; leading with `sessionId`
13126
+ // gives one small partition per conversation. Order is also
13127
+ // migration-locked, so it cannot be corrected later.
13128
+ //
13129
+ // A composite matches a LEADING RUN of its fields, so `(sessionId,
13130
+ // disposition)` also answers `sessionId` alone. The plain `sessionId`
13131
+ // lookup is kept anyway: under a partial tuple the unspecified field
13132
+ // becomes part of the ordering, so results come back grouped by
13133
+ // `disposition` rather than in one sequence — right for a filtered view,
13134
+ // wrong for "this conversation's candidates, oldest first", which is the
13135
+ // read the review surface and the queue/record agreement check both make.
13136
+ // One declaration out of ten is a cheap price for not depending on that
13137
+ // subtlety in the two places that must not be wrong.
13138
+ //
13139
+ // No `sortBy`: it defaults to the record's creation time, which is already
13140
+ // the primary key the review queue orders by. Naming `proposedAt` instead
13141
+ // would order by the WORKER's clock, and that is stamped when a proposal is
13142
+ // queued rather than when it is stored — the two disagree by exactly the
13143
+ // outage a durable queue exists to survive.
13144
+ lookupFields: [
13145
+ "disposition",
13146
+ "sessionId",
13147
+ { fieldNames: ["sessionId", "disposition"] },
13148
+ { fieldName: "proposedAt", rangeEnabled: true }
13149
+ ]
12268
13150
  }
12269
13151
  ],
12270
13152
  // Least-privilege, data-plane only. The scope of the `ssk_*` key the bootstrap
12271
13153
  // mints for THIS blueprint's service principal (the MCP/API runtime). See
12272
13154
  // DATA_PLANE_ACTIONS above for the action set + rationale.
12273
13155
  accessProfile: {
12274
- 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: {}
12275
13161
  },
12276
13162
  // A reusable `editor` role for the HUMAN owner — DISTINCT from `accessProfile`
12277
13163
  // (which scopes only the service-principal key). `bootstrap` provisions this
@@ -12291,7 +13177,9 @@ var agenticSdlc = {
12291
13177
  // which includes members' private memory — the trusted-administrator tradeoff,
12292
13178
  // called out in the bundled guide. Scope it down if your deployment wants
12293
13179
  // curator access without private-memory visibility.
12294
- 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: {} }],
12295
13183
  // A team member (or their agent) — TWO memory tiers as two UNIONed clauses
12296
13184
  // (a record is accessible when ANY clause grants it; within one clause every
12297
13185
  // dataScope dimension must match):
@@ -12311,14 +13199,21 @@ var agenticSdlc = {
12311
13199
  // `vectros join agentic-sdlc --role member`; verify a binding with
12312
13200
  // `vectros access explain --principal me --context agentic-sdlc`.
12313
13201
  member: [
12314
- { 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: {} },
12315
13205
  {
12316
13206
  // `inference:r` is IN this self-scoped clause (not only clause 1) so a
12317
13207
  // member's `rag_ask` can ground on its OWN private memory — the read
12318
13208
  // grant (`records:r:memory`) and the inference capability travel in the
12319
13209
  // SAME clause, so grounding works regardless of cross-clause admission
12320
13210
  // semantics. Still self-fenced by dataScope: never another principal's.
12321
- allowedActions: ["records:cru:memory", "search:r", "inference:r"],
13211
+ // `candidate` is READ-only here, unlike `memory`'s full CRU. A candidate's
13212
+ // verdict is the output of a verification step, so it is written by the
13213
+ // agent runtime that performed the check — a human editing the field
13214
+ // directly would record a verdict nothing actually verified. Reading them
13215
+ // is the point: the queue, the verdicts and the corrections are browsable.
13216
+ allowedActions: ["records:cru:memory", "records:r:candidate", "search:r", "inference:r"],
12322
13217
  dataScope: { userId: ["${{ self.userId }}"] }
12323
13218
  }
12324
13219
  ]
@@ -12421,7 +13316,11 @@ var secondBrain = {
12421
13316
  // (so the agent can `rag_ask` a grounded, cited question over your notes). No
12422
13317
  // records:d: notes are archived (status → 'archived'), never deleted.
12423
13318
  accessProfile: {
12424
- 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: {}
12425
13324
  },
12426
13325
  servicePrincipal: {
12427
13326
  externalId: "second-brain",
@@ -12553,7 +13452,11 @@ var clinicalIntake = {
12553
13452
  // reveal qualifier (the demo key literally cannot un-redact the PHI fields),
12554
13453
  // and NO inference:r (we never point a RAG capability at a PHI corpus here).
12555
13454
  accessProfile: {
12556
- 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: {}
12557
13460
  },
12558
13461
  servicePrincipal: {
12559
13462
  externalId: "clinical-intake",
@@ -12582,7 +13485,6 @@ var clinicalIntake = {
12582
13485
  var clinical_intake_default = clinicalIntake;
12583
13486
  var BUNDLED_BLUEPRINTS = [
12584
13487
  task_management_default,
12585
- coding_agent_memory_default,
12586
13488
  agentic_sdlc_default,
12587
13489
  second_brain_default,
12588
13490
  clinical_intake_default
@@ -12881,18 +13783,17 @@ async function mintBridgeToken(opts) {
12881
13783
  ttlSeconds = DEFAULT_BRIDGE_TTL_SECONDS,
12882
13784
  tenant = "live",
12883
13785
  useScopedToken = false,
13786
+ contextId,
12884
13787
  fetchImpl = fetch
12885
13788
  } = opts;
12886
13789
  const base = apiBaseUrl.replace(/\/$/, "");
12887
13790
  const path4 = bridgeTokenPath(useScopedToken);
12888
- const url = `${base}${path4}?ttl=${encodeURIComponent(String(ttlSeconds))}&tenant=${encodeURIComponent(tenant)}`;
12889
- return fetchStToken(
12890
- url,
12891
- cognitoToken,
12892
- fetchImpl,
12893
- path4,
12894
- (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)` : ""
12895
- );
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
+ });
12896
13797
  }
12897
13798
  async function mintContextToken(opts) {
12898
13799
  const {
@@ -13164,7 +14065,17 @@ async function resolveAuthContext(opts) {
13164
14065
  bearer: bridge.token,
13165
14066
  cognitoToken,
13166
14067
  baseUrl,
13167
- env: opts.env,
14068
+ /**
14069
+ * Derived from the REQUEST'S actual `baseUrl`, not echoed from `opts.env` — the two
14070
+ * diverge whenever `VECTROS_API_BASE_URL`/`--base-url` alone selects the environment
14071
+ * (the common shape when targeting staging without also passing `--env staging`,
14072
+ * since `envToBaseUrl` lets an explicit override win over `opts.env`'s default). Every
14073
+ * display surface that shows "on {env}" (blueprint-test's banner, `whoami`, etc.) reads
14074
+ * this field, so it must reflect where the request actually went, not which hidden flag
14075
+ * defaulted to what. `deriveEnvironment` falls through to `opts.env` only for a base URL
14076
+ * it doesn't recognize (a non-standard override, e.g. a future custom/self-hosted host).
14077
+ */
14078
+ env: deriveEnvironment(baseUrl) ?? opts.env,
13168
14079
  identity,
13169
14080
  tenantId: typeof identity.tenantId === "string" ? identity.tenantId : void 0,
13170
14081
  tenant
@@ -13248,7 +14159,8 @@ var DATA_PLANE_ALLOWLIST = [
13248
14159
  "search",
13249
14160
  "documents",
13250
14161
  "folders",
13251
- "inference"
14162
+ "inference",
14163
+ "entities"
13252
14164
  ];
13253
14165
  var KNOWN_CONTROL_PLANE_RESOURCES = [
13254
14166
  "keys",
@@ -13257,14 +14169,36 @@ var KNOWN_CONTROL_PLANE_RESOURCES = [
13257
14169
  "users",
13258
14170
  "billing",
13259
14171
  "admin",
13260
- // Identity/compartment provisioning. The flat `orgs`/`clients` resources were
13261
- // folded into the generic `entities` (per-namespace) + `namespaces` surfaces,
13262
- // which is what the API now scopes on — so those are the current spellings a
13263
- // pack could name. (Cosmetic: any non-data-plane resource hard-rejects whether
13264
- // or not it is listed here; this list only sharpens the error message.)
13265
- "entities",
14172
+ // The namespace REGISTRY (as opposed to `entities`, which creates/reads a
14173
+ // ROW within an already-registered namespace — see the data-plane-allowlist
14174
+ // note above). Registering a new namespace TYPE is a tenant-config action,
14175
+ // root-key-gated regardless of any scope a token carries — genuinely
14176
+ // control-plane, unlike per-namespace entity CRUD. (Cosmetic: any
14177
+ // non-data-plane resource hard-rejects whether or not it is listed here;
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.
13266
14191
  "namespaces"
13267
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
+ ]);
13268
14202
  var VALID_OPS = /* @__PURE__ */ new Set(["c", "r", "u", "d"]);
13269
14203
  var DATA_PLANE_SET = new Set(DATA_PLANE_ALLOWLIST);
13270
14204
  var CONTROL_PLANE_SET = new Set(KNOWN_CONTROL_PLANE_RESOURCES);
@@ -13281,6 +14215,59 @@ ${summary}`
13281
14215
  this.rejected = rejected;
13282
14216
  }
13283
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
+ }
13284
14271
  function evaluateScopeToken(action) {
13285
14272
  const token = action.trim();
13286
14273
  if (token.length === 0) {
@@ -13302,6 +14289,48 @@ function evaluateScopeToken(action) {
13302
14289
  };
13303
14290
  }
13304
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
+ }
13305
14334
  if (CONTROL_PLANE_SET.has(resource)) {
13306
14335
  return {
13307
14336
  action,
@@ -13316,13 +14345,12 @@ function evaluateScopeToken(action) {
13316
14345
  if (ops === "*") {
13317
14346
  return null;
13318
14347
  }
13319
- for (const ch of ops) {
13320
- if (!VALID_OPS.has(ch)) {
13321
- return {
13322
- action,
13323
- reason: `invalid operation character '${ch}' (valid operations are c, r, u, d, or '*')`
13324
- };
13325
- }
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
+ };
13326
14354
  }
13327
14355
  return null;
13328
14356
  }
@@ -13345,6 +14373,16 @@ function assertScopeGate(allowedActions) {
13345
14373
  throw new ScopeGateError(result.rejected);
13346
14374
  }
13347
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
+ }
13348
14386
 
13349
14387
  // src/machine-id.ts
13350
14388
  import { promises as fs } from "fs";
@@ -13449,6 +14487,68 @@ async function resolveApplyTimeIdentities(blueprint, client, log) {
13449
14487
  function actionOf(created) {
13450
14488
  return created ? "created" : "reconciled";
13451
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
+ }
13452
14552
  var LostKeyError = class extends Error {
13453
14553
  constructor(keyName) {
13454
14554
  super(
@@ -13459,6 +14559,20 @@ var LostKeyError = class extends Error {
13459
14559
  }
13460
14560
  keyName;
13461
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
+ };
13462
14576
  function mapEnumValues(enumValues) {
13463
14577
  if (!enumValues) return void 0;
13464
14578
  return enumValues.map((value) => ({ value }));
@@ -13533,7 +14647,8 @@ function toCapabilities(capabilities) {
13533
14647
  function toRoleScopeClauses(clauses) {
13534
14648
  return clauses.map((c) => ({
13535
14649
  allowed_actions: c.allowedActions,
13536
- data_scope: c.dataScope
14650
+ data_scope: c.dataScope,
14651
+ granted_capabilities: c.capabilities
13537
14652
  }));
13538
14653
  }
13539
14654
  var UnresolvedIdentitiesError = class extends Error {
@@ -13553,11 +14668,18 @@ function assertNoUnresolvedIdentities(blueprint) {
13553
14668
  }
13554
14669
  }
13555
14670
  async function runLoader(client, input) {
13556
- const { blueprint, tenantId, machineId, rotate = false, skipSeed = false, readExistingKey, persistMintedKey, log } = input;
13557
- assertScopeGate(blueprint.accessProfile.allowedActions);
13558
- for (const clauses of Object.values(blueprint.roles ?? {})) {
13559
- for (const clause of clauses) assertScopeGate(clause.allowedActions);
13560
- }
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);
13561
14683
  assertNoUnresolvedIdentities(blueprint);
13562
14684
  const ctxResp = await client.auth.createAppContext({ body: { contextId: blueprint.contextId, name: contextNameOf(blueprint) } });
13563
14685
  const contextAction = actionOf(ctxResp.created);
@@ -13575,7 +14697,65 @@ async function runLoader(client, input) {
13575
14697
  if (!userId) {
13576
14698
  throw new Error("Service-principal create returned no user id; cannot bind AccessProfile/key.");
13577
14699
  }
13578
- 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
+ }
14733
+ const issuerActions = [];
14734
+ for (const issuer of blueprint.issuers ?? []) {
14735
+ const issuerResp = await ctxBootstrapClient.auth.registerIssuer({
14736
+ issuerId: issuer.issuerId,
14737
+ issuer: issuer.issuer,
14738
+ jwksUri: issuer.jwksUri,
14739
+ audience: issuer.audience,
14740
+ contextId: issuer.contextId,
14741
+ subClaim: issuer.subClaim,
14742
+ emailClaim: issuer.emailClaim,
14743
+ selfSignupPolicies: issuer.selfSignupPolicies?.map((p) => ({
14744
+ signup_type: p.signupType,
14745
+ role_id: p.roleId
14746
+ }))
14747
+ });
14748
+ const action = actionOf(issuerResp.created);
14749
+ issuerActions.push({ issuerId: issuer.issuerId, action });
14750
+ if (action === "reconciled") {
14751
+ log.warn(
14752
+ { component: "loader", issuerId: issuer.issuerId },
14753
+ "issuer already registered \u2014 the platform has no update-issuer endpoint, so any edited fields (jwksUri, audience, subClaim, emailClaim, selfSignupPolicies) on this apply were NOT applied to the existing registration"
14754
+ );
14755
+ } else {
14756
+ log.debug({ component: "loader", issuerId: issuer.issuerId, action }, "issuer registered");
14757
+ }
14758
+ }
13579
14759
  const ctxClient = input.remintForContext ? await input.remintForContext(blueprint.contextId) : client;
13580
14760
  log.debug({ component: "loader", contextId: blueprint.contextId }, "re-minted per-context client");
13581
14761
  const schemaIds = {};
@@ -13622,7 +14802,10 @@ async function runLoader(client, input) {
13622
14802
  // literal JSON null, which is exactly what the platform's scope
13623
14803
  // matcher tests via `allowedValues.contains(null)`. The cast bridges
13624
14804
  // to the SDK's loosely-typed Record<string, Record<string,unknown>>.
13625
- 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
13626
14809
  }
13627
14810
  ],
13628
14811
  // Optional identity overrides — scope values the service key STAMPS onto
@@ -13680,7 +14863,7 @@ async function runLoader(client, input) {
13680
14863
  keyId = keyResp.keyId ?? "";
13681
14864
  rawKey = keyResp.rawKey;
13682
14865
  if (keyId) {
13683
- await persistMintedKey(blueprint.contextId, { keyId, rawKey, keyName, principalId: userId, revokedKeyId });
14866
+ await persistMintedKey(blueprint.contextId, { keyId, rawKey, keyName, principalId: `usr_${userId}`, revokedKeyId });
13684
14867
  }
13685
14868
  if (keyAction !== "rotated") keyAction = "created";
13686
14869
  } else {
@@ -13754,9 +14937,12 @@ async function runLoader(client, input) {
13754
14937
  keyName,
13755
14938
  keyAction,
13756
14939
  contextAction,
14940
+ userAction,
13757
14941
  profileAction,
13758
14942
  schemaActions,
13759
14943
  roleActions,
14944
+ issuerActions,
14945
+ namespaceActions,
13760
14946
  rawKey,
13761
14947
  keyId,
13762
14948
  revokedKeyId,
@@ -13775,6 +14961,22 @@ function buildPlan(blueprint, opts = {}) {
13775
14961
  kind: "principal",
13776
14962
  summary: `principal '${blueprint.servicePrincipal.externalId}' (SERVICE)`
13777
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
+ }
14973
+ for (const iss of blueprint.issuers ?? []) {
14974
+ const selfSignup = iss.selfSignupPolicies?.length ? `, self-signup: ${iss.selfSignupPolicies.length} polic${iss.selfSignupPolicies.length === 1 ? "y" : "ies"}` : "";
14975
+ steps.push({
14976
+ kind: "issuer",
14977
+ summary: `issuer '${iss.issuerId}' (${iss.issuer} \u2192 context '${iss.contextId}'${selfSignup}) [${BLUEPRINT_FIELD_PHASES.issuers}-token phase]`
14978
+ });
14979
+ }
13778
14980
  for (const s of blueprint.schemas) {
13779
14981
  const lookups = toLookupDefs(s.lookupFields)?.map((l) => l.fieldNames ? `(${l.fieldNames.join("+")})` : l.fieldName) ?? [];
13780
14982
  const lookupNote = lookups.length ? ` lookup: [${lookups.join(", ")}]` : "";
@@ -13790,15 +14992,18 @@ function buildPlan(blueprint, opts = {}) {
13790
14992
  }
13791
14993
  const actions = blueprint.accessProfile.allowedActions;
13792
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(", ")}]` : "";
13793
14996
  steps.push({
13794
14997
  kind: "profile",
13795
- summary: `profile scopes [${actions.join(", ")}] (data-plane \u2713)${scopeNote}`
14998
+ summary: `profile scopes [${actions.join(", ")}] (data-plane \u2713)${scopeNote}${capabilityNote}`
13796
14999
  });
13797
15000
  for (const [roleId, clauses] of Object.entries(blueprint.roles ?? {})) {
13798
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(", ")}]` : "";
13799
15004
  steps.push({
13800
15005
  kind: "role",
13801
- 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})`
13802
15007
  });
13803
15008
  }
13804
15009
  steps.push({ kind: "key", summary: `key ${blueprint.name}-<machine> (mint 1 scoped ssk_*)` });
@@ -13815,8 +15020,24 @@ function buildPlan(blueprint, opts = {}) {
13815
15020
  }
13816
15021
  function renderPlan(blueprint, steps) {
13817
15022
  const header = ` Plan for blueprint '${blueprint.name}' (v${blueprint.version}) \u2192 context '${blueprint.contextId}':`;
13818
- const legend = ` Apply is a non-destructive reconcile \u2014 each item is created if absent, else
13819
- updated in place to match. Nothing is dropped or recreated.`;
15023
+ const legendLines = [
15024
+ ` Apply is a non-destructive reconcile \u2014 each item is created if absent, else`,
15025
+ ` updated in place to match. Nothing is dropped or recreated.`
15026
+ ];
15027
+ if (steps.some((s) => s.kind === "issuer")) {
15028
+ legendLines.push(
15029
+ ` Exception: issuers cannot be updated once registered \u2014 an existing`,
15030
+ ` issuer is left unchanged, not reconciled to match.`
15031
+ );
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
+ }
15040
+ const legend = legendLines.join("\n");
13820
15041
  const lines = steps.map((s) => ` + ${s.summary}`);
13821
15042
  return [header, legend, "", ...lines].join("\n");
13822
15043
  }
@@ -14064,6 +15285,15 @@ var OsKeychainSecretStore = class {
14064
15285
  * against the real Windows store on every MR — the dev-machine probe is a point
14065
15286
  * measurement, not a regression gate. (Tracked internally; this file ships to
14066
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.
14067
15297
  */
14068
15298
  async deleteSecret(ref) {
14069
15299
  try {
@@ -14997,6 +16227,26 @@ The key itself WAS created server-side and is live \u2014 its id is ${entry.keyI
14997
16227
  const entry = await this.getActive();
14998
16228
  return entry ? this.readSecret(entry) : void 0;
14999
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
+ }
15000
16250
  /**
15001
16251
  * Read one entry's secret, moving it to this machine's preferred store if it is
15002
16252
  * somewhere else.
@@ -15123,26 +16373,31 @@ import * as readline from "readline/promises";
15123
16373
  async function runKeyringList(opts, deps = {}) {
15124
16374
  try {
15125
16375
  const keyring = deps.keyring ?? await Keyring.open();
15126
- const entries = await keyring.list();
15127
- const active = await keyring.activeAlias();
15128
16376
  if (opts.json) {
16377
+ const entries = await keyring.list();
16378
+ const active = await keyring.activeAlias();
15129
16379
  process.stdout.write(`${formatJson({ active, entries })}
15130
16380
  `);
15131
16381
  return 0;
15132
16382
  }
15133
- if (entries.length === 0) {
16383
+ const { health } = await assessEntries(keyring);
16384
+ if (health.length === 0) {
15134
16385
  process.stdout.write("\n (no keyring entries yet \u2014 run `vectros join <context>` to add one)\n");
15135
16386
  return 0;
15136
16387
  }
15137
16388
  process.stdout.write(
15138
- "\n" + formatTable(entries, [
15139
- { header: "", get: (e) => e.alias === active ? "*" : "" },
15140
- { header: "alias", get: (e) => e.alias },
15141
- { header: "context", get: (e) => e.context },
15142
- { header: "principal", get: (e) => e.principalId },
15143
- { header: "role", get: (e) => e.role },
15144
- { header: "env", get: (e) => e.env },
15145
- { 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 }
15146
16401
  ]) + "\n"
15147
16402
  );
15148
16403
  return 0;
@@ -15324,6 +16579,10 @@ async function assessEntries(keyring) {
15324
16579
  }))
15325
16580
  };
15326
16581
  }
16582
+ function tenantCell(h, opts) {
16583
+ if (h.status !== "ok") return void 0;
16584
+ return h.tenant ?? (opts.upper ? "UNKNOWN" : "unknown");
16585
+ }
15327
16586
  function describeEntry(h) {
15328
16587
  return `${h.entry.alias} (${h.entry.context}, ${h.entry.keyId})`;
15329
16588
  }
@@ -15468,9 +16727,7 @@ async function runKeyringDoctor(opts, deps = {}) {
15468
16727
  // `unknown` can escalate; one that sees a confident wrong answer cannot.
15469
16728
  store: storeKindOf(h.entry) ?? "unknown",
15470
16729
  secret: h.status,
15471
- // `unknown` rather than absent: a consumer must be able to tell "not a
15472
- // live key" from "we could not read the shape of this one".
15473
- tenant: h.status === "ok" ? h.tenant ?? "unknown" : void 0
16730
+ tenant: tenantCell(h, { upper: false })
15474
16731
  })),
15475
16732
  liveKeys: liveKeys.map((h) => h.entry.alias),
15476
16733
  findings
@@ -15485,7 +16742,7 @@ async function runKeyringDoctor(opts, deps = {}) {
15485
16742
  { header: "", get: (h) => h.active ? "*" : "" },
15486
16743
  { header: "alias", get: (h) => h.entry.alias },
15487
16744
  { header: "context", get: (h) => h.entry.context },
15488
- { header: "tenant", get: (h) => h.status === "ok" ? h.tenant ?? "UNKNOWN" : void 0 },
16745
+ { header: "tenant", get: (h) => tenantCell(h, { upper: true }) },
15489
16746
  { header: "key id", get: (h) => h.entry.keyId },
15490
16747
  { header: "secret", get: (h) => h.status === "ok" ? "ok" : h.status.toUpperCase() }
15491
16748
  ]) + "\n"
@@ -15671,6 +16928,55 @@ function revokeWarning(targets) {
15671
16928
  }
15672
16929
 
15673
16930
  // src/minted-key-store.ts
16931
+ function keyringAliasFor(alias, tenant) {
16932
+ if (tenant === "live" && alias.endsWith(".test")) {
16933
+ throw new Error(
16934
+ `'${alias}' ends in the reserved '.test' suffix this CLI uses to separate tenants in the local keyring \u2014 a live-tenant request for it would collide with (and could silently overwrite) a real --tenant test entry. Choose a different alias, or pass --tenant test if that's the credential you actually mean.`
16935
+ );
16936
+ }
16937
+ return tenant === "test" ? `${alias}.test` : alias;
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
+ }
15674
16980
  function nowIso() {
15675
16981
  return (/* @__PURE__ */ new Date()).toISOString();
15676
16982
  }
@@ -15767,10 +17073,7 @@ async function storeMintedKey(keyring, binding, secret, opts = {}) {
15767
17073
  // src/cli-bootstrap.ts
15768
17074
  function validateBlueprint(input) {
15769
17075
  const blueprint = parseBlueprint(input);
15770
- assertScopeGate(blueprint.accessProfile.allowedActions);
15771
- for (const clauses of Object.values(blueprint.roles ?? {})) {
15772
- for (const clause of clauses) assertScopeGate(clause.allowedActions);
15773
- }
17076
+ assertBlueprintGates(blueprint);
15774
17077
  return blueprint;
15775
17078
  }
15776
17079
  var BLANK_SLATE = {
@@ -15787,10 +17090,12 @@ var BLANK_SLATE = {
15787
17090
  };
15788
17091
  function parseBootstrapArgs(argv) {
15789
17092
  const args = {
17093
+ set: [],
15790
17094
  env: "production",
15791
17095
  yes: false,
15792
17096
  noMerge: false,
15793
17097
  rotate: false,
17098
+ confirmExistingPrincipal: false,
15794
17099
  useScopedToken: false,
15795
17100
  blankSlate: false,
15796
17101
  noSeed: false,
@@ -15804,6 +17109,12 @@ function parseBootstrapArgs(argv) {
15804
17109
  case "--blueprint":
15805
17110
  args.blueprint = argv[++i];
15806
17111
  break;
17112
+ case "--set":
17113
+ args.set.push(argv[++i]);
17114
+ break;
17115
+ case "--values":
17116
+ args.values = argv[++i];
17117
+ break;
15807
17118
  case "--env": {
15808
17119
  const v = argv[++i];
15809
17120
  if (v !== "staging" && v !== "production") {
@@ -15836,6 +17147,9 @@ function parseBootstrapArgs(argv) {
15836
17147
  case "--rotate":
15837
17148
  args.rotate = true;
15838
17149
  break;
17150
+ case "--confirm-existing-principal":
17151
+ args.confirmExistingPrincipal = true;
17152
+ break;
15839
17153
  case "--use-scoped-token":
15840
17154
  args.useScopedToken = true;
15841
17155
  break;
@@ -15857,6 +17171,12 @@ function parseBootstrapArgs(argv) {
15857
17171
  case "--test":
15858
17172
  args.tenant = "test";
15859
17173
  break;
17174
+ case "--activate":
17175
+ args.activate = true;
17176
+ break;
17177
+ case "--no-activate":
17178
+ args.activate = false;
17179
+ break;
15860
17180
  case "--help":
15861
17181
  case "-h":
15862
17182
  args.help = true;
@@ -15874,6 +17194,26 @@ function seedDeclined(answer) {
15874
17194
  const a = answer.trim().toLowerCase();
15875
17195
  return a === "n" || a === "no";
15876
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
+ }
15877
17217
  function seedSummaryLine(opts) {
15878
17218
  if (opts.noSeed) {
15879
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";
@@ -15901,22 +17241,45 @@ function reconcileTally(items, noun) {
15901
17241
  if (reconciled) seg.push(`${reconciled} reconciled`);
15902
17242
  return `${items.length} ${noun}${items.length === 1 ? "" : "s"} (${seg.join(", ")})`;
15903
17243
  }
17244
+ function issuerTally(items) {
17245
+ if (items.length === 0) return null;
17246
+ const created = items.filter((i) => i.action === "created").length;
17247
+ const unchanged = items.length - created;
17248
+ const seg = [];
17249
+ if (created) seg.push(`${created} created`);
17250
+ if (unchanged) seg.push(`${unchanged} already registered \u2014 unchanged (issuers cannot be updated once registered)`);
17251
+ return `${items.length} issuer${items.length === 1 ? "" : "s"} (${seg.join(", ")})`;
17252
+ }
17253
+ 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
+ }
15904
17262
  function applyOutcomeSummary(result) {
15905
17263
  const parts = [
15906
17264
  reconcileTally(result.schemaActions, "schema"),
15907
17265
  reconcileTally(result.roleActions, "role"),
17266
+ issuerTally(result.issuerActions),
17267
+ namespaceTally(result.namespaceActions),
15908
17268
  `profile ${result.profileAction}`
15909
17269
  ].filter((p) => p !== null);
15910
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
+ ` : "";
15911
17273
  return ` \u2714 context '${result.contextId}' ${result.contextAction}${detail}
15912
- `;
17274
+ ${principalNote}`;
15913
17275
  }
15914
17276
  var BOOTSTRAP_HELP = `vectros bootstrap \u2014 provision a least-privilege MCP credential.
15915
17277
 
15916
17278
  Usage:
15917
17279
  vectros bootstrap [--blueprint <name> | --blank] [--no-seed]
17280
+ [--set name=value ...] [--values <file>]
15918
17281
  [--tenant test|live | --test] [--client desktop|code]
15919
- [--yes] [--no-merge] [--rotate]
17282
+ [--yes] [--no-merge] [--rotate] [--confirm-existing-principal]
15920
17283
  [--token <t> | env VECTROS_BOOTSTRAP_TOKEN]
15921
17284
  [--base-url <url>] [--use-scoped-token]
15922
17285
 
@@ -15924,6 +17287,10 @@ Options:
15924
17287
  --blueprint <name|file> A bundled blueprint (${BLUEPRINT_NAMES.join(", ")}),
15925
17288
  OR a path to your own blueprint YAML/JSON (./my.yaml) \u2014 the "copy &
15926
17289
  own" path. File blueprints get the identical scope gate (no override).
17290
+ --set name=value Set an install-time input value (repeatable) for a file blueprint's
17291
+ 'inputs:' block. Beats --values + the declared default. (Same flag as
17292
+ 'vectros blueprint validate/plan'.)
17293
+ --values <file> A YAML/JSON map of input values. (File blueprints only.)
15927
17294
  --blank Credential only (read-only data-plane profile, no schemas).
15928
17295
  --no-seed Provision the blueprint's schemas, profile and key but SKIP its
15929
17296
  sample seed records \u2014 the right choice for a production context
@@ -15935,6 +17302,10 @@ Options:
15935
17302
  The tenant rides the key (ssk_test_* vs ssk_live_*), so
15936
17303
  the MCP/client config is otherwise identical.
15937
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.
15938
17309
  --base-url <url> Override the API base URL (beats VECTROS_API_BASE_URL).
15939
17310
  --client <desktop|code> MCP host to configure (default: desktop). 'desktop' merges
15940
17311
  claude_desktop_config.json (also works for Cursor/Cline);
@@ -15949,6 +17320,13 @@ Options:
15949
17320
  print the config snippet instead of merging it into your client's
15950
17321
  config file. Not a dry run; see 'blueprint plan' for that.
15951
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.
15952
17330
  --use-scoped-token Legacy fallback.
15953
17331
  NOTE: it mints a token WITHOUT the provisioning
15954
17332
  capability, so it cannot create a blueprint's
@@ -15958,10 +17336,21 @@ Options:
15958
17336
 
15959
17337
  No --blueprint/--blank in a TTY \u2192 interactive menu.`;
15960
17338
  async function resolveBlueprint(args, prompt, isTty) {
15961
- if (args.blankSlate) return BLANK_SLATE;
17339
+ if (args.blankSlate) {
17340
+ if (args.set.length > 0 || args.values) {
17341
+ throw new Error(`--set/--values apply only to file blueprints; --blank has no inputs to set.`);
17342
+ }
17343
+ return BLANK_SLATE;
17344
+ }
15962
17345
  if (args.blueprint) {
15963
17346
  if (looksLikePath(args.blueprint)) {
15964
- return await loadBlueprintFile(args.blueprint);
17347
+ const supplied = await buildInputSupply(args.set, args.values);
17348
+ return await loadBlueprintFile(args.blueprint, supplied);
17349
+ }
17350
+ if (args.set.length > 0 || args.values) {
17351
+ throw new Error(
17352
+ `--set/--values apply only to file blueprints; '${args.blueprint}' is a bundled blueprint (no inputs to set).`
17353
+ );
15965
17354
  }
15966
17355
  const b = getBlueprint(args.blueprint);
15967
17356
  if (!b) {
@@ -15974,6 +17363,11 @@ async function resolveBlueprint(args, prompt, isTty) {
15974
17363
  if (!isTty) {
15975
17364
  throw new Error("No --blueprint or --blank specified and not a TTY (cannot show the menu).");
15976
17365
  }
17366
+ if (args.set.length > 0 || args.values) {
17367
+ throw new Error(
17368
+ `--set/--values need an explicit --blueprint <file>; the interactive menu only offers bundled blueprints and blank slate, neither of which has inputs to set.`
17369
+ );
17370
+ }
15977
17371
  const options = [
15978
17372
  { label: "Blank slate (credential only)", blueprint: BLANK_SLATE },
15979
17373
  ...BUNDLED_BLUEPRINTS.map((b) => ({ label: `${b.name} \u2014 ${b.description}`, blueprint: b }))
@@ -16020,6 +17414,22 @@ function buildRemintForContext(opts) {
16020
17414
  });
16021
17415
  };
16022
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
+ }
16023
17433
  async function runBootstrapCli(argv, deps = {}) {
16024
17434
  const log = deps.log ?? createLogger2();
16025
17435
  let args;
@@ -16053,6 +17463,15 @@ ${BOOTSTRAP_HELP}
16053
17463
  \u2716 Scope gate refused blueprint '${blueprint.name}':
16054
17464
  `);
16055
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}
16056
17475
  `);
16057
17476
  process.stderr.write(" Minting nothing.\n");
16058
17477
  return 2;
@@ -16064,6 +17483,11 @@ ${BOOTSTRAP_HELP}
16064
17483
  Blueprint: ${blueprint.name}
16065
17484
  \u2714 Scope gate: [${gate.accepted.join(", ")}] \u2014 all data-plane \u2713
16066
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
+ }
16067
17491
  let noSeed = args.noSeed;
16068
17492
  let seedDeclinedInteractively = false;
16069
17493
  if (shouldOfferSeedOptOut(args, blueprint, isTty)) {
@@ -16137,13 +17561,16 @@ ${renderPlan(blueprint, planSteps)}
16137
17561
  keyringOpenError = err;
16138
17562
  }
16139
17563
  let storedFreshInKeyring = false;
17564
+ const lastResolvedSecret = /* @__PURE__ */ new Map();
16140
17565
  const readExistingKey = async (contextId) => {
16141
17566
  if (!keyring) return void 0;
17567
+ const alias = keyringAliasFor(contextId, args.tenant);
16142
17568
  try {
16143
- const existing = await keyring.get(contextId);
17569
+ const existing = await keyring.get(alias);
16144
17570
  if (!existing?.keyId) return void 0;
16145
- const rawKey = await keyring.resolveSecret(contextId);
16146
- return rawKey ? { keyId: existing.keyId, rawKey } : void 0;
17571
+ const rawKey = await keyring.resolveSecret(alias);
17572
+ lastResolvedSecret.set(alias, rawKey);
17573
+ return rawKey ? { keyId: existing.keyId, rawKey, principalId: existing.principalId } : void 0;
16147
17574
  } catch {
16148
17575
  return void 0;
16149
17576
  }
@@ -16159,17 +17586,36 @@ ${renderPlan(blueprint, planSteps)}
16159
17586
  );
16160
17587
  return;
16161
17588
  }
17589
+ const alias = keyringAliasFor(contextId, args.tenant);
16162
17590
  try {
16163
- warnReplacingAlias(await keyring.get(contextId), contextId, record.revokedKeyId);
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
+ });
16164
17597
  await storeMintedKey(
16165
17598
  keyring,
16166
- { alias: contextId, context: contextId, principalId: record.principalId, keyId: record.keyId, env: args.env },
17599
+ {
17600
+ alias,
17601
+ context: contextId,
17602
+ principalId: record.principalId,
17603
+ keyId: record.keyId,
17604
+ // Derived from the REQUEST's actual baseUrl, not echoed from args.env — the two
17605
+ // diverge whenever VECTROS_API_BASE_URL/--base-url alone selected the environment.
17606
+ // `keyring list`/`keyring doctor` display this field; a stale echo would show
17607
+ // "env: production" next to a key this run actually minted against staging.
17608
+ env: deriveEnvironment(baseUrl) ?? args.env
17609
+ },
16167
17610
  record.rawKey,
16168
- { activate: true, revokedKeyId: record.revokedKeyId }
17611
+ { activate: willActivate, revokedKeyId: record.revokedKeyId }
16169
17612
  );
16170
17613
  if (record.revokedKeyId) {
16171
- await warnOtherAliasesNaming(keyring, record.revokedKeyId, contextId);
17614
+ await warnOtherAliasesNaming(keyring, record.revokedKeyId, alias);
16172
17615
  }
17616
+ process.stdout.write(
17617
+ tenantActivateMessage({ alias, tenant: args.tenant, willActivate, explicitlyDeclined: args.activate === false })
17618
+ );
16173
17619
  storedFreshInKeyring = true;
16174
17620
  } catch (err) {
16175
17621
  process.stderr.write(
@@ -16190,12 +17636,20 @@ ${renderPlan(blueprint, planSteps)}
16190
17636
  skipSeed: noSeed,
16191
17637
  readExistingKey,
16192
17638
  persistMintedKey,
17639
+ remintBootstrapForContext: buildRemintBootstrapForContext({
17640
+ cognitoToken,
17641
+ apiBaseUrl: baseUrl,
17642
+ tenant: args.tenant,
17643
+ useScopedToken: args.useScopedToken,
17644
+ mintBridge
17645
+ }),
16193
17646
  remintForContext: buildRemintForContext({
16194
17647
  cognitoToken,
16195
17648
  apiBaseUrl: baseUrl,
16196
17649
  tenant: args.tenant,
16197
17650
  mintContext
16198
17651
  }),
17652
+ confirmExistingPrincipal: buildConfirmExistingPrincipal(args, promptCtx.prompt, isTty),
16199
17653
  log
16200
17654
  });
16201
17655
  process.stdout.write(
@@ -16213,7 +17667,7 @@ ${renderPlan(blueprint, planSteps)}
16213
17667
  );
16214
17668
  entry = buildVectrosServerEntry({ apiKey: result.rawKey, apiBaseUrl: baseUrl });
16215
17669
  } else if (storedFreshInKeyring || result.keyAction === "reused") {
16216
- entry = buildVectrosServerEntry({ alias: blueprint.contextId, apiBaseUrl: baseUrl });
17670
+ entry = buildVectrosServerEntry({ alias: keyringAliasFor(blueprint.contextId, args.tenant), apiBaseUrl: baseUrl });
16217
17671
  } else {
16218
17672
  entry = buildVectrosServerEntry({ apiKey: result.rawKey, apiBaseUrl: baseUrl });
16219
17673
  }
@@ -16273,6 +17727,18 @@ ${codeAddHint}`);
16273
17727
  `);
16274
17728
  return 3;
16275
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
+ }
16276
17742
  if (err instanceof BlueprintIdentityError || err instanceof UnresolvedIdentitiesError) {
16277
17743
  process.stderr.write(`
16278
17744
  \u2716 ${err.message}
@@ -16296,7 +17762,7 @@ ${formatCliError(err, { action: "Bootstrap failed" })}`);
16296
17762
  import { writeFile as writeFile2, access } from "fs/promises";
16297
17763
 
16298
17764
  // src/scope-grammar.ts
16299
- var FORBIDDEN_SCOPE_NAMESPACES = /* @__PURE__ */ new Set([
17765
+ var FORBIDDEN_SCOPE_NAMESPACES2 = /* @__PURE__ */ new Set([
16300
17766
  "user",
16301
17767
  "record",
16302
17768
  "document",
@@ -16308,23 +17774,23 @@ var FORBIDDEN_SCOPE_NAMESPACES = /* @__PURE__ */ new Set([
16308
17774
  "versions",
16309
17775
  "lookup"
16310
17776
  ]);
16311
- var SCOPE_NAMESPACE_RE = /^[a-z][a-z0-9_-]{1,31}$/;
17777
+ var SCOPE_NAMESPACE_RE2 = /^[a-z][a-z0-9_-]{1,31}$/;
16312
17778
  var SCOPE_NAMESPACE_MIN_LENGTH = 2;
16313
17779
  var SCOPE_NAMESPACE_MAX_LENGTH = 32;
16314
- var SCOPE_VALUE_RE = /^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$/;
17780
+ var SCOPE_VALUE_RE2 = /^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$/;
16315
17781
  var SCOPE_VALUE_MIN_LENGTH = 1;
16316
17782
  var SCOPE_VALUE_MAX_LENGTH = 128;
16317
17783
  function valueGrammarError(value) {
16318
- if (!SCOPE_VALUE_RE.test(value)) {
17784
+ if (!SCOPE_VALUE_RE2.test(value)) {
16319
17785
  return `'${value}' is not a valid scope value \u2014 ${SCOPE_VALUE_MIN_LENGTH}-${SCOPE_VALUE_MAX_LENGTH} characters, starting with a letter or digit and continuing with letters, digits, '_' or '-'. A value may not contain a colon.`;
16320
17786
  }
16321
17787
  return null;
16322
17788
  }
16323
17789
  function namespaceGrammarError(ns) {
16324
- if (FORBIDDEN_SCOPE_NAMESPACES.has(ns)) {
17790
+ if (FORBIDDEN_SCOPE_NAMESPACES2.has(ns)) {
16325
17791
  return `'${ns}' is a reserved namespace and cannot be used as an ownership scope`;
16326
17792
  }
16327
- if (!SCOPE_NAMESPACE_RE.test(ns)) {
17793
+ if (!SCOPE_NAMESPACE_RE2.test(ns)) {
16328
17794
  return `'${ns}' is not a valid namespace \u2014 lowercase, ${SCOPE_NAMESPACE_MIN_LENGTH}-${SCOPE_NAMESPACE_MAX_LENGTH} characters, starting with a letter and continuing with letters, digits, '_' or '-'`;
16329
17795
  }
16330
17796
  return null;
@@ -16493,6 +17959,10 @@ function lintBlueprint(bp) {
16493
17959
  }
16494
17960
  }
16495
17961
  }
17962
+ {
17963
+ const reached = ownerScopedReachWithoutDataScope(bp.accessProfile.allowedActions, ds);
17964
+ if (reached.length > 0) findings.push(unconstrainedReachFinding("accessProfile", reached));
17965
+ }
16496
17966
  for (const roleId of Object.keys(bp.roles ?? {})) {
16497
17967
  if (!ROLE_ID_RE.test(roleId)) {
16498
17968
  findings.push({
@@ -16505,6 +17975,10 @@ function lintBlueprint(bp) {
16505
17975
  for (const [roleId, clauses] of Object.entries(bp.roles ?? {})) {
16506
17976
  clauses.forEach((clause, i) => {
16507
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
+ }
16508
17982
  if (!cds) return;
16509
17983
  for (const [key, values] of Object.entries(cds)) {
16510
17984
  for (const value of values) {
@@ -16521,6 +17995,17 @@ function lintBlueprint(bp) {
16521
17995
  }
16522
17996
  });
16523
17997
  }
17998
+ for (const issuer of bp.issuers ?? []) {
17999
+ for (const policy of issuer.selfSignupPolicies ?? []) {
18000
+ if (!Object.prototype.hasOwnProperty.call(bp.roles ?? {}, policy.roleId)) {
18001
+ findings.push({
18002
+ severity: "warning",
18003
+ rule: "self-signup-role-unknown",
18004
+ message: `issuer '${issuer.issuerId}': selfSignupPolicies entry '${policy.signupType}' names roleId '${policy.roleId}', which this blueprint does not declare under 'roles:'. The platform accepts the registration anyway (its elevated-scope check is skipped for an as-yet-unknown role, re-checked unskippable at actual signup time) \u2014 a typo here registers clean and only surfaces when someone tries to self-signup.`
18005
+ });
18006
+ }
18007
+ }
18008
+ }
16524
18009
  return findings;
16525
18010
  }
16526
18011
  var ROLE_ID_RE = /^[a-z][a-z0-9-]{2,30}$/;
@@ -16546,6 +18031,25 @@ function placeholderFinding(where, key, value) {
16546
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.`
16547
18032
  };
16548
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
+ }
16549
18053
  function lintPasses(findings) {
16550
18054
  return !findings.some((f) => f.severity === "error");
16551
18055
  }
@@ -16584,8 +18088,12 @@ schemas:
16584
18088
  lookupFields: [title] # extra O(1) exact-match lookup indexes (max 10)
16585
18089
 
16586
18090
  accessProfile:
16587
- # SCOPE GATE: data-plane only (records/schemas/search/documents/folders/inference).
16588
- # Anything else (keys/profiles/admin/*) 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/*.
16589
18097
  # Ops: c|r|u|d, combinable (records:cru). Prefer the narrowest set (least privilege).
16590
18098
  allowedActions: [records:r, records:c, records:u, search:r, schemas:r]
16591
18099
  # dataScope binds the key to specific owners. Include null to ALSO see
@@ -16593,6 +18101,17 @@ accessProfile:
16593
18101
  # owners' records, not shared/seed data. Keys are userId (the principal) and
16594
18102
  # namespaced scopes (scope:org, scope:client, ...):
16595
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]
16596
18115
 
16597
18116
  servicePrincipal:
16598
18117
  externalId: ${name}-sp # stable; the principal the profile + key bind to
@@ -16712,15 +18231,19 @@ async function resolveAndValidate(arg, verb, supplied) {
16712
18231
  return null;
16713
18232
  }
16714
18233
  try {
16715
- assertScopeGate(blueprint.accessProfile.allowedActions);
16716
- for (const clauses of Object.values(blueprint.roles ?? {})) {
16717
- for (const clause of clauses) assertScopeGate(clause.allowedActions);
16718
- }
18234
+ assertBlueprintGates(blueprint);
16719
18235
  } catch (err) {
16720
18236
  if (err instanceof ScopeGateError) {
16721
18237
  process.stderr.write(` \u2716 Scope gate rejected '${blueprint.name}':
16722
18238
  `);
16723
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}
16724
18247
  `);
16725
18248
  return null;
16726
18249
  }
@@ -16752,9 +18275,10 @@ async function runValidate(args) {
16752
18275
  const blueprint = await resolveAndValidate(args.file, "validate", supplied);
16753
18276
  if (!blueprint) return 2;
16754
18277
  const gate = evaluateScopeGate(blueprint.accessProfile.allowedActions);
18278
+ const capGate = evaluateCapabilityGate(blueprint.accessProfile.capabilities);
16755
18279
  const warnCount = lintBlueprint(blueprint).length;
16756
18280
  process.stdout.write(
16757
- ` \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"
16758
18282
  );
16759
18283
  return 0;
16760
18284
  }
@@ -17085,7 +18609,14 @@ async function resolveKeyDestination(opts, deps) {
17085
18609
  `);
17086
18610
  return 2;
17087
18611
  }
17088
- 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
+ }
17089
18620
  if (opts.activate === false && await keyring.activeAlias() === alias) {
17090
18621
  process.stderr.write(
17091
18622
  ` \u2716 --no-activate cannot be used on '${alias}': it is your ACTIVE identity, and storing
@@ -17106,6 +18637,8 @@ async function storeAndReport(dest, key, auth, opts) {
17106
18637
  return 1;
17107
18638
  }
17108
18639
  const principalId = opts.principalId;
18640
+ const tenant = keyTenant(key.rawKey) ?? "test";
18641
+ const willActivate = await determineActivateDecision(dest.keyring, { tenant, explicitActivate: opts.activate });
17109
18642
  let entry;
17110
18643
  try {
17111
18644
  warnReplacingAlias(await dest.keyring.get(dest.alias), dest.alias, opts.revokedKeyId);
@@ -17120,7 +18653,7 @@ async function storeAndReport(dest, key, auth, opts) {
17120
18653
  env: auth.env
17121
18654
  },
17122
18655
  key.rawKey,
17123
- { activate: opts.activate !== false, revokedKeyId: opts.revokedKeyId }
18656
+ { activate: willActivate, revokedKeyId: opts.revokedKeyId }
17124
18657
  ));
17125
18658
  } catch (err) {
17126
18659
  process.stderr.write(
@@ -17132,7 +18665,6 @@ async function storeAndReport(dest, key, auth, opts) {
17132
18665
  );
17133
18666
  return 1;
17134
18667
  }
17135
- const active = opts.activate !== false;
17136
18668
  process.stdout.write(
17137
18669
  `
17138
18670
  \u2714 Stored the new key in your ${describeEntryStoreNoun(entry)} \u2014 it was not printed.
@@ -17142,9 +18674,19 @@ async function storeAndReport(dest, key, auth, opts) {
17142
18674
  ["principal", entry.principalId || void 0],
17143
18675
  ["key id", entry.keyId],
17144
18676
  ["env", entry.env],
17145
- ["active", active ? "yes" : "no"]
17146
- ]) + "\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.
17147
- `)
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
+ })
17148
18690
  );
17149
18691
  return 0;
17150
18692
  }
@@ -17365,9 +18907,25 @@ async function runKeyRotate(opts, deps = {}) {
17365
18907
  return 1;
17366
18908
  }
17367
18909
  }
17368
- const existing = (await drainPages(() => client.auth.listScopedKeys())).find(
17369
- (k) => matchesTuple(k, opts.context, userId, keyName)
17370
- );
18910
+ const activeKeys = await drainPages(() => client.auth.listScopedKeys());
18911
+ const existing = activeKeys.find((k) => matchesTuple(k, opts.context, userId, keyName));
18912
+ if (!existing && keyName === DEFAULT_KEY_NAME && !opts.forceNew) {
18913
+ const others = activeKeys.filter(
18914
+ (k) => k.contextId === opts.context && k.userId === userId && k.status === "active"
18915
+ );
18916
+ if (others.length > 0) {
18917
+ const list = others.map((k) => ` \u2022 ${k.keyId} (name: '${k.keyName}')`).join("\n");
18918
+ process.stderr.write(
18919
+ ` \u2716 No active key named '${DEFAULT_KEY_NAME}' for usr_${userId} in '${opts.context}', but ${others.length} other active key(s) already exist for this principal/context \u2014 refusing to mint a second one silently, since one of these may be the key you actually meant to rotate (e.g. one \`bootstrap\` minted):
18920
+ ${list}
18921
+ If one of these is it, re-run with --name '<its name>'.
18922
+ If you really want a new key alongside them, re-run with --force-new.
18923
+ Nothing was revoked or minted.
18924
+ `
18925
+ );
18926
+ return 1;
18927
+ }
18928
+ }
17371
18929
  if (existing?.keyId) {
17372
18930
  await client.auth.revokeScopedKey({ keyId: existing.keyId });
17373
18931
  process.stdout.write(` \u2714 Revoked prior key ${existing.keyId}
@@ -17800,7 +19358,7 @@ function isRecognizedOwnershipKey(key) {
17800
19358
  if (key === "userId" || key === "partnerUserId") return true;
17801
19359
  if (!key.startsWith("scope:")) return false;
17802
19360
  const ns = key.slice("scope:".length);
17803
- return !FORBIDDEN_SCOPE_NAMESPACES.has(ns) && SCOPE_NAMESPACE_RE.test(ns);
19361
+ return !FORBIDDEN_SCOPE_NAMESPACES2.has(ns) && SCOPE_NAMESPACE_RE2.test(ns);
17804
19362
  }
17805
19363
  var DIM_WILDCARD = "*";
17806
19364
  var ANY_TOKEN_RE = /^\$\{\{\s*any\s*\}\}$/;
@@ -18547,7 +20105,24 @@ async function captureSnapshot(client, blueprint) {
18547
20105
  }
18548
20106
  return { existingSchemaTypes, existingProfilePrincipals, existingRoleIds, failed };
18549
20107
  }
18550
- async function runAssertions(client, deps, blueprint, tenantId, result) {
20108
+ async function captureIssuerSnapshot(bootstrapClient) {
20109
+ const existingIssuerIds = /* @__PURE__ */ new Set();
20110
+ let failed = false;
20111
+ try {
20112
+ let startFrom;
20113
+ for (; ; ) {
20114
+ const page = await bootstrapClient.auth.listIssuers({ limit: SNAPSHOT_PAGE_SIZE, startFrom });
20115
+ for (const i of page.data ?? []) if (i.issuerId) existingIssuerIds.add(i.issuerId);
20116
+ const next = page.nextCursor;
20117
+ if (!next) break;
20118
+ startFrom = next;
20119
+ }
20120
+ } catch {
20121
+ failed = true;
20122
+ }
20123
+ return { existingIssuerIds, failed };
20124
+ }
20125
+ async function runAssertions(client, bootstrapClient, deps, blueprint, tenantId, result) {
18551
20126
  const assertions = [];
18552
20127
  try {
18553
20128
  const ping = await deps.pingWithKey(result.rawKey);
@@ -18632,9 +20207,35 @@ async function runAssertions(client, deps, blueprint, tenantId, result) {
18632
20207
  }
18633
20208
  }
18634
20209
  }
20210
+ for (const { issuerId } of result.issuerActions) {
20211
+ try {
20212
+ const issuer = await bootstrapClient.auth.getIssuer({ issuerId });
20213
+ const ok = issuer.issuerId === issuerId;
20214
+ assertions.push({
20215
+ name: `issuer:${issuerId}`,
20216
+ ok,
20217
+ detail: ok ? `issuer '${issuerId}' registered + readable` : `getIssuer(${issuerId}) returned issuerId '${issuer.issuerId ?? "(none)"}'`
20218
+ });
20219
+ } catch (e) {
20220
+ assertions.push({ name: `issuer:${issuerId}`, ok: false, detail: `getIssuer(${issuerId}) failed: ${errMsg(e)}` });
20221
+ }
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
+ }
18635
20236
  return assertions;
18636
20237
  }
18637
- async function runTeardown(client, blueprint, machineId, snapshot, result) {
20238
+ async function runTeardown(client, bootstrapClient, blueprint, machineId, snapshot, result) {
18638
20239
  const steps = [];
18639
20240
  const keyName = result?.keyName ?? keyNameFor(blueprint.name, machineId);
18640
20241
  try {
@@ -18728,10 +20329,34 @@ async function runTeardown(client, blueprint, machineId, snapshot, result) {
18728
20329
  for (const [typeName] of toDelete) {
18729
20330
  steps.push({ step: `delete schema ${typeName}`, ok: false, error: lastError.get(typeName) ?? "could not delete" });
18730
20331
  }
20332
+ for (const { issuerId } of result.issuerActions) {
20333
+ if (snapshot.failed) {
20334
+ steps.push({ step: `delete issuer ${issuerId}`, ok: true, skipped: true, error: "skipped \u2014 pre-apply snapshot failed; cannot prove ownership" });
20335
+ continue;
20336
+ }
20337
+ if (snapshot.existingIssuerIds.has(issuerId)) {
20338
+ steps.push({ step: `delete issuer ${issuerId}`, ok: true, skipped: true, error: "skipped \u2014 pre-existed this run" });
20339
+ continue;
20340
+ }
20341
+ try {
20342
+ await bootstrapClient.auth.deleteIssuer({ issuerId });
20343
+ steps.push({ step: `delete issuer ${issuerId}`, ok: true });
20344
+ } catch (e) {
20345
+ steps.push({ step: `delete issuer ${issuerId}`, ok: false, error: errMsg(e) });
20346
+ }
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
+ }
18731
20356
  return steps;
18732
20357
  }
18733
20358
  async function runBlueprintHarness(client, input, deps) {
18734
- const { tenantId, machineId, keep = false, log, remintForContext } = input;
20359
+ const { tenantId, machineId, keep = false, log, remintForContext, remintBootstrapForContext } = input;
18735
20360
  const readExistingKey = async () => void 0;
18736
20361
  const persistMintedKey = async () => {
18737
20362
  };
@@ -18741,12 +20366,15 @@ async function runBlueprintHarness(client, input, deps) {
18741
20366
  log
18742
20367
  );
18743
20368
  const remint = remintForContext ?? (async () => client);
18744
- let snapshot;
20369
+ const remintBootstrap = remintBootstrapForContext ?? (async () => client);
20370
+ const hasIssuers = (blueprint.issuers ?? []).length > 0;
20371
+ const hasNamespaces = (blueprint.namespaces ?? []).length > 0;
20372
+ let contextSnapshot;
18745
20373
  if (remintForContext) {
18746
20374
  try {
18747
- snapshot = await captureSnapshot(await remintForContext(blueprint.contextId), blueprint);
20375
+ contextSnapshot = await captureSnapshot(await remintForContext(blueprint.contextId), blueprint);
18748
20376
  } catch (e) {
18749
- snapshot = {
20377
+ contextSnapshot = {
18750
20378
  existingSchemaTypes: /* @__PURE__ */ new Set(),
18751
20379
  existingProfilePrincipals: /* @__PURE__ */ new Set(),
18752
20380
  existingRoleIds: /* @__PURE__ */ new Set(),
@@ -18754,15 +20382,48 @@ async function runBlueprintHarness(client, input, deps) {
18754
20382
  };
18755
20383
  }
18756
20384
  } else {
18757
- snapshot = await captureSnapshot(client, blueprint);
20385
+ contextSnapshot = await captureSnapshot(client, blueprint);
20386
+ }
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);
18758
20398
  }
20399
+ const snapshot = {
20400
+ ...contextSnapshot,
20401
+ existingIssuerIds: issuerSnapshot.existingIssuerIds,
20402
+ failed: contextSnapshot.failed || issuerSnapshot.failed
20403
+ };
18759
20404
  let result;
18760
20405
  let assertions = [];
18761
20406
  let applyError;
18762
20407
  try {
18763
- 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
+ });
18764
20418
  log.debug({ component: "harness", contextId: result.contextId, keyName: result.keyName }, "applied");
18765
- assertions = await runAssertions(await remint(blueprint.contextId), 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
+ );
18766
20427
  } catch (e) {
18767
20428
  applyError = errMsg(e);
18768
20429
  log.warn({ component: "harness", err: applyError }, "apply phase failed");
@@ -18774,7 +20435,14 @@ async function runBlueprintHarness(client, input, deps) {
18774
20435
  tdClient = await remint(blueprint.contextId);
18775
20436
  } catch {
18776
20437
  }
18777
- teardown = await runTeardown(tdClient, 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);
18778
20446
  }
18779
20447
  const passed = applyError === void 0 && assertions.length > 0 && assertions.every((a) => a.ok);
18780
20448
  return {
@@ -18848,6 +20516,13 @@ async function runBlueprintTest(fileOrName, opts, deps = {}) {
18848
20516
  process.stderr.write(` \u2716 Scope gate refused blueprint:
18849
20517
  `);
18850
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}
18851
20526
  `);
18852
20527
  return 2;
18853
20528
  }
@@ -18883,9 +20558,22 @@ async function runBlueprintTest(fileOrName, opts, deps = {}) {
18883
20558
  environment: auth.baseUrl
18884
20559
  });
18885
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
+ };
18886
20574
  const report = await runBlueprintHarness(
18887
20575
  client,
18888
- { blueprint, tenantId, machineId, keep: opts.keep, log, remintForContext },
20576
+ { blueprint, tenantId, machineId, keep: opts.keep, log, remintForContext, remintBootstrapForContext },
18889
20577
  { pingWithKey }
18890
20578
  );
18891
20579
  if (opts.json) {
@@ -18966,7 +20654,15 @@ async function runJoin(opts, deps = {}) {
18966
20654
  return 2;
18967
20655
  }
18968
20656
  const context = opts.contextArg;
18969
- const alias = opts.alias ?? context;
20657
+ const tenant = opts.tenant ?? "live";
20658
+ let alias;
20659
+ try {
20660
+ alias = keyringAliasFor(opts.alias ?? context, tenant);
20661
+ } catch (err) {
20662
+ process.stderr.write(` \u2716 ${formatError(err)}
20663
+ `);
20664
+ return 2;
20665
+ }
18970
20666
  let keyring;
18971
20667
  try {
18972
20668
  keyring = deps.keyring ?? await Keyring.open();
@@ -18996,8 +20692,23 @@ async function runJoin(opts, deps = {}) {
18996
20692
  const heldSecret = existingEntry ? await keyring.resolveSecret(alias) : void 0;
18997
20693
  const canReuse = existingEntry && heldSecret && existingEntry.context === context && existingEntry.principalId === principalId && existingEntry.env === auth.env;
18998
20694
  if (canReuse && existingEntry) {
18999
- await keyring.setActive(alias);
19000
- 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
+ });
19001
20712
  return 0;
19002
20713
  }
19003
20714
  warnReplacingAlias(existingEntry, alias);
@@ -19009,13 +20720,24 @@ async function runJoin(opts, deps = {}) {
19009
20720
  );
19010
20721
  }
19011
20722
  if (revokedKeyId) await warnOtherAliasesNaming(keyring, revokedKeyId, alias);
20723
+ const willActivate = await determineActivateDecision(keyring, { tenant, explicitActivate: opts.activate });
19012
20724
  const { entry } = await storeMintedKey(
19013
20725
  keyring,
19014
20726
  { alias, context, principalId, role: opts.role, keyId: key.keyId, env: auth.env },
19015
20727
  key.rawKey,
19016
- { revokedKeyId }
20728
+ { activate: willActivate, revokedKeyId }
19017
20729
  );
19018
- 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
+ });
19019
20741
  return 0;
19020
20742
  } catch (err) {
19021
20743
  process.stderr.write(formatCliError(err, { action: "join failed" }));
@@ -19023,27 +20745,34 @@ async function runJoin(opts, deps = {}) {
19023
20745
  }
19024
20746
  });
19025
20747
  }
19026
- function emitJoined(alias, context, principalId, role, keyId, reused) {
19027
- process.stdout.write(
19028
- `
19029
- \u2714 Joined '${context}' as ${principalId}` + (reused ? " (existing key reused)" : "") + "\n" + formatKeyValues([
19030
- ["alias", alias],
19031
- ["context", context],
19032
- ["principal", principalId],
19033
- ["role", role],
19034
- ["key id", keyId]
19035
- ]) + `
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 ? `
19036
20758
 
19037
- Now active. Use \`vectros switch ${alias}\` to return to it, \`vectros keyring show --format env\` to export the key for the MCP server.
19038
- `
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
+ })
19039
20768
  );
19040
20769
  }
19041
20770
 
19042
20771
  // src/build-info.ts
19043
20772
  var BUILD_INFO = {
19044
- cli: true ? "0.14.0" : "dev",
19045
- sdk: true ? "0.38.0" : "dev",
19046
- blueprints: true ? "0.11.0" : "dev"
20773
+ cli: true ? "0.16.0" : "dev",
20774
+ sdk: true ? "0.40.0" : "dev",
20775
+ blueprints: true ? "0.13.0" : "dev"
19047
20776
  };
19048
20777
  function formatBuildInfo() {
19049
20778
  return `vectros ${BUILD_INFO.cli} (sdk ${BUILD_INFO.sdk}, blueprints ${BUILD_INFO.blueprints})`;
@@ -19067,6 +20796,14 @@ function addCommonAuthOptions(cmd) {
19067
20796
  "emit machine-readable JSON instead of the human-readable view"
19068
20797
  );
19069
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
+ }
19070
20807
  function toCommonOptions(flags) {
19071
20808
  return {
19072
20809
  env: flags.env,
@@ -19120,7 +20857,10 @@ function buildProgram(deps = {}) {
19120
20857
  return program;
19121
20858
  }
19122
20859
  function registerJoinCommand(program, deps) {
19123
- 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");
19124
20864
  addAuthOptions(join3).action(
19125
20865
  async (contextArg, flags) => {
19126
20866
  if (flags.tenant !== void 0 && flags.tenant !== "test" && flags.tenant !== "live") {
@@ -19138,7 +20878,8 @@ function registerJoinCommand(program, deps) {
19138
20878
  as: flags.as,
19139
20879
  scope: flags.scope,
19140
20880
  alias: flags.alias,
19141
- tenant: flags.tenant
20881
+ tenant: flags.tenant,
20882
+ activate: flags.activate
19142
20883
  },
19143
20884
  deps
19144
20885
  )
@@ -19385,9 +21126,10 @@ function toKeyDestination(flags) {
19385
21126
  function registerKeyCommands(program, deps) {
19386
21127
  const key = program.command("key").description("Manage scoped API keys (ssk_*) \u2014 issue, list, revoke, rotate.");
19387
21128
  const issue = addKeyDestinationOptions(
19388
- 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)")
19389
21130
  );
19390
21131
  addAuthOptions(issue).action(async (flags) => {
21132
+ const tenant = parseTenantFlag(flags.tenant);
19391
21133
  process.exit(
19392
21134
  await runKeyIssue({
19393
21135
  ...toCommonOptions(flags),
@@ -19395,6 +21137,7 @@ function registerKeyCommands(program, deps) {
19395
21137
  context: flags.context,
19396
21138
  name: flags.name,
19397
21139
  label: flags.label,
21140
+ tenant,
19398
21141
  ...toKeyDestination(flags)
19399
21142
  }, deps)
19400
21143
  );
@@ -19402,40 +21145,58 @@ function registerKeyCommands(program, deps) {
19402
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(
19403
21146
  "--all-contexts",
19404
21147
  "list every key across ALL your contexts (owner-only) \u2014 the tenant-wide view for the key limit"
19405
- );
21148
+ ).option("--tenant <tenant>", "tenant to list: 'test' or 'live' (default live; not combinable with --all-contexts)");
19406
21149
  addCommonAuthOptions(list).action(
19407
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);
19408
21156
  process.exit(
19409
21157
  await runKeyList(
19410
21158
  {
19411
21159
  ...toCommonOptions(flags),
19412
21160
  principal: flags.principal,
19413
21161
  context: flags.context,
19414
- allContexts: Boolean(flags.allContexts)
21162
+ allContexts: Boolean(flags.allContexts),
21163
+ tenant
19415
21164
  },
19416
21165
  deps
19417
21166
  )
19418
21167
  );
19419
21168
  }
19420
21169
  );
19421
- 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)");
19422
- addCommonAuthOptions(get).action(async (keyId, flags) => {
19423
- process.exit(await runKeyGet(keyId, { ...toCommonOptions(flags), context: flags.context }, deps));
19424
- });
19425
- 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)");
19426
- addAuthOptions(revoke).action(async (keyId, flags) => {
19427
- process.exit(await runKeyRevoke(keyId, { ...toCommonOptions(flags), context: flags.context }, deps));
19428
- });
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
+ );
19429
21184
  const rotate = addKeyDestinationOptions(
19430
- 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)
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(
21186
+ "--force-new",
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)"
21188
+ ).option("--tenant <tenant>", "tenant the key lives in: 'test' or 'live' (default live)")
19431
21189
  );
19432
21190
  addAuthOptions(rotate).action(async (flags) => {
21191
+ const tenant = parseTenantFlag(flags.tenant);
19433
21192
  process.exit(
19434
21193
  await runKeyRotate({
19435
21194
  ...toCommonOptions(flags),
19436
21195
  principal: flags.principal,
19437
21196
  context: flags.context,
19438
21197
  name: flags.name,
21198
+ forceNew: flags.forceNew,
21199
+ tenant,
19439
21200
  ...toKeyDestination(flags)
19440
21201
  }, deps)
19441
21202
  );