@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.js CHANGED
@@ -1675,6 +1675,7 @@ __export(api_exports, {
1675
1675
  TooManyRequestsError: () => TooManyRequestsError,
1676
1676
  TruncationWarningEvent: () => TruncationWarningEvent,
1677
1677
  UnauthorizedError: () => UnauthorizedError,
1678
+ UserExistsResponse: () => UserExistsResponse,
1678
1679
  UserRequest: () => UserRequest,
1679
1680
  UserResponse: () => UserResponse,
1680
1681
  auth: () => auth_exports,
@@ -2351,6 +2352,14 @@ var TruncationWarningEvent;
2351
2352
  ContextWindowBudget: "context_window_budget"
2352
2353
  };
2353
2354
  })(TruncationWarningEvent || (TruncationWarningEvent = {}));
2355
+ var UserExistsResponse;
2356
+ ((UserExistsResponse2) => {
2357
+ UserExistsResponse2.Status = {
2358
+ Active: "ACTIVE",
2359
+ Suspended: "SUSPENDED",
2360
+ Pending: "PENDING"
2361
+ };
2362
+ })(UserExistsResponse || (UserExistsResponse = {}));
2354
2363
  var UserRequest;
2355
2364
  ((UserRequest2) => {
2356
2365
  UserRequest2.Status = {
@@ -4094,7 +4103,7 @@ var AuthClient = class {
4094
4103
  return handleNonStatusCodeError(_response.error, _response.rawResponse, "GET", "/v1/admin/keys/scoped");
4095
4104
  }
4096
4105
  /**
4097
- * 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.
4106
+ * Creates a scoped API key (an `ssk_*` secret) that inherits its permissions from an existing access profile in your account. The call is idempotent on the combination of tenant, context, user, and key name: re-issuing the same request returns the existing key WITHOUT re-disclosing its raw secret. The raw key is returned ONLY in this response — store it securely, as it cannot be retrieved again. Requires the `keys:c` scope. If you use a scoped credential, `keys:c` alone is not sufficient: because the minted key is durably bound to the profile you name, the profile's effective scopes may not exceed your own, and you may only mint against a profile whose `identityOverrides` values your own identity holds. A root API key (`sk_`) is exempt from both bounds.
4098
4107
  *
4099
4108
  * @param {Vectros.CreateScopedKeyRequest} request
4100
4109
  * @param {AuthClient.RequestOptions} requestOptions - Request-specific configuration.
@@ -4426,7 +4435,7 @@ var AuthClient = class {
4426
4435
  );
4427
4436
  }
4428
4437
  /**
4429
- * Creates a new access profile under the given app context. This call is idempotent by `principalId`: if a profile with the same `principalId` already exists, the existing profile is returned (with status 200) instead of creating a duplicate. The response's `created` field (and the HTTP status — 201 when created, 200 when an existing profile was returned) tells the two apart. To overwrite an existing profile's `scopes`/`roleId`, `identityOverrides`, and `status` instead of returning it unchanged, set `?upsert=true` (this also requires the `profiles:u` scope, and applies the same `identityOverrides` bounds the update endpoint documents — a scoped credential may not repoint or clear an identity value it does not itself hold). You must provide exactly one of `scopes` (an inline list of scopes) or `roleId` (a reference to a role); supplying both, or neither, is rejected. `identityOverrides` is keyed by ownership namespace in `scope:<namespace>` form — `scope:org` and `scope:client` for the reserved namespaces, or any namespace you have registered — and may name at most two; any other key (including the account identifier or `userId`) is rejected. If you use a scoped credential, the profile's effective scopes may not exceed your own; a root API key (`sk_`) is exempt. Requires the `profiles:c` scope.
4438
+ * Creates a new access profile under the given app context. This call is idempotent by `principalId`: if a profile with the same `principalId` already exists, the existing profile is returned (with status 200) instead of creating a duplicate. The response's `created` field (and the HTTP status — 201 when created, 200 when an existing profile was returned) tells the two apart. To overwrite an existing profile's `scopes`/`roleId`, `identityOverrides`, and `status` instead of returning it unchanged, set `?upsert=true` (this also requires the `profiles:u` scope, and applies the same `identityOverrides` bounds the update endpoint documents — a scoped credential may not repoint or clear an identity value it does not itself hold). The `principalId` must name a principal that already exists: a `usr_` principal must be a live user in your tenant, so create the user before granting it a profile. A `usr_` id that names no such user is rejected, and no profile is created. `key_` principals are not checked this way. You must provide exactly one of `scopes` (an inline list of scopes) or `roleId` (a reference to a role); supplying both, or neither, is rejected. `identityOverrides` is keyed by ownership namespace in `scope:<namespace>` form — `scope:org` and `scope:client` for the reserved namespaces, or any namespace you have registered — and may name at most two; any other key (including the account identifier or `userId`) is rejected. If you use a scoped credential, the profile's effective scopes may not exceed your own; a root API key (`sk_`) is exempt. Requires the `profiles:c` scope.
4430
4439
  *
4431
4440
  * @param {Vectros.CreateAccessProfileRequest} request
4432
4441
  * @param {AuthClient.RequestOptions} requestOptions - Request-specific configuration.
@@ -4844,11 +4853,12 @@ var AuthClient = class {
4844
4853
  );
4845
4854
  }
4846
4855
  /**
4847
- * 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.
4856
+ * 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.
4848
4857
  *
4849
4858
  * @param {Vectros.UpdateAccessProfileRequest} request
4850
4859
  * @param {AuthClient.RequestOptions} requestOptions - Request-specific configuration.
4851
4860
  *
4861
+ * @throws {@link Vectros.BadRequestError}
4852
4862
  * @throws {@link Vectros.ForbiddenError}
4853
4863
  * @throws {@link Vectros.NotFoundError}
4854
4864
  * @throws {@link Vectros.TooManyRequestsError}
@@ -4895,6 +4905,8 @@ var AuthClient = class {
4895
4905
  }
4896
4906
  if (_response.error.reason === "status-code") {
4897
4907
  switch (_response.error.statusCode) {
4908
+ case 400:
4909
+ throw new BadRequestError(_response.error.body, _response.rawResponse);
4898
4910
  case 403:
4899
4911
  throw new ForbiddenError(_response.error.body, _response.rawResponse);
4900
4912
  case 404:
@@ -5524,7 +5536,7 @@ var AuthClient = class {
5524
5536
  );
5525
5537
  }
5526
5538
  /**
5527
- * Returns full usage detail for the requested calendar month, broken down by category (search, documents, and records) with per-category credit estimates and a split between your live and test environments. Defaults to the current month when `year` and `month` are omitted. Requires the `billing:r` scope on scoped tokens; API keys always have access.
5539
+ * Returns full usage detail for the requested calendar month, broken down by category (search, documents, and records) with per-category credit estimates and a split between your live and test environments. Defaults to the current month when `year` and `month` are omitted. Requires the `billing:r` scope on scoped tokens; API keys always have access. **A token confined to a single app context sees only that context's usage**: totals, the environment split, and the `contexts` breakdown narrow to it, and the environment your context is not bound to is omitted (`null`), not zeroed. Only a token with cross-context reach sees your full account-wide totals. Two exceptions to the narrowing, since they have no per-context breakdown to narrow to: `reads.calls.used`/`reads.dataOut.bytes` (metered per account, not per context) read as `0` for a confined token rather than a narrowed figure — the corresponding overage-credit charge fields narrow correctly; and `credits.limit` stays your whole plan's ceiling, so `credits.remaining` may overstate the account's true remaining room.
5528
5540
  *
5529
5541
  * @param {Vectros.GetUsageRequest} request
5530
5542
  * @param {AuthClient.RequestOptions} requestOptions - Request-specific configuration.
@@ -5586,6 +5598,257 @@ var AuthClient = class {
5586
5598
  }
5587
5599
  return handleNonStatusCodeError(_response.error, _response.rawResponse, "GET", "/v1/usage");
5588
5600
  }
5601
+ /**
5602
+ * Retrieves a single registered issuer by issuerId. Requires a root API key or the bootstrap's provisioning capability. A credential confined to one app context sees only an issuer registered in that context; naming one registered in another context returns 404, identically to a nonexistent issuerId. A root API key sees every context.
5603
+ *
5604
+ * @param {Vectros.GetIssuerRequest} request
5605
+ * @param {AuthClient.RequestOptions} requestOptions - Request-specific configuration.
5606
+ *
5607
+ * @throws {@link Vectros.ForbiddenError}
5608
+ * @throws {@link Vectros.NotFoundError}
5609
+ *
5610
+ * @example
5611
+ * await client.auth.getIssuer({
5612
+ * issuerId: "auth0-prod"
5613
+ * })
5614
+ */
5615
+ getIssuer(request, requestOptions) {
5616
+ return HttpResponsePromise.fromPromise(this.__getIssuer(request, requestOptions));
5617
+ }
5618
+ async __getIssuer(request, requestOptions) {
5619
+ const { issuerId } = request;
5620
+ const _authRequest = await this._options.authProvider.getAuthRequest();
5621
+ const _headers = mergeHeaders(
5622
+ _authRequest.headers,
5623
+ this._options?.headers,
5624
+ requestOptions?.headers
5625
+ );
5626
+ const _response = await fetcher({
5627
+ url: url_exports.join(
5628
+ await Supplier.get(this._options.baseUrl) ?? await Supplier.get(this._options.environment),
5629
+ `v1/auth/issuers/${url_exports.encodePathParam(issuerId)}`
5630
+ ),
5631
+ method: "GET",
5632
+ headers: _headers,
5633
+ queryString: url_exports.queryBuilder().mergeAdditional(requestOptions?.queryParams).build(),
5634
+ timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1e3,
5635
+ maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries,
5636
+ abortSignal: requestOptions?.abortSignal,
5637
+ fetchFn: this._options?.fetch,
5638
+ logging: this._options.logging
5639
+ });
5640
+ if (_response.ok) {
5641
+ return { data: _response.body, rawResponse: _response.rawResponse };
5642
+ }
5643
+ if (_response.error.reason === "status-code") {
5644
+ switch (_response.error.statusCode) {
5645
+ case 403:
5646
+ throw new ForbiddenError(_response.error.body, _response.rawResponse);
5647
+ case 404:
5648
+ throw new NotFoundError(_response.error.body, _response.rawResponse);
5649
+ default:
5650
+ throw new VectrosError({
5651
+ statusCode: _response.error.statusCode,
5652
+ body: _response.error.body,
5653
+ rawResponse: _response.rawResponse
5654
+ });
5655
+ }
5656
+ }
5657
+ return handleNonStatusCodeError(_response.error, _response.rawResponse, "GET", "/v1/auth/issuers/{issuerId}");
5658
+ }
5659
+ /**
5660
+ * Deregisters a trusted third-party IdP issuer. Requires a root API key or the bootstrap's provisioning capability. A credential confined to one app context may only deregister an issuer registered in that context; naming one registered in another context returns 404, identically to a nonexistent issuerId. A root API key may deregister any issuer. Refused if any user account was ever created or matched via this issuer (by a prior self-signup or accepted invite, through `POST /v1/auth/token/exchange`) — that access cannot be silently orphaned. Deactivate the affected users first if you intend to cut off their access, or register a replacement issuer before removing this one. An issuer that has never been used for an exchange (no bound users yet) can always be deregistered.
5661
+ *
5662
+ * @param {Vectros.DeleteIssuerRequest} request
5663
+ * @param {AuthClient.RequestOptions} requestOptions - Request-specific configuration.
5664
+ *
5665
+ * @throws {@link Vectros.ForbiddenError}
5666
+ * @throws {@link Vectros.NotFoundError}
5667
+ * @throws {@link Vectros.ConflictError}
5668
+ * @throws {@link Vectros.TooManyRequestsError}
5669
+ *
5670
+ * @example
5671
+ * await client.auth.deleteIssuer({
5672
+ * issuerId: "auth0-prod"
5673
+ * })
5674
+ */
5675
+ deleteIssuer(request, requestOptions) {
5676
+ return HttpResponsePromise.fromPromise(this.__deleteIssuer(request, requestOptions));
5677
+ }
5678
+ async __deleteIssuer(request, requestOptions) {
5679
+ const { issuerId } = request;
5680
+ const _authRequest = await this._options.authProvider.getAuthRequest();
5681
+ const _headers = mergeHeaders(
5682
+ _authRequest.headers,
5683
+ this._options?.headers,
5684
+ requestOptions?.headers
5685
+ );
5686
+ const _response = await fetcher({
5687
+ url: url_exports.join(
5688
+ await Supplier.get(this._options.baseUrl) ?? await Supplier.get(this._options.environment),
5689
+ `v1/auth/issuers/${url_exports.encodePathParam(issuerId)}`
5690
+ ),
5691
+ method: "DELETE",
5692
+ headers: _headers,
5693
+ queryString: url_exports.queryBuilder().mergeAdditional(requestOptions?.queryParams).build(),
5694
+ timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1e3,
5695
+ maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries,
5696
+ abortSignal: requestOptions?.abortSignal,
5697
+ fetchFn: this._options?.fetch,
5698
+ logging: this._options.logging
5699
+ });
5700
+ if (_response.ok) {
5701
+ return { data: void 0, rawResponse: _response.rawResponse };
5702
+ }
5703
+ if (_response.error.reason === "status-code") {
5704
+ switch (_response.error.statusCode) {
5705
+ case 403:
5706
+ throw new ForbiddenError(_response.error.body, _response.rawResponse);
5707
+ case 404:
5708
+ throw new NotFoundError(_response.error.body, _response.rawResponse);
5709
+ case 409:
5710
+ throw new ConflictError(_response.error.body, _response.rawResponse);
5711
+ case 429:
5712
+ throw new TooManyRequestsError(_response.error.body, _response.rawResponse);
5713
+ default:
5714
+ throw new VectrosError({
5715
+ statusCode: _response.error.statusCode,
5716
+ body: _response.error.body,
5717
+ rawResponse: _response.rawResponse
5718
+ });
5719
+ }
5720
+ }
5721
+ return handleNonStatusCodeError(
5722
+ _response.error,
5723
+ _response.rawResponse,
5724
+ "DELETE",
5725
+ "/v1/auth/issuers/{issuerId}"
5726
+ );
5727
+ }
5728
+ /**
5729
+ * Returns the issuers registered in your tenant. Requires a root API key or the bootstrap's provisioning capability. A credential confined to one app context sees only the issuers registered in that context; a root API key sees every context. Returns a `{data, nextCursor}` envelope.
5730
+ *
5731
+ * @param {Vectros.ListIssuersRequest} request
5732
+ * @param {AuthClient.RequestOptions} requestOptions - Request-specific configuration.
5733
+ *
5734
+ * @throws {@link Vectros.ForbiddenError}
5735
+ *
5736
+ * @example
5737
+ * await client.auth.listIssuers()
5738
+ */
5739
+ listIssuers(request = {}, requestOptions) {
5740
+ return HttpResponsePromise.fromPromise(this.__listIssuers(request, requestOptions));
5741
+ }
5742
+ async __listIssuers(request = {}, requestOptions) {
5743
+ const { startFrom, limit } = request;
5744
+ const _queryParams = {
5745
+ startFrom,
5746
+ limit
5747
+ };
5748
+ const _authRequest = await this._options.authProvider.getAuthRequest();
5749
+ const _headers = mergeHeaders(
5750
+ _authRequest.headers,
5751
+ this._options?.headers,
5752
+ requestOptions?.headers
5753
+ );
5754
+ const _response = await fetcher({
5755
+ url: url_exports.join(
5756
+ await Supplier.get(this._options.baseUrl) ?? await Supplier.get(this._options.environment),
5757
+ "v1/auth/issuers"
5758
+ ),
5759
+ method: "GET",
5760
+ headers: _headers,
5761
+ queryString: url_exports.queryBuilder().addMany(_queryParams).mergeAdditional(requestOptions?.queryParams).build(),
5762
+ timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1e3,
5763
+ maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries,
5764
+ abortSignal: requestOptions?.abortSignal,
5765
+ fetchFn: this._options?.fetch,
5766
+ logging: this._options.logging
5767
+ });
5768
+ if (_response.ok) {
5769
+ return { data: _response.body, rawResponse: _response.rawResponse };
5770
+ }
5771
+ if (_response.error.reason === "status-code") {
5772
+ switch (_response.error.statusCode) {
5773
+ case 403:
5774
+ throw new ForbiddenError(_response.error.body, _response.rawResponse);
5775
+ default:
5776
+ throw new VectrosError({
5777
+ statusCode: _response.error.statusCode,
5778
+ body: _response.error.body,
5779
+ rawResponse: _response.rawResponse
5780
+ });
5781
+ }
5782
+ }
5783
+ return handleNonStatusCodeError(_response.error, _response.rawResponse, "GET", "/v1/auth/issuers");
5784
+ }
5785
+ /**
5786
+ * Registers a trusted third-party IdP issuer that BYO-IdP token exchange (`POST /v1/auth/token/exchange`) may accept a `subject_token` from. Requires a root API key or the CLI bootstrap's provisioning capability — never an ordinary partner-grantable scope. Idempotent by `issuerId` within your tenant; the `(issuer, audience)` pair must not already be registered by a different issuerId/tenant. An app context may have at most one active issuer — deregister the existing one first if you need to replace it. One issuer MAY serve several contexts today, each via its own registration row with a distinct `audience`.
5787
+ *
5788
+ * @param {Vectros.IssuerRequest} request
5789
+ * @param {AuthClient.RequestOptions} requestOptions - Request-specific configuration.
5790
+ *
5791
+ * @throws {@link Vectros.BadRequestError}
5792
+ * @throws {@link Vectros.ForbiddenError}
5793
+ * @throws {@link Vectros.TooManyRequestsError}
5794
+ *
5795
+ * @example
5796
+ * await client.auth.registerIssuer({
5797
+ * issuerId: "auth0-prod",
5798
+ * issuer: "https://your-tenant.us.auth0.com/",
5799
+ * jwksUri: "https://your-tenant.us.auth0.com/.well-known/jwks.json",
5800
+ * audience: "https://api.your-app.example.com",
5801
+ * contextId: "casework"
5802
+ * })
5803
+ */
5804
+ registerIssuer(request, requestOptions) {
5805
+ return HttpResponsePromise.fromPromise(this.__registerIssuer(request, requestOptions));
5806
+ }
5807
+ async __registerIssuer(request, requestOptions) {
5808
+ const _authRequest = await this._options.authProvider.getAuthRequest();
5809
+ const _headers = mergeHeaders(
5810
+ _authRequest.headers,
5811
+ this._options?.headers,
5812
+ requestOptions?.headers
5813
+ );
5814
+ const _response = await fetcher({
5815
+ url: url_exports.join(
5816
+ await Supplier.get(this._options.baseUrl) ?? await Supplier.get(this._options.environment),
5817
+ "v1/auth/issuers"
5818
+ ),
5819
+ method: "POST",
5820
+ headers: _headers,
5821
+ contentType: "application/json",
5822
+ queryString: url_exports.queryBuilder().mergeAdditional(requestOptions?.queryParams).build(),
5823
+ requestType: "json",
5824
+ body: request,
5825
+ timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1e3,
5826
+ maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries,
5827
+ abortSignal: requestOptions?.abortSignal,
5828
+ fetchFn: this._options?.fetch,
5829
+ logging: this._options.logging
5830
+ });
5831
+ if (_response.ok) {
5832
+ return { data: _response.body, rawResponse: _response.rawResponse };
5833
+ }
5834
+ if (_response.error.reason === "status-code") {
5835
+ switch (_response.error.statusCode) {
5836
+ case 400:
5837
+ throw new BadRequestError(_response.error.body, _response.rawResponse);
5838
+ case 403:
5839
+ throw new ForbiddenError(_response.error.body, _response.rawResponse);
5840
+ case 429:
5841
+ throw new TooManyRequestsError(_response.error.body, _response.rawResponse);
5842
+ default:
5843
+ throw new VectrosError({
5844
+ statusCode: _response.error.statusCode,
5845
+ body: _response.error.body,
5846
+ rawResponse: _response.rawResponse
5847
+ });
5848
+ }
5849
+ }
5850
+ return handleNonStatusCodeError(_response.error, _response.rawResponse, "POST", "/v1/auth/issuers");
5851
+ }
5589
5852
  /**
5590
5853
  * 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.
5591
5854
  *
@@ -5631,7 +5894,7 @@ var AuthClient = class {
5631
5894
  return handleNonStatusCodeError(_response.error, _response.rawResponse, "GET", "/v1/ping");
5632
5895
  }
5633
5896
  /**
5634
- * Returns the access profiles for the given principal across all of your contexts. Use this to answer questions like "which apps does this user have access to?" — for example, to build a member-access summary. Results are confined to your account. Requires the `profiles:r` scope.
5897
+ * Returns the access profiles for the given principal. Looking up your OWN principal — or holding the `context-directory-read` capability — returns the profiles across ALL of your contexts, letting you answer questions like "which apps does this user have access to?". A context-bound credential looking up a DIFFERENT principal instead sees only that principal's profile in your credential's own context (at most one result), never across contexts it has no authority over. Results are always confined to your account. Requires the `profiles:r` scope.
5635
5898
  *
5636
5899
  * @param {Vectros.ListProfilesForPrincipalRequest} request
5637
5900
  * @param {AuthClient.RequestOptions} requestOptions - Request-specific configuration.
@@ -5909,40 +6172,34 @@ var AuthClient = class {
5909
6172
  }
5910
6173
  return handleNonStatusCodeError(_response.error, _response.rawResponse, "POST", "/v1/users/invite/resend");
5911
6174
  }
5912
- };
5913
- var ComplianceClient = class {
5914
- constructor(options) {
5915
- this._options = normalizeClientOptionsWithAuth(options);
5916
- }
5917
6175
  /**
5918
- * 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.
6176
+ * RFC 8693 OAuth 2.0 Token Exchange. Trades a JWT issued by a third-party identity provider you've registered (`POST /v1/auth/issuers`) for a Vectros `st_*` scoped bearer token — no Vectros credential required to call this endpoint. The exchanged token's scope is resolved entirely server-side from the matched user's access profile; this endpoint accepts no caller-supplied scope, resource, or audience parameter (RFC 8693 §2.1's `resource`/`audience`/`scope` are not used in v1 — the registered `(issuer, audience)` pair alone pins the target tenant and app context). On a first-time login (no existing Vectros identity for this subject), two opt-in binding paths exist: `invite_token` (a `PENDING` sub-user invitation), and — if the registration declares one or more self-signup policies — `signup_type` (a brand-new user is created and bound to the policy's configured role). If `invite_token` is present at all, it is the ONLY path tried — a failed invite never falls through to self-signup. Neither field is required for a subject with an existing identity. If your issuer is registered against more than one app context (each via its own audience), `context_id` selects which one to target; omit it when your token's `aud` claim matches only one registered context — the common case, unaffected by this field. Uses the OAuth-standard error envelope (`{"error":..., "error_description":...}`, RFC 6749 §5.2), NOT this API's usual `{"message":...}` shape — its client is generic OAuth tooling, not the Vectros SDK.
5919
6177
  *
5920
- * @param {Vectros.ErasureRequest} request
5921
- * @param {ComplianceClient.RequestOptions} requestOptions - Request-specific configuration.
6178
+ * @param {Vectros.TokenExchangeRequest} request
6179
+ * @param {AuthClient.RequestOptions} requestOptions - Request-specific configuration.
5922
6180
  *
6181
+ * @throws {@link Vectros.BadRequestError}
6182
+ * @throws {@link Vectros.UnauthorizedError}
5923
6183
  * @throws {@link Vectros.ForbiddenError}
6184
+ * @throws {@link Vectros.NotFoundError}
5924
6185
  * @throws {@link Vectros.TooManyRequestsError}
5925
- * @throws {@link Vectros.NotImplementedError}
5926
6186
  *
5927
6187
  * @example
5928
- * await client.compliance.createErasureRequest({
5929
- * subjectType: "user"
6188
+ * await client.auth.exchangeToken({
6189
+ * grant_type: "urn:ietf:params:oauth:grant-type:token-exchange",
6190
+ * subject_token: "subject_token",
6191
+ * subject_token_type: "urn:ietf:params:oauth:token-type:jwt"
5930
6192
  * })
5931
6193
  */
5932
- createErasureRequest(request, requestOptions) {
5933
- return HttpResponsePromise.fromPromise(this.__createErasureRequest(request, requestOptions));
6194
+ exchangeToken(request, requestOptions) {
6195
+ return HttpResponsePromise.fromPromise(this.__exchangeToken(request, requestOptions));
5934
6196
  }
5935
- async __createErasureRequest(request, requestOptions) {
5936
- const _authRequest = await this._options.authProvider.getAuthRequest();
5937
- const _headers = mergeHeaders(
5938
- _authRequest.headers,
5939
- this._options?.headers,
5940
- requestOptions?.headers
5941
- );
6197
+ async __exchangeToken(request, requestOptions) {
6198
+ const _headers = mergeHeaders(this._options?.headers, requestOptions?.headers);
5942
6199
  const _response = await fetcher({
5943
6200
  url: url_exports.join(
5944
6201
  await Supplier.get(this._options.baseUrl) ?? await Supplier.get(this._options.environment),
5945
- "v1/erasure-requests"
6202
+ "v1/auth/token/exchange"
5946
6203
  ),
5947
6204
  method: "POST",
5948
6205
  headers: _headers,
@@ -5961,12 +6218,16 @@ var ComplianceClient = class {
5961
6218
  }
5962
6219
  if (_response.error.reason === "status-code") {
5963
6220
  switch (_response.error.statusCode) {
6221
+ case 400:
6222
+ throw new BadRequestError(_response.error.body, _response.rawResponse);
6223
+ case 401:
6224
+ throw new UnauthorizedError(_response.error.body, _response.rawResponse);
5964
6225
  case 403:
5965
6226
  throw new ForbiddenError(_response.error.body, _response.rawResponse);
6227
+ case 404:
6228
+ throw new NotFoundError(_response.error.body, _response.rawResponse);
5966
6229
  case 429:
5967
6230
  throw new TooManyRequestsError(_response.error.body, _response.rawResponse);
5968
- case 501:
5969
- throw new NotImplementedError(_response.error.body, _response.rawResponse);
5970
6231
  default:
5971
6232
  throw new VectrosError({
5972
6233
  statusCode: _response.error.statusCode,
@@ -5975,10 +6236,78 @@ var ComplianceClient = class {
5975
6236
  });
5976
6237
  }
5977
6238
  }
5978
- return handleNonStatusCodeError(_response.error, _response.rawResponse, "POST", "/v1/erasure-requests");
6239
+ return handleNonStatusCodeError(_response.error, _response.rawResponse, "POST", "/v1/auth/token/exchange");
6240
+ }
6241
+ };
6242
+ var ComplianceClient = class {
6243
+ constructor(options) {
6244
+ this._options = normalizeClientOptionsWithAuth(options);
5979
6245
  }
5980
6246
  /**
5981
- * 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.
6247
+ * 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.
6248
+ *
6249
+ * @param {Vectros.ErasureRequest} request
6250
+ * @param {ComplianceClient.RequestOptions} requestOptions - Request-specific configuration.
6251
+ *
6252
+ * @throws {@link Vectros.ForbiddenError}
6253
+ * @throws {@link Vectros.TooManyRequestsError}
6254
+ * @throws {@link Vectros.NotImplementedError}
6255
+ *
6256
+ * @example
6257
+ * await client.compliance.createErasureRequest({
6258
+ * subjectType: "user"
6259
+ * })
6260
+ */
6261
+ createErasureRequest(request, requestOptions) {
6262
+ return HttpResponsePromise.fromPromise(this.__createErasureRequest(request, requestOptions));
6263
+ }
6264
+ async __createErasureRequest(request, requestOptions) {
6265
+ const _authRequest = await this._options.authProvider.getAuthRequest();
6266
+ const _headers = mergeHeaders(
6267
+ _authRequest.headers,
6268
+ this._options?.headers,
6269
+ requestOptions?.headers
6270
+ );
6271
+ const _response = await fetcher({
6272
+ url: url_exports.join(
6273
+ await Supplier.get(this._options.baseUrl) ?? await Supplier.get(this._options.environment),
6274
+ "v1/erasure-requests"
6275
+ ),
6276
+ method: "POST",
6277
+ headers: _headers,
6278
+ contentType: "application/json",
6279
+ queryString: url_exports.queryBuilder().mergeAdditional(requestOptions?.queryParams).build(),
6280
+ requestType: "json",
6281
+ body: request,
6282
+ timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1e3,
6283
+ maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries,
6284
+ abortSignal: requestOptions?.abortSignal,
6285
+ fetchFn: this._options?.fetch,
6286
+ logging: this._options.logging
6287
+ });
6288
+ if (_response.ok) {
6289
+ return { data: _response.body, rawResponse: _response.rawResponse };
6290
+ }
6291
+ if (_response.error.reason === "status-code") {
6292
+ switch (_response.error.statusCode) {
6293
+ case 403:
6294
+ throw new ForbiddenError(_response.error.body, _response.rawResponse);
6295
+ case 429:
6296
+ throw new TooManyRequestsError(_response.error.body, _response.rawResponse);
6297
+ case 501:
6298
+ throw new NotImplementedError(_response.error.body, _response.rawResponse);
6299
+ default:
6300
+ throw new VectrosError({
6301
+ statusCode: _response.error.statusCode,
6302
+ body: _response.error.body,
6303
+ rawResponse: _response.rawResponse
6304
+ });
6305
+ }
6306
+ }
6307
+ return handleNonStatusCodeError(_response.error, _response.rawResponse, "POST", "/v1/erasure-requests");
6308
+ }
6309
+ /**
6310
+ * 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.
5982
6311
  *
5983
6312
  * @param {Vectros.GetErasureRequestRequest} request
5984
6313
  * @param {ComplianceClient.RequestOptions} requestOptions - Request-specific configuration.
@@ -7386,6 +7715,7 @@ var IdentityClient = class {
7386
7715
  * @example
7387
7716
  * await client.identity.listEntities({
7388
7717
  * namespace: "team",
7718
+ * contextId: "myapp",
7389
7719
  * startFrom: "b3BhcXVlLWN1cnNvci1mcm9tLXRoZS1wcmV2aW91cy1wYWdl"
7390
7720
  * })
7391
7721
  */
@@ -7395,6 +7725,7 @@ var IdentityClient = class {
7395
7725
  async __listEntities(request, requestOptions) {
7396
7726
  const {
7397
7727
  namespace,
7728
+ contextId,
7398
7729
  userId,
7399
7730
  externalId,
7400
7731
  scope,
@@ -7409,6 +7740,7 @@ var IdentityClient = class {
7409
7740
  limit
7410
7741
  } = request;
7411
7742
  const _queryParams = {
7743
+ contextId,
7412
7744
  userId,
7413
7745
  externalId,
7414
7746
  scope,
@@ -7467,6 +7799,7 @@ var IdentityClient = class {
7467
7799
  * @example
7468
7800
  * await client.identity.createEntity({
7469
7801
  * namespace: "team",
7802
+ * contextId: "myapp",
7470
7803
  * body: {
7471
7804
  * externalId: "team_eng_platform"
7472
7805
  * }
@@ -7476,9 +7809,10 @@ var IdentityClient = class {
7476
7809
  return HttpResponsePromise.fromPromise(this.__createEntity(request, requestOptions));
7477
7810
  }
7478
7811
  async __createEntity(request, requestOptions) {
7479
- const { namespace, upsert, body: _body } = request;
7812
+ const { namespace, upsert, contextId, body: _body } = request;
7480
7813
  const _queryParams = {
7481
- upsert
7814
+ upsert,
7815
+ contextId
7482
7816
  };
7483
7817
  const _authRequest = await this._options.authProvider.getAuthRequest();
7484
7818
  const _headers = mergeHeaders(
@@ -7535,14 +7869,18 @@ var IdentityClient = class {
7535
7869
  * @example
7536
7870
  * await client.identity.getEntity({
7537
7871
  * namespace: "team",
7538
- * id: "6ba7b810-9dad-11d1-80b4-00c04fd430c8"
7872
+ * id: "6ba7b810-9dad-11d1-80b4-00c04fd430c8",
7873
+ * contextId: "myapp"
7539
7874
  * })
7540
7875
  */
7541
7876
  getEntity(request, requestOptions) {
7542
7877
  return HttpResponsePromise.fromPromise(this.__getEntity(request, requestOptions));
7543
7878
  }
7544
7879
  async __getEntity(request, requestOptions) {
7545
- const { namespace, id } = request;
7880
+ const { namespace, id, contextId } = request;
7881
+ const _queryParams = {
7882
+ contextId
7883
+ };
7546
7884
  const _authRequest = await this._options.authProvider.getAuthRequest();
7547
7885
  const _headers = mergeHeaders(
7548
7886
  _authRequest.headers,
@@ -7556,7 +7894,7 @@ var IdentityClient = class {
7556
7894
  ),
7557
7895
  method: "GET",
7558
7896
  headers: _headers,
7559
- queryString: url_exports.queryBuilder().mergeAdditional(requestOptions?.queryParams).build(),
7897
+ queryString: url_exports.queryBuilder().addMany(_queryParams).mergeAdditional(requestOptions?.queryParams).build(),
7560
7898
  timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1e3,
7561
7899
  maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries,
7562
7900
  abortSignal: requestOptions?.abortSignal,
@@ -7593,6 +7931,7 @@ var IdentityClient = class {
7593
7931
  * await client.identity.updateEntity({
7594
7932
  * namespace: "team",
7595
7933
  * id: "id",
7934
+ * contextId: "myapp",
7596
7935
  * body: {
7597
7936
  * externalId: "team_eng_platform"
7598
7937
  * }
@@ -7602,7 +7941,10 @@ var IdentityClient = class {
7602
7941
  return HttpResponsePromise.fromPromise(this.__updateEntity(request, requestOptions));
7603
7942
  }
7604
7943
  async __updateEntity(request, requestOptions) {
7605
- const { namespace, id, body: _body } = request;
7944
+ const { namespace, id, contextId, body: _body } = request;
7945
+ const _queryParams = {
7946
+ contextId
7947
+ };
7606
7948
  const _authRequest = await this._options.authProvider.getAuthRequest();
7607
7949
  const _headers = mergeHeaders(
7608
7950
  _authRequest.headers,
@@ -7617,7 +7959,7 @@ var IdentityClient = class {
7617
7959
  method: "PUT",
7618
7960
  headers: _headers,
7619
7961
  contentType: "application/json",
7620
- queryString: url_exports.queryBuilder().mergeAdditional(requestOptions?.queryParams).build(),
7962
+ queryString: url_exports.queryBuilder().addMany(_queryParams).mergeAdditional(requestOptions?.queryParams).build(),
7621
7963
  requestType: "json",
7622
7964
  body: _body,
7623
7965
  timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1e3,
@@ -7657,14 +7999,18 @@ var IdentityClient = class {
7657
7999
  * @example
7658
8000
  * await client.identity.deleteEntity({
7659
8001
  * namespace: "team",
7660
- * id: "id"
8002
+ * id: "id",
8003
+ * contextId: "myapp"
7661
8004
  * })
7662
8005
  */
7663
8006
  deleteEntity(request, requestOptions) {
7664
8007
  return HttpResponsePromise.fromPromise(this.__deleteEntity(request, requestOptions));
7665
8008
  }
7666
8009
  async __deleteEntity(request, requestOptions) {
7667
- const { namespace, id } = request;
8010
+ const { namespace, id, contextId } = request;
8011
+ const _queryParams = {
8012
+ contextId
8013
+ };
7668
8014
  const _authRequest = await this._options.authProvider.getAuthRequest();
7669
8015
  const _headers = mergeHeaders(
7670
8016
  _authRequest.headers,
@@ -7678,7 +8024,7 @@ var IdentityClient = class {
7678
8024
  ),
7679
8025
  method: "DELETE",
7680
8026
  headers: _headers,
7681
- queryString: url_exports.queryBuilder().mergeAdditional(requestOptions?.queryParams).build(),
8027
+ queryString: url_exports.queryBuilder().addMany(_queryParams).mergeAdditional(requestOptions?.queryParams).build(),
7682
8028
  timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1e3,
7683
8029
  maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries,
7684
8030
  abortSignal: requestOptions?.abortSignal,
@@ -7721,6 +8067,7 @@ var IdentityClient = class {
7721
8067
  * @example
7722
8068
  * await client.identity.lookupEntities({
7723
8069
  * namespace: "team",
8070
+ * contextId: "myapp",
7724
8071
  * body: {
7725
8072
  * type: "person_v1",
7726
8073
  * field: "ssn"
@@ -7731,7 +8078,10 @@ var IdentityClient = class {
7731
8078
  return HttpResponsePromise.fromPromise(this.__lookupEntities(request, requestOptions));
7732
8079
  }
7733
8080
  async __lookupEntities(request, requestOptions) {
7734
- const { namespace, body: _body } = request;
8081
+ const { namespace, contextId, body: _body } = request;
8082
+ const _queryParams = {
8083
+ contextId
8084
+ };
7735
8085
  const _authRequest = await this._options.authProvider.getAuthRequest();
7736
8086
  const _headers = mergeHeaders(
7737
8087
  _authRequest.headers,
@@ -7746,7 +8096,7 @@ var IdentityClient = class {
7746
8096
  method: "POST",
7747
8097
  headers: _headers,
7748
8098
  contentType: "application/json",
7749
- queryString: url_exports.queryBuilder().mergeAdditional(requestOptions?.queryParams).build(),
8099
+ queryString: url_exports.queryBuilder().addMany(_queryParams).mergeAdditional(requestOptions?.queryParams).build(),
7750
8100
  requestType: "json",
7751
8101
  body: _body,
7752
8102
  timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1e3,
@@ -7790,15 +8140,17 @@ var IdentityClient = class {
7790
8140
  * @example
7791
8141
  * await client.identity.getEntityVersions({
7792
8142
  * namespace: "team",
7793
- * id: "id"
8143
+ * id: "id",
8144
+ * contextId: "myapp"
7794
8145
  * })
7795
8146
  */
7796
8147
  getEntityVersions(request, requestOptions) {
7797
8148
  return HttpResponsePromise.fromPromise(this.__getEntityVersions(request, requestOptions));
7798
8149
  }
7799
8150
  async __getEntityVersions(request, requestOptions) {
7800
- const { namespace, id, startFrom } = request;
8151
+ const { namespace, id, contextId, startFrom } = request;
7801
8152
  const _queryParams = {
8153
+ contextId,
7802
8154
  startFrom
7803
8155
  };
7804
8156
  const _authRequest = await this._options.authProvider.getAuthRequest();
@@ -7860,7 +8212,10 @@ var IdentityClient = class {
7860
8212
  return HttpResponsePromise.fromPromise(this.__getNamespace(request, requestOptions));
7861
8213
  }
7862
8214
  async __getNamespace(request, requestOptions) {
7863
- const { namespace } = request;
8215
+ const { namespace, contextId } = request;
8216
+ const _queryParams = {
8217
+ contextId
8218
+ };
7864
8219
  const _authRequest = await this._options.authProvider.getAuthRequest();
7865
8220
  const _headers = mergeHeaders(
7866
8221
  _authRequest.headers,
@@ -7874,7 +8229,7 @@ var IdentityClient = class {
7874
8229
  ),
7875
8230
  method: "GET",
7876
8231
  headers: _headers,
7877
- queryString: url_exports.queryBuilder().mergeAdditional(requestOptions?.queryParams).build(),
8232
+ queryString: url_exports.queryBuilder().addMany(_queryParams).mergeAdditional(requestOptions?.queryParams).build(),
7878
8233
  timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1e3,
7879
8234
  maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries,
7880
8235
  abortSignal: requestOptions?.abortSignal,
@@ -7899,7 +8254,7 @@ var IdentityClient = class {
7899
8254
  return handleNonStatusCodeError(_response.error, _response.rawResponse, "GET", "/v1/namespaces/{namespace}");
7900
8255
  }
7901
8256
  /**
7902
- * Updates the mutable fields (`entityBacked`, `defaultSchemaId`, `specificityRank`) of a registered namespace. The namespace name itself is immutable. Requires a root API key. The reserved built-ins cannot be updated.
8257
+ * Updates the mutable fields (`entityBacked`, `defaultSchemaId`, `specificityRank`) of a registered namespace. The namespace name and its `contextId` (which row is selected) are both immutable. Requires a root API key. `org` and `client` are updatable like any other namespace — there is no reserved-built-in exception.
7903
8258
  *
7904
8259
  * @param {Vectros.UpdateNamespaceRequest} request
7905
8260
  * @param {IdentityClient.RequestOptions} requestOptions - Request-specific configuration.
@@ -7921,7 +8276,10 @@ var IdentityClient = class {
7921
8276
  return HttpResponsePromise.fromPromise(this.__updateNamespace(request, requestOptions));
7922
8277
  }
7923
8278
  async __updateNamespace(request, requestOptions) {
7924
- const { namespace, body: _body } = request;
8279
+ const { namespace, contextId, body: _body } = request;
8280
+ const _queryParams = {
8281
+ contextId
8282
+ };
7925
8283
  const _authRequest = await this._options.authProvider.getAuthRequest();
7926
8284
  const _headers = mergeHeaders(
7927
8285
  _authRequest.headers,
@@ -7936,7 +8294,7 @@ var IdentityClient = class {
7936
8294
  method: "PUT",
7937
8295
  headers: _headers,
7938
8296
  contentType: "application/json",
7939
- queryString: url_exports.queryBuilder().mergeAdditional(requestOptions?.queryParams).build(),
8297
+ queryString: url_exports.queryBuilder().addMany(_queryParams).mergeAdditional(requestOptions?.queryParams).build(),
7940
8298
  requestType: "json",
7941
8299
  body: _body,
7942
8300
  timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1e3,
@@ -7967,7 +8325,7 @@ var IdentityClient = class {
7967
8325
  return handleNonStatusCodeError(_response.error, _response.rawResponse, "PUT", "/v1/namespaces/{namespace}");
7968
8326
  }
7969
8327
  /**
7970
- * Deletes a registered scope namespace. Requires a root API key. The reserved built-ins cannot be deleted. A namespace that still has entities cannot be deleted (409) — delete its entities first; this keeps them reachable by the account-teardown and erasure sweeps.
8328
+ * Deletes a registered scope namespace. Requires a root API key. `org` and `client` are deletable like any other namespace — there is no reserved-built-in exception. A namespace that still has entities cannot be deleted (409) — delete its entities first; this keeps them reachable by the account-teardown and erasure sweeps.
7971
8329
  *
7972
8330
  * @param {Vectros.DeleteNamespaceRequest} request
7973
8331
  * @param {IdentityClient.RequestOptions} requestOptions - Request-specific configuration.
@@ -7985,7 +8343,10 @@ var IdentityClient = class {
7985
8343
  return HttpResponsePromise.fromPromise(this.__deleteNamespace(request, requestOptions));
7986
8344
  }
7987
8345
  async __deleteNamespace(request, requestOptions) {
7988
- const { namespace } = request;
8346
+ const { namespace, contextId } = request;
8347
+ const _queryParams = {
8348
+ contextId
8349
+ };
7989
8350
  const _authRequest = await this._options.authProvider.getAuthRequest();
7990
8351
  const _headers = mergeHeaders(
7991
8352
  _authRequest.headers,
@@ -7999,7 +8360,7 @@ var IdentityClient = class {
7999
8360
  ),
8000
8361
  method: "DELETE",
8001
8362
  headers: _headers,
8002
- queryString: url_exports.queryBuilder().mergeAdditional(requestOptions?.queryParams).build(),
8363
+ queryString: url_exports.queryBuilder().addMany(_queryParams).mergeAdditional(requestOptions?.queryParams).build(),
8003
8364
  timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1e3,
8004
8365
  maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries,
8005
8366
  abortSignal: requestOptions?.abortSignal,
@@ -8028,7 +8389,7 @@ var IdentityClient = class {
8028
8389
  return handleNonStatusCodeError(_response.error, _response.rawResponse, "DELETE", "/v1/namespaces/{namespace}");
8029
8390
  }
8030
8391
  /**
8031
- * Returns the scope namespaces registered in your account, with the reserved built-ins `org` and `client` listed first. Returns a `{data, nextCursor}` envelope.
8392
+ * Returns the scope namespaces in your account, including `org` and `client`, which are provisioned for you when the account is created. Returns a `{data, nextCursor}` envelope.
8032
8393
  *
8033
8394
  * @param {Vectros.ListNamespacesRequest} request
8034
8395
  * @param {IdentityClient.RequestOptions} requestOptions - Request-specific configuration.
@@ -8040,10 +8401,11 @@ var IdentityClient = class {
8040
8401
  return HttpResponsePromise.fromPromise(this.__listNamespaces(request, requestOptions));
8041
8402
  }
8042
8403
  async __listNamespaces(request = {}, requestOptions) {
8043
- const { startFrom, limit } = request;
8404
+ const { startFrom, limit, contextId } = request;
8044
8405
  const _queryParams = {
8045
8406
  startFrom,
8046
- limit
8407
+ limit,
8408
+ contextId
8047
8409
  };
8048
8410
  const _authRequest = await this._options.authProvider.getAuthRequest();
8049
8411
  const _headers = mergeHeaders(
@@ -8078,9 +8440,9 @@ var IdentityClient = class {
8078
8440
  return handleNonStatusCodeError(_response.error, _response.rawResponse, "GET", "/v1/namespaces");
8079
8441
  }
8080
8442
  /**
8081
- * Registers a new scope namespace and declares whether its values resolve to identity entities (`entityBacked`). Also requires `specificityRank`, an explicit, account-unique position in the specificity order used to break recordType schema-resolution ties. Requires a root API key. The reserved names `org` and `client` are built in and cannot be registered.
8443
+ * Registers a new scope namespace and declares whether its values resolve to identity entities (`entityBacked`). Also requires `specificityRank`, an explicit, account-unique position in the specificity order used to break recordType schema-resolution ties. Requires a root API key or the CLI bootstrap's provisioning capability — never an ordinary partner-grantable scope. `org` and `client` are provisioned when your account is created, so registering either returns the already-registered error.
8082
8444
  *
8083
- * @param {Vectros.NamespaceRequest} request
8445
+ * @param {Vectros.RegisterNamespaceRequest} request
8084
8446
  * @param {IdentityClient.RequestOptions} requestOptions - Request-specific configuration.
8085
8447
  *
8086
8448
  * @throws {@link Vectros.BadRequestError}
@@ -8089,14 +8451,20 @@ var IdentityClient = class {
8089
8451
  *
8090
8452
  * @example
8091
8453
  * await client.identity.registerNamespace({
8092
- * namespace: "team",
8093
- * specificityRank: 1500
8454
+ * body: {
8455
+ * namespace: "team",
8456
+ * specificityRank: 1500
8457
+ * }
8094
8458
  * })
8095
8459
  */
8096
8460
  registerNamespace(request, requestOptions) {
8097
8461
  return HttpResponsePromise.fromPromise(this.__registerNamespace(request, requestOptions));
8098
8462
  }
8099
8463
  async __registerNamespace(request, requestOptions) {
8464
+ const { contextId, body: _body } = request;
8465
+ const _queryParams = {
8466
+ contextId
8467
+ };
8100
8468
  const _authRequest = await this._options.authProvider.getAuthRequest();
8101
8469
  const _headers = mergeHeaders(
8102
8470
  _authRequest.headers,
@@ -8111,9 +8479,9 @@ var IdentityClient = class {
8111
8479
  method: "POST",
8112
8480
  headers: _headers,
8113
8481
  contentType: "application/json",
8114
- queryString: url_exports.queryBuilder().mergeAdditional(requestOptions?.queryParams).build(),
8482
+ queryString: url_exports.queryBuilder().addMany(_queryParams).mergeAdditional(requestOptions?.queryParams).build(),
8115
8483
  requestType: "json",
8116
- body: request,
8484
+ body: _body,
8117
8485
  timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1e3,
8118
8486
  maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries,
8119
8487
  abortSignal: requestOptions?.abortSignal,
@@ -8398,12 +8766,13 @@ var IdentityClient = class {
8398
8766
  return handleNonStatusCodeError(_response.error, _response.rawResponse, "PUT", "/v1/users/{id}");
8399
8767
  }
8400
8768
  /**
8401
- * Permanently deletes a user identity. This cannot be undone. If the user is a pending invitation, the associated access profile created for that invitation is also removed. Requires the `users:d` scope.
8769
+ * Permanently deletes a user identity. This cannot be undone. If the user is a pending invitation, the associated access profile created for that invitation is also removed. Requires the `users:d` scope. Deleting your account's last OWNER is refused (409) — an account must always retain at least one owner.
8402
8770
  *
8403
8771
  * @param {Vectros.DeleteUserRequest} request
8404
8772
  * @param {IdentityClient.RequestOptions} requestOptions - Request-specific configuration.
8405
8773
  *
8406
8774
  * @throws {@link Vectros.NotFoundError}
8775
+ * @throws {@link Vectros.ConflictError}
8407
8776
  * @throws {@link Vectros.TooManyRequestsError}
8408
8777
  *
8409
8778
  * @example
@@ -8443,6 +8812,8 @@ var IdentityClient = class {
8443
8812
  switch (_response.error.statusCode) {
8444
8813
  case 404:
8445
8814
  throw new NotFoundError(_response.error.body, _response.rawResponse);
8815
+ case 409:
8816
+ throw new ConflictError(_response.error.body, _response.rawResponse);
8446
8817
  case 429:
8447
8818
  throw new TooManyRequestsError(_response.error.body, _response.rawResponse);
8448
8819
  default:
@@ -8516,6 +8887,69 @@ var IdentityClient = class {
8516
8887
  }
8517
8888
  return handleNonStatusCodeError(_response.error, _response.rawResponse, "POST", "/v1/users/lookup");
8518
8889
  }
8890
+ /**
8891
+ * 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.
8892
+ *
8893
+ * @param {Vectros.UserExistsByEmailRequest} request
8894
+ * @param {IdentityClient.RequestOptions} requestOptions - Request-specific configuration.
8895
+ *
8896
+ * @throws {@link Vectros.BadRequestError}
8897
+ * @throws {@link Vectros.ForbiddenError}
8898
+ *
8899
+ * @example
8900
+ * await client.identity.userExistsByEmail({
8901
+ * email: "user@example.com",
8902
+ * contextId: "myapp"
8903
+ * })
8904
+ */
8905
+ userExistsByEmail(request = {}, requestOptions) {
8906
+ return HttpResponsePromise.fromPromise(this.__userExistsByEmail(request, requestOptions));
8907
+ }
8908
+ async __userExistsByEmail(request = {}, requestOptions) {
8909
+ const { email, contextId } = request;
8910
+ const _queryParams = {
8911
+ email,
8912
+ contextId
8913
+ };
8914
+ const _authRequest = await this._options.authProvider.getAuthRequest();
8915
+ const _headers = mergeHeaders(
8916
+ _authRequest.headers,
8917
+ this._options?.headers,
8918
+ requestOptions?.headers
8919
+ );
8920
+ const _response = await fetcher({
8921
+ url: url_exports.join(
8922
+ await Supplier.get(this._options.baseUrl) ?? await Supplier.get(this._options.environment),
8923
+ "v1/users/exists-by-email"
8924
+ ),
8925
+ method: "GET",
8926
+ headers: _headers,
8927
+ queryString: url_exports.queryBuilder().addMany(_queryParams).mergeAdditional(requestOptions?.queryParams).build(),
8928
+ timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1e3,
8929
+ maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries,
8930
+ abortSignal: requestOptions?.abortSignal,
8931
+ fetchFn: this._options?.fetch,
8932
+ logging: this._options.logging
8933
+ });
8934
+ if (_response.ok) {
8935
+ return { data: _response.body, rawResponse: _response.rawResponse };
8936
+ }
8937
+ if (_response.error.reason === "status-code") {
8938
+ switch (_response.error.statusCode) {
8939
+ case 400:
8940
+ throw new BadRequestError(_response.error.body, _response.rawResponse);
8941
+ case 403:
8942
+ throw new ForbiddenError(_response.error.body, _response.rawResponse);
8943
+ default:
8944
+ throw new VectrosError({
8945
+ statusCode: _response.error.statusCode,
8946
+ body: _response.error.body,
8947
+ rawResponse: _response.rawResponse
8948
+ });
8949
+ }
8950
+ }
8951
+ return handleNonStatusCodeError(_response.error, _response.rawResponse, "GET", "/v1/users/exists-by-email");
8952
+ }
8519
8953
  /**
8520
8954
  * 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.
8521
8955
  *
@@ -10233,10 +10667,11 @@ var logging;
10233
10667
  logging2.ConsoleLogger = ConsoleLogger;
10234
10668
  })(logging || (logging = {}));
10235
10669
 
10236
- // node_modules/@vectros-ai/blueprints/dist/index.mjs
10670
+ // ../blueprints/dist/index.mjs
10237
10671
  var import_zod = require("zod");
10238
10672
  var import_zod2 = require("zod");
10239
10673
  var CONTEXT_ID_RE = /^[a-z][a-z0-9-]{2,30}$/;
10674
+ var MAX_SCOPE_DIMENSIONS = 2;
10240
10675
  var ValidationRulesSchema = import_zod.z.object({
10241
10676
  required: import_zod.z.boolean().optional(),
10242
10677
  minLength: import_zod.z.number().int().optional(),
@@ -10431,11 +10866,14 @@ var BlueprintSchemaSchema = import_zod.z.object({
10431
10866
  // Schema-level ownership defaults, matching `SchemaRequest`: the principal
10432
10867
  // `userId` plus `scopes` — namespaced parent edges as `<namespace>:<value>`
10433
10868
  // (`org:...`, `client:...`, or a namespace you registered), at most two
10434
- // namespaces. The flat `orgId`/`clientId` fields fold into `scopes`.
10869
+ // namespaces. The flat `orgId`/`clientId` fields fold into `scopes`. Each
10870
+ // entry's namespace + value are validated below
10871
+ // (`lintNamespacedScopeArrays`) — this `.max(MAX_SCOPE_DIMENSIONS)` is
10872
+ // only the shape-level cap, not the grammar check.
10435
10873
  // With a scoped token these must be consistent with the profile's dataScope
10436
10874
  // (a cross-consistency lint is deferred to the lint slice).
10437
10875
  userId: import_zod.z.string().min(1).optional(),
10438
- scopes: import_zod.z.array(import_zod.z.string().min(1)).max(2).optional(),
10876
+ scopes: import_zod.z.array(import_zod.z.string().min(1)).max(MAX_SCOPE_DIMENSIONS).optional(),
10439
10877
  // The id of an existing schema this one is a CUSTOMIZATION of, when a schema
10440
10878
  // named `typeName` already exists in this context — required in that case,
10441
10879
  // and must be omitted when this create is the first schema under that name
@@ -10458,6 +10896,35 @@ var BlueprintSchemaSchema = import_zod.z.object({
10458
10896
  });
10459
10897
  }
10460
10898
  });
10899
+ var CAPABILITY_NAME_RE = /^[a-z][a-z0-9-]*$/;
10900
+ var GrantedCapabilitiesSchema = import_zod.z.array(import_zod.z.string().min(1)).optional().superRefine((caps, ctx) => {
10901
+ if (!caps) return;
10902
+ const seen = /* @__PURE__ */ new Set();
10903
+ caps.forEach((name, i) => {
10904
+ if (name === "") return;
10905
+ if (name === "*") {
10906
+ ctx.addIssue({
10907
+ code: import_zod.z.ZodIssueCode.custom,
10908
+ path: [i],
10909
+ message: "'*' is not a capability \u2014 granted_capabilities has no wildcard, and a '*' in allowedActions confers zero capabilities. Name each capability explicitly."
10910
+ });
10911
+ } else if (!CAPABILITY_NAME_RE.test(name)) {
10912
+ ctx.addIssue({
10913
+ code: import_zod.z.ZodIssueCode.custom,
10914
+ path: [i],
10915
+ message: `'${name}' is not a valid capability name (expected lowercase letters, digits and hyphens, starting with a letter, and no ':' \u2014 a capability is not an action)`
10916
+ });
10917
+ }
10918
+ if (seen.has(name)) {
10919
+ ctx.addIssue({
10920
+ code: import_zod.z.ZodIssueCode.custom,
10921
+ path: [i],
10922
+ message: `'${name}' is declared more than once`
10923
+ });
10924
+ }
10925
+ seen.add(name);
10926
+ });
10927
+ });
10461
10928
  var BlueprintAccessProfileSchema = import_zod.z.object({
10462
10929
  // Validated structurally here; the SCOPE GATE (in @vectros-ai/cli) is
10463
10930
  // what enforces the data-plane-only security boundary.
@@ -10473,7 +10940,9 @@ var BlueprintAccessProfileSchema = import_zod.z.object({
10473
10940
  // ...) — the flat `orgId`/`clientId` keys are gone. e.g.
10474
10941
  // `{ "scope:org": ["org_x", null] }` = "org_x's records AND tenant-shared
10475
10942
  // records". Omitting null restricts to the listed owners ONLY (the key will
10476
- // NOT see tenant-level/seed records).
10943
+ // NOT see tenant-level/seed records). Every KEY is validated below
10944
+ // (`lintDataScopeKeys`) — `userId` or a grammar-valid `scope:<ns>`, no
10945
+ // other shape.
10477
10946
  dataScope: import_zod.z.record(import_zod.z.array(import_zod.z.union([import_zod.z.string().min(1), import_zod.z.null()]))).optional(),
10478
10947
  // Optional identity overrides for the service principal's profile — scope
10479
10948
  // values its key STAMPS onto everything it writes (a credential can only
@@ -10482,11 +10951,36 @@ var BlueprintAccessProfileSchema = import_zod.z.object({
10482
10951
  // `orgId`/`clientId` keys are gone); values may be `${{ identities.* }}`
10483
10952
  // tokens (substituted at apply time). e.g. `{ "scope:org": '${{ identities.team }}' }`
10484
10953
  // makes every record the service key writes org/team-owned.
10485
- identityOverrides: import_zod.z.record(import_zod.z.string().min(1)).optional()
10954
+ //
10955
+ // Every KEY, the ≤2-dimension cap, and every LITERAL value are validated
10956
+ // below (`lintIdentityOverrides`, placeholder-aware — see that function's
10957
+ // docstring); this `.min(1)` is only the shape-level "non-blank" guard on
10958
+ // the value, not the grammar check.
10959
+ identityOverrides: import_zod.z.record(import_zod.z.string().min(1)).optional(),
10960
+ // Optional capabilities granted to the blueprint's own service-principal
10961
+ // profile. DECISION: allowed here, not role-clause-only. On the platform,
10962
+ // an access profile's scopes are themselves a list of clauses — the exact
10963
+ // same wire shape a role clause carries — and the loader already builds
10964
+ // this profile as ONE such clause. Refusing `capabilities` here while
10965
+ // allowing it on a role clause would be an asymmetry this FORMAT package
10966
+ // has no basis to invent: nothing on the platform side distinguishes the
10967
+ // two, and whether granting a capability to a blueprint's own service key
10968
+ // is sound POLICY is exactly the question the CLI's scope gate exists to
10969
+ // answer (a separate package, out of scope for this format layer) — the
10970
+ // same posture this package already takes with `allowedActions` above.
10971
+ //
10972
+ // Worth naming for whoever writes that gate: this profile is a long-lived,
10973
+ // non-human-attributable machine credential (typically stored in CI/CD
10974
+ // secrets), not an ephemeral human session — a materially different
10975
+ // blast-radius question from granting the same capability to a role that
10976
+ // a person is bound to. The gate should weigh the two separately rather
10977
+ // than reuse one policy for both.
10978
+ capabilities: GrantedCapabilitiesSchema
10486
10979
  }).strict();
10487
10980
  var BlueprintRoleClauseSchema = import_zod.z.object({
10488
10981
  allowedActions: import_zod.z.array(import_zod.z.string().min(1)).min(1),
10489
- dataScope: import_zod.z.record(import_zod.z.array(import_zod.z.union([import_zod.z.string().min(1), import_zod.z.null()]))).optional()
10982
+ dataScope: import_zod.z.record(import_zod.z.array(import_zod.z.union([import_zod.z.string().min(1), import_zod.z.null()]))).optional(),
10983
+ capabilities: GrantedCapabilitiesSchema
10490
10984
  }).strict();
10491
10985
  var BlueprintRolesSchema = import_zod.z.record(import_zod.z.array(BlueprintRoleClauseSchema).min(1));
10492
10986
  var IDENTITY_NAMESPACE_RE = /^[a-z][a-z0-9_-]{1,31}$/;
@@ -10511,57 +11005,260 @@ var IdentityDeclSchema = import_zod.z.object({
10511
11005
  displayName: import_zod.z.string().min(1).optional(),
10512
11006
  metadata: import_zod.z.record(import_zod.z.unknown()).optional()
10513
11007
  }).strict();
10514
- var IdentitiesDeclSchema = import_zod.z.record(IdentityDeclSchema);
11008
+ var IDENTITY_NAME_RE = /^[A-Za-z_]\w*$/;
11009
+ var IdentitiesDeclSchema = import_zod.z.record(
11010
+ import_zod.z.string().regex(IDENTITY_NAME_RE, {
11011
+ 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"
11012
+ }),
11013
+ IdentityDeclSchema
11014
+ );
10515
11015
  var BlueprintServicePrincipalSchema = import_zod.z.object({
10516
11016
  externalId: import_zod.z.string().min(1),
10517
11017
  displayName: import_zod.z.string().min(1)
10518
11018
  }).strict();
10519
- var SeedCommonShape = {
10520
- /** The schema typeName this seed instantiates (should match a declared schema). */
10521
- typeName: import_zod.z.string().min(1),
11019
+ var ISSUER_ID_RE = CONTEXT_ID_RE;
11020
+ var BlueprintIssuerSchema = import_zod.z.object({
11021
+ /** Short slug identifying this issuer within your account. Immutable once registered. */
11022
+ issuerId: import_zod.z.string().regex(ISSUER_ID_RE, {
11023
+ message: "issuerId must be 3-31 chars, start with a lowercase letter, then lowercase letters/digits/dashes (e.g. 'auth0-prod')"
11024
+ }),
11025
+ /** The IdP's `iss` claim value, exactly as it appears in tokens it issues. */
11026
+ issuer: import_zod.z.string().min(1),
11027
+ /** The IdP's remote JWKS endpoint, used to verify presented tokens' signatures. */
11028
+ jwksUri: import_zod.z.string().min(1),
10522
11029
  /**
10523
- * Stable, caller-supplied id — the loader's idempotency key AND the value other
10524
- * seeds resolve a `reference` against (across surfaces: a record seed may
10525
- * reference a document seed by its externalId, and vice versa).
11030
+ * The `aud` claim value a presented subject_token must carry. Must be globally
11031
+ * unique in combination with `issuer` — use a distinct audience per
11032
+ * environment/context sharing one IdP account (most OIDC providers support
11033
+ * this as an ordinary per-API/application default).
10526
11034
  */
10527
- externalId: import_zod.z.string().min(1),
11035
+ audience: import_zod.z.string().min(1),
10528
11036
  /**
10529
- * Optional scope ownership for the seeded item, as `namespace:value` entries
10530
- * (≤2, e.g. `['org:${{ identities.team }}']`; tokens substitute at apply
10531
- * time). `[]` = a private, user-owned item. Omitted = the seeding
10532
- * credential's full identity (the default).
11037
+ * Which app context an exchanged token targets. **Must equal the blueprint's own
11038
+ * `contextId`** — an issuer is a trust anchor, so a blueprint may only attach one to the
11039
+ * context it actually provisions. One IdP account serving several contexts needs one entry
11040
+ * per context, each in that context's own blueprint.
10533
11041
  */
10534
- scopes: import_zod.z.array(import_zod.z.string()).optional()
10535
- };
10536
- var RecordSeedSchema = import_zod.z.object({
10537
- surface: import_zod.z.literal("record"),
10538
- ...SeedCommonShape,
10539
- /** The record payload, validated against the bound schema. */
10540
- fields: import_zod.z.record(import_zod.z.unknown())
11042
+ contextId: import_zod.z.string().regex(CONTEXT_ID_RE, {
11043
+ message: "contextId must be 3-31 chars, start with a lowercase letter, then lowercase letters/digits/dashes (e.g. 'casework')"
11044
+ }),
11045
+ /** The claim carrying the subject identifier. Platform defaults to 'sub' when omitted. */
11046
+ subClaim: import_zod.z.string().min(1).optional(),
11047
+ /** The claim carrying the subject's email (first-login invite matching). Platform defaults to 'email' when omitted. */
11048
+ emailClaim: import_zod.z.string().min(1).optional(),
11049
+ /**
11050
+ * Opt-in self-service signup: a list of {signupType, roleId} pairs. When a first-time exchange
11051
+ * caller presents no invite token but names a signupType matching one of these (or omits it and
11052
+ * exactly one entry exists), a brand-new user is created and bound to that entry's role, no
11053
+ * invite required. Every entry must be something you're willing to grant to ANY caller who can
11054
+ * present a token from this issuer — a role carrying elevated (provisioning or wildcard) scope
11055
+ * is rejected. Omit entirely to leave self-signup disabled (the default).
11056
+ */
11057
+ selfSignupPolicies: import_zod.z.array(
11058
+ import_zod.z.object({
11059
+ signupType: import_zod.z.string().regex(ISSUER_ID_RE, {
11060
+ message: "signupType must be 3-31 chars, start with a lowercase letter, then lowercase letters/digits/dashes (e.g. 'practitioner')"
11061
+ }),
11062
+ roleId: import_zod.z.string().regex(ISSUER_ID_RE, {
11063
+ message: "roleId must be 3-31 chars, start with a lowercase letter, then lowercase letters/digits/dashes (e.g. 'practitioner-member')"
11064
+ })
11065
+ }).strict()
11066
+ ).optional()
10541
11067
  }).strict();
10542
- var DocumentSeedSchema = import_zod.z.object({
10543
- surface: import_zod.z.literal("document"),
10544
- ...SeedCommonShape,
10545
- /** Human-readable document title — REQUIRED by the text-ingest path. */
10546
- title: import_zod.z.string().min(1),
10547
- /** Raw text content to ingest + index — REQUIRED and non-empty (the platform rejects a blank ingest). */
10548
- text: import_zod.z.string().min(1),
10549
- /** Optional structured payload bound to the schema (the document's `fields`). */
10550
- fields: import_zod.z.record(import_zod.z.unknown()).optional()
11068
+ var BlueprintNamespaceSchema = import_zod.z.object({
11069
+ /** The namespace to register — same grammar as a `scope:<namespace>` key. */
11070
+ namespace: import_zod.z.string().min(1),
11071
+ /**
11072
+ * When `true`, every value in this namespace must resolve to an existing
11073
+ * identity entity (create via `entities:c` at the in-context phase, or a
11074
+ * declared `identities:` entry). When `false`/omitted, values are
11075
+ * free-form strings validated by grammar only.
11076
+ */
11077
+ entityBacked: import_zod.z.boolean().optional(),
11078
+ /**
11079
+ * This namespace's position in your account's specificity order (used to
11080
+ * break a tie when a caller holds two scope dimensions at once). Required
11081
+ * by the platform on registration; MUST be unique across every namespace
11082
+ * in the account, including `org` (1000) and `client` (2000) — pick a
11083
+ * value outside that pair, and outside any other namespace this same
11084
+ * account might register (its own, or another blueprint's).
11085
+ */
11086
+ specificityRank: import_zod.z.number().int().min(0).max(1e6),
11087
+ /**
11088
+ * Optional membership backing — declares which record type + field hold
11089
+ * grants of THIS namespace's values. Supply together with
11090
+ * `membershipTargetField`, or omit both. Declaring this grants nobody
11091
+ * anything on its own: a role opts in explicitly with
11092
+ * `${{ member.scope.<namespace> }}` in its `dataScope`.
11093
+ */
11094
+ membershipRecordType: import_zod.z.string().min(1).optional(),
11095
+ /** The field on `membershipRecordType` naming the user a grant is FOR. Required alongside `membershipRecordType`. */
11096
+ membershipTargetField: import_zod.z.string().min(1).optional(),
11097
+ /**
11098
+ * The field on `membershipRecordType` naming a grant's LEVEL, so the same
11099
+ * user can hold different levels in different values of this namespace.
11100
+ * Supply together with `membershipLevels`, or omit both for plain
11101
+ * in-or-out membership.
11102
+ */
11103
+ membershipLevelField: import_zod.z.string().min(1).optional(),
11104
+ /** The complete set of level labels this namespace allows. Required alongside `membershipLevelField`. */
11105
+ membershipLevels: import_zod.z.array(import_zod.z.string().min(1)).optional()
10551
11106
  }).strict();
10552
- var BlueprintSeedRecordSchema = import_zod.z.discriminatedUnion("surface", [
10553
- RecordSeedSchema,
10554
- DocumentSeedSchema
10555
- ]);
10556
- var SELF_TOKEN_RE = /\$\{\{\s*self\.[A-Za-z_]\w*\s*\}\}/;
10557
- function lintSelfTokenPlacement(value, ctx) {
10558
- const walk = (node, path4, inRoleDataScope) => {
10559
- if (typeof node === "string") {
10560
- if (!inRoleDataScope && SELF_TOKEN_RE.test(node)) {
11107
+ var MEMBERSHIP_FIELD_NAME_RE = /^[A-Za-z0-9_-]{1,64}$/;
11108
+ function lintNamespaces(value, ctx) {
11109
+ if (!value || typeof value !== "object" || !("namespaces" in value)) return;
11110
+ const namespaces = value.namespaces;
11111
+ if (!Array.isArray(namespaces)) return;
11112
+ const rawSchemas = value.schemas;
11113
+ const declaredSchemaTypes = new Set(
11114
+ (Array.isArray(rawSchemas) ? rawSchemas : []).map((s) => s && typeof s === "object" && "typeName" in s ? s.typeName : void 0).filter((t) => typeof t === "string")
11115
+ );
11116
+ const seenNames = /* @__PURE__ */ new Set();
11117
+ const seenRanks = /* @__PURE__ */ new Map();
11118
+ namespaces.forEach((entry, i) => {
11119
+ if (!entry || typeof entry !== "object") return;
11120
+ const ns = entry;
11121
+ const path4 = ["namespaces", i];
11122
+ if (typeof ns.namespace === "string") {
11123
+ const err = scopeNamespaceGrammarError(ns.namespace);
11124
+ if (err) ctx.addIssue({ code: import_zod.z.ZodIssueCode.custom, path: [...path4, "namespace"], message: err });
11125
+ if (seenNames.has(ns.namespace)) {
11126
+ ctx.addIssue({
11127
+ code: import_zod.z.ZodIssueCode.custom,
11128
+ path: [...path4, "namespace"],
11129
+ message: `'${ns.namespace}' is declared more than once in namespaces[]`
11130
+ });
11131
+ }
11132
+ seenNames.add(ns.namespace);
11133
+ }
11134
+ if (typeof ns.specificityRank === "number") {
11135
+ const prior = seenRanks.get(ns.specificityRank);
11136
+ if (prior !== void 0) {
11137
+ ctx.addIssue({
11138
+ code: import_zod.z.ZodIssueCode.custom,
11139
+ path: [...path4, "specificityRank"],
11140
+ message: `specificityRank ${ns.specificityRank} is already used by namespaces[${prior}] \u2014 every namespace's rank must be unique`
11141
+ });
11142
+ }
11143
+ seenRanks.set(ns.specificityRank, i);
11144
+ }
11145
+ const hasRecordType = ns.membershipRecordType !== void 0;
11146
+ const hasTargetField = ns.membershipTargetField !== void 0;
11147
+ if (hasRecordType !== hasTargetField) {
11148
+ ctx.addIssue({
11149
+ code: import_zod.z.ZodIssueCode.custom,
11150
+ path: [...path4, "membershipRecordType"],
11151
+ message: "membershipRecordType and membershipTargetField must be declared together, or neither"
11152
+ });
11153
+ }
11154
+ if (typeof ns.membershipRecordType === "string" && !declaredSchemaTypes.has(ns.membershipRecordType)) {
11155
+ ctx.addIssue({
11156
+ code: import_zod.z.ZodIssueCode.custom,
11157
+ path: [...path4, "membershipRecordType"],
11158
+ message: `membershipRecordType '${ns.membershipRecordType}' must name a typeName this blueprint declares in schemas[] \u2014 a namespace's membership can only resolve over a record type the blueprint itself ships, never a type that merely happens to already exist in the target context.`
11159
+ });
11160
+ }
11161
+ for (const [field, value2] of [
11162
+ ["membershipRecordType", ns.membershipRecordType],
11163
+ ["membershipTargetField", ns.membershipTargetField],
11164
+ ["membershipLevelField", ns.membershipLevelField]
11165
+ ]) {
11166
+ if (typeof value2 === "string" && !MEMBERSHIP_FIELD_NAME_RE.test(value2)) {
11167
+ ctx.addIssue({
11168
+ code: import_zod.z.ZodIssueCode.custom,
11169
+ path: [...path4, field],
11170
+ message: `${field} '${value2}' is invalid \u2014 must be 1-64 characters of letters, digits, underscores, or hyphens`
11171
+ });
11172
+ }
11173
+ }
11174
+ const hasLevelField = ns.membershipLevelField !== void 0;
11175
+ const hasLevels = Array.isArray(ns.membershipLevels) && ns.membershipLevels.length > 0;
11176
+ if (hasLevelField !== hasLevels) {
11177
+ ctx.addIssue({
11178
+ code: import_zod.z.ZodIssueCode.custom,
11179
+ path: [...path4, "membershipLevelField"],
11180
+ message: "membershipLevelField and a non-empty membershipLevels must be declared together, or neither"
11181
+ });
11182
+ }
11183
+ if ((hasLevelField || hasLevels) && !hasRecordType) {
11184
+ ctx.addIssue({
11185
+ code: import_zod.z.ZodIssueCode.custom,
11186
+ path: [...path4, "membershipLevelField"],
11187
+ message: "membershipLevelField/membershipLevels require membershipRecordType + membershipTargetField too"
11188
+ });
11189
+ }
11190
+ if (Array.isArray(ns.membershipLevels)) {
11191
+ const seenLevels = /* @__PURE__ */ new Set();
11192
+ for (const level of ns.membershipLevels) {
11193
+ if (typeof level !== "string") continue;
11194
+ const err = scopeNamespaceGrammarError(level);
11195
+ if (err) {
11196
+ ctx.addIssue({
11197
+ code: import_zod.z.ZodIssueCode.custom,
11198
+ path: [...path4, "membershipLevels"],
11199
+ message: `level '${level}': ${err}`
11200
+ });
11201
+ } else if (seenLevels.has(level)) {
11202
+ ctx.addIssue({
11203
+ code: import_zod.z.ZodIssueCode.custom,
11204
+ path: [...path4, "membershipLevels"],
11205
+ message: `membershipLevels contains '${level}' twice`
11206
+ });
11207
+ }
11208
+ seenLevels.add(level);
11209
+ }
11210
+ }
11211
+ });
11212
+ }
11213
+ var SeedCommonShape = {
11214
+ /** The schema typeName this seed instantiates (should match a declared schema). */
11215
+ typeName: import_zod.z.string().min(1),
11216
+ /**
11217
+ * Stable, caller-supplied id — the loader's idempotency key AND the value other
11218
+ * seeds resolve a `reference` against (across surfaces: a record seed may
11219
+ * reference a document seed by its externalId, and vice versa).
11220
+ */
11221
+ externalId: import_zod.z.string().min(1),
11222
+ /**
11223
+ * Optional scope ownership for the seeded item, as `namespace:value` entries
11224
+ * (≤2, e.g. `['org:${{ identities.team }}']`; tokens substitute at apply
11225
+ * time). `[]` = a private, user-owned item. Omitted = the seeding
11226
+ * credential's full identity (the default). Each entry's namespace + value
11227
+ * are validated below (`lintNamespacedScopeArrays`) — this
11228
+ * `.max(MAX_SCOPE_DIMENSIONS)` is only the shape-level cap, not the grammar
11229
+ * check.
11230
+ */
11231
+ scopes: import_zod.z.array(import_zod.z.string()).max(MAX_SCOPE_DIMENSIONS).optional()
11232
+ };
11233
+ var RecordSeedSchema = import_zod.z.object({
11234
+ surface: import_zod.z.literal("record"),
11235
+ ...SeedCommonShape,
11236
+ /** The record payload, validated against the bound schema. */
11237
+ fields: import_zod.z.record(import_zod.z.unknown())
11238
+ }).strict();
11239
+ var DocumentSeedSchema = import_zod.z.object({
11240
+ surface: import_zod.z.literal("document"),
11241
+ ...SeedCommonShape,
11242
+ /** Human-readable document title — REQUIRED by the text-ingest path. */
11243
+ title: import_zod.z.string().min(1),
11244
+ /** Raw text content to ingest + index — REQUIRED and non-empty (the platform rejects a blank ingest). */
11245
+ text: import_zod.z.string().min(1),
11246
+ /** Optional structured payload bound to the schema (the document's `fields`). */
11247
+ fields: import_zod.z.record(import_zod.z.unknown()).optional()
11248
+ }).strict();
11249
+ var BlueprintSeedRecordSchema = import_zod.z.discriminatedUnion("surface", [
11250
+ RecordSeedSchema,
11251
+ DocumentSeedSchema
11252
+ ]);
11253
+ var SELF_TOKEN_RE = /\$\{\{\s*(?:self|under|member)\.[^}]*?\s*\}\}/;
11254
+ function lintSelfTokenPlacement(value, ctx) {
11255
+ const walk = (node, path4, inRoleDataScope) => {
11256
+ if (typeof node === "string") {
11257
+ if (!inRoleDataScope && SELF_TOKEN_RE.test(node)) {
10561
11258
  ctx.addIssue({
10562
11259
  code: import_zod.z.ZodIssueCode.custom,
10563
11260
  path: path4,
10564
- message: "'${{ self.* }}' is a runtime per-principal placeholder \u2014 it is only valid inside a roles[].dataScope value"
11261
+ message: "'${{ self.* }}' / '${{ member.* }}' is a runtime placeholder \u2014 it is only valid inside a roles[].dataScope value"
10565
11262
  });
10566
11263
  }
10567
11264
  return;
@@ -10580,12 +11277,20 @@ function lintSelfTokenPlacement(value, ctx) {
10580
11277
  walk(value, [], false);
10581
11278
  }
10582
11279
  var IDENTITY_REF_RE = /\$\{\{\s*identities\.([A-Za-z_]\w*)\s*\}\}/g;
10583
- function lintIdentityRefsDeclared(value, ctx) {
11280
+ var IDENTITY_REF_LOOSE_RE = /\$\{\{\s*identities\.([^}]*?)\s*\}\}/g;
11281
+ function lintIdentityRefs(value, ctx) {
10584
11282
  const declared = new Set(
10585
11283
  value && typeof value === "object" && "identities" in value && value.identities ? Object.keys(value.identities) : []
10586
11284
  );
10587
11285
  const walk = (node, path4) => {
10588
11286
  if (typeof node === "string") {
11287
+ for (const m of node.matchAll(IDENTITY_REF_LOOSE_RE)) {
11288
+ const captured = m[1];
11289
+ if (IDENTITY_NAME_RE.test(captured)) continue;
11290
+ const dotIndex = captured.indexOf(".");
11291
+ 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)} }}'`;
11292
+ ctx.addIssue({ code: import_zod.z.ZodIssueCode.custom, path: path4, message });
11293
+ }
10589
11294
  for (const m of node.matchAll(IDENTITY_REF_RE)) {
10590
11295
  if (!declared.has(m[1])) {
10591
11296
  ctx.addIssue({
@@ -10608,6 +11313,134 @@ function lintIdentityRefsDeclared(value, ctx) {
10608
11313
  };
10609
11314
  walk(value, []);
10610
11315
  }
11316
+ var SCOPE_VALUE_RE = /^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$/;
11317
+ var SCOPE_NAMESPACE_RE = /^[a-z][a-z0-9_-]{1,31}$/;
11318
+ var FORBIDDEN_SCOPE_NAMESPACES = /* @__PURE__ */ new Set([
11319
+ "user",
11320
+ "record",
11321
+ "document",
11322
+ "entity",
11323
+ "self",
11324
+ "tenant",
11325
+ "context",
11326
+ "scope",
11327
+ "versions",
11328
+ "lookup"
11329
+ ]);
11330
+ function scopeNamespaceGrammarError(namespace) {
11331
+ if (namespace === "") return "scope namespace must not be blank";
11332
+ if (FORBIDDEN_SCOPE_NAMESPACES.has(namespace)) {
11333
+ return `'${namespace}' is a reserved namespace and cannot be used as an ownership scope`;
11334
+ }
11335
+ if (!SCOPE_NAMESPACE_RE.test(namespace)) {
11336
+ return `'${namespace}' is not a valid namespace \u2014 2-32 chars, lowercase letter first, then lowercase letters, digits, '_' or '-'`;
11337
+ }
11338
+ return null;
11339
+ }
11340
+ var IDENTITY_PLACEHOLDER_WHOLE_RE = new RegExp(`^${IDENTITY_REF_LOOSE_RE.source}$`);
11341
+ function lintIdentityOverrides(value, ctx) {
11342
+ const overrides = value.accessProfile?.identityOverrides;
11343
+ if (!overrides) return;
11344
+ const entries = Object.entries(overrides);
11345
+ if (entries.length > MAX_SCOPE_DIMENSIONS) {
11346
+ ctx.addIssue({
11347
+ code: import_zod.z.ZodIssueCode.custom,
11348
+ path: ["accessProfile", "identityOverrides"],
11349
+ 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}.`
11350
+ });
11351
+ }
11352
+ for (const [key, v] of entries) {
11353
+ const ns = key.startsWith("scope:") ? key.slice("scope:".length) : null;
11354
+ const nsError = ns === null ? null : scopeNamespaceGrammarError(ns);
11355
+ if (ns === null || nsError !== null) {
11356
+ ctx.addIssue({
11357
+ code: import_zod.z.ZodIssueCode.custom,
11358
+ path: ["accessProfile", "identityOverrides", key],
11359
+ 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}`
11360
+ });
11361
+ }
11362
+ if (typeof v !== "string") continue;
11363
+ if (IDENTITY_PLACEHOLDER_WHOLE_RE.test(v)) continue;
11364
+ if (!SCOPE_VALUE_RE.test(v)) {
11365
+ ctx.addIssue({
11366
+ code: import_zod.z.ZodIssueCode.custom,
11367
+ path: ["accessProfile", "identityOverrides", key],
11368
+ 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).`
11369
+ });
11370
+ }
11371
+ }
11372
+ }
11373
+ function lintScopeEntries(entries, path4, ctx) {
11374
+ if (!entries) return;
11375
+ const seenValueByNamespace = /* @__PURE__ */ new Map();
11376
+ for (let i = 0; i < entries.length; i++) {
11377
+ const raw = entries[i];
11378
+ if (typeof raw !== "string") continue;
11379
+ const sep = raw.indexOf(":");
11380
+ if (sep <= 0 || sep === raw.length - 1) {
11381
+ ctx.addIssue({
11382
+ code: import_zod.z.ZodIssueCode.custom,
11383
+ path: [...path4, i],
11384
+ message: `'${raw}' must be '<namespace>:<value>' (e.g. 'org:6ba7...' or 'team:${"${{ identities.team }}"}')`
11385
+ });
11386
+ continue;
11387
+ }
11388
+ const namespace = raw.slice(0, sep);
11389
+ const val = raw.slice(sep + 1);
11390
+ const nsError = scopeNamespaceGrammarError(namespace);
11391
+ if (nsError !== null) {
11392
+ ctx.addIssue({ code: import_zod.z.ZodIssueCode.custom, path: [...path4, i], message: `'${raw}' ${nsError}` });
11393
+ } else {
11394
+ const priorValue = seenValueByNamespace.get(namespace);
11395
+ if (priorValue !== void 0 && priorValue !== val) {
11396
+ ctx.addIssue({
11397
+ code: import_zod.z.ZodIssueCode.custom,
11398
+ path: [...path4, i],
11399
+ message: `'${namespace}' appears more than once with different values ('${priorValue}' and '${val}') \u2014 an item carries at most one value per namespace`
11400
+ });
11401
+ }
11402
+ seenValueByNamespace.set(namespace, val);
11403
+ }
11404
+ if (IDENTITY_PLACEHOLDER_WHOLE_RE.test(val)) continue;
11405
+ if (!SCOPE_VALUE_RE.test(val)) {
11406
+ ctx.addIssue({
11407
+ code: import_zod.z.ZodIssueCode.custom,
11408
+ path: [...path4, i],
11409
+ 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).`
11410
+ });
11411
+ }
11412
+ }
11413
+ }
11414
+ function lintNamespacedScopeArrays(value, ctx) {
11415
+ const bp = value;
11416
+ (bp.schemas ?? []).forEach((s, i) => lintScopeEntries(s.scopes, ["schemas", i, "scopes"], ctx));
11417
+ (bp.seed ?? []).forEach((s, i) => lintScopeEntries(s.scopes, ["seed", i, "scopes"], ctx));
11418
+ }
11419
+ function lintDataScopeKey(key, path4, ctx) {
11420
+ if (key === "userId") return;
11421
+ const ns = key.startsWith("scope:") ? key.slice("scope:".length) : null;
11422
+ const nsError = ns === null ? null : scopeNamespaceGrammarError(ns);
11423
+ if (ns === null || nsError !== null) {
11424
+ ctx.addIssue({
11425
+ code: import_zod.z.ZodIssueCode.custom,
11426
+ path: path4,
11427
+ 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}`
11428
+ });
11429
+ }
11430
+ }
11431
+ function lintDataScopeKeys(value, ctx) {
11432
+ const bp = value;
11433
+ for (const key of Object.keys(bp.accessProfile?.dataScope ?? {})) {
11434
+ lintDataScopeKey(key, ["accessProfile", "dataScope", key], ctx);
11435
+ }
11436
+ for (const [roleId, clauses] of Object.entries(bp.roles ?? {})) {
11437
+ clauses.forEach((clause, i) => {
11438
+ for (const key of Object.keys(clause.dataScope ?? {})) {
11439
+ lintDataScopeKey(key, ["roles", roleId, i, "dataScope", key], ctx);
11440
+ }
11441
+ });
11442
+ }
11443
+ }
10611
11444
  function lintSeedSurfaces(value, ctx) {
10612
11445
  const bp = value;
10613
11446
  if (!Array.isArray(bp.seed)) return;
@@ -10625,6 +11458,41 @@ function lintSeedSurfaces(value, ctx) {
10625
11458
  }
10626
11459
  });
10627
11460
  }
11461
+ function lintIdentitySurfaces(value, ctx) {
11462
+ const bp = value;
11463
+ if (!Array.isArray(bp.schemas)) return;
11464
+ bp.schemas.forEach((schema, i) => {
11465
+ if (!schema.allowedSurfaces?.includes("user")) return;
11466
+ ctx.addIssue({
11467
+ code: import_zod.z.ZodIssueCode.custom,
11468
+ path: ["schemas", i, "allowedSurfaces"],
11469
+ message: `schema '${schema.typeName}': allowedSurfaces cannot include 'user' \u2014 a user-surfaced schema is account-global and requires a root API key, which no blueprint credential ever holds, so this would fail at apply with a 403. Model the data as a 'record' scoped to the user, or as an 'entity' schema (which a blueprint CAN provision, in its own app context).`
11470
+ });
11471
+ });
11472
+ }
11473
+ function lintIssuerContext(value, ctx) {
11474
+ const bp = value;
11475
+ if (!Array.isArray(bp.issuers)) return;
11476
+ bp.issuers.forEach((issuer, i) => {
11477
+ if (issuer.contextId === void 0 || issuer.contextId === bp.contextId) return;
11478
+ ctx.addIssue({
11479
+ code: import_zod.z.ZodIssueCode.custom,
11480
+ path: ["issuers", i, "contextId"],
11481
+ message: `issuer '${issuer.issuerId}' targets app context '${issuer.contextId}', but this blueprint provisions '${bp.contextId}'. An issuer must target the blueprint's own context \u2014 everything else a blueprint creates does, and an issuer that points elsewhere attaches an identity provider to an app context the blueprint never mentions. Set it to '${bp.contextId}', or move this issuer into a blueprint for '${issuer.contextId}'.`
11482
+ });
11483
+ });
11484
+ }
11485
+ var BLUEPRINT_FIELD_PHASES = Object.freeze({
11486
+ servicePrincipal: "bootstrap",
11487
+ issuers: "bootstrap",
11488
+ // Registration requires the platform's provisioning:c capability, which
11489
+ // only the bootstrap credential carries — same reasoning as issuers.
11490
+ namespaces: "bootstrap",
11491
+ schemas: "in-context",
11492
+ accessProfile: "in-context",
11493
+ roles: "in-context",
11494
+ seed: "in-context"
11495
+ });
10628
11496
  var BlueprintSchema = import_zod.z.object({
10629
11497
  /** Stable blueprint id (the `--blueprint <name>` selector + idempotency key). */
10630
11498
  name: import_zod.z.string().min(1),
@@ -10643,11 +11511,33 @@ var BlueprintSchema = import_zod.z.object({
10643
11511
  /** Optional multi-clause roles, bound to principals via `access grant --role`. */
10644
11512
  roles: BlueprintRolesSchema.optional(),
10645
11513
  /** Optional principals ensured-exist at apply; referenced via ${{ identities.* }}. */
10646
- identities: IdentitiesDeclSchema.optional()
11514
+ identities: IdentitiesDeclSchema.optional(),
11515
+ /**
11516
+ * Optional trusted third-party IdP issuers to register (BYO-IdP token
11517
+ * exchange). Applied in the BOOTSTRAP-token phase, alongside app-context/
11518
+ * service-principal creation — NOT the in-context load (see
11519
+ * {@link BlueprintIssuerSchema}).
11520
+ */
11521
+ issuers: import_zod.z.array(BlueprintIssuerSchema).optional(),
11522
+ /**
11523
+ * Optional namespaces to register before anything else needs them — see
11524
+ * {@link BlueprintNamespaceSchema}. Applied in the BOOTSTRAP-token phase,
11525
+ * alongside app-context/service-principal creation and issuers, BEFORE
11526
+ * schemas/seed (an `entityBacked` namespace's entities, and an
11527
+ * `allowedSurfaces: ['entity']` schema, both need the registration to
11528
+ * already exist). `org`/`client` need no entry — they exist already.
11529
+ */
11530
+ namespaces: import_zod.z.array(BlueprintNamespaceSchema).optional()
10647
11531
  }).strict().superRefine((bp, ctx) => {
10648
11532
  lintSelfTokenPlacement(bp, ctx);
10649
- lintIdentityRefsDeclared(bp, ctx);
11533
+ lintIdentityRefs(bp, ctx);
11534
+ lintIdentityOverrides(bp, ctx);
11535
+ lintNamespacedScopeArrays(bp, ctx);
11536
+ lintDataScopeKeys(bp, ctx);
10650
11537
  lintSeedSurfaces(bp, ctx);
11538
+ lintIdentitySurfaces(bp, ctx);
11539
+ lintIssuerContext(bp, ctx);
11540
+ lintNamespaces(bp, ctx);
10651
11541
  });
10652
11542
  function formatIssuePath(path4) {
10653
11543
  let out = "";
@@ -10802,7 +11692,11 @@ function deriveSuffix(contextId) {
10802
11692
  var WHOLE_TOKEN_RE = /^\$\{\{\s*([A-Za-z_]\w*)\.([A-Za-z_]\w*)\s*\}\}$/;
10803
11693
  var TOKEN_SCAN_RE = /\$\$\{\{|\$\{\{([\s\S]*?)\}\}|\$\{\{/g;
10804
11694
  var INNER_REF_RE = /^([A-Za-z_]\w*)\.([A-Za-z_]\w*)$/;
10805
- var DEFERRED_NAMESPACES = /* @__PURE__ */ new Set(["self", "identities"]);
11695
+ var DEFERRED_NAMESPACES = /* @__PURE__ */ new Set(["self", "identities", "under", "member"]);
11696
+ var DEFERRED_NS_ALTERNATION = [...DEFERRED_NAMESPACES].join("|");
11697
+ var DEFERRED_INNER_RE = new RegExp(
11698
+ `^(?:${DEFERRED_NS_ALTERNATION})(?:\\.[A-Za-z_]\\w*)+(?::[A-Za-z_]\\w*)?$`
11699
+ );
10806
11700
  function resolveRef(ns, name, ctx, path4) {
10807
11701
  if (ns === "inputs") {
10808
11702
  if (!(name in ctx.declared)) {
@@ -10860,14 +11754,17 @@ function interpolate(str, ctx, path4) {
10860
11754
  });
10861
11755
  } else {
10862
11756
  const inner = (m[1] ?? "").trim();
11757
+ if (DEFERRED_INNER_RE.test(inner)) {
11758
+ out += m[0];
11759
+ last = TOKEN_SCAN_RE.lastIndex;
11760
+ continue;
11761
+ }
10863
11762
  const ref = inner.match(INNER_REF_RE);
10864
11763
  if (!ref) {
10865
11764
  ctx.issues.push({
10866
11765
  path: path4,
10867
11766
  message: `malformed reference '\${{ ${inner} }}' \u2014 expected '\${{ namespace.name }}' (escape a literal with '$\${{')`
10868
11767
  });
10869
- } else if (DEFERRED_NAMESPACES.has(ref[1])) {
10870
- out += m[0];
10871
11768
  } else {
10872
11769
  const v = resolveRef(ref[1], ref[2], ctx, path4);
10873
11770
  if (v !== void 0) out += String(v);
@@ -11049,324 +11946,82 @@ var taskManagement = {
11049
11946
  // Equality, NOT range: the values are an ordinal vocabulary but they sort
11050
11947
  // LEXICALLY (high < low < medium < urgent), not by severity — a range
11051
11948
  // index here would be permanently wrong. Filter by exact priority instead.
11052
- renderHints: { label: "Priority", widget: "select", order: 4, section: "Tracking" }
11053
- },
11054
- {
11055
- fieldId: "assignee",
11056
- fieldType: "string",
11057
- filterable: true,
11058
- description: "userId or display name.",
11059
- renderHints: { label: "Assignee", widget: "text", order: 5, section: "Tracking" }
11060
- },
11061
- {
11062
- fieldId: "project",
11063
- fieldType: "string",
11064
- filterable: true,
11065
- description: "Grouping key for cross-session continuity.",
11066
- renderHints: { label: "Project", widget: "text", order: 6, section: "Tracking" }
11067
- },
11068
- {
11069
- fieldId: "dueDate",
11070
- fieldType: "date",
11071
- description: "ISO-8601. Range-queryable (see lookupFields) \u2014 list tasks due in a window.",
11072
- renderHints: { label: "Due date", widget: "date", order: 7, section: "Tracking" }
11073
- },
11074
- {
11075
- fieldId: "tags",
11076
- fieldType: "array",
11077
- filterable: true,
11078
- renderHints: { label: "Tags", order: 8, section: "Tracking" }
11079
- }
11080
- // externalId is the record's FIRST-CLASS identifier (the dedup/lookup key + the
11081
- // value a `reference` resolves against) — not a payload field; the loader sends
11082
- // it top-level on the RecordRequest.
11083
- ],
11084
- // Two lookup shapes, chosen deliberately because the equality-vs-range choice
11085
- // per field is MIGRATION-LOCKED once the schema is live (you cannot flip a
11086
- // field slot↔range later, even by removing and re-adding it):
11087
- // • `project` — EQUALITY (a grouping key, not ordered): enumerate one
11088
- // project's tasks directly, no search. Each equality lookup uses 1 of the
11089
- // schema's 7 fast index slots.
11090
- // • `dueDate` — RANGE: ordered `from`/`to`/`prefix` queries ("tasks due this
11091
- // week", "due in 2026-06"). Range lookups use a relationship row, not a
11092
- // slot, and are billed at the range rate. ISO-8601 sorts chronologically,
11093
- // so the order is correct.
11094
- // `externalId` has a built-in first-class finder and must NOT be redeclared here.
11095
- lookupFields: ["project", { fieldName: "dueDate", rangeEnabled: true }]
11096
- }
11097
- ],
11098
- // Least-privilege profile — MUST pass the CLI scope gate.
11099
- // r/c/u + search + schema discovery. NOT records:d (least privilege).
11100
- accessProfile: {
11101
- allowedActions: ["records:r", "records:c", "records:u", "search:r", "schemas:r"]
11102
- // dataScope omitted → tenant-level shared tracker. Add a `scope:org`
11103
- // entry for per-org isolation.
11104
- },
11105
- servicePrincipal: {
11106
- externalId: "task-management",
11107
- displayName: "Task Management"
11108
- },
11109
- seed: [
11110
- {
11111
- surface: "record",
11112
- typeName: "task",
11113
- externalId: "seed-welcome",
11114
- fields: {
11115
- title: "Welcome to your Vectros task tracker",
11116
- description: "Created by the bootstrap loader. Ask your agent to add tasks.",
11117
- status: "todo",
11118
- priority: "low",
11119
- project: "getting-started",
11120
- dueDate: "2026-06-30"
11121
- }
11122
- }
11123
- ]
11124
- };
11125
- var task_management_default = taskManagement;
11126
- var codingAgentMemory = {
11127
- name: "coding-agent-memory",
11128
- version: "1.0.0",
11129
- description: "Persistent project memory for a coding agent \u2014 decisions, conventions, and gotchas that survive across sessions.",
11130
- contextId: "coding-memory",
11131
- contextName: "Coding Agent \u2014 Project Memory",
11132
- schemas: [
11133
- {
11134
- // A durable architectural/product decision: what was decided, and WHY.
11135
- // The rationale is the high-value, RAG-able body — "why did we do X?"
11136
- // is the question a cold-context agent most needs answered.
11137
- typeName: "decision",
11138
- displayName: "Decision",
11139
- indexMode: "HYBRID",
11140
- // keyword on titles + semantic on statement/rationale
11141
- fields: [
11142
- {
11143
- fieldId: "title",
11144
- fieldType: "string",
11145
- required: true,
11146
- searchable: true,
11147
- validation: { minLength: 1, maxLength: 200 },
11148
- renderHints: { label: "Title", widget: "text", order: 1, displayField: true }
11149
- },
11150
- {
11151
- fieldId: "statement",
11152
- fieldType: "string",
11153
- searchable: true,
11154
- description: "What was decided, in one or two sentences.",
11155
- renderHints: { label: "Statement", widget: "textarea", order: 2 }
11156
- },
11157
- {
11158
- fieldId: "rationale",
11159
- fieldType: "string",
11160
- searchable: true,
11161
- description: "Why \u2014 the trade-offs and the context. The most-recalled field.",
11162
- renderHints: { label: "Rationale", widget: "textarea", order: 3 }
11163
- },
11164
- {
11165
- fieldId: "status",
11166
- fieldType: "enum",
11167
- filterable: true,
11168
- enumValues: ["proposed", "active", "superseded"],
11169
- renderHints: { label: "Status", widget: "select", order: 4 }
11170
- },
11171
- {
11172
- fieldId: "area",
11173
- fieldType: "string",
11174
- filterable: true,
11175
- description: 'Subsystem / module the decision applies to (e.g. "auth", "search").',
11176
- renderHints: { label: "Area", widget: "text", order: 5 }
11177
- },
11178
- {
11179
- fieldId: "decidedOn",
11180
- fieldType: "date",
11181
- description: 'ISO-8601 date. Range-queryable \u2014 "decisions made this quarter".',
11182
- renderHints: { label: "Decided on", widget: "date", order: 6 }
11183
- }
11184
- // externalId is the record's FIRST-CLASS identifier (the dedup/upsert key + the
11185
- // value a `reference` resolves against) — it is NOT a payload field, so it is not
11186
- // declared here; the loader sends it top-level on the RecordRequest.
11187
- ],
11188
- // `externalId` is a first-class identifier with its own finder (exact
11189
- // get/upsert) — look it up directly, never redeclare it as a schema lookup.
11190
- // The choice below is MIGRATION-LOCKED once the schema is live:
11191
- // • `area`/`status` — EQUALITY (categorical): "list active decisions",
11192
- // "all decisions in the auth area". 2 of the 7 fast index slots.
11193
- // • `decidedOn` — RANGE: ordered `from`/`to`/`prefix` over the date.
11194
- lookupFields: ["area", "status", { fieldName: "decidedOn", rangeEnabled: true }]
11195
- },
11196
- {
11197
- // A coding convention the team follows — the agent reads these before it
11198
- // writes code so it matches the surrounding style instead of inventing one.
11199
- typeName: "convention",
11200
- displayName: "Convention",
11201
- indexMode: "HYBRID",
11202
- fields: [
11203
- {
11204
- fieldId: "name",
11205
- fieldType: "string",
11206
- required: true,
11207
- searchable: true,
11208
- validation: { minLength: 1, maxLength: 200 },
11209
- renderHints: { label: "Name", widget: "text", order: 1, displayField: true }
11210
- },
11211
- {
11212
- fieldId: "rule",
11213
- fieldType: "string",
11214
- searchable: true,
11215
- description: "The convention itself, stated as an imperative.",
11216
- renderHints: { label: "Rule", widget: "textarea", order: 2 }
11217
- },
11218
- {
11219
- fieldId: "area",
11220
- fieldType: "string",
11221
- filterable: true,
11222
- renderHints: { label: "Area", widget: "text", order: 3 }
11223
- },
11224
- {
11225
- // Parallels `decision.status` so a convention can be RETIRED rather than
11226
- // deleted (the blueprint omits records:d — memory is superseded, not lost).
11227
- fieldId: "status",
11228
- fieldType: "enum",
11229
- filterable: true,
11230
- enumValues: ["active", "retired"],
11231
- renderHints: { label: "Status", widget: "select", order: 4 }
11232
- },
11233
- {
11234
- // A typed REFERENCE to the decision that established this convention — a
11235
- // foreign-key link between record types (the knowledge-graph edge). The
11236
- // platform requires both the target type AND its surface; the value is the
11237
- // target decision's externalId (the default `targetField`). Provenance:
11238
- // "why does this convention exist? → open the decision behind it." Not
11239
- // searchable (a foreign-key id is search noise); declared as an equality
11240
- // lookup below so you can also enumerate "conventions from decision X".
11241
- fieldId: "establishedByDecision",
11242
- fieldType: "reference",
11243
- targetTypeName: "decision",
11244
- targetSurface: "record",
11245
- targetField: "externalId",
11246
- cardinality: "one",
11247
- renderHints: { label: "Established by (decision)", order: 5 }
11248
- },
11249
- {
11250
- fieldId: "adoptedOn",
11251
- fieldType: "date",
11252
- description: "ISO-8601 \u2014 when this convention was adopted. Range-queryable.",
11253
- renderHints: { label: "Adopted on", widget: "date", order: 6 }
11254
- }
11255
- // externalId: first-class identifier, not a payload field (see `decision`).
11256
- ],
11257
- // externalId has its own first-class finder for exact get — look it up
11258
- // directly, not as a schema lookup. Locked equality-vs-range choices:
11259
- // • `area`/`status` — EQUALITY: "active conventions for auth".
11260
- // • `establishedByDecision` — EQUALITY on the reference value: enumerate
11261
- // "conventions established by decision X" (a forward link query; the
11262
- // platform has no reverse-reference index on this surface).
11263
- // • `adoptedOn` — RANGE over the adoption date.
11264
- // 3 equality lookups (of 7 fast slots) + 1 range row.
11265
- lookupFields: ["area", "status", "establishedByDecision", { fieldName: "adoptedOn", rangeEnabled: true }]
11266
- },
11267
- {
11268
- // A gotcha / sharp edge: a symptom, its cause, and the fix. Saves the
11269
- // agent (and the human) from re-discovering the same trap.
11270
- typeName: "gotcha",
11271
- displayName: "Gotcha",
11272
- indexMode: "HYBRID",
11273
- fields: [
11274
- {
11275
- fieldId: "symptom",
11276
- fieldType: "string",
11277
- required: true,
11278
- searchable: true,
11279
- validation: { minLength: 1, maxLength: 500 },
11280
- renderHints: { label: "Symptom", widget: "textarea", order: 1, displayField: true }
11281
- },
11282
- {
11283
- fieldId: "cause",
11284
- fieldType: "string",
11285
- searchable: true,
11286
- renderHints: { label: "Cause", widget: "textarea", order: 2 }
11949
+ renderHints: { label: "Priority", widget: "select", order: 4, section: "Tracking" }
11287
11950
  },
11288
11951
  {
11289
- fieldId: "fix",
11952
+ fieldId: "assignee",
11290
11953
  fieldType: "string",
11291
- searchable: true,
11292
- renderHints: { label: "Fix", widget: "textarea", order: 3 }
11954
+ filterable: true,
11955
+ description: "userId or display name.",
11956
+ renderHints: { label: "Assignee", widget: "text", order: 5, section: "Tracking" }
11293
11957
  },
11294
11958
  {
11295
- fieldId: "area",
11959
+ fieldId: "project",
11296
11960
  fieldType: "string",
11297
11961
  filterable: true,
11298
- renderHints: { label: "Area", widget: "text", order: 4 }
11962
+ description: "Grouping key for cross-session continuity.",
11963
+ renderHints: { label: "Project", widget: "text", order: 6, section: "Tracking" }
11299
11964
  },
11300
11965
  {
11301
- // A gotcha can be RETIRED once the underlying trap is fixed for good —
11302
- // superseded, not deleted (the blueprint omits records:d).
11303
- fieldId: "status",
11304
- fieldType: "enum",
11305
- filterable: true,
11306
- enumValues: ["active", "retired"],
11307
- renderHints: { label: "Status", widget: "select", order: 5 }
11966
+ fieldId: "dueDate",
11967
+ fieldType: "date",
11968
+ description: "ISO-8601. Range-queryable (see lookupFields) \u2014 list tasks due in a window.",
11969
+ renderHints: { label: "Due date", widget: "date", order: 7, section: "Tracking" }
11308
11970
  },
11309
11971
  {
11310
- fieldId: "discoveredOn",
11311
- fieldType: "date",
11312
- description: "ISO-8601 \u2014 when this gotcha was first hit. Range-queryable.",
11313
- renderHints: { label: "Discovered on", widget: "date", order: 6 }
11972
+ fieldId: "tags",
11973
+ fieldType: "array",
11974
+ filterable: true,
11975
+ renderHints: { label: "Tags", order: 8, section: "Tracking" }
11314
11976
  }
11315
- // externalId: first-class identifier, not a payload field (see `decision`).
11977
+ // externalId is the record's FIRST-CLASS identifier (the dedup/lookup key + the
11978
+ // value a `reference` resolves against) — not a payload field; the loader sends
11979
+ // it top-level on the RecordRequest.
11316
11980
  ],
11317
- // externalId has its own first-class finder for exact get — look it up
11318
- // directly, not as a schema lookup. `area`/`status` EQUALITY ("active
11319
- // gotchas in search"); `discoveredOn` RANGE over the discovery date.
11320
- lookupFields: ["area", "status", { fieldName: "discoveredOn", rangeEnabled: true }]
11981
+ // Two lookup shapes, chosen deliberately because the equality-vs-range choice
11982
+ // per field is MIGRATION-LOCKED once the schema is live (you cannot flip a
11983
+ // field slot↔range later, even by removing and re-adding it):
11984
+ // • `project` — EQUALITY (a grouping key, not ordered): enumerate one
11985
+ // project's tasks directly, no search. Each equality lookup uses 1 of the
11986
+ // schema's 7 fast index slots.
11987
+ // • `dueDate` — RANGE: ordered `from`/`to`/`prefix` queries ("tasks due this
11988
+ // week", "due in 2026-06"). Range lookups use a relationship row, not a
11989
+ // slot, and are billed at the range rate. ISO-8601 sorts chronologically,
11990
+ // so the order is correct.
11991
+ // `externalId` has a built-in first-class finder and must NOT be redeclared here.
11992
+ lookupFields: ["project", { fieldName: "dueDate", rangeEnabled: true }]
11321
11993
  }
11322
11994
  ],
11323
11995
  // Least-privilege profile — MUST pass the CLI scope gate.
11324
- // r/c/u + search + schema discovery + inference:r (grounded "why did we do X?"
11325
- // recall over the captured rationale). NOT records:d: memory is superseded,
11326
- // not deleted, so the project's decision history stays auditable.
11996
+ // r/c/u + search + schema discovery. NOT records:d (least privilege).
11327
11997
  accessProfile: {
11328
- allowedActions: ["records:r", "records:c", "records:u", "search:r", "schemas:r", "inference:r"]
11998
+ allowedActions: ["records:r", "records:c", "records:u", "search:r", "schemas:r"],
11999
+ // dataScope: {} (present, empty) is the deliberate marker for a
12000
+ // tenant-level shared tracker — every task visible to every holder of
12001
+ // this credential. Add a `scope:org` entry instead for per-org isolation.
12002
+ dataScope: {}
11329
12003
  },
11330
12004
  servicePrincipal: {
11331
- externalId: "coding-agent-memory",
11332
- displayName: "Coding Agent \u2014 Project Memory"
12005
+ externalId: "task-management",
12006
+ displayName: "Task Management"
11333
12007
  },
11334
12008
  seed: [
11335
12009
  {
11336
- // Seeded FIRST so the convention below resolves its reference to it — the
11337
- // loader creates seeds in array order, and a reference target must exist when
11338
- // the referencing record is written.
11339
- surface: "record",
11340
- typeName: "decision",
11341
- externalId: "seed-use-vectros-for-memory",
11342
- fields: {
11343
- title: "Use Vectros as the coding agent\u2019s project memory",
11344
- statement: "Persist decisions, conventions, and gotchas as Vectros records so they survive across agent sessions.",
11345
- 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.",
11346
- status: "active",
11347
- area: "getting-started",
11348
- decidedOn: "2026-06-14"
11349
- }
11350
- },
11351
- {
11352
- // Demonstrates a live typed link at bootstrap: this convention references the
11353
- // decision above by its externalId, so "open the decision behind this rule"
11354
- // works the moment the blueprint is applied.
11355
12010
  surface: "record",
11356
- typeName: "convention",
11357
- externalId: "seed-record-the-why",
12011
+ typeName: "task",
12012
+ externalId: "seed-welcome",
11358
12013
  fields: {
11359
- name: "Record the why, not just the what",
11360
- 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.",
11361
- area: "getting-started",
11362
- status: "active",
11363
- establishedByDecision: "seed-use-vectros-for-memory",
11364
- adoptedOn: "2026-06-14"
12014
+ title: "Welcome to your Vectros task tracker",
12015
+ description: "Created by the bootstrap loader. Ask your agent to add tasks.",
12016
+ status: "todo",
12017
+ priority: "low",
12018
+ project: "getting-started",
12019
+ dueDate: "2026-06-30"
11365
12020
  }
11366
12021
  }
11367
12022
  ]
11368
12023
  };
11369
- var coding_agent_memory_default = codingAgentMemory;
12024
+ var task_management_default = taskManagement;
11370
12025
  var DATA_PLANE_ACTIONS = [
11371
12026
  "records:r",
11372
12027
  "records:c",
@@ -11394,7 +12049,7 @@ var MEMBER_SHARED_READ_ACTIONS = [
11394
12049
  ];
11395
12050
  var agenticSdlc = {
11396
12051
  name: "agentic-sdlc",
11397
- version: "1.6.0",
12052
+ version: "1.9.0",
11398
12053
  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.",
11399
12054
  contextId: "agentic-sdlc",
11400
12055
  contextName: "Agentic SDLC Knowledge Base",
@@ -12260,13 +12915,244 @@ var agenticSdlc = {
12260
12915
  { fieldName: "updatedOn", rangeEnabled: true },
12261
12916
  { fieldName: "priority", rangeEnabled: true }
12262
12917
  ]
12918
+ },
12919
+ {
12920
+ // A PROPOSED memory, waiting on a verification step — the staging area in
12921
+ // front of the `memory` type above. An agent that distills its own working
12922
+ // sessions produces claims faster than it can check them, and an unchecked
12923
+ // claim is worse than no claim: it is a plausible sentence that will be
12924
+ // recalled as fact. So a proposal lands here, an agent later verifies it,
12925
+ // and only a verified one is written to `memory`.
12926
+ //
12927
+ // ⚠ indexMode NONE IS THE LOAD-BEARING DECISION, not a tuning choice.
12928
+ // Store-only means these records are never indexed, so a search or a
12929
+ // grounded answer CANNOT return them under any query — the separation
12930
+ // between unverified proposals and trusted knowledge is enforced by the
12931
+ // platform rather than by every caller remembering to filter. Make this
12932
+ // HYBRID and the corpus of unchecked claims starts competing with the
12933
+ // curated KB for retrieval slots, which is the one failure this type
12934
+ // exists to prevent. They stay reachable by id and by the two lookups
12935
+ // below, which is all a disposition workflow needs.
12936
+ //
12937
+ // Note the consequence for the fields: `filterable` narrows SEARCH, and
12938
+ // nothing here is searchable, so no field declares it. Query paths are the
12939
+ // lookups, deliberately — a flag that does nothing would just read as
12940
+ // capability that isn't there.
12941
+ typeName: "candidate",
12942
+ displayName: "Memory candidate",
12943
+ indexMode: "NONE",
12944
+ fields: [
12945
+ {
12946
+ fieldId: "title",
12947
+ fieldType: "string",
12948
+ required: true,
12949
+ validation: { minLength: 1, maxLength: 200 },
12950
+ description: "One line naming the claim \u2014 what a reviewer reads first.",
12951
+ renderHints: { label: "Title", widget: "text", order: 1, displayField: true }
12952
+ },
12953
+ {
12954
+ fieldId: "body",
12955
+ fieldType: "string",
12956
+ description: "The proposed fact in full, as the distiller stated it \u2014 the thing to verify.",
12957
+ renderHints: { label: "Body", widget: "textarea", order: 2 }
12958
+ },
12959
+ {
12960
+ // Mirrors `memory.kind` exactly, so a verified candidate maps 1:1 onto
12961
+ // the memory it becomes with no vocabulary translation in between.
12962
+ fieldId: "kind",
12963
+ fieldType: "enum",
12964
+ enumValues: ["user", "feedback", "project", "reference", "observation"],
12965
+ description: "Which flavour of memory this would become \u2014 the same vocabulary as `memory.kind`.",
12966
+ renderHints: { label: "Kind", widget: "select", order: 3 }
12967
+ },
12968
+ {
12969
+ // The distiller's ROUTING suggestion, and the one field here with no
12970
+ // counterpart on `memory` — because it stops existing the moment the
12971
+ // question it asks is answered. A verified claim either becomes a
12972
+ // memory record or becomes a paragraph in a document; this is the
12973
+ // proposer's guess about which, and the verifier is free to overrule
12974
+ // it (the verdict lands in `disposition`, not here).
12975
+ fieldId: "dest",
12976
+ fieldType: "enum",
12977
+ enumValues: ["memory", "doc"],
12978
+ description: "Where the proposer thinks this belongs if it survives review \u2014 a suggestion, not the verdict.",
12979
+ renderHints: { label: "Suggested destination", widget: "select", order: 4 }
12980
+ },
12981
+ {
12982
+ // `area`, `tags` and `sourceRef` below mirror `memory`'s exactly, for
12983
+ // the same reason `kind` does: a verified candidate is COPIED into a
12984
+ // memory, and any field that exists on only one side has to be
12985
+ // re-derived or dropped at exactly the moment a human is deciding
12986
+ // whether the claim is true.
12987
+ fieldId: "area",
12988
+ fieldType: "string",
12989
+ description: 'Subsystem/topic the claim applies to (e.g. "auth", "search") \u2014 the shared area vocabulary.',
12990
+ renderHints: { label: "Area", widget: "text", order: 5 }
12991
+ },
12992
+ {
12993
+ fieldId: "tags",
12994
+ fieldType: "array",
12995
+ description: "Freeform labels carried over from the proposal.",
12996
+ renderHints: { label: "Tags", order: 6 }
12997
+ },
12998
+ {
12999
+ // Provenance the VERIFIER needs most: checking a claim starts with the
13000
+ // file or issue it is about. A candidate whose sourceRef was dropped
13001
+ // in transit costs the reviewer the search that the proposer had
13002
+ // already done.
13003
+ fieldId: "sourceRef",
13004
+ fieldType: "string",
13005
+ description: "Provenance: the file path / issue / URL the claim is about \u2014 where a verifier starts.",
13006
+ renderHints: { label: "Source ref", widget: "text", order: 7 }
13007
+ },
13008
+ {
13009
+ fieldId: "sessionId",
13010
+ fieldType: "string",
13011
+ description: "The conversation that proposed it \u2014 your runtime's session identifier.",
13012
+ renderHints: { label: "Session", widget: "text", order: 8 }
13013
+ },
13014
+ {
13015
+ // Also the stable ordering key: a display ordinal is derived by sorting
13016
+ // on this, so proposals keep the numbering a reviewer saw. Nothing is
13017
+ // ever hard-deleted here, which is what keeps that ordering stable.
13018
+ fieldId: "proposedAt",
13019
+ fieldType: "date",
13020
+ description: "ISO-8601 \u2014 when it was proposed. Also the stable ordering key for display.",
13021
+ renderHints: { label: "Proposed at", widget: "date", order: 9 }
13022
+ },
13023
+ {
13024
+ // THE VERDICT, and deliberately a plain payload field rather than the
13025
+ // record's own ACTIVE/ARCHIVED status. Settling a candidate is
13026
+ // REVERSIBLE — a wrong dismissal has to be undoable — and a write-side
13027
+ // visibility flip is the wrong tool for a reversible state: it is
13028
+ // asynchronous, it moves the record out from under readers, and it
13029
+ // leaves the index status of a once-indexed row stranded. A filterable
13030
+ // payload field flips synchronously and reads the same for everyone.
13031
+ fieldId: "disposition",
13032
+ fieldType: "enum",
13033
+ enumValues: ["pending", "stored", "documented", "ignored"],
13034
+ description: "The verdict: still pending, written to memory, captured in a doc, or dismissed.",
13035
+ renderHints: { label: "Disposition", widget: "select", order: 10 }
13036
+ },
13037
+ {
13038
+ fieldId: "ref",
13039
+ fieldType: "string",
13040
+ description: "The citation given when settling it \u2014 the record id, document path, or reason.",
13041
+ renderHints: { label: "Ref", widget: "text", order: 11 }
13042
+ },
13043
+ {
13044
+ // What the settling step actually CHECKED, which is not always what the
13045
+ // agent typed. Keeping both makes a bad citation auditable after the
13046
+ // fact instead of indistinguishable from a good one.
13047
+ fieldId: "resolved",
13048
+ fieldType: "string",
13049
+ description: "What was verified when it was settled \u2014 kept alongside `ref`, which is what was claimed.",
13050
+ renderHints: { label: "Resolved", widget: "text", order: 12 }
13051
+ },
13052
+ {
13053
+ fieldId: "origin",
13054
+ fieldType: "enum",
13055
+ enumValues: ["session", "recovered"],
13056
+ description: "Proposed by a live session, or recovered later from one that ended without settling.",
13057
+ renderHints: { label: "Origin", widget: "select", order: 13 }
13058
+ },
13059
+ {
13060
+ fieldId: "reopenedWhy",
13061
+ fieldType: "string",
13062
+ description: "Why a settled candidate was reopened \u2014 the audit trail for undoing a verdict.",
13063
+ renderHints: { label: "Reopened why", widget: "textarea", order: 14 }
13064
+ },
13065
+ {
13066
+ // The forward half of a correction: this candidate restates an earlier
13067
+ // one whose claim was wrong. Written at create, so it costs nothing.
13068
+ fieldId: "revises",
13069
+ fieldType: "reference",
13070
+ targetTypeName: "candidate",
13071
+ targetSurface: "record",
13072
+ targetField: "externalId",
13073
+ cardinality: "one",
13074
+ description: "The candidate this one corrects (by externalId).",
13075
+ renderHints: { label: "Revises", order: 15 }
13076
+ },
13077
+ {
13078
+ // The reverse half, and NOT redundant: "is this still open?" has to be
13079
+ // answerable without asking whether some OTHER record points here, and
13080
+ // there is no reverse-reference read. So the superseded candidate is
13081
+ // marked directly.
13082
+ //
13083
+ // Kept SEPARATE from `disposition` on purpose. Superseded and dismissed
13084
+ // are different facts: a dismissal may be undone, while resurrecting a
13085
+ // claim that a later run already corrected would re-offer the version
13086
+ // known to be wrong. Folding both into one enum makes that distinction
13087
+ // unrepresentable exactly when it matters.
13088
+ fieldId: "supersededBy",
13089
+ fieldType: "reference",
13090
+ targetTypeName: "candidate",
13091
+ targetSurface: "record",
13092
+ targetField: "externalId",
13093
+ cardinality: "one",
13094
+ description: "The candidate that corrected this one (by externalId) \u2014 set on the older record.",
13095
+ renderHints: { label: "Superseded by", order: 16 }
13096
+ }
13097
+ ],
13098
+ // FOUR lookups, because this type is asked three genuinely different
13099
+ // questions and one of them is a conjunction:
13100
+ //
13101
+ // `disposition` — "what is waiting ANYWHERE?" The cross-session
13102
+ // review queue. Low cardinality, so its
13103
+ // partitions are large; that is inherent to the
13104
+ // question and the pending set is small.
13105
+ // `sessionId` — "everything belonging to this conversation",
13106
+ // settled candidates included.
13107
+ // (sessionId, disposition) — "what is still waiting IN this conversation".
13108
+ // `proposedAt` (range) — worked by age: "what has been sitting
13109
+ // longest", "what came in this week". A range
13110
+ // read, not a filter over everything ever
13111
+ // proposed.
13112
+ //
13113
+ // The composite earns its slot because that third question is the HOTTEST
13114
+ // read in the system — it is what an agent is shown as it works, so it runs
13115
+ // far more often than the queue-wide scan does. Without it the answer is the
13116
+ // more selective single lookup plus a filter in the caller.
13117
+ //
13118
+ // `sessionId` LEADS, and the order is not cosmetic: the leading field
13119
+ // becomes the partition key. Leading with `disposition` would sort every
13120
+ // candidate ever proposed into four partitions; leading with `sessionId`
13121
+ // gives one small partition per conversation. Order is also
13122
+ // migration-locked, so it cannot be corrected later.
13123
+ //
13124
+ // A composite matches a LEADING RUN of its fields, so `(sessionId,
13125
+ // disposition)` also answers `sessionId` alone. The plain `sessionId`
13126
+ // lookup is kept anyway: under a partial tuple the unspecified field
13127
+ // becomes part of the ordering, so results come back grouped by
13128
+ // `disposition` rather than in one sequence — right for a filtered view,
13129
+ // wrong for "this conversation's candidates, oldest first", which is the
13130
+ // read the review surface and the queue/record agreement check both make.
13131
+ // One declaration out of ten is a cheap price for not depending on that
13132
+ // subtlety in the two places that must not be wrong.
13133
+ //
13134
+ // No `sortBy`: it defaults to the record's creation time, which is already
13135
+ // the primary key the review queue orders by. Naming `proposedAt` instead
13136
+ // would order by the WORKER's clock, and that is stamped when a proposal is
13137
+ // queued rather than when it is stored — the two disagree by exactly the
13138
+ // outage a durable queue exists to survive.
13139
+ lookupFields: [
13140
+ "disposition",
13141
+ "sessionId",
13142
+ { fieldNames: ["sessionId", "disposition"] },
13143
+ { fieldName: "proposedAt", rangeEnabled: true }
13144
+ ]
12263
13145
  }
12264
13146
  ],
12265
13147
  // Least-privilege, data-plane only. The scope of the `ssk_*` key the bootstrap
12266
13148
  // mints for THIS blueprint's service principal (the MCP/API runtime). See
12267
13149
  // DATA_PLANE_ACTIONS above for the action set + rationale.
12268
13150
  accessProfile: {
12269
- allowedActions: DATA_PLANE_ACTIONS
13151
+ allowedActions: DATA_PLANE_ACTIONS,
13152
+ // dataScope: {} (present, empty) is the deliberate marker for tenant-wide
13153
+ // reach — this service key IS the runtime's own access to the whole KB,
13154
+ // not a multi-owner resource to isolate.
13155
+ dataScope: {}
12270
13156
  },
12271
13157
  // A reusable `editor` role for the HUMAN owner — DISTINCT from `accessProfile`
12272
13158
  // (which scopes only the service-principal key). `bootstrap` provisions this
@@ -12286,7 +13172,9 @@ var agenticSdlc = {
12286
13172
  // which includes members' private memory — the trusted-administrator tradeoff,
12287
13173
  // called out in the bundled guide. Scope it down if your deployment wants
12288
13174
  // curator access without private-memory visibility.
12289
- editor: [{ allowedActions: EDITOR_ACTIONS }],
13175
+ // dataScope: {} echoes the "deliberately UNSCOPED (whole-context)" call-out
13176
+ // above as an explicit marker, not just prose — see it for the tradeoff.
13177
+ editor: [{ allowedActions: EDITOR_ACTIONS, dataScope: {} }],
12290
13178
  // A team member (or their agent) — TWO memory tiers as two UNIONed clauses
12291
13179
  // (a record is accessible when ANY clause grants it; within one clause every
12292
13180
  // dataScope dimension must match):
@@ -12306,14 +13194,21 @@ var agenticSdlc = {
12306
13194
  // `vectros join agentic-sdlc --role member`; verify a binding with
12307
13195
  // `vectros access explain --principal me --context agentic-sdlc`.
12308
13196
  member: [
12309
- { allowedActions: MEMBER_SHARED_READ_ACTIONS },
13197
+ // dataScope: {} echoes clause 1's own "UNSCOPED, but safe" call-out above
13198
+ // as an explicit marker, not just prose.
13199
+ { allowedActions: MEMBER_SHARED_READ_ACTIONS, dataScope: {} },
12310
13200
  {
12311
13201
  // `inference:r` is IN this self-scoped clause (not only clause 1) so a
12312
13202
  // member's `rag_ask` can ground on its OWN private memory — the read
12313
13203
  // grant (`records:r:memory`) and the inference capability travel in the
12314
13204
  // SAME clause, so grounding works regardless of cross-clause admission
12315
13205
  // semantics. Still self-fenced by dataScope: never another principal's.
12316
- allowedActions: ["records:cru:memory", "search:r", "inference:r"],
13206
+ // `candidate` is READ-only here, unlike `memory`'s full CRU. A candidate's
13207
+ // verdict is the output of a verification step, so it is written by the
13208
+ // agent runtime that performed the check — a human editing the field
13209
+ // directly would record a verdict nothing actually verified. Reading them
13210
+ // is the point: the queue, the verdicts and the corrections are browsable.
13211
+ allowedActions: ["records:cru:memory", "records:r:candidate", "search:r", "inference:r"],
12317
13212
  dataScope: { userId: ["${{ self.userId }}"] }
12318
13213
  }
12319
13214
  ]
@@ -12416,7 +13311,11 @@ var secondBrain = {
12416
13311
  // (so the agent can `rag_ask` a grounded, cited question over your notes). No
12417
13312
  // records:d: notes are archived (status → 'archived'), never deleted.
12418
13313
  accessProfile: {
12419
- allowedActions: ["records:r", "records:c", "records:u", "search:r", "schemas:r", "inference:r"]
13314
+ allowedActions: ["records:r", "records:c", "records:u", "search:r", "schemas:r", "inference:r"],
13315
+ // dataScope: {} (present, empty) is the deliberate marker for tenant-wide
13316
+ // reach — this is a single personal-knowledge-base credential with one
13317
+ // holder, not a multi-owner shared resource to isolate.
13318
+ dataScope: {}
12420
13319
  },
12421
13320
  servicePrincipal: {
12422
13321
  externalId: "second-brain",
@@ -12548,7 +13447,11 @@ var clinicalIntake = {
12548
13447
  // reveal qualifier (the demo key literally cannot un-redact the PHI fields),
12549
13448
  // and NO inference:r (we never point a RAG capability at a PHI corpus here).
12550
13449
  accessProfile: {
12551
- allowedActions: ["records:r", "records:c", "records:u", "search:r", "schemas:r"]
13450
+ allowedActions: ["records:r", "records:c", "records:u", "search:r", "schemas:r"],
13451
+ // dataScope: {} (present, empty) is the deliberate marker for tenant-wide
13452
+ // reach, unchanged from this demo's original intent — a real clinical
13453
+ // deployment should scope this (e.g. `scope:org`) before handling real PHI.
13454
+ dataScope: {}
12552
13455
  },
12553
13456
  servicePrincipal: {
12554
13457
  externalId: "clinical-intake",
@@ -12577,7 +13480,6 @@ var clinicalIntake = {
12577
13480
  var clinical_intake_default = clinicalIntake;
12578
13481
  var BUNDLED_BLUEPRINTS = [
12579
13482
  task_management_default,
12580
- coding_agent_memory_default,
12581
13483
  agentic_sdlc_default,
12582
13484
  second_brain_default,
12583
13485
  clinical_intake_default
@@ -12876,18 +13778,17 @@ async function mintBridgeToken(opts) {
12876
13778
  ttlSeconds = DEFAULT_BRIDGE_TTL_SECONDS,
12877
13779
  tenant = "live",
12878
13780
  useScopedToken = false,
13781
+ contextId,
12879
13782
  fetchImpl = fetch
12880
13783
  } = opts;
12881
13784
  const base = apiBaseUrl.replace(/\/$/, "");
12882
13785
  const path4 = bridgeTokenPath(useScopedToken);
12883
- const url = `${base}${path4}?ttl=${encodeURIComponent(String(ttlSeconds))}&tenant=${encodeURIComponent(tenant)}`;
12884
- return fetchStToken(
12885
- url,
12886
- cognitoToken,
12887
- fetchImpl,
12888
- path4,
12889
- (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)` : ""
12890
- );
13786
+ let url = `${base}${path4}?ttl=${encodeURIComponent(String(ttlSeconds))}&tenant=${encodeURIComponent(tenant)}`;
13787
+ if (contextId) url += `&context=${encodeURIComponent(contextId)}`;
13788
+ return fetchStToken(url, cognitoToken, fetchImpl, path4, (status) => {
13789
+ if (status !== 404 || useScopedToken || contextId) return "";
13790
+ return ` (the dedicated ${CLI_BOOTSTRAP_TOKEN_PATH} endpoint may not be deployed yet \u2014 retry with --use-scoped-token for the fallback path)`;
13791
+ });
12891
13792
  }
12892
13793
  async function mintContextToken(opts) {
12893
13794
  const {
@@ -13159,7 +14060,17 @@ async function resolveAuthContext(opts) {
13159
14060
  bearer: bridge.token,
13160
14061
  cognitoToken,
13161
14062
  baseUrl,
13162
- env: opts.env,
14063
+ /**
14064
+ * Derived from the REQUEST'S actual `baseUrl`, not echoed from `opts.env` — the two
14065
+ * diverge whenever `VECTROS_API_BASE_URL`/`--base-url` alone selects the environment
14066
+ * (the common shape when targeting staging without also passing `--env staging`,
14067
+ * since `envToBaseUrl` lets an explicit override win over `opts.env`'s default). Every
14068
+ * display surface that shows "on {env}" (blueprint-test's banner, `whoami`, etc.) reads
14069
+ * this field, so it must reflect where the request actually went, not which hidden flag
14070
+ * defaulted to what. `deriveEnvironment` falls through to `opts.env` only for a base URL
14071
+ * it doesn't recognize (a non-standard override, e.g. a future custom/self-hosted host).
14072
+ */
14073
+ env: deriveEnvironment(baseUrl) ?? opts.env,
13163
14074
  identity,
13164
14075
  tenantId: typeof identity.tenantId === "string" ? identity.tenantId : void 0,
13165
14076
  tenant
@@ -13243,7 +14154,8 @@ var DATA_PLANE_ALLOWLIST = [
13243
14154
  "search",
13244
14155
  "documents",
13245
14156
  "folders",
13246
- "inference"
14157
+ "inference",
14158
+ "entities"
13247
14159
  ];
13248
14160
  var KNOWN_CONTROL_PLANE_RESOURCES = [
13249
14161
  "keys",
@@ -13252,14 +14164,36 @@ var KNOWN_CONTROL_PLANE_RESOURCES = [
13252
14164
  "users",
13253
14165
  "billing",
13254
14166
  "admin",
13255
- // Identity/compartment provisioning. The flat `orgs`/`clients` resources were
13256
- // folded into the generic `entities` (per-namespace) + `namespaces` surfaces,
13257
- // which is what the API now scopes on — so those are the current spellings a
13258
- // pack could name. (Cosmetic: any non-data-plane resource hard-rejects whether
13259
- // or not it is listed here; this list only sharpens the error message.)
13260
- "entities",
14167
+ // The namespace REGISTRY (as opposed to `entities`, which creates/reads a
14168
+ // ROW within an already-registered namespace — see the data-plane-allowlist
14169
+ // note above). Registering a new namespace TYPE is a tenant-config action,
14170
+ // root-key-gated regardless of any scope a token carries — genuinely
14171
+ // control-plane, unlike per-namespace entity CRUD. (Cosmetic: any
14172
+ // non-data-plane resource hard-rejects whether or not it is listed here;
14173
+ // this list only sharpens the error message.)
14174
+ //
14175
+ // ⚠️ This bars `namespaces` as a SCOPE — no partner-authored role can ever
14176
+ // be granted it. It does NOT bar a blueprint's own top-level `namespaces[]`
14177
+ // FORMAT field (@vectros-ai/blueprints), which deliberately DOES write this
14178
+ // same registry — the same "DISCRETIONARY vs UNCONDITIONAL" distinction
14179
+ // {@link PARTIALLY_ADMITTED_OPS} draws for `entities`, applied here: the
14180
+ // write rides `provisioning:c` (non-grantable — no partner role can ever
14181
+ // hold it, the same bearer `keys`/`app-contexts` writes already require)
14182
+ // and the platform confines it to the blueprint's OWN context
14183
+ // unconditionally, never tenant-wide, so the reach is bounded to what the
14184
+ // blueprint already provisions, never the pack's say-so alone. See
14185
+ // `@vectros-ai/blueprints`' `namespaces[]` doc for the full design.
13261
14186
  "namespaces"
13262
14187
  ];
14188
+ var PARTIALLY_ADMITTED_OPS = /* @__PURE__ */ new Map([
14189
+ ["profiles", /* @__PURE__ */ new Set(["r", "u", "d"])]
14190
+ ]);
14191
+ var SELF_QUALIFIED_OPS = /* @__PURE__ */ new Map([
14192
+ ["profiles", /* @__PURE__ */ new Set(["c"])]
14193
+ ]);
14194
+ var UNQUALIFIABLE_WITH_ANY_TAIL = /* @__PURE__ */ new Map([
14195
+ ["profiles", /* @__PURE__ */ new Set(["r"])]
14196
+ ]);
13263
14197
  var VALID_OPS = /* @__PURE__ */ new Set(["c", "r", "u", "d"]);
13264
14198
  var DATA_PLANE_SET = new Set(DATA_PLANE_ALLOWLIST);
13265
14199
  var CONTROL_PLANE_SET = new Set(KNOWN_CONTROL_PLANE_RESOURCES);
@@ -13276,6 +14210,59 @@ ${summary}`
13276
14210
  this.rejected = rejected;
13277
14211
  }
13278
14212
  };
14213
+ var GRANTED_CAPABILITIES_ALLOWLIST = ["member-lifecycle"];
14214
+ var GRANTED_CAPABILITIES_SET = new Set(GRANTED_CAPABILITIES_ALLOWLIST);
14215
+ var CAPABILITIES_HINT = GRANTED_CAPABILITIES_ALLOWLIST.join("/");
14216
+ var CapabilityGateError = class extends Error {
14217
+ rejected;
14218
+ constructor(rejected) {
14219
+ const summary = rejected.map((r) => ` \u2022 ${r.action}: ${r.reason}`).join("\n");
14220
+ super(
14221
+ `Capability gate rejected ${rejected.length} requested capability(ies); the bootstrap loader grants (${CAPABILITIES_HINT}) only and mints nothing on reject:
14222
+ ${summary}`
14223
+ );
14224
+ this.name = "CapabilityGateError";
14225
+ this.rejected = rejected;
14226
+ }
14227
+ };
14228
+ function evaluateCapabilityToken(name) {
14229
+ const token = name.trim();
14230
+ if (token.length === 0) {
14231
+ return { action: name, reason: "empty capability name" };
14232
+ }
14233
+ if (!GRANTED_CAPABILITIES_SET.has(token)) {
14234
+ return {
14235
+ action: name,
14236
+ reason: `requests capability '${token}', which the bootstrap loader does not grant (${CAPABILITIES_HINT} only). A capability outside this list must be requested deliberately in the developer portal, never via the bootstrap loader`
14237
+ };
14238
+ }
14239
+ return null;
14240
+ }
14241
+ function evaluateCapabilityGate(capabilities) {
14242
+ const accepted = [];
14243
+ const rejected = [];
14244
+ for (const name of capabilities ?? []) {
14245
+ const rejection = evaluateCapabilityToken(name);
14246
+ if (rejection) {
14247
+ rejected.push(rejection);
14248
+ } else {
14249
+ accepted.push(name);
14250
+ }
14251
+ }
14252
+ return { ok: rejected.length === 0, accepted, rejected };
14253
+ }
14254
+ function assertCapabilityGate(capabilities) {
14255
+ const result = evaluateCapabilityGate(capabilities);
14256
+ if (!result.ok) {
14257
+ throw new CapabilityGateError(result.rejected);
14258
+ }
14259
+ }
14260
+ function firstInvalidOp(ops) {
14261
+ for (const ch of ops) {
14262
+ if (!VALID_OPS.has(ch)) return ch;
14263
+ }
14264
+ return void 0;
14265
+ }
13279
14266
  function evaluateScopeToken(action) {
13280
14267
  const token = action.trim();
13281
14268
  if (token.length === 0) {
@@ -13297,6 +14284,48 @@ function evaluateScopeToken(action) {
13297
14284
  };
13298
14285
  }
13299
14286
  if (!DATA_PLANE_SET.has(resource)) {
14287
+ const admittedOps = PARTIALLY_ADMITTED_OPS.get(resource);
14288
+ const selfOnlyOps = SELF_QUALIFIED_OPS.get(resource);
14289
+ if (admittedOps || selfOnlyOps) {
14290
+ const admittedHint = [.../* @__PURE__ */ new Set([...admittedOps ?? [], ...selfOnlyOps ?? []])].sort().join("/");
14291
+ if (ops === "*") {
14292
+ return {
14293
+ action,
14294
+ reason: `requests '${resource}:*', which includes operation(s) outside the narrow allowlist (${admittedHint}) admitted for this control-plane resource. Request only the specific operations you need`
14295
+ };
14296
+ }
14297
+ const invalidOp2 = firstInvalidOp(ops);
14298
+ if (invalidOp2) {
14299
+ return {
14300
+ action,
14301
+ reason: `invalid operation character '${invalidOp2}' (valid operations are c, r, u, d, or '*')`
14302
+ };
14303
+ }
14304
+ const qualifier = parts.length > 2 ? parts.slice(2).join(":") : void 0;
14305
+ const isSelfQualified = qualifier === "self";
14306
+ const unqualifiableOps = UNQUALIFIABLE_WITH_ANY_TAIL.get(resource);
14307
+ for (const ch of ops) {
14308
+ if (qualifier !== void 0 && unqualifiableOps?.has(ch)) {
14309
+ return {
14310
+ action,
14311
+ reason: `requests '${resource}:${ch}' alongside a qualifier ('${qualifier}'), but the platform does not resolve a qualifier for '${ch}' on '${resource}' at all \u2014 it will reject the WHOLE entry as malformed, not just ignore the qualifier. Split into separate entries (e.g. '${resource}:${ch}' + the other op(s) with their qualifier).`
14312
+ };
14313
+ }
14314
+ if (admittedOps?.has(ch)) continue;
14315
+ if (selfOnlyOps?.has(ch)) {
14316
+ if (isSelfQualified) continue;
14317
+ return {
14318
+ action,
14319
+ reason: `requests control-plane scope '${resource}:${ch}' without the required ':self' qualifier. A bare or arbitrarily-qualified '${resource}:${ch}' is outside the narrow allowlist (${admittedHint}) admitted for this resource \u2014 only '${resource}:${ch}:self' is. That operation must otherwise be done deliberately in the developer portal, never via the bootstrap loader`
14320
+ };
14321
+ }
14322
+ return {
14323
+ action,
14324
+ reason: `requests control-plane scope '${resource}:${ch}', which is outside the narrow allowlist (${admittedHint}) admitted for this resource. That operation must be done deliberately in the developer portal, never via the bootstrap loader`
14325
+ };
14326
+ }
14327
+ return null;
14328
+ }
13300
14329
  if (CONTROL_PLANE_SET.has(resource)) {
13301
14330
  return {
13302
14331
  action,
@@ -13311,13 +14340,12 @@ function evaluateScopeToken(action) {
13311
14340
  if (ops === "*") {
13312
14341
  return null;
13313
14342
  }
13314
- for (const ch of ops) {
13315
- if (!VALID_OPS.has(ch)) {
13316
- return {
13317
- action,
13318
- reason: `invalid operation character '${ch}' (valid operations are c, r, u, d, or '*')`
13319
- };
13320
- }
14343
+ const invalidOp = firstInvalidOp(ops);
14344
+ if (invalidOp) {
14345
+ return {
14346
+ action,
14347
+ reason: `invalid operation character '${invalidOp}' (valid operations are c, r, u, d, or '*')`
14348
+ };
13321
14349
  }
13322
14350
  return null;
13323
14351
  }
@@ -13340,6 +14368,16 @@ function assertScopeGate(allowedActions) {
13340
14368
  throw new ScopeGateError(result.rejected);
13341
14369
  }
13342
14370
  }
14371
+ function assertBlueprintGates(blueprint) {
14372
+ assertScopeGate(blueprint.accessProfile.allowedActions);
14373
+ assertCapabilityGate(blueprint.accessProfile.capabilities);
14374
+ for (const clauses of Object.values(blueprint.roles ?? {})) {
14375
+ for (const clause of clauses) {
14376
+ assertScopeGate(clause.allowedActions);
14377
+ assertCapabilityGate(clause.capabilities);
14378
+ }
14379
+ }
14380
+ }
13343
14381
 
13344
14382
  // src/machine-id.ts
13345
14383
  var import_node_fs = require("fs");
@@ -13444,6 +14482,68 @@ async function resolveApplyTimeIdentities(blueprint, client, log) {
13444
14482
  function actionOf(created) {
13445
14483
  return created ? "created" : "reconciled";
13446
14484
  }
14485
+ async function registerNamespaceIdempotently(client, contextIdForRequest, decl) {
14486
+ const body = {
14487
+ namespace: decl.namespace,
14488
+ entityBacked: decl.entityBacked,
14489
+ specificityRank: decl.specificityRank,
14490
+ membershipRecordType: decl.membershipRecordType,
14491
+ membershipTargetField: decl.membershipTargetField,
14492
+ membershipLevelField: decl.membershipLevelField,
14493
+ membershipLevels: decl.membershipLevels
14494
+ };
14495
+ try {
14496
+ await client.registerNamespace({ contextId: contextIdForRequest, body });
14497
+ return "created";
14498
+ } catch (err) {
14499
+ const info = extractServerError(err);
14500
+ if (info.statusCode !== 400) throw err;
14501
+ let existing;
14502
+ try {
14503
+ existing = await client.getNamespace({ namespace: decl.namespace, contextId: contextIdForRequest });
14504
+ } catch {
14505
+ throw err;
14506
+ }
14507
+ if (existing.contextId !== contextIdForRequest) throw err;
14508
+ const mismatch = describeNamespaceMismatch(decl, existing);
14509
+ if (mismatch) {
14510
+ throw new Error(
14511
+ `namespace '${decl.namespace}' is already registered with different settings than this blueprint declares (${mismatch}) \u2014 this looks like a collision with another blueprint or a manual registration, not a benign re-apply. Resolve the disagreement (or rename this blueprint's namespace) before re-applying.`
14512
+ );
14513
+ }
14514
+ return "reconciled";
14515
+ }
14516
+ }
14517
+ function describeNamespaceMismatch(decl, existing) {
14518
+ const mismatches = [];
14519
+ const declaredEntityBacked = decl.entityBacked ?? false;
14520
+ const existingEntityBacked = existing.entityBacked ?? false;
14521
+ if (declaredEntityBacked !== existingEntityBacked) {
14522
+ mismatches.push(
14523
+ `entityBacked: declared ${JSON.stringify(declaredEntityBacked)}, registered ${JSON.stringify(existingEntityBacked)}`
14524
+ );
14525
+ }
14526
+ const fields = [
14527
+ "specificityRank",
14528
+ "membershipRecordType",
14529
+ "membershipTargetField",
14530
+ "membershipLevelField"
14531
+ ];
14532
+ for (const field of fields) {
14533
+ const declared = decl[field];
14534
+ const existingValue = existing[field];
14535
+ if (declared === void 0 && (existingValue === void 0 || existingValue === null)) continue;
14536
+ if (declared === existingValue) continue;
14537
+ mismatches.push(`${field}: declared ${JSON.stringify(declared)}, registered ${JSON.stringify(existingValue)}`);
14538
+ }
14539
+ const declaredLevels = decl.membershipLevels;
14540
+ const existingLevels = existing.membershipLevels;
14541
+ const levelsDiffer = declaredLevels === void 0 ? existingLevels != null && existingLevels.length > 0 : JSON.stringify([...declaredLevels].sort()) !== JSON.stringify([...existingLevels ?? []].sort());
14542
+ if (levelsDiffer) {
14543
+ mismatches.push(`membershipLevels: declared ${JSON.stringify(declaredLevels)}, registered ${JSON.stringify(existingLevels)}`);
14544
+ }
14545
+ return mismatches.length > 0 ? mismatches.join("; ") : null;
14546
+ }
13447
14547
  var LostKeyError = class extends Error {
13448
14548
  constructor(keyName) {
13449
14549
  super(
@@ -13454,6 +14554,20 @@ var LostKeyError = class extends Error {
13454
14554
  }
13455
14555
  keyName;
13456
14556
  };
14557
+ var ExistingPrincipalNotConfirmedError = class extends Error {
14558
+ constructor(contextId, userId, externalId) {
14559
+ super(
14560
+ `About to bind a NEW scoped key to a principal that already existed in this tenant before this run (externalId '${externalId}', userId '${userId}', context '${contextId}'). This is either an intentional multi-machine setup for an integration you already own, or this blueprint's servicePrincipal.externalId collided with an unrelated existing identity \u2014 confirm before a new credential is minted bound to it.`
14561
+ );
14562
+ this.contextId = contextId;
14563
+ this.userId = userId;
14564
+ this.externalId = externalId;
14565
+ this.name = "ExistingPrincipalNotConfirmedError";
14566
+ }
14567
+ contextId;
14568
+ userId;
14569
+ externalId;
14570
+ };
13457
14571
  function mapEnumValues(enumValues) {
13458
14572
  if (!enumValues) return void 0;
13459
14573
  return enumValues.map((value) => ({ value }));
@@ -13528,7 +14642,8 @@ function toCapabilities(capabilities) {
13528
14642
  function toRoleScopeClauses(clauses) {
13529
14643
  return clauses.map((c) => ({
13530
14644
  allowed_actions: c.allowedActions,
13531
- data_scope: c.dataScope
14645
+ data_scope: c.dataScope,
14646
+ granted_capabilities: c.capabilities
13532
14647
  }));
13533
14648
  }
13534
14649
  var UnresolvedIdentitiesError = class extends Error {
@@ -13548,11 +14663,18 @@ function assertNoUnresolvedIdentities(blueprint) {
13548
14663
  }
13549
14664
  }
13550
14665
  async function runLoader(client, input) {
13551
- const { blueprint, tenantId, machineId, rotate = false, skipSeed = false, readExistingKey, persistMintedKey, log } = input;
13552
- assertScopeGate(blueprint.accessProfile.allowedActions);
13553
- for (const clauses of Object.values(blueprint.roles ?? {})) {
13554
- for (const clause of clauses) assertScopeGate(clause.allowedActions);
13555
- }
14666
+ const {
14667
+ blueprint,
14668
+ tenantId,
14669
+ machineId,
14670
+ rotate = false,
14671
+ skipSeed = false,
14672
+ readExistingKey,
14673
+ persistMintedKey,
14674
+ confirmExistingPrincipal,
14675
+ log
14676
+ } = input;
14677
+ assertBlueprintGates(blueprint);
13556
14678
  assertNoUnresolvedIdentities(blueprint);
13557
14679
  const ctxResp = await client.auth.createAppContext({ body: { contextId: blueprint.contextId, name: contextNameOf(blueprint) } });
13558
14680
  const contextAction = actionOf(ctxResp.created);
@@ -13570,7 +14692,65 @@ async function runLoader(client, input) {
13570
14692
  if (!userId) {
13571
14693
  throw new Error("Service-principal create returned no user id; cannot bind AccessProfile/key.");
13572
14694
  }
13573
- log.debug({ component: "loader", externalId: blueprint.servicePrincipal.externalId, userId }, "service principal ensured");
14695
+ const userAction = actionOf(user.created);
14696
+ log.debug(
14697
+ { component: "loader", externalId: blueprint.servicePrincipal.externalId, userId, userAction },
14698
+ "service principal ensured"
14699
+ );
14700
+ const localKeyForContext = await readExistingKey(blueprint.contextId);
14701
+ const storedPrincipalId = localKeyForContext?.principalId;
14702
+ const localKeyNamesThisPrincipal = storedPrincipalId === `usr_${userId}` || storedPrincipalId === userId;
14703
+ if (userAction === "reconciled" && !localKeyNamesThisPrincipal) {
14704
+ const proceed = confirmExistingPrincipal ? await confirmExistingPrincipal({
14705
+ contextId: blueprint.contextId,
14706
+ userId,
14707
+ externalId: blueprint.servicePrincipal.externalId
14708
+ }) : true;
14709
+ if (!proceed) {
14710
+ throw new ExistingPrincipalNotConfirmedError(blueprint.contextId, userId, blueprint.servicePrincipal.externalId);
14711
+ }
14712
+ log.debug(
14713
+ { component: "loader", contextId: blueprint.contextId, userId },
14714
+ "existing-principal collision confirmed (or unwired) \u2014 proceeding with any further write to this principal"
14715
+ );
14716
+ }
14717
+ const needsContextPinnedBootstrap = (blueprint.issuers ?? []).length > 0 || (blueprint.namespaces ?? []).length > 0;
14718
+ const ctxBootstrapClient = needsContextPinnedBootstrap ? input.remintBootstrapForContext ? await input.remintBootstrapForContext(blueprint.contextId) : client : void 0;
14719
+ if (ctxBootstrapClient && input.remintBootstrapForContext) {
14720
+ log.debug({ component: "loader", contextId: blueprint.contextId }, "re-minted context-pinned bootstrap client");
14721
+ }
14722
+ const namespaceActions = [];
14723
+ for (const ns of blueprint.namespaces ?? []) {
14724
+ const action = await registerNamespaceIdempotently(ctxBootstrapClient.identity, blueprint.contextId, ns);
14725
+ namespaceActions.push({ namespace: ns.namespace, action });
14726
+ log.debug({ component: "loader", namespace: ns.namespace, contextId: blueprint.contextId, action }, "namespace ensured");
14727
+ }
14728
+ const issuerActions = [];
14729
+ for (const issuer of blueprint.issuers ?? []) {
14730
+ const issuerResp = await ctxBootstrapClient.auth.registerIssuer({
14731
+ issuerId: issuer.issuerId,
14732
+ issuer: issuer.issuer,
14733
+ jwksUri: issuer.jwksUri,
14734
+ audience: issuer.audience,
14735
+ contextId: issuer.contextId,
14736
+ subClaim: issuer.subClaim,
14737
+ emailClaim: issuer.emailClaim,
14738
+ selfSignupPolicies: issuer.selfSignupPolicies?.map((p) => ({
14739
+ signup_type: p.signupType,
14740
+ role_id: p.roleId
14741
+ }))
14742
+ });
14743
+ const action = actionOf(issuerResp.created);
14744
+ issuerActions.push({ issuerId: issuer.issuerId, action });
14745
+ if (action === "reconciled") {
14746
+ log.warn(
14747
+ { component: "loader", issuerId: issuer.issuerId },
14748
+ "issuer already registered \u2014 the platform has no update-issuer endpoint, so any edited fields (jwksUri, audience, subClaim, emailClaim, selfSignupPolicies) on this apply were NOT applied to the existing registration"
14749
+ );
14750
+ } else {
14751
+ log.debug({ component: "loader", issuerId: issuer.issuerId, action }, "issuer registered");
14752
+ }
14753
+ }
13574
14754
  const ctxClient = input.remintForContext ? await input.remintForContext(blueprint.contextId) : client;
13575
14755
  log.debug({ component: "loader", contextId: blueprint.contextId }, "re-minted per-context client");
13576
14756
  const schemaIds = {};
@@ -13617,7 +14797,10 @@ async function runLoader(client, input) {
13617
14797
  // literal JSON null, which is exactly what the platform's scope
13618
14798
  // matcher tests via `allowedValues.contains(null)`. The cast bridges
13619
14799
  // to the SDK's loosely-typed Record<string, Record<string,unknown>>.
13620
- data_scope: blueprint.accessProfile.dataScope
14800
+ data_scope: blueprint.accessProfile.dataScope,
14801
+ // Already gate-validated (assertBlueprintGates, above this function's
14802
+ // call site) before this body is ever built.
14803
+ granted_capabilities: blueprint.accessProfile.capabilities
13621
14804
  }
13622
14805
  ],
13623
14806
  // Optional identity overrides — scope values the service key STAMPS onto
@@ -13675,7 +14858,7 @@ async function runLoader(client, input) {
13675
14858
  keyId = keyResp.keyId ?? "";
13676
14859
  rawKey = keyResp.rawKey;
13677
14860
  if (keyId) {
13678
- await persistMintedKey(blueprint.contextId, { keyId, rawKey, keyName, principalId: userId, revokedKeyId });
14861
+ await persistMintedKey(blueprint.contextId, { keyId, rawKey, keyName, principalId: `usr_${userId}`, revokedKeyId });
13679
14862
  }
13680
14863
  if (keyAction !== "rotated") keyAction = "created";
13681
14864
  } else {
@@ -13749,9 +14932,12 @@ async function runLoader(client, input) {
13749
14932
  keyName,
13750
14933
  keyAction,
13751
14934
  contextAction,
14935
+ userAction,
13752
14936
  profileAction,
13753
14937
  schemaActions,
13754
14938
  roleActions,
14939
+ issuerActions,
14940
+ namespaceActions,
13755
14941
  rawKey,
13756
14942
  keyId,
13757
14943
  revokedKeyId,
@@ -13770,6 +14956,22 @@ function buildPlan(blueprint, opts = {}) {
13770
14956
  kind: "principal",
13771
14957
  summary: `principal '${blueprint.servicePrincipal.externalId}' (SERVICE)`
13772
14958
  });
14959
+ for (const ns of blueprint.namespaces ?? []) {
14960
+ const ownerNote = `owned by '${blueprint.contextId}'`;
14961
+ const entityNote = ns.entityBacked ? ", entity-backed" : "";
14962
+ const membershipNote = ns.membershipRecordType ? `, membership: ${ns.membershipRecordType}.${ns.membershipTargetField}` + (ns.membershipLevelField ? ` (levels: ${ns.membershipLevels?.join("/")})` : "") : "";
14963
+ steps.push({
14964
+ kind: "namespace",
14965
+ summary: `namespace '${ns.namespace}' (${ownerNote}${entityNote}${membershipNote}) [${BLUEPRINT_FIELD_PHASES.namespaces}-token phase]`
14966
+ });
14967
+ }
14968
+ for (const iss of blueprint.issuers ?? []) {
14969
+ const selfSignup = iss.selfSignupPolicies?.length ? `, self-signup: ${iss.selfSignupPolicies.length} polic${iss.selfSignupPolicies.length === 1 ? "y" : "ies"}` : "";
14970
+ steps.push({
14971
+ kind: "issuer",
14972
+ summary: `issuer '${iss.issuerId}' (${iss.issuer} \u2192 context '${iss.contextId}'${selfSignup}) [${BLUEPRINT_FIELD_PHASES.issuers}-token phase]`
14973
+ });
14974
+ }
13773
14975
  for (const s of blueprint.schemas) {
13774
14976
  const lookups = toLookupDefs(s.lookupFields)?.map((l) => l.fieldNames ? `(${l.fieldNames.join("+")})` : l.fieldName) ?? [];
13775
14977
  const lookupNote = lookups.length ? ` lookup: [${lookups.join(", ")}]` : "";
@@ -13785,15 +14987,18 @@ function buildPlan(blueprint, opts = {}) {
13785
14987
  }
13786
14988
  const actions = blueprint.accessProfile.allowedActions;
13787
14989
  const scopeNote = blueprint.accessProfile.dataScope && Object.keys(blueprint.accessProfile.dataScope).length ? ` dataScope: ${Object.keys(blueprint.accessProfile.dataScope).join(", ")}` : "";
14990
+ const capabilityNote = blueprint.accessProfile.capabilities?.length ? ` capabilities: [${blueprint.accessProfile.capabilities.join(", ")}]` : "";
13788
14991
  steps.push({
13789
14992
  kind: "profile",
13790
- summary: `profile scopes [${actions.join(", ")}] (data-plane \u2713)${scopeNote}`
14993
+ summary: `profile scopes [${actions.join(", ")}] (data-plane \u2713)${scopeNote}${capabilityNote}`
13791
14994
  });
13792
14995
  for (const [roleId, clauses] of Object.entries(blueprint.roles ?? {})) {
13793
14996
  const actionSet = [...new Set(clauses.flatMap((c) => c.allowedActions))];
14997
+ const capSet = [...new Set(clauses.flatMap((c) => c.capabilities ?? []))];
14998
+ const roleCapabilityNote = capSet.length ? `, capabilities [${capSet.join(", ")}]` : "";
13794
14999
  steps.push({
13795
15000
  kind: "role",
13796
- summary: `role '${roleId}' (${clauses.length} clause${clauses.length === 1 ? "" : "s"}, actions [${actionSet.join(", ")}])`
15001
+ summary: `role '${roleId}' (${clauses.length} clause${clauses.length === 1 ? "" : "s"}, actions [${actionSet.join(", ")}]${roleCapabilityNote})`
13797
15002
  });
13798
15003
  }
13799
15004
  steps.push({ kind: "key", summary: `key ${blueprint.name}-<machine> (mint 1 scoped ssk_*)` });
@@ -13810,8 +15015,24 @@ function buildPlan(blueprint, opts = {}) {
13810
15015
  }
13811
15016
  function renderPlan(blueprint, steps) {
13812
15017
  const header = ` Plan for blueprint '${blueprint.name}' (v${blueprint.version}) \u2192 context '${blueprint.contextId}':`;
13813
- const legend = ` Apply is a non-destructive reconcile \u2014 each item is created if absent, else
13814
- updated in place to match. Nothing is dropped or recreated.`;
15018
+ const legendLines = [
15019
+ ` Apply is a non-destructive reconcile \u2014 each item is created if absent, else`,
15020
+ ` updated in place to match. Nothing is dropped or recreated.`
15021
+ ];
15022
+ if (steps.some((s) => s.kind === "issuer")) {
15023
+ legendLines.push(
15024
+ ` Exception: issuers cannot be updated once registered \u2014 an existing`,
15025
+ ` issuer is left unchanged, not reconciled to match.`
15026
+ );
15027
+ }
15028
+ if (steps.some((s) => s.kind === "namespace")) {
15029
+ legendLines.push(
15030
+ ` Exception: namespace registration is not idempotent \u2014 a re-apply matching`,
15031
+ ` an existing registration converges silently, but one that disagrees FAILS`,
15032
+ ` the apply rather than reconciling it.`
15033
+ );
15034
+ }
15035
+ const legend = legendLines.join("\n");
13815
15036
  const lines = steps.map((s) => ` + ${s.summary}`);
13816
15037
  return [header, legend, "", ...lines].join("\n");
13817
15038
  }
@@ -14059,6 +15280,15 @@ var OsKeychainSecretStore = class {
14059
15280
  * against the real Windows store on every MR — the dev-machine probe is a point
14060
15281
  * measurement, not a regression gate. (Tracked internally; this file ships to
14061
15282
  * public npm, so no tracker number here.)
15283
+ *
15284
+ * ⚠️ THIS METHOD'S FAIL-CLOSED GUARANTEE IS AN UPSTREAM BEHAVIOURAL CONTRACT, NOT
15285
+ * SOMETHING THIS CODE ENFORCES. `@napi-rs/keyring` is pinned to an EXACT version
15286
+ * in `package.json` for exactly this reason: if a future release returned `[]`
15287
+ * from `findCredentials()` on a locked store instead of throwing, the `catch`
15288
+ * block above would never run — `!.some(...)` on an empty array reads as "gone",
15289
+ * `deleteSecret` returns `true`, and `remove`/`prune` drop the index row of a
15290
+ * still-live credential. A version bump of this dependency is a security review
15291
+ * of the paragraph above against the new release, not a routine dependency bump.
14062
15292
  */
14063
15293
  async deleteSecret(ref) {
14064
15294
  try {
@@ -14992,6 +16222,26 @@ The key itself WAS created server-side and is live \u2014 its id is ${entry.keyI
14992
16222
  const entry = await this.getActive();
14993
16223
  return entry ? this.readSecret(entry) : void 0;
14994
16224
  }
16225
+ /**
16226
+ * The ACTIVE entry's tenant (test/live), or undefined if there is no active
16227
+ * entry or its tenant cannot be determined — WITHOUT migrating its secret.
16228
+ *
16229
+ * Deliberately NOT built on {@link resolveActiveSecret}/{@link readSecret}:
16230
+ * both migrate the secret to this machine's preferred store as a side effect
16231
+ * of reading it, and a caller that only wants to CLASSIFY the active identity
16232
+ * (e.g. "is it safe to activate a different one over it?") must not silently
16233
+ * relocate — and take the cross-process index lock for — a credential it has
16234
+ * no other reason to touch. Same non-migrating shape {@link assess} already
16235
+ * uses per entry (`store.getSecret` directly, never `readSecret`).
16236
+ */
16237
+ async activeTenant() {
16238
+ const entry = await this.getActive();
16239
+ if (!entry) return void 0;
16240
+ const store = this.storeFor(entry);
16241
+ if (!store) return void 0;
16242
+ const secret = await store.getSecret(entry.secretRef);
16243
+ return secret !== void 0 ? keyTenant(secret) : void 0;
16244
+ }
14995
16245
  /**
14996
16246
  * Read one entry's secret, moving it to this machine's preferred store if it is
14997
16247
  * somewhere else.
@@ -15118,26 +16368,31 @@ var readline = __toESM(require("readline/promises"));
15118
16368
  async function runKeyringList(opts, deps = {}) {
15119
16369
  try {
15120
16370
  const keyring = deps.keyring ?? await Keyring.open();
15121
- const entries = await keyring.list();
15122
- const active = await keyring.activeAlias();
15123
16371
  if (opts.json) {
16372
+ const entries = await keyring.list();
16373
+ const active = await keyring.activeAlias();
15124
16374
  process.stdout.write(`${formatJson({ active, entries })}
15125
16375
  `);
15126
16376
  return 0;
15127
16377
  }
15128
- if (entries.length === 0) {
16378
+ const { health } = await assessEntries(keyring);
16379
+ if (health.length === 0) {
15129
16380
  process.stdout.write("\n (no keyring entries yet \u2014 run `vectros join <context>` to add one)\n");
15130
16381
  return 0;
15131
16382
  }
15132
16383
  process.stdout.write(
15133
- "\n" + formatTable(entries, [
15134
- { header: "", get: (e) => e.alias === active ? "*" : "" },
15135
- { header: "alias", get: (e) => e.alias },
15136
- { header: "context", get: (e) => e.context },
15137
- { header: "principal", get: (e) => e.principalId },
15138
- { header: "role", get: (e) => e.role },
15139
- { header: "env", get: (e) => e.env },
15140
- { header: "key id", get: (e) => e.keyId }
16384
+ "\n" + formatTable(health, [
16385
+ { header: "", get: (h) => h.active ? "*" : "" },
16386
+ { header: "alias", get: (h) => h.entry.alias },
16387
+ { header: "context", get: (h) => h.entry.context },
16388
+ { header: "principal", get: (h) => h.entry.principalId },
16389
+ { header: "role", get: (h) => h.entry.role },
16390
+ // `tenant` (test/live) replaces the old `env` (staging/production) column
16391
+ // here: env is an internal-team concern an external developer's CLI never
16392
+ // sees, while tenant is the distinction every user routinely holds two
16393
+ // credentials side by side over.
16394
+ { header: "tenant", get: (h) => tenantCell(h, { upper: true }) },
16395
+ { header: "key id", get: (h) => h.entry.keyId }
15141
16396
  ]) + "\n"
15142
16397
  );
15143
16398
  return 0;
@@ -15319,6 +16574,10 @@ async function assessEntries(keyring) {
15319
16574
  }))
15320
16575
  };
15321
16576
  }
16577
+ function tenantCell(h, opts) {
16578
+ if (h.status !== "ok") return void 0;
16579
+ return h.tenant ?? (opts.upper ? "UNKNOWN" : "unknown");
16580
+ }
15322
16581
  function describeEntry(h) {
15323
16582
  return `${h.entry.alias} (${h.entry.context}, ${h.entry.keyId})`;
15324
16583
  }
@@ -15463,9 +16722,7 @@ async function runKeyringDoctor(opts, deps = {}) {
15463
16722
  // `unknown` can escalate; one that sees a confident wrong answer cannot.
15464
16723
  store: storeKindOf(h.entry) ?? "unknown",
15465
16724
  secret: h.status,
15466
- // `unknown` rather than absent: a consumer must be able to tell "not a
15467
- // live key" from "we could not read the shape of this one".
15468
- tenant: h.status === "ok" ? h.tenant ?? "unknown" : void 0
16725
+ tenant: tenantCell(h, { upper: false })
15469
16726
  })),
15470
16727
  liveKeys: liveKeys.map((h) => h.entry.alias),
15471
16728
  findings
@@ -15480,7 +16737,7 @@ async function runKeyringDoctor(opts, deps = {}) {
15480
16737
  { header: "", get: (h) => h.active ? "*" : "" },
15481
16738
  { header: "alias", get: (h) => h.entry.alias },
15482
16739
  { header: "context", get: (h) => h.entry.context },
15483
- { header: "tenant", get: (h) => h.status === "ok" ? h.tenant ?? "UNKNOWN" : void 0 },
16740
+ { header: "tenant", get: (h) => tenantCell(h, { upper: true }) },
15484
16741
  { header: "key id", get: (h) => h.entry.keyId },
15485
16742
  { header: "secret", get: (h) => h.status === "ok" ? "ok" : h.status.toUpperCase() }
15486
16743
  ]) + "\n"
@@ -15666,6 +16923,55 @@ function revokeWarning(targets) {
15666
16923
  }
15667
16924
 
15668
16925
  // src/minted-key-store.ts
16926
+ function keyringAliasFor(alias, tenant) {
16927
+ if (tenant === "live" && alias.endsWith(".test")) {
16928
+ throw new Error(
16929
+ `'${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.`
16930
+ );
16931
+ }
16932
+ return tenant === "test" ? `${alias}.test` : alias;
16933
+ }
16934
+ function resolveActivateDecision(opts) {
16935
+ if (opts.explicitActivate !== void 0) return opts.explicitActivate;
16936
+ if (opts.tenant === "live") return true;
16937
+ return opts.currentActiveTenant !== "live";
16938
+ }
16939
+ async function determineActivateDecision(keyring, opts) {
16940
+ if (opts.explicitActivate !== void 0 || opts.tenant === "live") {
16941
+ return resolveActivateDecision(opts);
16942
+ }
16943
+ let currentActiveTenant;
16944
+ try {
16945
+ if (opts.hint && opts.hint.alias === await keyring.activeAlias()) {
16946
+ currentActiveTenant = keyTenant(opts.hint.secret);
16947
+ } else {
16948
+ currentActiveTenant = await keyring.activeTenant();
16949
+ }
16950
+ } catch {
16951
+ }
16952
+ return resolveActivateDecision({ ...opts, currentActiveTenant });
16953
+ }
16954
+ function tenantActivateMessage(opts) {
16955
+ if (opts.willActivate) {
16956
+ if (opts.tenant !== "test") return "";
16957
+ return `
16958
+ \u26A0 '${opts.alias}' is now your ACTIVE identity \u2014 a TEST tenant credential. Anything
16959
+ resolving a key with no explicit --alias (including the MCP server) follows it.
16960
+ `;
16961
+ }
16962
+ if (opts.explicitlyDeclined) {
16963
+ return `
16964
+ \u26A0 Stored '${opts.alias}' (${opts.tenant} tenant) without activating it \u2014 you passed --no-activate.
16965
+ Run \`vectros switch ${opts.alias}\` when you're ready to use it.
16966
+ `;
16967
+ }
16968
+ return `
16969
+ \u26A0 Stored '${opts.alias}' (test tenant) WITHOUT activating it \u2014 your currently-active
16970
+ identity is a LIVE credential, and switching it out from under you silently is
16971
+ exactly the mistake this check exists to prevent. Run \`vectros switch ${opts.alias}\`
16972
+ to use the test key, or re-run with --activate to do so automatically.
16973
+ `;
16974
+ }
15669
16975
  function nowIso() {
15670
16976
  return (/* @__PURE__ */ new Date()).toISOString();
15671
16977
  }
@@ -15762,10 +17068,7 @@ async function storeMintedKey(keyring, binding, secret, opts = {}) {
15762
17068
  // src/cli-bootstrap.ts
15763
17069
  function validateBlueprint(input) {
15764
17070
  const blueprint = parseBlueprint(input);
15765
- assertScopeGate(blueprint.accessProfile.allowedActions);
15766
- for (const clauses of Object.values(blueprint.roles ?? {})) {
15767
- for (const clause of clauses) assertScopeGate(clause.allowedActions);
15768
- }
17071
+ assertBlueprintGates(blueprint);
15769
17072
  return blueprint;
15770
17073
  }
15771
17074
  var BLANK_SLATE = {
@@ -15782,10 +17085,12 @@ var BLANK_SLATE = {
15782
17085
  };
15783
17086
  function parseBootstrapArgs(argv) {
15784
17087
  const args = {
17088
+ set: [],
15785
17089
  env: "production",
15786
17090
  yes: false,
15787
17091
  noMerge: false,
15788
17092
  rotate: false,
17093
+ confirmExistingPrincipal: false,
15789
17094
  useScopedToken: false,
15790
17095
  blankSlate: false,
15791
17096
  noSeed: false,
@@ -15799,6 +17104,12 @@ function parseBootstrapArgs(argv) {
15799
17104
  case "--blueprint":
15800
17105
  args.blueprint = argv[++i];
15801
17106
  break;
17107
+ case "--set":
17108
+ args.set.push(argv[++i]);
17109
+ break;
17110
+ case "--values":
17111
+ args.values = argv[++i];
17112
+ break;
15802
17113
  case "--env": {
15803
17114
  const v = argv[++i];
15804
17115
  if (v !== "staging" && v !== "production") {
@@ -15831,6 +17142,9 @@ function parseBootstrapArgs(argv) {
15831
17142
  case "--rotate":
15832
17143
  args.rotate = true;
15833
17144
  break;
17145
+ case "--confirm-existing-principal":
17146
+ args.confirmExistingPrincipal = true;
17147
+ break;
15834
17148
  case "--use-scoped-token":
15835
17149
  args.useScopedToken = true;
15836
17150
  break;
@@ -15852,6 +17166,12 @@ function parseBootstrapArgs(argv) {
15852
17166
  case "--test":
15853
17167
  args.tenant = "test";
15854
17168
  break;
17169
+ case "--activate":
17170
+ args.activate = true;
17171
+ break;
17172
+ case "--no-activate":
17173
+ args.activate = false;
17174
+ break;
15855
17175
  case "--help":
15856
17176
  case "-h":
15857
17177
  args.help = true;
@@ -15869,6 +17189,26 @@ function seedDeclined(answer) {
15869
17189
  const a = answer.trim().toLowerCase();
15870
17190
  return a === "n" || a === "no";
15871
17191
  }
17192
+ function existingPrincipalConfirmed(answer) {
17193
+ const a = answer.trim().toLowerCase();
17194
+ return a === "y" || a === "yes";
17195
+ }
17196
+ function buildConfirmExistingPrincipal(args, prompt, isTty) {
17197
+ return async (details) => {
17198
+ if (!args.yes && isTty) {
17199
+ const ans = await prompt(
17200
+ `
17201
+ \u26A0 '${details.externalId}' already exists in your tenant (context '${details.contextId}') \u2014 this key will bind to that EXISTING identity, not a new one.
17202
+ Expected if you're re-applying a blueprint from a new machine, or setting up a second
17203
+ credential for an integration you already own. If you don't recognize this identity,
17204
+ do NOT proceed \u2014 this may be a name collision with something you don't control.
17205
+ Proceed and bind to the existing identity? [y/N]: `
17206
+ );
17207
+ return existingPrincipalConfirmed(ans);
17208
+ }
17209
+ return args.confirmExistingPrincipal;
17210
+ };
17211
+ }
15872
17212
  function seedSummaryLine(opts) {
15873
17213
  if (opts.noSeed) {
15874
17214
  return opts.declinedInteractively ? " \u2714 Seeded 0 record(s) \u2014 skipped (you declined sample data)\n" : " \u2714 Seeded 0 record(s) \u2014 skipped (--no-seed)\n";
@@ -15896,22 +17236,45 @@ function reconcileTally(items, noun) {
15896
17236
  if (reconciled) seg.push(`${reconciled} reconciled`);
15897
17237
  return `${items.length} ${noun}${items.length === 1 ? "" : "s"} (${seg.join(", ")})`;
15898
17238
  }
17239
+ function issuerTally(items) {
17240
+ if (items.length === 0) return null;
17241
+ const created = items.filter((i) => i.action === "created").length;
17242
+ const unchanged = items.length - created;
17243
+ const seg = [];
17244
+ if (created) seg.push(`${created} created`);
17245
+ if (unchanged) seg.push(`${unchanged} already registered \u2014 unchanged (issuers cannot be updated once registered)`);
17246
+ return `${items.length} issuer${items.length === 1 ? "" : "s"} (${seg.join(", ")})`;
17247
+ }
17248
+ function namespaceTally(items) {
17249
+ if (items.length === 0) return null;
17250
+ const created = items.filter((i) => i.action === "created").length;
17251
+ const matched = items.length - created;
17252
+ const seg = [];
17253
+ if (created) seg.push(`${created} created`);
17254
+ if (matched) seg.push(`${matched} already registered \u2014 confirmed matching`);
17255
+ return `${items.length} namespace${items.length === 1 ? "" : "s"} (${seg.join(", ")})`;
17256
+ }
15899
17257
  function applyOutcomeSummary(result) {
15900
17258
  const parts = [
15901
17259
  reconcileTally(result.schemaActions, "schema"),
15902
17260
  reconcileTally(result.roleActions, "role"),
17261
+ issuerTally(result.issuerActions),
17262
+ namespaceTally(result.namespaceActions),
15903
17263
  `profile ${result.profileAction}`
15904
17264
  ].filter((p) => p !== null);
15905
17265
  const detail = parts.length ? ` \u2014 ${parts.join("; ")}` : "";
17266
+ const principalNote = result.userAction === "reconciled" ? ` \u26A0 service principal already existed in this tenant before this run (not newly created)
17267
+ ` : "";
15906
17268
  return ` \u2714 context '${result.contextId}' ${result.contextAction}${detail}
15907
- `;
17269
+ ${principalNote}`;
15908
17270
  }
15909
17271
  var BOOTSTRAP_HELP = `vectros bootstrap \u2014 provision a least-privilege MCP credential.
15910
17272
 
15911
17273
  Usage:
15912
17274
  vectros bootstrap [--blueprint <name> | --blank] [--no-seed]
17275
+ [--set name=value ...] [--values <file>]
15913
17276
  [--tenant test|live | --test] [--client desktop|code]
15914
- [--yes] [--no-merge] [--rotate]
17277
+ [--yes] [--no-merge] [--rotate] [--confirm-existing-principal]
15915
17278
  [--token <t> | env VECTROS_BOOTSTRAP_TOKEN]
15916
17279
  [--base-url <url>] [--use-scoped-token]
15917
17280
 
@@ -15919,6 +17282,10 @@ Options:
15919
17282
  --blueprint <name|file> A bundled blueprint (${BLUEPRINT_NAMES.join(", ")}),
15920
17283
  OR a path to your own blueprint YAML/JSON (./my.yaml) \u2014 the "copy &
15921
17284
  own" path. File blueprints get the identical scope gate (no override).
17285
+ --set name=value Set an install-time input value (repeatable) for a file blueprint's
17286
+ 'inputs:' block. Beats --values + the declared default. (Same flag as
17287
+ 'vectros blueprint validate/plan'.)
17288
+ --values <file> A YAML/JSON map of input values. (File blueprints only.)
15922
17289
  --blank Credential only (read-only data-plane profile, no schemas).
15923
17290
  --no-seed Provision the blueprint's schemas, profile and key but SKIP its
15924
17291
  sample seed records \u2014 the right choice for a production context
@@ -15930,6 +17297,10 @@ Options:
15930
17297
  The tenant rides the key (ssk_test_* vs ssk_live_*), so
15931
17298
  the MCP/client config is otherwise identical.
15932
17299
  --test Shorthand for --tenant test.
17300
+ --activate Force the freshly-stored key to become your active identity, even
17301
+ if that would switch away from a currently-active LIVE credential
17302
+ (the one case --tenant test does NOT do this automatically).
17303
+ --no-activate Store the key without activating it, regardless of tenant.
15933
17304
  --base-url <url> Override the API base URL (beats VECTROS_API_BASE_URL).
15934
17305
  --client <desktop|code> MCP host to configure (default: desktop). 'desktop' merges
15935
17306
  claude_desktop_config.json (also works for Cursor/Cline);
@@ -15944,6 +17315,13 @@ Options:
15944
17315
  print the config snippet instead of merging it into your client's
15945
17316
  config file. Not a dry run; see 'blueprint plan' for that.
15946
17317
  --rotate Revoke + re-mint THIS machine's key.
17318
+ --confirm-existing-principal
17319
+ Required (with --yes, or with no TTY) to mint a NEW key bound to a
17320
+ service principal that already existed in your tenant before this run
17321
+ \u2014 e.g. a deliberate second machine for an integration you already own.
17322
+ Refused by default so an unexpected collision halts a CI run loudly
17323
+ instead of silently minting a credential bound to the wrong identity.
17324
+ Never needed for --rotate or an ordinary same-machine re-apply.
15947
17325
  --use-scoped-token Legacy fallback.
15948
17326
  NOTE: it mints a token WITHOUT the provisioning
15949
17327
  capability, so it cannot create a blueprint's
@@ -15953,10 +17331,21 @@ Options:
15953
17331
 
15954
17332
  No --blueprint/--blank in a TTY \u2192 interactive menu.`;
15955
17333
  async function resolveBlueprint(args, prompt, isTty) {
15956
- if (args.blankSlate) return BLANK_SLATE;
17334
+ if (args.blankSlate) {
17335
+ if (args.set.length > 0 || args.values) {
17336
+ throw new Error(`--set/--values apply only to file blueprints; --blank has no inputs to set.`);
17337
+ }
17338
+ return BLANK_SLATE;
17339
+ }
15957
17340
  if (args.blueprint) {
15958
17341
  if (looksLikePath(args.blueprint)) {
15959
- return await loadBlueprintFile(args.blueprint);
17342
+ const supplied = await buildInputSupply(args.set, args.values);
17343
+ return await loadBlueprintFile(args.blueprint, supplied);
17344
+ }
17345
+ if (args.set.length > 0 || args.values) {
17346
+ throw new Error(
17347
+ `--set/--values apply only to file blueprints; '${args.blueprint}' is a bundled blueprint (no inputs to set).`
17348
+ );
15960
17349
  }
15961
17350
  const b = getBlueprint(args.blueprint);
15962
17351
  if (!b) {
@@ -15969,6 +17358,11 @@ async function resolveBlueprint(args, prompt, isTty) {
15969
17358
  if (!isTty) {
15970
17359
  throw new Error("No --blueprint or --blank specified and not a TTY (cannot show the menu).");
15971
17360
  }
17361
+ if (args.set.length > 0 || args.values) {
17362
+ throw new Error(
17363
+ `--set/--values need an explicit --blueprint <file>; the interactive menu only offers bundled blueprints and blank slate, neither of which has inputs to set.`
17364
+ );
17365
+ }
15972
17366
  const options = [
15973
17367
  { label: "Blank slate (credential only)", blueprint: BLANK_SLATE },
15974
17368
  ...BUNDLED_BLUEPRINTS.map((b) => ({ label: `${b.name} \u2014 ${b.description}`, blueprint: b }))
@@ -16015,6 +17409,22 @@ function buildRemintForContext(opts) {
16015
17409
  });
16016
17410
  };
16017
17411
  }
17412
+ function buildRemintBootstrapForContext(opts) {
17413
+ const mintBridge = opts.mintBridge ?? mintBridgeToken;
17414
+ return async (contextId) => {
17415
+ const ctxToken = await mintBridge({
17416
+ cognitoToken: opts.cognitoToken,
17417
+ apiBaseUrl: opts.apiBaseUrl,
17418
+ contextId,
17419
+ tenant: opts.tenant,
17420
+ useScopedToken: opts.useScopedToken
17421
+ });
17422
+ return new VectrosClient({
17423
+ token: ctxToken.token,
17424
+ environment: opts.apiBaseUrl
17425
+ });
17426
+ };
17427
+ }
16018
17428
  async function runBootstrapCli(argv, deps = {}) {
16019
17429
  const log = deps.log ?? createLogger2();
16020
17430
  let args;
@@ -16048,6 +17458,15 @@ ${BOOTSTRAP_HELP}
16048
17458
  \u2716 Scope gate refused blueprint '${blueprint.name}':
16049
17459
  `);
16050
17460
  for (const r of err.rejected) process.stderr.write(` \u2022 ${r.action}: ${r.reason}
17461
+ `);
17462
+ process.stderr.write(" Minting nothing.\n");
17463
+ return 2;
17464
+ }
17465
+ if (err instanceof CapabilityGateError) {
17466
+ process.stderr.write(`
17467
+ \u2716 Capability gate refused blueprint '${blueprint.name}':
17468
+ `);
17469
+ for (const r of err.rejected) process.stderr.write(` \u2022 ${r.action}: ${r.reason}
16051
17470
  `);
16052
17471
  process.stderr.write(" Minting nothing.\n");
16053
17472
  return 2;
@@ -16059,6 +17478,11 @@ ${BOOTSTRAP_HELP}
16059
17478
  Blueprint: ${blueprint.name}
16060
17479
  \u2714 Scope gate: [${gate.accepted.join(", ")}] \u2014 all data-plane \u2713
16061
17480
  `);
17481
+ if (blueprint.accessProfile.capabilities?.length) {
17482
+ const capGate = evaluateCapabilityGate(blueprint.accessProfile.capabilities);
17483
+ process.stdout.write(` \u2714 Capability gate: [${capGate.accepted.join(", ")}] \u2014 granted \u2713
17484
+ `);
17485
+ }
16062
17486
  let noSeed = args.noSeed;
16063
17487
  let seedDeclinedInteractively = false;
16064
17488
  if (shouldOfferSeedOptOut(args, blueprint, isTty)) {
@@ -16132,13 +17556,16 @@ ${renderPlan(blueprint, planSteps)}
16132
17556
  keyringOpenError = err;
16133
17557
  }
16134
17558
  let storedFreshInKeyring = false;
17559
+ const lastResolvedSecret = /* @__PURE__ */ new Map();
16135
17560
  const readExistingKey = async (contextId) => {
16136
17561
  if (!keyring) return void 0;
17562
+ const alias = keyringAliasFor(contextId, args.tenant);
16137
17563
  try {
16138
- const existing = await keyring.get(contextId);
17564
+ const existing = await keyring.get(alias);
16139
17565
  if (!existing?.keyId) return void 0;
16140
- const rawKey = await keyring.resolveSecret(contextId);
16141
- return rawKey ? { keyId: existing.keyId, rawKey } : void 0;
17566
+ const rawKey = await keyring.resolveSecret(alias);
17567
+ lastResolvedSecret.set(alias, rawKey);
17568
+ return rawKey ? { keyId: existing.keyId, rawKey, principalId: existing.principalId } : void 0;
16142
17569
  } catch {
16143
17570
  return void 0;
16144
17571
  }
@@ -16154,17 +17581,36 @@ ${renderPlan(blueprint, planSteps)}
16154
17581
  );
16155
17582
  return;
16156
17583
  }
17584
+ const alias = keyringAliasFor(contextId, args.tenant);
16157
17585
  try {
16158
- warnReplacingAlias(await keyring.get(contextId), contextId, record.revokedKeyId);
17586
+ warnReplacingAlias(await keyring.get(alias), alias, record.revokedKeyId);
17587
+ const willActivate = await determineActivateDecision(keyring, {
17588
+ tenant: args.tenant,
17589
+ explicitActivate: args.activate,
17590
+ hint: lastResolvedSecret.has(alias) ? { alias, secret: lastResolvedSecret.get(alias) } : void 0
17591
+ });
16159
17592
  await storeMintedKey(
16160
17593
  keyring,
16161
- { alias: contextId, context: contextId, principalId: record.principalId, keyId: record.keyId, env: args.env },
17594
+ {
17595
+ alias,
17596
+ context: contextId,
17597
+ principalId: record.principalId,
17598
+ keyId: record.keyId,
17599
+ // Derived from the REQUEST's actual baseUrl, not echoed from args.env — the two
17600
+ // diverge whenever VECTROS_API_BASE_URL/--base-url alone selected the environment.
17601
+ // `keyring list`/`keyring doctor` display this field; a stale echo would show
17602
+ // "env: production" next to a key this run actually minted against staging.
17603
+ env: deriveEnvironment(baseUrl) ?? args.env
17604
+ },
16162
17605
  record.rawKey,
16163
- { activate: true, revokedKeyId: record.revokedKeyId }
17606
+ { activate: willActivate, revokedKeyId: record.revokedKeyId }
16164
17607
  );
16165
17608
  if (record.revokedKeyId) {
16166
- await warnOtherAliasesNaming(keyring, record.revokedKeyId, contextId);
17609
+ await warnOtherAliasesNaming(keyring, record.revokedKeyId, alias);
16167
17610
  }
17611
+ process.stdout.write(
17612
+ tenantActivateMessage({ alias, tenant: args.tenant, willActivate, explicitlyDeclined: args.activate === false })
17613
+ );
16168
17614
  storedFreshInKeyring = true;
16169
17615
  } catch (err) {
16170
17616
  process.stderr.write(
@@ -16185,12 +17631,20 @@ ${renderPlan(blueprint, planSteps)}
16185
17631
  skipSeed: noSeed,
16186
17632
  readExistingKey,
16187
17633
  persistMintedKey,
17634
+ remintBootstrapForContext: buildRemintBootstrapForContext({
17635
+ cognitoToken,
17636
+ apiBaseUrl: baseUrl,
17637
+ tenant: args.tenant,
17638
+ useScopedToken: args.useScopedToken,
17639
+ mintBridge
17640
+ }),
16188
17641
  remintForContext: buildRemintForContext({
16189
17642
  cognitoToken,
16190
17643
  apiBaseUrl: baseUrl,
16191
17644
  tenant: args.tenant,
16192
17645
  mintContext
16193
17646
  }),
17647
+ confirmExistingPrincipal: buildConfirmExistingPrincipal(args, promptCtx.prompt, isTty),
16194
17648
  log
16195
17649
  });
16196
17650
  process.stdout.write(
@@ -16208,7 +17662,7 @@ ${renderPlan(blueprint, planSteps)}
16208
17662
  );
16209
17663
  entry = buildVectrosServerEntry({ apiKey: result.rawKey, apiBaseUrl: baseUrl });
16210
17664
  } else if (storedFreshInKeyring || result.keyAction === "reused") {
16211
- entry = buildVectrosServerEntry({ alias: blueprint.contextId, apiBaseUrl: baseUrl });
17665
+ entry = buildVectrosServerEntry({ alias: keyringAliasFor(blueprint.contextId, args.tenant), apiBaseUrl: baseUrl });
16212
17666
  } else {
16213
17667
  entry = buildVectrosServerEntry({ apiKey: result.rawKey, apiBaseUrl: baseUrl });
16214
17668
  }
@@ -16268,6 +17722,18 @@ ${codeAddHint}`);
16268
17722
  `);
16269
17723
  return 3;
16270
17724
  }
17725
+ if (err instanceof ExistingPrincipalNotConfirmedError) {
17726
+ process.stderr.write(
17727
+ `
17728
+ \u2716 ${err.message}
17729
+
17730
+ If this is expected (a re-apply, or a deliberate second machine for an integration you already own),
17731
+ re-run with --confirm-existing-principal, or confirm interactively.
17732
+ If you don't recognize this identity, investigate the blueprint's servicePrincipal.externalId before re-running.
17733
+ `
17734
+ );
17735
+ return 3;
17736
+ }
16271
17737
  if (err instanceof BlueprintIdentityError || err instanceof UnresolvedIdentitiesError) {
16272
17738
  process.stderr.write(`
16273
17739
  \u2716 ${err.message}
@@ -16291,7 +17757,7 @@ ${formatCliError(err, { action: "Bootstrap failed" })}`);
16291
17757
  var import_promises3 = require("fs/promises");
16292
17758
 
16293
17759
  // src/scope-grammar.ts
16294
- var FORBIDDEN_SCOPE_NAMESPACES = /* @__PURE__ */ new Set([
17760
+ var FORBIDDEN_SCOPE_NAMESPACES2 = /* @__PURE__ */ new Set([
16295
17761
  "user",
16296
17762
  "record",
16297
17763
  "document",
@@ -16303,23 +17769,23 @@ var FORBIDDEN_SCOPE_NAMESPACES = /* @__PURE__ */ new Set([
16303
17769
  "versions",
16304
17770
  "lookup"
16305
17771
  ]);
16306
- var SCOPE_NAMESPACE_RE = /^[a-z][a-z0-9_-]{1,31}$/;
17772
+ var SCOPE_NAMESPACE_RE2 = /^[a-z][a-z0-9_-]{1,31}$/;
16307
17773
  var SCOPE_NAMESPACE_MIN_LENGTH = 2;
16308
17774
  var SCOPE_NAMESPACE_MAX_LENGTH = 32;
16309
- var SCOPE_VALUE_RE = /^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$/;
17775
+ var SCOPE_VALUE_RE2 = /^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$/;
16310
17776
  var SCOPE_VALUE_MIN_LENGTH = 1;
16311
17777
  var SCOPE_VALUE_MAX_LENGTH = 128;
16312
17778
  function valueGrammarError(value) {
16313
- if (!SCOPE_VALUE_RE.test(value)) {
17779
+ if (!SCOPE_VALUE_RE2.test(value)) {
16314
17780
  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.`;
16315
17781
  }
16316
17782
  return null;
16317
17783
  }
16318
17784
  function namespaceGrammarError(ns) {
16319
- if (FORBIDDEN_SCOPE_NAMESPACES.has(ns)) {
17785
+ if (FORBIDDEN_SCOPE_NAMESPACES2.has(ns)) {
16320
17786
  return `'${ns}' is a reserved namespace and cannot be used as an ownership scope`;
16321
17787
  }
16322
- if (!SCOPE_NAMESPACE_RE.test(ns)) {
17788
+ if (!SCOPE_NAMESPACE_RE2.test(ns)) {
16323
17789
  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 '-'`;
16324
17790
  }
16325
17791
  return null;
@@ -16488,6 +17954,10 @@ function lintBlueprint(bp) {
16488
17954
  }
16489
17955
  }
16490
17956
  }
17957
+ {
17958
+ const reached = ownerScopedReachWithoutDataScope(bp.accessProfile.allowedActions, ds);
17959
+ if (reached.length > 0) findings.push(unconstrainedReachFinding("accessProfile", reached));
17960
+ }
16491
17961
  for (const roleId of Object.keys(bp.roles ?? {})) {
16492
17962
  if (!ROLE_ID_RE.test(roleId)) {
16493
17963
  findings.push({
@@ -16500,6 +17970,10 @@ function lintBlueprint(bp) {
16500
17970
  for (const [roleId, clauses] of Object.entries(bp.roles ?? {})) {
16501
17971
  clauses.forEach((clause, i) => {
16502
17972
  const cds = clause.dataScope;
17973
+ const reached = ownerScopedReachWithoutDataScope(clause.allowedActions, cds);
17974
+ if (reached.length > 0) {
17975
+ findings.push(unconstrainedReachFinding(`role '${roleId}' clause ${i}`, reached));
17976
+ }
16503
17977
  if (!cds) return;
16504
17978
  for (const [key, values] of Object.entries(cds)) {
16505
17979
  for (const value of values) {
@@ -16516,6 +17990,17 @@ function lintBlueprint(bp) {
16516
17990
  }
16517
17991
  });
16518
17992
  }
17993
+ for (const issuer of bp.issuers ?? []) {
17994
+ for (const policy of issuer.selfSignupPolicies ?? []) {
17995
+ if (!Object.prototype.hasOwnProperty.call(bp.roles ?? {}, policy.roleId)) {
17996
+ findings.push({
17997
+ severity: "warning",
17998
+ rule: "self-signup-role-unknown",
17999
+ 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.`
18000
+ });
18001
+ }
18002
+ }
18003
+ }
16519
18004
  return findings;
16520
18005
  }
16521
18006
  var ROLE_ID_RE = /^[a-z][a-z0-9-]{2,30}$/;
@@ -16541,6 +18026,25 @@ function placeholderFinding(where, key, value) {
16541
18026
  message: `${where}: dataScope.${key} value '${value}' is not a placeholder the platform recognizes \u2014 it will be rejected at apply. Valid: \${{ any }}, \${{ self.userId }}, \${{ self.scope.<namespace> }}, \${{ under.self.userId }}, \${{ under.self.scope.<namespace> }}. Spelling is exact and lowercase; use a plain value for a literal owner id.`
16542
18027
  };
16543
18028
  }
18029
+ var OWNER_SCOPED_RESOURCES = /* @__PURE__ */ new Set(["records", "documents", "entities"]);
18030
+ var UNCONSTRAINED_REACH_OPS = /* @__PURE__ */ new Set(["r", "u", "d"]);
18031
+ function ownerScopedReachWithoutDataScope(allowedActions, dataScope) {
18032
+ const reached = /* @__PURE__ */ new Set();
18033
+ for (const action of allowedActions) {
18034
+ const [resource, ops] = action.split(":", 2);
18035
+ if (!resource || !ops || !OWNER_SCOPED_RESOURCES.has(resource)) continue;
18036
+ if (ops === "*" || [...ops].some((op) => UNCONSTRAINED_REACH_OPS.has(op))) reached.add(resource);
18037
+ }
18038
+ if (dataScope !== void 0) return [];
18039
+ return [...reached].sort();
18040
+ }
18041
+ function unconstrainedReachFinding(where, resources) {
18042
+ return {
18043
+ severity: "warning",
18044
+ rule: "datascope-absent-unconstrained-reach",
18045
+ message: `${where}: no dataScope at all, but allowedActions grants r/u/d on ${resources.join("/")} \u2014 this reaches EVERY row of EVERY owner on ${resources.length > 1 ? "those resources" : "that resource"}, not just "your own org" or any other value you may have intended. If that's deliberate (a genuinely shared/tenant-wide resource), you can ignore this; otherwise add a dataScope entry.`
18046
+ };
18047
+ }
16544
18048
  function lintPasses(findings) {
16545
18049
  return !findings.some((f) => f.severity === "error");
16546
18050
  }
@@ -16579,8 +18083,12 @@ schemas:
16579
18083
  lookupFields: [title] # extra O(1) exact-match lookup indexes (max 10)
16580
18084
 
16581
18085
  accessProfile:
16582
- # SCOPE GATE: data-plane only (records/schemas/search/documents/folders/inference).
16583
- # Anything else (keys/profiles/admin/*) is HARD-REJECTED \u2014 by design, no override.
18086
+ # SCOPE GATE: data-plane only (records/schemas/search/documents/folders/inference/entities),
18087
+ # PLUS profiles:r/u/d (bounded by the platform to redistributing access you already have,
18088
+ # never granting more) and profiles:c:self (create a profile for yourself only \u2014 the
18089
+ # platform resolves "self" from the caller, not from anything you declare here). A bare
18090
+ # profiles:c, or profiles:c:usr_<id> naming another principal, is HARD-REJECTED \u2014 by
18091
+ # design, no override. Same for keys/users/admin/namespaces/*.
16584
18092
  # Ops: c|r|u|d, combinable (records:cru). Prefer the narrowest set (least privilege).
16585
18093
  allowedActions: [records:r, records:c, records:u, search:r, schemas:r]
16586
18094
  # dataScope binds the key to specific owners. Include null to ALSO see
@@ -16588,6 +18096,17 @@ accessProfile:
16588
18096
  # owners' records, not shared/seed data. Keys are userId (the principal) and
16589
18097
  # namespaced scopes (scope:org, scope:client, ...):
16590
18098
  # dataScope: { "scope:org": [<org-uuid>, null] }
18099
+ # Left empty ({}) below \u2014 the explicit marker for "deliberately tenant-wide,
18100
+ # every record visible" ('blueprint validate' warns on a MISSING dataScope
18101
+ # key, but not on this one). This is a SCAFFOLD PLACEHOLDER, not a
18102
+ # recommendation: it also grants records:c/u tenant-wide, so before this
18103
+ # profile touches real data, replace it with a real scope (the example
18104
+ # above) unless every owner genuinely sharing every record is what you want.
18105
+ dataScope: {}
18106
+ # capabilities is a SEPARATE gated dimension from allowedActions \u2014 named platform
18107
+ # effects a resource:ops verb can't express. Only 'member-lifecycle' is admitted
18108
+ # today; every other name is HARD-REJECTED, same posture as the scope gate above.
18109
+ # capabilities: [member-lifecycle]
16591
18110
 
16592
18111
  servicePrincipal:
16593
18112
  externalId: ${name}-sp # stable; the principal the profile + key bind to
@@ -16707,15 +18226,19 @@ async function resolveAndValidate(arg, verb, supplied) {
16707
18226
  return null;
16708
18227
  }
16709
18228
  try {
16710
- assertScopeGate(blueprint.accessProfile.allowedActions);
16711
- for (const clauses of Object.values(blueprint.roles ?? {})) {
16712
- for (const clause of clauses) assertScopeGate(clause.allowedActions);
16713
- }
18229
+ assertBlueprintGates(blueprint);
16714
18230
  } catch (err) {
16715
18231
  if (err instanceof ScopeGateError) {
16716
18232
  process.stderr.write(` \u2716 Scope gate rejected '${blueprint.name}':
16717
18233
  `);
16718
18234
  for (const r of err.rejected) process.stderr.write(` \u2022 ${r.action}: ${r.reason}
18235
+ `);
18236
+ return null;
18237
+ }
18238
+ if (err instanceof CapabilityGateError) {
18239
+ process.stderr.write(` \u2716 Capability gate rejected '${blueprint.name}':
18240
+ `);
18241
+ for (const r of err.rejected) process.stderr.write(` \u2022 ${r.action}: ${r.reason}
16719
18242
  `);
16720
18243
  return null;
16721
18244
  }
@@ -16747,9 +18270,10 @@ async function runValidate(args) {
16747
18270
  const blueprint = await resolveAndValidate(args.file, "validate", supplied);
16748
18271
  if (!blueprint) return 2;
16749
18272
  const gate = evaluateScopeGate(blueprint.accessProfile.allowedActions);
18273
+ const capGate = evaluateCapabilityGate(blueprint.accessProfile.capabilities);
16750
18274
  const warnCount = lintBlueprint(blueprint).length;
16751
18275
  process.stdout.write(
16752
- ` \u2714 '${blueprint.name}' is valid \u2014 structural \u2713, scope gate \u2713 [${gate.accepted.join(", ")}], lint \u2713` + (warnCount ? ` (${warnCount} warning${warnCount === 1 ? "" : "s"})` : "") + "\n"
18276
+ ` \u2714 '${blueprint.name}' is valid \u2014 structural \u2713, scope gate \u2713 [${gate.accepted.join(", ")}]` + (capGate.accepted.length ? `, capability gate \u2713 [${capGate.accepted.join(", ")}]` : "") + `, lint \u2713` + (warnCount ? ` (${warnCount} warning${warnCount === 1 ? "" : "s"})` : "") + "\n"
16753
18277
  );
16754
18278
  return 0;
16755
18279
  }
@@ -17080,7 +18604,14 @@ async function resolveKeyDestination(opts, deps) {
17080
18604
  `);
17081
18605
  return 2;
17082
18606
  }
17083
- const alias = opts.alias ?? opts.context;
18607
+ let alias;
18608
+ try {
18609
+ alias = keyringAliasFor(opts.alias ?? opts.context, opts.tenant ?? "live");
18610
+ } catch (err) {
18611
+ process.stderr.write(` \u2716 ${formatError(err)}
18612
+ `);
18613
+ return 2;
18614
+ }
17084
18615
  if (opts.activate === false && await keyring.activeAlias() === alias) {
17085
18616
  process.stderr.write(
17086
18617
  ` \u2716 --no-activate cannot be used on '${alias}': it is your ACTIVE identity, and storing
@@ -17101,6 +18632,8 @@ async function storeAndReport(dest, key, auth, opts) {
17101
18632
  return 1;
17102
18633
  }
17103
18634
  const principalId = opts.principalId;
18635
+ const tenant = keyTenant(key.rawKey) ?? "test";
18636
+ const willActivate = await determineActivateDecision(dest.keyring, { tenant, explicitActivate: opts.activate });
17104
18637
  let entry;
17105
18638
  try {
17106
18639
  warnReplacingAlias(await dest.keyring.get(dest.alias), dest.alias, opts.revokedKeyId);
@@ -17115,7 +18648,7 @@ async function storeAndReport(dest, key, auth, opts) {
17115
18648
  env: auth.env
17116
18649
  },
17117
18650
  key.rawKey,
17118
- { activate: opts.activate !== false, revokedKeyId: opts.revokedKeyId }
18651
+ { activate: willActivate, revokedKeyId: opts.revokedKeyId }
17119
18652
  ));
17120
18653
  } catch (err) {
17121
18654
  process.stderr.write(
@@ -17127,7 +18660,6 @@ async function storeAndReport(dest, key, auth, opts) {
17127
18660
  );
17128
18661
  return 1;
17129
18662
  }
17130
- const active = opts.activate !== false;
17131
18663
  process.stdout.write(
17132
18664
  `
17133
18665
  \u2714 Stored the new key in your ${describeEntryStoreNoun(entry)} \u2014 it was not printed.
@@ -17137,9 +18669,19 @@ async function storeAndReport(dest, key, auth, opts) {
17137
18669
  ["principal", entry.principalId || void 0],
17138
18670
  ["key id", entry.keyId],
17139
18671
  ["env", entry.env],
17140
- ["active", active ? "yes" : "no"]
17141
- ]) + "\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.
17142
- `)
18672
+ ["active", willActivate ? "yes" : "no"]
18673
+ ]) + "\n\n" + // The plain "now active" note fires only on the activated path. When it
18674
+ // did NOT activate, tenantActivateMessage below is the ONLY explanation —
18675
+ // it already says to run `switch`, AND (unlike a bare "Not active") says
18676
+ // WHY: an explicit --no-activate vs. this call's tenant-safety default.
18677
+ // Saying both here would print two different "run switch" instructions
18678
+ // for one outcome.
18679
+ (willActivate ? " Anything that resolves a key through the CLI \u2014 the MCP server included \u2014 now\n reads this one. No VECTROS_API_KEY needed.\n" : "") + tenantActivateMessage({
18680
+ alias: entry.alias,
18681
+ tenant,
18682
+ willActivate,
18683
+ explicitlyDeclined: opts.activate === false
18684
+ })
17143
18685
  );
17144
18686
  return 0;
17145
18687
  }
@@ -17360,9 +18902,25 @@ async function runKeyRotate(opts, deps = {}) {
17360
18902
  return 1;
17361
18903
  }
17362
18904
  }
17363
- const existing = (await drainPages(() => client.auth.listScopedKeys())).find(
17364
- (k) => matchesTuple(k, opts.context, userId, keyName)
17365
- );
18905
+ const activeKeys = await drainPages(() => client.auth.listScopedKeys());
18906
+ const existing = activeKeys.find((k) => matchesTuple(k, opts.context, userId, keyName));
18907
+ if (!existing && keyName === DEFAULT_KEY_NAME && !opts.forceNew) {
18908
+ const others = activeKeys.filter(
18909
+ (k) => k.contextId === opts.context && k.userId === userId && k.status === "active"
18910
+ );
18911
+ if (others.length > 0) {
18912
+ const list = others.map((k) => ` \u2022 ${k.keyId} (name: '${k.keyName}')`).join("\n");
18913
+ process.stderr.write(
18914
+ ` \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):
18915
+ ${list}
18916
+ If one of these is it, re-run with --name '<its name>'.
18917
+ If you really want a new key alongside them, re-run with --force-new.
18918
+ Nothing was revoked or minted.
18919
+ `
18920
+ );
18921
+ return 1;
18922
+ }
18923
+ }
17366
18924
  if (existing?.keyId) {
17367
18925
  await client.auth.revokeScopedKey({ keyId: existing.keyId });
17368
18926
  process.stdout.write(` \u2714 Revoked prior key ${existing.keyId}
@@ -17795,7 +19353,7 @@ function isRecognizedOwnershipKey(key) {
17795
19353
  if (key === "userId" || key === "partnerUserId") return true;
17796
19354
  if (!key.startsWith("scope:")) return false;
17797
19355
  const ns = key.slice("scope:".length);
17798
- return !FORBIDDEN_SCOPE_NAMESPACES.has(ns) && SCOPE_NAMESPACE_RE.test(ns);
19356
+ return !FORBIDDEN_SCOPE_NAMESPACES2.has(ns) && SCOPE_NAMESPACE_RE2.test(ns);
17799
19357
  }
17800
19358
  var DIM_WILDCARD = "*";
17801
19359
  var ANY_TOKEN_RE = /^\$\{\{\s*any\s*\}\}$/;
@@ -18542,7 +20100,24 @@ async function captureSnapshot(client, blueprint) {
18542
20100
  }
18543
20101
  return { existingSchemaTypes, existingProfilePrincipals, existingRoleIds, failed };
18544
20102
  }
18545
- async function runAssertions(client, deps, blueprint, tenantId, result) {
20103
+ async function captureIssuerSnapshot(bootstrapClient) {
20104
+ const existingIssuerIds = /* @__PURE__ */ new Set();
20105
+ let failed = false;
20106
+ try {
20107
+ let startFrom;
20108
+ for (; ; ) {
20109
+ const page = await bootstrapClient.auth.listIssuers({ limit: SNAPSHOT_PAGE_SIZE, startFrom });
20110
+ for (const i of page.data ?? []) if (i.issuerId) existingIssuerIds.add(i.issuerId);
20111
+ const next = page.nextCursor;
20112
+ if (!next) break;
20113
+ startFrom = next;
20114
+ }
20115
+ } catch {
20116
+ failed = true;
20117
+ }
20118
+ return { existingIssuerIds, failed };
20119
+ }
20120
+ async function runAssertions(client, bootstrapClient, deps, blueprint, tenantId, result) {
18546
20121
  const assertions = [];
18547
20122
  try {
18548
20123
  const ping = await deps.pingWithKey(result.rawKey);
@@ -18627,9 +20202,35 @@ async function runAssertions(client, deps, blueprint, tenantId, result) {
18627
20202
  }
18628
20203
  }
18629
20204
  }
20205
+ for (const { issuerId } of result.issuerActions) {
20206
+ try {
20207
+ const issuer = await bootstrapClient.auth.getIssuer({ issuerId });
20208
+ const ok = issuer.issuerId === issuerId;
20209
+ assertions.push({
20210
+ name: `issuer:${issuerId}`,
20211
+ ok,
20212
+ detail: ok ? `issuer '${issuerId}' registered + readable` : `getIssuer(${issuerId}) returned issuerId '${issuer.issuerId ?? "(none)"}'`
20213
+ });
20214
+ } catch (e) {
20215
+ assertions.push({ name: `issuer:${issuerId}`, ok: false, detail: `getIssuer(${issuerId}) failed: ${errMsg(e)}` });
20216
+ }
20217
+ }
20218
+ for (const { namespace } of result.namespaceActions) {
20219
+ try {
20220
+ const ns = await bootstrapClient.identity.getNamespace({ namespace, contextId: blueprint.contextId });
20221
+ const ok = ns.namespace === namespace && ns.contextId === blueprint.contextId;
20222
+ assertions.push({
20223
+ name: `namespace:${namespace}`,
20224
+ ok,
20225
+ detail: ok ? `namespace '${namespace}' registered + readable in context '${blueprint.contextId}'` : `getNamespace(${namespace}) returned namespace '${ns.namespace ?? "(none)"}' in context '${ns.contextId ?? "(tenant-wide)"}', expected '${blueprint.contextId}'`
20226
+ });
20227
+ } catch (e) {
20228
+ assertions.push({ name: `namespace:${namespace}`, ok: false, detail: `getNamespace(${namespace}) failed: ${errMsg(e)}` });
20229
+ }
20230
+ }
18630
20231
  return assertions;
18631
20232
  }
18632
- async function runTeardown(client, blueprint, machineId, snapshot, result) {
20233
+ async function runTeardown(client, bootstrapClient, blueprint, machineId, snapshot, result) {
18633
20234
  const steps = [];
18634
20235
  const keyName = result?.keyName ?? keyNameFor(blueprint.name, machineId);
18635
20236
  try {
@@ -18723,10 +20324,34 @@ async function runTeardown(client, blueprint, machineId, snapshot, result) {
18723
20324
  for (const [typeName] of toDelete) {
18724
20325
  steps.push({ step: `delete schema ${typeName}`, ok: false, error: lastError.get(typeName) ?? "could not delete" });
18725
20326
  }
20327
+ for (const { issuerId } of result.issuerActions) {
20328
+ if (snapshot.failed) {
20329
+ steps.push({ step: `delete issuer ${issuerId}`, ok: true, skipped: true, error: "skipped \u2014 pre-apply snapshot failed; cannot prove ownership" });
20330
+ continue;
20331
+ }
20332
+ if (snapshot.existingIssuerIds.has(issuerId)) {
20333
+ steps.push({ step: `delete issuer ${issuerId}`, ok: true, skipped: true, error: "skipped \u2014 pre-existed this run" });
20334
+ continue;
20335
+ }
20336
+ try {
20337
+ await bootstrapClient.auth.deleteIssuer({ issuerId });
20338
+ steps.push({ step: `delete issuer ${issuerId}`, ok: true });
20339
+ } catch (e) {
20340
+ steps.push({ step: `delete issuer ${issuerId}`, ok: false, error: errMsg(e) });
20341
+ }
20342
+ }
20343
+ for (const { namespace } of result.namespaceActions) {
20344
+ steps.push({
20345
+ step: `delete namespace ${namespace}`,
20346
+ ok: true,
20347
+ skipped: true,
20348
+ error: "namespace deletion requires a root API key (sk_*) \u2014 blueprint-test never holds one; the registration is left in place"
20349
+ });
20350
+ }
18726
20351
  return steps;
18727
20352
  }
18728
20353
  async function runBlueprintHarness(client, input, deps) {
18729
- const { tenantId, machineId, keep = false, log, remintForContext } = input;
20354
+ const { tenantId, machineId, keep = false, log, remintForContext, remintBootstrapForContext } = input;
18730
20355
  const readExistingKey = async () => void 0;
18731
20356
  const persistMintedKey = async () => {
18732
20357
  };
@@ -18736,12 +20361,15 @@ async function runBlueprintHarness(client, input, deps) {
18736
20361
  log
18737
20362
  );
18738
20363
  const remint = remintForContext ?? (async () => client);
18739
- let snapshot;
20364
+ const remintBootstrap = remintBootstrapForContext ?? (async () => client);
20365
+ const hasIssuers = (blueprint.issuers ?? []).length > 0;
20366
+ const hasNamespaces = (blueprint.namespaces ?? []).length > 0;
20367
+ let contextSnapshot;
18740
20368
  if (remintForContext) {
18741
20369
  try {
18742
- snapshot = await captureSnapshot(await remintForContext(blueprint.contextId), blueprint);
20370
+ contextSnapshot = await captureSnapshot(await remintForContext(blueprint.contextId), blueprint);
18743
20371
  } catch (e) {
18744
- snapshot = {
20372
+ contextSnapshot = {
18745
20373
  existingSchemaTypes: /* @__PURE__ */ new Set(),
18746
20374
  existingProfilePrincipals: /* @__PURE__ */ new Set(),
18747
20375
  existingRoleIds: /* @__PURE__ */ new Set(),
@@ -18749,15 +20377,48 @@ async function runBlueprintHarness(client, input, deps) {
18749
20377
  };
18750
20378
  }
18751
20379
  } else {
18752
- snapshot = await captureSnapshot(client, blueprint);
20380
+ contextSnapshot = await captureSnapshot(client, blueprint);
20381
+ }
20382
+ let issuerSnapshot;
20383
+ if (!hasIssuers) {
20384
+ issuerSnapshot = { existingIssuerIds: /* @__PURE__ */ new Set(), failed: false };
20385
+ } else if (remintBootstrapForContext) {
20386
+ try {
20387
+ issuerSnapshot = await captureIssuerSnapshot(await remintBootstrapForContext(blueprint.contextId));
20388
+ } catch (e) {
20389
+ issuerSnapshot = { existingIssuerIds: /* @__PURE__ */ new Set(), failed: !isNotFound(e) };
20390
+ }
20391
+ } else {
20392
+ issuerSnapshot = await captureIssuerSnapshot(client);
18753
20393
  }
20394
+ const snapshot = {
20395
+ ...contextSnapshot,
20396
+ existingIssuerIds: issuerSnapshot.existingIssuerIds,
20397
+ failed: contextSnapshot.failed || issuerSnapshot.failed
20398
+ };
18754
20399
  let result;
18755
20400
  let assertions = [];
18756
20401
  let applyError;
18757
20402
  try {
18758
- result = await runLoader(client, { blueprint, tenantId, machineId, readExistingKey, persistMintedKey, log, remintForContext });
20403
+ result = await runLoader(client, {
20404
+ blueprint,
20405
+ tenantId,
20406
+ machineId,
20407
+ readExistingKey,
20408
+ persistMintedKey,
20409
+ log,
20410
+ remintForContext,
20411
+ remintBootstrapForContext
20412
+ });
18759
20413
  log.debug({ component: "harness", contextId: result.contextId, keyName: result.keyName }, "applied");
18760
- assertions = await runAssertions(await remint(blueprint.contextId), deps, blueprint, tenantId, result);
20414
+ assertions = await runAssertions(
20415
+ await remint(blueprint.contextId),
20416
+ hasIssuers || hasNamespaces ? await remintBootstrap(blueprint.contextId) : client,
20417
+ deps,
20418
+ blueprint,
20419
+ tenantId,
20420
+ result
20421
+ );
18761
20422
  } catch (e) {
18762
20423
  applyError = errMsg(e);
18763
20424
  log.warn({ component: "harness", err: applyError }, "apply phase failed");
@@ -18769,7 +20430,14 @@ async function runBlueprintHarness(client, input, deps) {
18769
20430
  tdClient = await remint(blueprint.contextId);
18770
20431
  } catch {
18771
20432
  }
18772
- teardown = await runTeardown(tdClient, blueprint, machineId, snapshot, result);
20433
+ let tdBootstrapClient = client;
20434
+ if (hasIssuers) {
20435
+ try {
20436
+ tdBootstrapClient = await remintBootstrap(blueprint.contextId);
20437
+ } catch {
20438
+ }
20439
+ }
20440
+ teardown = await runTeardown(tdClient, tdBootstrapClient, blueprint, machineId, snapshot, result);
18773
20441
  }
18774
20442
  const passed = applyError === void 0 && assertions.length > 0 && assertions.every((a) => a.ok);
18775
20443
  return {
@@ -18843,6 +20511,13 @@ async function runBlueprintTest(fileOrName, opts, deps = {}) {
18843
20511
  process.stderr.write(` \u2716 Scope gate refused blueprint:
18844
20512
  `);
18845
20513
  for (const r of err.rejected) process.stderr.write(` \u2022 ${r.action}: ${r.reason}
20514
+ `);
20515
+ return 2;
20516
+ }
20517
+ if (err instanceof CapabilityGateError) {
20518
+ process.stderr.write(` \u2716 Capability gate refused blueprint:
20519
+ `);
20520
+ for (const r of err.rejected) process.stderr.write(` \u2022 ${r.action}: ${r.reason}
18846
20521
  `);
18847
20522
  return 2;
18848
20523
  }
@@ -18878,9 +20553,22 @@ async function runBlueprintTest(fileOrName, opts, deps = {}) {
18878
20553
  environment: auth.baseUrl
18879
20554
  });
18880
20555
  };
20556
+ const remintBootstrapForContext = async (contextId) => {
20557
+ const ctxToken = await mintBridgeToken({
20558
+ cognitoToken: auth.cognitoToken,
20559
+ apiBaseUrl: auth.baseUrl,
20560
+ contextId,
20561
+ tenant: auth.tenant,
20562
+ useScopedToken: opts.useScopedToken
20563
+ });
20564
+ return new VectrosClient({
20565
+ token: ctxToken.token,
20566
+ environment: auth.baseUrl
20567
+ });
20568
+ };
18881
20569
  const report = await runBlueprintHarness(
18882
20570
  client,
18883
- { blueprint, tenantId, machineId, keep: opts.keep, log, remintForContext },
20571
+ { blueprint, tenantId, machineId, keep: opts.keep, log, remintForContext, remintBootstrapForContext },
18884
20572
  { pingWithKey }
18885
20573
  );
18886
20574
  if (opts.json) {
@@ -18961,7 +20649,15 @@ async function runJoin(opts, deps = {}) {
18961
20649
  return 2;
18962
20650
  }
18963
20651
  const context = opts.contextArg;
18964
- const alias = opts.alias ?? context;
20652
+ const tenant = opts.tenant ?? "live";
20653
+ let alias;
20654
+ try {
20655
+ alias = keyringAliasFor(opts.alias ?? context, tenant);
20656
+ } catch (err) {
20657
+ process.stderr.write(` \u2716 ${formatError(err)}
20658
+ `);
20659
+ return 2;
20660
+ }
18965
20661
  let keyring;
18966
20662
  try {
18967
20663
  keyring = deps.keyring ?? await Keyring.open();
@@ -18991,8 +20687,23 @@ async function runJoin(opts, deps = {}) {
18991
20687
  const heldSecret = existingEntry ? await keyring.resolveSecret(alias) : void 0;
18992
20688
  const canReuse = existingEntry && heldSecret && existingEntry.context === context && existingEntry.principalId === principalId && existingEntry.env === auth.env;
18993
20689
  if (canReuse && existingEntry) {
18994
- await keyring.setActive(alias);
18995
- emitJoined(alias, context, principalId, opts.role, existingEntry.keyId, true);
20690
+ const willActivate2 = await determineActivateDecision(keyring, {
20691
+ tenant,
20692
+ explicitActivate: opts.activate,
20693
+ hint: { alias, secret: heldSecret }
20694
+ });
20695
+ if (willActivate2) await keyring.setActive(alias);
20696
+ emitJoined({
20697
+ alias,
20698
+ context,
20699
+ principalId,
20700
+ role: opts.role,
20701
+ keyId: existingEntry.keyId,
20702
+ reused: true,
20703
+ tenant,
20704
+ willActivate: willActivate2,
20705
+ explicitlyDeclined: opts.activate === false
20706
+ });
18996
20707
  return 0;
18997
20708
  }
18998
20709
  warnReplacingAlias(existingEntry, alias);
@@ -19004,13 +20715,24 @@ async function runJoin(opts, deps = {}) {
19004
20715
  );
19005
20716
  }
19006
20717
  if (revokedKeyId) await warnOtherAliasesNaming(keyring, revokedKeyId, alias);
20718
+ const willActivate = await determineActivateDecision(keyring, { tenant, explicitActivate: opts.activate });
19007
20719
  const { entry } = await storeMintedKey(
19008
20720
  keyring,
19009
20721
  { alias, context, principalId, role: opts.role, keyId: key.keyId, env: auth.env },
19010
20722
  key.rawKey,
19011
- { revokedKeyId }
20723
+ { activate: willActivate, revokedKeyId }
19012
20724
  );
19013
- emitJoined(alias, context, principalId, opts.role, entry.keyId, false);
20725
+ emitJoined({
20726
+ alias,
20727
+ context,
20728
+ principalId,
20729
+ role: opts.role,
20730
+ keyId: entry.keyId,
20731
+ reused: false,
20732
+ tenant,
20733
+ willActivate,
20734
+ explicitlyDeclined: opts.activate === false
20735
+ });
19014
20736
  return 0;
19015
20737
  } catch (err) {
19016
20738
  process.stderr.write(formatCliError(err, { action: "join failed" }));
@@ -19018,27 +20740,34 @@ async function runJoin(opts, deps = {}) {
19018
20740
  }
19019
20741
  });
19020
20742
  }
19021
- function emitJoined(alias, context, principalId, role, keyId, reused) {
19022
- process.stdout.write(
19023
- `
19024
- \u2714 Joined '${context}' as ${principalId}` + (reused ? " (existing key reused)" : "") + "\n" + formatKeyValues([
19025
- ["alias", alias],
19026
- ["context", context],
19027
- ["principal", principalId],
19028
- ["role", role],
19029
- ["key id", keyId]
19030
- ]) + `
20743
+ function emitJoined(opts) {
20744
+ const header = `
20745
+ \u2714 Joined '${opts.context}' as ${opts.principalId}` + (opts.reused ? " (existing key reused)" : "") + "\n" + formatKeyValues([
20746
+ ["alias", opts.alias],
20747
+ ["context", opts.context],
20748
+ ["principal", opts.principalId],
20749
+ ["role", opts.role],
20750
+ ["key id", opts.keyId]
20751
+ ]);
20752
+ const activationNote = opts.willActivate ? `
19031
20753
 
19032
- Now active. Use \`vectros switch ${alias}\` to return to it, \`vectros keyring show --format env\` to export the key for the MCP server.
19033
- `
20754
+ Now active. Use \`vectros switch ${opts.alias}\` to return to it, \`vectros keyring show --format env\` to export the key for the MCP server.
20755
+ ` : "";
20756
+ process.stdout.write(
20757
+ header + activationNote + tenantActivateMessage({
20758
+ alias: opts.alias,
20759
+ tenant: opts.tenant,
20760
+ willActivate: opts.willActivate,
20761
+ explicitlyDeclined: opts.explicitlyDeclined
20762
+ })
19034
20763
  );
19035
20764
  }
19036
20765
 
19037
20766
  // src/build-info.ts
19038
20767
  var BUILD_INFO = {
19039
- cli: true ? "0.14.0" : "dev",
19040
- sdk: true ? "0.38.0" : "dev",
19041
- blueprints: true ? "0.11.0" : "dev"
20768
+ cli: true ? "0.16.0" : "dev",
20769
+ sdk: true ? "0.40.0" : "dev",
20770
+ blueprints: true ? "0.13.0" : "dev"
19042
20771
  };
19043
20772
  function formatBuildInfo() {
19044
20773
  return `vectros ${BUILD_INFO.cli} (sdk ${BUILD_INFO.sdk}, blueprints ${BUILD_INFO.blueprints})`;
@@ -19062,6 +20791,14 @@ function addCommonAuthOptions(cmd) {
19062
20791
  "emit machine-readable JSON instead of the human-readable view"
19063
20792
  );
19064
20793
  }
20794
+ function parseTenantFlag(raw) {
20795
+ if (raw !== void 0 && raw !== "test" && raw !== "live") {
20796
+ process.stderr.write(` \u2716 --tenant must be 'test' or 'live' (got '${raw}')
20797
+ `);
20798
+ process.exit(2);
20799
+ }
20800
+ return raw;
20801
+ }
19065
20802
  function toCommonOptions(flags) {
19066
20803
  return {
19067
20804
  env: flags.env,
@@ -19115,7 +20852,10 @@ function buildProgram(deps = {}) {
19115
20852
  return program;
19116
20853
  }
19117
20854
  function registerJoinCommand(program, deps) {
19118
- const join3 = program.command("join").description("Enroll a principal in a context (grant + key) and keep the key in the keyring.").argument("<context>", "the app context to join").option("--role <roleId>", "reference a Role to grant (XOR --actions)").option("--actions <csv>", "inline single-clause actions to grant, comma-separated (XOR --role)").option("--as <principal>", "who to enroll: 'me' (default) or an explicit 'usr_<id>'", "me").option("--scope <ns:value>", "identity override: a scope the member acts under (repeatable, e.g. org:<id>)", collect, []).option("--alias <name>", "keyring entry name (defaults to the context id)").option("--tenant <tenant>", "target tenant: 'test' or 'live' (default live)");
20855
+ const join3 = program.command("join").description("Enroll a principal in a context (grant + key) and keep the key in the keyring.").argument("<context>", "the app context to join").option("--role <roleId>", "reference a Role to grant (XOR --actions)").option("--actions <csv>", "inline single-clause actions to grant, comma-separated (XOR --role)").option("--as <principal>", "who to enroll: 'me' (default) or an explicit 'usr_<id>'", "me").option("--scope <ns:value>", "identity override: a scope the member acts under (repeatable, e.g. org:<id>)", collect, []).option("--alias <name>", "keyring entry name (defaults to the context id)").option("--tenant <tenant>", "target tenant: 'test' or 'live' (default live)").option(
20856
+ "--activate",
20857
+ "force the freshly-stored key to become your active identity, even over a currently-active LIVE credential (the one case --tenant test does NOT do automatically)"
20858
+ ).option("--no-activate", "store the key without activating it, regardless of tenant");
19119
20859
  addAuthOptions(join3).action(
19120
20860
  async (contextArg, flags) => {
19121
20861
  if (flags.tenant !== void 0 && flags.tenant !== "test" && flags.tenant !== "live") {
@@ -19133,7 +20873,8 @@ function registerJoinCommand(program, deps) {
19133
20873
  as: flags.as,
19134
20874
  scope: flags.scope,
19135
20875
  alias: flags.alias,
19136
- tenant: flags.tenant
20876
+ tenant: flags.tenant,
20877
+ activate: flags.activate
19137
20878
  },
19138
20879
  deps
19139
20880
  )
@@ -19380,9 +21121,10 @@ function toKeyDestination(flags) {
19380
21121
  function registerKeyCommands(program, deps) {
19381
21122
  const key = program.command("key").description("Manage scoped API keys (ssk_*) \u2014 issue, list, revoke, rotate.");
19382
21123
  const issue = addKeyDestinationOptions(
19383
- key.command("issue").description("Mint an ssk_* for a principal that already has a profile in the context.").requiredOption("--principal <p>", "principal (usr_<id> or <id>) with an AccessProfile in the context").requiredOption("--context <c>", "app context the key operates in").option("--name <name>", "key name (idempotency tuple component)", DEFAULT_KEY_NAME).option("--label <label>", "optional principal label (surfaced in /v1/ping)")
21124
+ key.command("issue").description("Mint an ssk_* for a principal that already has a profile in the context.").requiredOption("--principal <p>", "principal (usr_<id> or <id>) with an AccessProfile in the context").requiredOption("--context <c>", "app context the key operates in").option("--name <name>", "key name (idempotency tuple component)", DEFAULT_KEY_NAME).option("--label <label>", "optional principal label (surfaced in /v1/ping)").option("--tenant <tenant>", "tenant the key lives in: 'test' or 'live' (default live)")
19384
21125
  );
19385
21126
  addAuthOptions(issue).action(async (flags) => {
21127
+ const tenant = parseTenantFlag(flags.tenant);
19386
21128
  process.exit(
19387
21129
  await runKeyIssue({
19388
21130
  ...toCommonOptions(flags),
@@ -19390,6 +21132,7 @@ function registerKeyCommands(program, deps) {
19390
21132
  context: flags.context,
19391
21133
  name: flags.name,
19392
21134
  label: flags.label,
21135
+ tenant,
19393
21136
  ...toKeyDestination(flags)
19394
21137
  }, deps)
19395
21138
  );
@@ -19397,40 +21140,58 @@ function registerKeyCommands(program, deps) {
19397
21140
  const list = key.command("list").description("List active scoped keys in one context (default, or --context <id>); --all-contexts for every context.").option("--principal <p>", "only keys for this principal (usr_<id> or <id>)").option("--context <c>", "only keys in this context (default: the default context)").option(
19398
21141
  "--all-contexts",
19399
21142
  "list every key across ALL your contexts (owner-only) \u2014 the tenant-wide view for the key limit"
19400
- );
21143
+ ).option("--tenant <tenant>", "tenant to list: 'test' or 'live' (default live; not combinable with --all-contexts)");
19401
21144
  addCommonAuthOptions(list).action(
19402
21145
  async (flags) => {
21146
+ if (flags.allContexts && flags.tenant !== void 0) {
21147
+ process.stderr.write(" \u2716 --tenant is not combinable with --all-contexts (which already spans both tenants).\n");
21148
+ process.exit(2);
21149
+ }
21150
+ const tenant = parseTenantFlag(flags.tenant);
19403
21151
  process.exit(
19404
21152
  await runKeyList(
19405
21153
  {
19406
21154
  ...toCommonOptions(flags),
19407
21155
  principal: flags.principal,
19408
21156
  context: flags.context,
19409
- allContexts: Boolean(flags.allContexts)
21157
+ allContexts: Boolean(flags.allContexts),
21158
+ tenant
19410
21159
  },
19411
21160
  deps
19412
21161
  )
19413
21162
  );
19414
21163
  }
19415
21164
  );
19416
- 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)");
19417
- addCommonAuthOptions(get).action(async (keyId, flags) => {
19418
- process.exit(await runKeyGet(keyId, { ...toCommonOptions(flags), context: flags.context }, deps));
19419
- });
19420
- 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)");
19421
- addAuthOptions(revoke).action(async (keyId, flags) => {
19422
- process.exit(await runKeyRevoke(keyId, { ...toCommonOptions(flags), context: flags.context }, deps));
19423
- });
21165
+ const get = key.command("get").description("Show one key\u2019s metadata (no secret).").argument("<keyId>").option("--context <c>", "context the key is bound to (default: the default context)").option("--tenant <tenant>", "tenant the key lives in: 'test' or 'live' (default live)");
21166
+ addCommonAuthOptions(get).action(
21167
+ async (keyId, flags) => {
21168
+ const tenant = parseTenantFlag(flags.tenant);
21169
+ process.exit(await runKeyGet(keyId, { ...toCommonOptions(flags), context: flags.context, tenant }, deps));
21170
+ }
21171
+ );
21172
+ const revoke = key.command("revoke").description("Revoke a key (soft-delete; ~5 min to take effect).").argument("<keyId>").option("--context <c>", "context the key is bound to (default: the default context)").option("--tenant <tenant>", "tenant the key lives in: 'test' or 'live' (default live)");
21173
+ addAuthOptions(revoke).action(
21174
+ async (keyId, flags) => {
21175
+ const tenant = parseTenantFlag(flags.tenant);
21176
+ process.exit(await runKeyRevoke(keyId, { ...toCommonOptions(flags), context: flags.context, tenant }, deps));
21177
+ }
21178
+ );
19424
21179
  const rotate = addKeyDestinationOptions(
19425
- 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)
21180
+ key.command("rotate").description("Revoke the matching active key and mint a fresh secret.").requiredOption("--principal <p>", "principal (usr_<id> or <id>)").requiredOption("--context <c>", "app context").option("--name <name>", "key name to rotate", DEFAULT_KEY_NAME).option(
21181
+ "--force-new",
21182
+ "mint a new key even though other active keys already exist for this principal/context under a different name (bypasses the default-name-mismatch guard)"
21183
+ ).option("--tenant <tenant>", "tenant the key lives in: 'test' or 'live' (default live)")
19426
21184
  );
19427
21185
  addAuthOptions(rotate).action(async (flags) => {
21186
+ const tenant = parseTenantFlag(flags.tenant);
19428
21187
  process.exit(
19429
21188
  await runKeyRotate({
19430
21189
  ...toCommonOptions(flags),
19431
21190
  principal: flags.principal,
19432
21191
  context: flags.context,
19433
21192
  name: flags.name,
21193
+ forceNew: flags.forceNew,
21194
+ tenant,
19434
21195
  ...toKeyDestination(flags)
19435
21196
  }, deps)
19436
21197
  );