@vectros-ai/cli 0.14.0 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.mjs CHANGED
@@ -1680,6 +1680,7 @@ __export(api_exports, {
1680
1680
  TooManyRequestsError: () => TooManyRequestsError,
1681
1681
  TruncationWarningEvent: () => TruncationWarningEvent,
1682
1682
  UnauthorizedError: () => UnauthorizedError,
1683
+ UserExistsResponse: () => UserExistsResponse,
1683
1684
  UserRequest: () => UserRequest,
1684
1685
  UserResponse: () => UserResponse,
1685
1686
  auth: () => auth_exports,
@@ -2356,6 +2357,14 @@ var TruncationWarningEvent;
2356
2357
  ContextWindowBudget: "context_window_budget"
2357
2358
  };
2358
2359
  })(TruncationWarningEvent || (TruncationWarningEvent = {}));
2360
+ var UserExistsResponse;
2361
+ ((UserExistsResponse2) => {
2362
+ UserExistsResponse2.Status = {
2363
+ Active: "ACTIVE",
2364
+ Suspended: "SUSPENDED",
2365
+ Pending: "PENDING"
2366
+ };
2367
+ })(UserExistsResponse || (UserExistsResponse = {}));
2359
2368
  var UserRequest;
2360
2369
  ((UserRequest2) => {
2361
2370
  UserRequest2.Status = {
@@ -4099,7 +4108,7 @@ var AuthClient = class {
4099
4108
  return handleNonStatusCodeError(_response.error, _response.rawResponse, "GET", "/v1/admin/keys/scoped");
4100
4109
  }
4101
4110
  /**
4102
- * Creates a scoped API key (an `ssk_*` secret) that inherits its permissions from an existing access profile in your account. The call is idempotent on the combination of tenant, context, user, and key name: re-issuing the same request returns the existing key WITHOUT re-disclosing its raw secret. The raw key is returned ONLY in this response — store it securely, as it cannot be retrieved again. Requires the `keys:c` scope.
4111
+ * Creates a scoped API key (an `ssk_*` secret) that inherits its permissions from an existing access profile in your account. The call is idempotent on the combination of tenant, context, user, and key name: re-issuing the same request returns the existing key WITHOUT re-disclosing its raw secret. The raw key is returned ONLY in this response — store it securely, as it cannot be retrieved again. Requires the `keys:c` scope. If you use a scoped credential, `keys:c` alone is not sufficient: because the minted key is durably bound to the profile you name, the profile's effective scopes may not exceed your own, and you may only mint against a profile whose `identityOverrides` values your own identity holds. A root API key (`sk_`) is exempt from both bounds.
4103
4112
  *
4104
4113
  * @param {Vectros.CreateScopedKeyRequest} request
4105
4114
  * @param {AuthClient.RequestOptions} requestOptions - Request-specific configuration.
@@ -4849,11 +4858,12 @@ var AuthClient = class {
4849
4858
  );
4850
4859
  }
4851
4860
  /**
4852
- * Updates an access profile. This is a partial update: any field you omit (or send as null) keeps its existing value. A profile must reference either inline `scopes` or a `roleId`, never both — so setting `scopes` clears any `roleId`, and setting `roleId` clears any inline `scopes`. The `contextId` and `principalId` are immutable. Status changes (for example active to suspended) take effect within about five minutes. If you use a scoped credential, the profile's effective scopes may not exceed your own, and its `identityOverrides` are bounded twice: you may only set a value your own identity holds, and you may only change or clear a value the profile already holds if that value is yours as well. Repointing or clearing another principal's established identity therefore returns 403. A root API key (`sk_`) is exempt. Requires the `profiles:u` scope.
4861
+ * Updates an access profile. This is a partial update: any field you omit (or send as null) keeps its existing value. A profile must reference either inline `scopes` or a `roleId`, never both — so setting `scopes` clears any `roleId`, and setting `roleId` clears any inline `scopes`. The `contextId` and `principalId` are immutable. Status changes (for example active to suspended) take effect within about five minutes. If you use a scoped credential, the profile's effective scopes may not exceed your own, and its `identityOverrides` are bounded twice: you may only set a value your own identity holds, and you may only change or clear a value the profile already holds if that value is yours as well. Repointing or clearing another principal's established identity therefore returns 403. A root API key (`sk_`) is exempt. If you set `roleId`, it must reference a role that already exists in this context. Requires the `profiles:u` scope.
4853
4862
  *
4854
4863
  * @param {Vectros.UpdateAccessProfileRequest} request
4855
4864
  * @param {AuthClient.RequestOptions} requestOptions - Request-specific configuration.
4856
4865
  *
4866
+ * @throws {@link Vectros.BadRequestError}
4857
4867
  * @throws {@link Vectros.ForbiddenError}
4858
4868
  * @throws {@link Vectros.NotFoundError}
4859
4869
  * @throws {@link Vectros.TooManyRequestsError}
@@ -4900,6 +4910,8 @@ var AuthClient = class {
4900
4910
  }
4901
4911
  if (_response.error.reason === "status-code") {
4902
4912
  switch (_response.error.statusCode) {
4913
+ case 400:
4914
+ throw new BadRequestError(_response.error.body, _response.rawResponse);
4903
4915
  case 403:
4904
4916
  throw new ForbiddenError(_response.error.body, _response.rawResponse);
4905
4917
  case 404:
@@ -5591,6 +5603,254 @@ var AuthClient = class {
5591
5603
  }
5592
5604
  return handleNonStatusCodeError(_response.error, _response.rawResponse, "GET", "/v1/usage");
5593
5605
  }
5606
+ /**
5607
+ * Retrieves a single registered issuer by issuerId. Requires a root API key or the bootstrap's provisioning capability.
5608
+ *
5609
+ * @param {Vectros.GetIssuerRequest} request
5610
+ * @param {AuthClient.RequestOptions} requestOptions - Request-specific configuration.
5611
+ *
5612
+ * @throws {@link Vectros.ForbiddenError}
5613
+ * @throws {@link Vectros.NotFoundError}
5614
+ *
5615
+ * @example
5616
+ * await client.auth.getIssuer({
5617
+ * issuerId: "auth0-prod"
5618
+ * })
5619
+ */
5620
+ getIssuer(request, requestOptions) {
5621
+ return HttpResponsePromise.fromPromise(this.__getIssuer(request, requestOptions));
5622
+ }
5623
+ async __getIssuer(request, requestOptions) {
5624
+ const { issuerId } = request;
5625
+ const _authRequest = await this._options.authProvider.getAuthRequest();
5626
+ const _headers = mergeHeaders(
5627
+ _authRequest.headers,
5628
+ this._options?.headers,
5629
+ requestOptions?.headers
5630
+ );
5631
+ const _response = await fetcher({
5632
+ url: url_exports.join(
5633
+ await Supplier.get(this._options.baseUrl) ?? await Supplier.get(this._options.environment),
5634
+ `v1/auth/issuers/${url_exports.encodePathParam(issuerId)}`
5635
+ ),
5636
+ method: "GET",
5637
+ headers: _headers,
5638
+ queryString: url_exports.queryBuilder().mergeAdditional(requestOptions?.queryParams).build(),
5639
+ timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1e3,
5640
+ maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries,
5641
+ abortSignal: requestOptions?.abortSignal,
5642
+ fetchFn: this._options?.fetch,
5643
+ logging: this._options.logging
5644
+ });
5645
+ if (_response.ok) {
5646
+ return { data: _response.body, rawResponse: _response.rawResponse };
5647
+ }
5648
+ if (_response.error.reason === "status-code") {
5649
+ switch (_response.error.statusCode) {
5650
+ case 403:
5651
+ throw new ForbiddenError(_response.error.body, _response.rawResponse);
5652
+ case 404:
5653
+ throw new NotFoundError(_response.error.body, _response.rawResponse);
5654
+ default:
5655
+ throw new VectrosError({
5656
+ statusCode: _response.error.statusCode,
5657
+ body: _response.error.body,
5658
+ rawResponse: _response.rawResponse
5659
+ });
5660
+ }
5661
+ }
5662
+ return handleNonStatusCodeError(_response.error, _response.rawResponse, "GET", "/v1/auth/issuers/{issuerId}");
5663
+ }
5664
+ /**
5665
+ * Deregisters a trusted third-party IdP issuer. Requires a root API key or the bootstrap's provisioning capability. Unconditional — unlike some other registry deletes on this API, this does not check for or block on any downstream reference. Any user account already created via this issuer (by a prior self-signup or accepted invite) is NOT deleted or modified, but that user can no longer obtain a NEW token this way once you deregister the issuer — every `POST /v1/auth/token/exchange` call against it will 404, with no distinction between an unknown issuer and one you just deregistered. Register a replacement issuer, or restore this one, before your users' current tokens expire if you want their access to continue uninterrupted.
5666
+ *
5667
+ * @param {Vectros.DeleteIssuerRequest} request
5668
+ * @param {AuthClient.RequestOptions} requestOptions - Request-specific configuration.
5669
+ *
5670
+ * @throws {@link Vectros.ForbiddenError}
5671
+ * @throws {@link Vectros.NotFoundError}
5672
+ * @throws {@link Vectros.TooManyRequestsError}
5673
+ *
5674
+ * @example
5675
+ * await client.auth.deleteIssuer({
5676
+ * issuerId: "auth0-prod"
5677
+ * })
5678
+ */
5679
+ deleteIssuer(request, requestOptions) {
5680
+ return HttpResponsePromise.fromPromise(this.__deleteIssuer(request, requestOptions));
5681
+ }
5682
+ async __deleteIssuer(request, requestOptions) {
5683
+ const { issuerId } = request;
5684
+ const _authRequest = await this._options.authProvider.getAuthRequest();
5685
+ const _headers = mergeHeaders(
5686
+ _authRequest.headers,
5687
+ this._options?.headers,
5688
+ requestOptions?.headers
5689
+ );
5690
+ const _response = await fetcher({
5691
+ url: url_exports.join(
5692
+ await Supplier.get(this._options.baseUrl) ?? await Supplier.get(this._options.environment),
5693
+ `v1/auth/issuers/${url_exports.encodePathParam(issuerId)}`
5694
+ ),
5695
+ method: "DELETE",
5696
+ headers: _headers,
5697
+ queryString: url_exports.queryBuilder().mergeAdditional(requestOptions?.queryParams).build(),
5698
+ timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1e3,
5699
+ maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries,
5700
+ abortSignal: requestOptions?.abortSignal,
5701
+ fetchFn: this._options?.fetch,
5702
+ logging: this._options.logging
5703
+ });
5704
+ if (_response.ok) {
5705
+ return { data: void 0, rawResponse: _response.rawResponse };
5706
+ }
5707
+ if (_response.error.reason === "status-code") {
5708
+ switch (_response.error.statusCode) {
5709
+ case 403:
5710
+ throw new ForbiddenError(_response.error.body, _response.rawResponse);
5711
+ case 404:
5712
+ throw new NotFoundError(_response.error.body, _response.rawResponse);
5713
+ case 429:
5714
+ throw new TooManyRequestsError(_response.error.body, _response.rawResponse);
5715
+ default:
5716
+ throw new VectrosError({
5717
+ statusCode: _response.error.statusCode,
5718
+ body: _response.error.body,
5719
+ rawResponse: _response.rawResponse
5720
+ });
5721
+ }
5722
+ }
5723
+ return handleNonStatusCodeError(
5724
+ _response.error,
5725
+ _response.rawResponse,
5726
+ "DELETE",
5727
+ "/v1/auth/issuers/{issuerId}"
5728
+ );
5729
+ }
5730
+ /**
5731
+ * Returns the issuers registered in your tenant. Requires a root API key or the bootstrap's provisioning capability. Returns a `{data, nextCursor}` envelope.
5732
+ *
5733
+ * @param {Vectros.ListIssuersRequest} request
5734
+ * @param {AuthClient.RequestOptions} requestOptions - Request-specific configuration.
5735
+ *
5736
+ * @throws {@link Vectros.ForbiddenError}
5737
+ *
5738
+ * @example
5739
+ * await client.auth.listIssuers()
5740
+ */
5741
+ listIssuers(request = {}, requestOptions) {
5742
+ return HttpResponsePromise.fromPromise(this.__listIssuers(request, requestOptions));
5743
+ }
5744
+ async __listIssuers(request = {}, requestOptions) {
5745
+ const { startFrom, limit } = request;
5746
+ const _queryParams = {
5747
+ startFrom,
5748
+ limit
5749
+ };
5750
+ const _authRequest = await this._options.authProvider.getAuthRequest();
5751
+ const _headers = mergeHeaders(
5752
+ _authRequest.headers,
5753
+ this._options?.headers,
5754
+ requestOptions?.headers
5755
+ );
5756
+ const _response = await fetcher({
5757
+ url: url_exports.join(
5758
+ await Supplier.get(this._options.baseUrl) ?? await Supplier.get(this._options.environment),
5759
+ "v1/auth/issuers"
5760
+ ),
5761
+ method: "GET",
5762
+ headers: _headers,
5763
+ queryString: url_exports.queryBuilder().addMany(_queryParams).mergeAdditional(requestOptions?.queryParams).build(),
5764
+ timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1e3,
5765
+ maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries,
5766
+ abortSignal: requestOptions?.abortSignal,
5767
+ fetchFn: this._options?.fetch,
5768
+ logging: this._options.logging
5769
+ });
5770
+ if (_response.ok) {
5771
+ return { data: _response.body, rawResponse: _response.rawResponse };
5772
+ }
5773
+ if (_response.error.reason === "status-code") {
5774
+ switch (_response.error.statusCode) {
5775
+ case 403:
5776
+ throw new ForbiddenError(_response.error.body, _response.rawResponse);
5777
+ default:
5778
+ throw new VectrosError({
5779
+ statusCode: _response.error.statusCode,
5780
+ body: _response.error.body,
5781
+ rawResponse: _response.rawResponse
5782
+ });
5783
+ }
5784
+ }
5785
+ return handleNonStatusCodeError(_response.error, _response.rawResponse, "GET", "/v1/auth/issuers");
5786
+ }
5787
+ /**
5788
+ * Registers a trusted third-party IdP issuer that BYO-IdP token exchange (`POST /v1/auth/token/exchange`) may accept a `subject_token` from. Requires a root API key or the CLI bootstrap's provisioning capability — never an ordinary partner-grantable scope. Idempotent by `issuerId` within your tenant; the `(issuer, audience)` pair must not already be registered by a different issuerId/tenant.
5789
+ *
5790
+ * @param {Vectros.IssuerRequest} request
5791
+ * @param {AuthClient.RequestOptions} requestOptions - Request-specific configuration.
5792
+ *
5793
+ * @throws {@link Vectros.BadRequestError}
5794
+ * @throws {@link Vectros.ForbiddenError}
5795
+ * @throws {@link Vectros.TooManyRequestsError}
5796
+ *
5797
+ * @example
5798
+ * await client.auth.registerIssuer({
5799
+ * issuerId: "auth0-prod",
5800
+ * issuer: "https://your-tenant.us.auth0.com/",
5801
+ * jwksUri: "https://your-tenant.us.auth0.com/.well-known/jwks.json",
5802
+ * audience: "https://api.your-app.example.com",
5803
+ * contextId: "casework"
5804
+ * })
5805
+ */
5806
+ registerIssuer(request, requestOptions) {
5807
+ return HttpResponsePromise.fromPromise(this.__registerIssuer(request, requestOptions));
5808
+ }
5809
+ async __registerIssuer(request, requestOptions) {
5810
+ const _authRequest = await this._options.authProvider.getAuthRequest();
5811
+ const _headers = mergeHeaders(
5812
+ _authRequest.headers,
5813
+ this._options?.headers,
5814
+ requestOptions?.headers
5815
+ );
5816
+ const _response = await fetcher({
5817
+ url: url_exports.join(
5818
+ await Supplier.get(this._options.baseUrl) ?? await Supplier.get(this._options.environment),
5819
+ "v1/auth/issuers"
5820
+ ),
5821
+ method: "POST",
5822
+ headers: _headers,
5823
+ contentType: "application/json",
5824
+ queryString: url_exports.queryBuilder().mergeAdditional(requestOptions?.queryParams).build(),
5825
+ requestType: "json",
5826
+ body: request,
5827
+ timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1e3,
5828
+ maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries,
5829
+ abortSignal: requestOptions?.abortSignal,
5830
+ fetchFn: this._options?.fetch,
5831
+ logging: this._options.logging
5832
+ });
5833
+ if (_response.ok) {
5834
+ return { data: _response.body, rawResponse: _response.rawResponse };
5835
+ }
5836
+ if (_response.error.reason === "status-code") {
5837
+ switch (_response.error.statusCode) {
5838
+ case 400:
5839
+ throw new BadRequestError(_response.error.body, _response.rawResponse);
5840
+ case 403:
5841
+ throw new ForbiddenError(_response.error.body, _response.rawResponse);
5842
+ case 429:
5843
+ throw new TooManyRequestsError(_response.error.body, _response.rawResponse);
5844
+ default:
5845
+ throw new VectrosError({
5846
+ statusCode: _response.error.statusCode,
5847
+ body: _response.error.body,
5848
+ rawResponse: _response.rawResponse
5849
+ });
5850
+ }
5851
+ }
5852
+ return handleNonStatusCodeError(_response.error, _response.rawResponse, "POST", "/v1/auth/issuers");
5853
+ }
5594
5854
  /**
5595
5855
  * Returns the identity bound to your credential — your account, principal type, key id, and scope details — so you can confirm who you are authenticated as and that the credential is valid. MCP clients use this to render "signed in as ..." in a chat UI without a separate identity endpoint.
5596
5856
  *
@@ -5914,6 +6174,72 @@ var AuthClient = class {
5914
6174
  }
5915
6175
  return handleNonStatusCodeError(_response.error, _response.rawResponse, "POST", "/v1/users/invite/resend");
5916
6176
  }
6177
+ /**
6178
+ * RFC 8693 OAuth 2.0 Token Exchange. Trades a JWT issued by a third-party identity provider you've registered (`POST /v1/auth/issuers`) for a Vectros `st_*` scoped bearer token — no Vectros credential required to call this endpoint. The exchanged token's scope is resolved entirely server-side from the matched user's access profile; this endpoint accepts no caller-supplied scope, resource, or audience parameter (RFC 8693 §2.1's `resource`/`audience`/`scope` are not used in v1 — the registered `(issuer, audience)` pair alone pins the target tenant and app context). On a first-time login (no existing Vectros identity for this subject), two opt-in binding paths exist: `invite_token` (a `PENDING` sub-user invitation), and — if the registration declares one or more self-signup policies — `signup_type` (a brand-new user is created and bound to the policy's configured role). If `invite_token` is present at all, it is the ONLY path tried — a failed invite never falls through to self-signup. Neither field is required for a subject with an existing identity. Uses the OAuth-standard error envelope (`{"error":..., "error_description":...}`, RFC 6749 §5.2), NOT this API's usual `{"message":...}` shape — its client is generic OAuth tooling, not the Vectros SDK.
6179
+ *
6180
+ * @param {Vectros.TokenExchangeRequest} request
6181
+ * @param {AuthClient.RequestOptions} requestOptions - Request-specific configuration.
6182
+ *
6183
+ * @throws {@link Vectros.BadRequestError}
6184
+ * @throws {@link Vectros.UnauthorizedError}
6185
+ * @throws {@link Vectros.ForbiddenError}
6186
+ * @throws {@link Vectros.NotFoundError}
6187
+ * @throws {@link Vectros.TooManyRequestsError}
6188
+ *
6189
+ * @example
6190
+ * await client.auth.exchangeToken({
6191
+ * grant_type: "urn:ietf:params:oauth:grant-type:token-exchange",
6192
+ * subject_token: "subject_token",
6193
+ * subject_token_type: "urn:ietf:params:oauth:token-type:jwt"
6194
+ * })
6195
+ */
6196
+ exchangeToken(request, requestOptions) {
6197
+ return HttpResponsePromise.fromPromise(this.__exchangeToken(request, requestOptions));
6198
+ }
6199
+ async __exchangeToken(request, requestOptions) {
6200
+ const _headers = mergeHeaders(this._options?.headers, requestOptions?.headers);
6201
+ const _response = await fetcher({
6202
+ url: url_exports.join(
6203
+ await Supplier.get(this._options.baseUrl) ?? await Supplier.get(this._options.environment),
6204
+ "v1/auth/token/exchange"
6205
+ ),
6206
+ method: "POST",
6207
+ headers: _headers,
6208
+ contentType: "application/json",
6209
+ queryString: url_exports.queryBuilder().mergeAdditional(requestOptions?.queryParams).build(),
6210
+ requestType: "json",
6211
+ body: request,
6212
+ timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1e3,
6213
+ maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries,
6214
+ abortSignal: requestOptions?.abortSignal,
6215
+ fetchFn: this._options?.fetch,
6216
+ logging: this._options.logging
6217
+ });
6218
+ if (_response.ok) {
6219
+ return { data: _response.body, rawResponse: _response.rawResponse };
6220
+ }
6221
+ if (_response.error.reason === "status-code") {
6222
+ switch (_response.error.statusCode) {
6223
+ case 400:
6224
+ throw new BadRequestError(_response.error.body, _response.rawResponse);
6225
+ case 401:
6226
+ throw new UnauthorizedError(_response.error.body, _response.rawResponse);
6227
+ case 403:
6228
+ throw new ForbiddenError(_response.error.body, _response.rawResponse);
6229
+ case 404:
6230
+ throw new NotFoundError(_response.error.body, _response.rawResponse);
6231
+ case 429:
6232
+ throw new TooManyRequestsError(_response.error.body, _response.rawResponse);
6233
+ default:
6234
+ throw new VectrosError({
6235
+ statusCode: _response.error.statusCode,
6236
+ body: _response.error.body,
6237
+ rawResponse: _response.rawResponse
6238
+ });
6239
+ }
6240
+ }
6241
+ return handleNonStatusCodeError(_response.error, _response.rawResponse, "POST", "/v1/auth/token/exchange");
6242
+ }
5917
6243
  };
5918
6244
  var ComplianceClient = class {
5919
6245
  constructor(options) {
@@ -8521,6 +8847,69 @@ var IdentityClient = class {
8521
8847
  }
8522
8848
  return handleNonStatusCodeError(_response.error, _response.rawResponse, "POST", "/v1/users/lookup");
8523
8849
  }
8850
+ /**
8851
+ * 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.
8852
+ *
8853
+ * @param {Vectros.UserExistsByEmailRequest} request
8854
+ * @param {IdentityClient.RequestOptions} requestOptions - Request-specific configuration.
8855
+ *
8856
+ * @throws {@link Vectros.BadRequestError}
8857
+ * @throws {@link Vectros.ForbiddenError}
8858
+ *
8859
+ * @example
8860
+ * await client.identity.userExistsByEmail({
8861
+ * email: "user@example.com",
8862
+ * contextId: "myapp"
8863
+ * })
8864
+ */
8865
+ userExistsByEmail(request = {}, requestOptions) {
8866
+ return HttpResponsePromise.fromPromise(this.__userExistsByEmail(request, requestOptions));
8867
+ }
8868
+ async __userExistsByEmail(request = {}, requestOptions) {
8869
+ const { email, contextId } = request;
8870
+ const _queryParams = {
8871
+ email,
8872
+ contextId
8873
+ };
8874
+ const _authRequest = await this._options.authProvider.getAuthRequest();
8875
+ const _headers = mergeHeaders(
8876
+ _authRequest.headers,
8877
+ this._options?.headers,
8878
+ requestOptions?.headers
8879
+ );
8880
+ const _response = await fetcher({
8881
+ url: url_exports.join(
8882
+ await Supplier.get(this._options.baseUrl) ?? await Supplier.get(this._options.environment),
8883
+ "v1/users/exists-by-email"
8884
+ ),
8885
+ method: "GET",
8886
+ headers: _headers,
8887
+ queryString: url_exports.queryBuilder().addMany(_queryParams).mergeAdditional(requestOptions?.queryParams).build(),
8888
+ timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1e3,
8889
+ maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries,
8890
+ abortSignal: requestOptions?.abortSignal,
8891
+ fetchFn: this._options?.fetch,
8892
+ logging: this._options.logging
8893
+ });
8894
+ if (_response.ok) {
8895
+ return { data: _response.body, rawResponse: _response.rawResponse };
8896
+ }
8897
+ if (_response.error.reason === "status-code") {
8898
+ switch (_response.error.statusCode) {
8899
+ case 400:
8900
+ throw new BadRequestError(_response.error.body, _response.rawResponse);
8901
+ case 403:
8902
+ throw new ForbiddenError(_response.error.body, _response.rawResponse);
8903
+ default:
8904
+ throw new VectrosError({
8905
+ statusCode: _response.error.statusCode,
8906
+ body: _response.error.body,
8907
+ rawResponse: _response.rawResponse
8908
+ });
8909
+ }
8910
+ }
8911
+ return handleNonStatusCodeError(_response.error, _response.rawResponse, "GET", "/v1/users/exists-by-email");
8912
+ }
8524
8913
  /**
8525
8914
  * Returns the audit trail of changes to a user, most recent first. Identity history is always recorded and always available. Sensitive field values are redacted in every historical version. Returns a page in the `{data, nextCursor}` envelope. Requires the `users:r` scope.
8526
8915
  *
@@ -10238,10 +10627,11 @@ var logging;
10238
10627
  logging2.ConsoleLogger = ConsoleLogger;
10239
10628
  })(logging || (logging = {}));
10240
10629
 
10241
- // node_modules/@vectros-ai/blueprints/dist/index.mjs
10630
+ // ../blueprints/dist/index.mjs
10242
10631
  import { z } from "zod";
10243
10632
  import { z as z2 } from "zod";
10244
10633
  var CONTEXT_ID_RE = /^[a-z][a-z0-9-]{2,30}$/;
10634
+ var MAX_SCOPE_DIMENSIONS = 2;
10245
10635
  var ValidationRulesSchema = z.object({
10246
10636
  required: z.boolean().optional(),
10247
10637
  minLength: z.number().int().optional(),
@@ -10436,11 +10826,14 @@ var BlueprintSchemaSchema = z.object({
10436
10826
  // Schema-level ownership defaults, matching `SchemaRequest`: the principal
10437
10827
  // `userId` plus `scopes` — namespaced parent edges as `<namespace>:<value>`
10438
10828
  // (`org:...`, `client:...`, or a namespace you registered), at most two
10439
- // namespaces. The flat `orgId`/`clientId` fields fold into `scopes`.
10829
+ // namespaces. The flat `orgId`/`clientId` fields fold into `scopes`. Each
10830
+ // entry's namespace + value are validated below
10831
+ // (`lintNamespacedScopeArrays`) — this `.max(MAX_SCOPE_DIMENSIONS)` is
10832
+ // only the shape-level cap, not the grammar check.
10440
10833
  // With a scoped token these must be consistent with the profile's dataScope
10441
10834
  // (a cross-consistency lint is deferred to the lint slice).
10442
10835
  userId: z.string().min(1).optional(),
10443
- scopes: z.array(z.string().min(1)).max(2).optional(),
10836
+ scopes: z.array(z.string().min(1)).max(MAX_SCOPE_DIMENSIONS).optional(),
10444
10837
  // The id of an existing schema this one is a CUSTOMIZATION of, when a schema
10445
10838
  // named `typeName` already exists in this context — required in that case,
10446
10839
  // and must be omitted when this create is the first schema under that name
@@ -10478,7 +10871,9 @@ var BlueprintAccessProfileSchema = z.object({
10478
10871
  // ...) — the flat `orgId`/`clientId` keys are gone. e.g.
10479
10872
  // `{ "scope:org": ["org_x", null] }` = "org_x's records AND tenant-shared
10480
10873
  // records". Omitting null restricts to the listed owners ONLY (the key will
10481
- // NOT see tenant-level/seed records).
10874
+ // NOT see tenant-level/seed records). Every KEY is validated below
10875
+ // (`lintDataScopeKeys`) — `userId` or a grammar-valid `scope:<ns>`, no
10876
+ // other shape.
10482
10877
  dataScope: z.record(z.array(z.union([z.string().min(1), z.null()]))).optional(),
10483
10878
  // Optional identity overrides for the service principal's profile — scope
10484
10879
  // values its key STAMPS onto everything it writes (a credential can only
@@ -10487,6 +10882,11 @@ var BlueprintAccessProfileSchema = z.object({
10487
10882
  // `orgId`/`clientId` keys are gone); values may be `${{ identities.* }}`
10488
10883
  // tokens (substituted at apply time). e.g. `{ "scope:org": '${{ identities.team }}' }`
10489
10884
  // makes every record the service key writes org/team-owned.
10885
+ //
10886
+ // Every KEY, the ≤2-dimension cap, and every LITERAL value are validated
10887
+ // below (`lintIdentityOverrides`, placeholder-aware — see that function's
10888
+ // docstring); this `.min(1)` is only the shape-level "non-blank" guard on
10889
+ // the value, not the grammar check.
10490
10890
  identityOverrides: z.record(z.string().min(1)).optional()
10491
10891
  }).strict();
10492
10892
  var BlueprintRoleClauseSchema = z.object({
@@ -10516,11 +10916,61 @@ var IdentityDeclSchema = z.object({
10516
10916
  displayName: z.string().min(1).optional(),
10517
10917
  metadata: z.record(z.unknown()).optional()
10518
10918
  }).strict();
10519
- var IdentitiesDeclSchema = z.record(IdentityDeclSchema);
10919
+ var IDENTITY_NAME_RE = /^[A-Za-z_]\w*$/;
10920
+ var IdentitiesDeclSchema = z.record(
10921
+ z.string().regex(IDENTITY_NAME_RE, {
10922
+ 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"
10923
+ }),
10924
+ IdentityDeclSchema
10925
+ );
10520
10926
  var BlueprintServicePrincipalSchema = z.object({
10521
10927
  externalId: z.string().min(1),
10522
10928
  displayName: z.string().min(1)
10523
10929
  }).strict();
10930
+ var ISSUER_ID_RE = CONTEXT_ID_RE;
10931
+ var BlueprintIssuerSchema = z.object({
10932
+ /** Short slug identifying this issuer within your tenant. Immutable once registered. */
10933
+ issuerId: z.string().regex(ISSUER_ID_RE, {
10934
+ message: "issuerId must be 3-31 chars, start with a lowercase letter, then lowercase letters/digits/dashes (e.g. 'auth0-prod')"
10935
+ }),
10936
+ /** The IdP's `iss` claim value, exactly as it appears in tokens it issues. */
10937
+ issuer: z.string().min(1),
10938
+ /** The IdP's remote JWKS endpoint, used to verify presented tokens' signatures. */
10939
+ jwksUri: z.string().min(1),
10940
+ /**
10941
+ * The `aud` claim value a presented subject_token must carry. Must be globally
10942
+ * unique in combination with `issuer` — use a distinct audience per
10943
+ * environment/context sharing one IdP account (most OIDC providers support
10944
+ * this as an ordinary per-API/application default).
10945
+ */
10946
+ audience: z.string().min(1),
10947
+ /** Which app context an exchanged token targets. Must be an existing app context. */
10948
+ contextId: z.string().regex(CONTEXT_ID_RE, {
10949
+ message: "contextId must be 3-31 chars, start with a lowercase letter, then lowercase letters/digits/dashes (e.g. 'casework')"
10950
+ }),
10951
+ /** The claim carrying the subject identifier. Platform defaults to 'sub' when omitted. */
10952
+ subClaim: z.string().min(1).optional(),
10953
+ /** The claim carrying the subject's email (first-login invite matching). Platform defaults to 'email' when omitted. */
10954
+ emailClaim: z.string().min(1).optional(),
10955
+ /**
10956
+ * Opt-in self-service signup: a list of {signupType, roleId} pairs. When a first-time exchange
10957
+ * caller presents no invite token but names a signupType matching one of these (or omits it and
10958
+ * exactly one entry exists), a brand-new user is created and bound to that entry's role, no
10959
+ * invite required. Every entry must be something you're willing to grant to ANY caller who can
10960
+ * present a token from this issuer — a role carrying elevated (provisioning or wildcard) scope
10961
+ * is rejected. Omit entirely to leave self-signup disabled (the default).
10962
+ */
10963
+ selfSignupPolicies: z.array(
10964
+ z.object({
10965
+ signupType: z.string().regex(ISSUER_ID_RE, {
10966
+ message: "signupType must be 3-31 chars, start with a lowercase letter, then lowercase letters/digits/dashes (e.g. 'practitioner')"
10967
+ }),
10968
+ roleId: z.string().regex(ISSUER_ID_RE, {
10969
+ message: "roleId must be 3-31 chars, start with a lowercase letter, then lowercase letters/digits/dashes (e.g. 'practitioner-member')"
10970
+ })
10971
+ }).strict()
10972
+ ).optional()
10973
+ }).strict();
10524
10974
  var SeedCommonShape = {
10525
10975
  /** The schema typeName this seed instantiates (should match a declared schema). */
10526
10976
  typeName: z.string().min(1),
@@ -10534,9 +10984,12 @@ var SeedCommonShape = {
10534
10984
  * Optional scope ownership for the seeded item, as `namespace:value` entries
10535
10985
  * (≤2, e.g. `['org:${{ identities.team }}']`; tokens substitute at apply
10536
10986
  * time). `[]` = a private, user-owned item. Omitted = the seeding
10537
- * credential's full identity (the default).
10987
+ * credential's full identity (the default). Each entry's namespace + value
10988
+ * are validated below (`lintNamespacedScopeArrays`) — this
10989
+ * `.max(MAX_SCOPE_DIMENSIONS)` is only the shape-level cap, not the grammar
10990
+ * check.
10538
10991
  */
10539
- scopes: z.array(z.string()).optional()
10992
+ scopes: z.array(z.string()).max(MAX_SCOPE_DIMENSIONS).optional()
10540
10993
  };
10541
10994
  var RecordSeedSchema = z.object({
10542
10995
  surface: z.literal("record"),
@@ -10558,7 +11011,7 @@ var BlueprintSeedRecordSchema = z.discriminatedUnion("surface", [
10558
11011
  RecordSeedSchema,
10559
11012
  DocumentSeedSchema
10560
11013
  ]);
10561
- var SELF_TOKEN_RE = /\$\{\{\s*self\.[A-Za-z_]\w*\s*\}\}/;
11014
+ var SELF_TOKEN_RE = /\$\{\{\s*(?:self|under)\.[^}]*?\s*\}\}/;
10562
11015
  function lintSelfTokenPlacement(value, ctx) {
10563
11016
  const walk = (node, path4, inRoleDataScope) => {
10564
11017
  if (typeof node === "string") {
@@ -10585,12 +11038,20 @@ function lintSelfTokenPlacement(value, ctx) {
10585
11038
  walk(value, [], false);
10586
11039
  }
10587
11040
  var IDENTITY_REF_RE = /\$\{\{\s*identities\.([A-Za-z_]\w*)\s*\}\}/g;
10588
- function lintIdentityRefsDeclared(value, ctx) {
11041
+ var IDENTITY_REF_LOOSE_RE = /\$\{\{\s*identities\.([^}]*?)\s*\}\}/g;
11042
+ function lintIdentityRefs(value, ctx) {
10589
11043
  const declared = new Set(
10590
11044
  value && typeof value === "object" && "identities" in value && value.identities ? Object.keys(value.identities) : []
10591
11045
  );
10592
11046
  const walk = (node, path4) => {
10593
11047
  if (typeof node === "string") {
11048
+ for (const m of node.matchAll(IDENTITY_REF_LOOSE_RE)) {
11049
+ const captured = m[1];
11050
+ if (IDENTITY_NAME_RE.test(captured)) continue;
11051
+ const dotIndex = captured.indexOf(".");
11052
+ 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)} }}'`;
11053
+ ctx.addIssue({ code: z.ZodIssueCode.custom, path: path4, message });
11054
+ }
10594
11055
  for (const m of node.matchAll(IDENTITY_REF_RE)) {
10595
11056
  if (!declared.has(m[1])) {
10596
11057
  ctx.addIssue({
@@ -10613,6 +11074,134 @@ function lintIdentityRefsDeclared(value, ctx) {
10613
11074
  };
10614
11075
  walk(value, []);
10615
11076
  }
11077
+ var SCOPE_VALUE_RE = /^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$/;
11078
+ var SCOPE_NAMESPACE_RE = /^[a-z][a-z0-9_-]{1,31}$/;
11079
+ var FORBIDDEN_SCOPE_NAMESPACES = /* @__PURE__ */ new Set([
11080
+ "user",
11081
+ "record",
11082
+ "document",
11083
+ "entity",
11084
+ "self",
11085
+ "tenant",
11086
+ "context",
11087
+ "scope",
11088
+ "versions",
11089
+ "lookup"
11090
+ ]);
11091
+ function scopeNamespaceGrammarError(namespace) {
11092
+ if (namespace === "") return "scope namespace must not be blank";
11093
+ if (FORBIDDEN_SCOPE_NAMESPACES.has(namespace)) {
11094
+ return `'${namespace}' is a reserved namespace and cannot be used as an ownership scope`;
11095
+ }
11096
+ if (!SCOPE_NAMESPACE_RE.test(namespace)) {
11097
+ return `'${namespace}' is not a valid namespace \u2014 2-32 chars, lowercase letter first, then lowercase letters, digits, '_' or '-'`;
11098
+ }
11099
+ return null;
11100
+ }
11101
+ var IDENTITY_PLACEHOLDER_WHOLE_RE = new RegExp(`^${IDENTITY_REF_LOOSE_RE.source}$`);
11102
+ function lintIdentityOverrides(value, ctx) {
11103
+ const overrides = value.accessProfile?.identityOverrides;
11104
+ if (!overrides) return;
11105
+ const entries = Object.entries(overrides);
11106
+ if (entries.length > MAX_SCOPE_DIMENSIONS) {
11107
+ ctx.addIssue({
11108
+ code: z.ZodIssueCode.custom,
11109
+ path: ["accessProfile", "identityOverrides"],
11110
+ 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}.`
11111
+ });
11112
+ }
11113
+ for (const [key, v] of entries) {
11114
+ const ns = key.startsWith("scope:") ? key.slice("scope:".length) : null;
11115
+ const nsError = ns === null ? null : scopeNamespaceGrammarError(ns);
11116
+ if (ns === null || nsError !== null) {
11117
+ ctx.addIssue({
11118
+ code: z.ZodIssueCode.custom,
11119
+ path: ["accessProfile", "identityOverrides", key],
11120
+ 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}`
11121
+ });
11122
+ }
11123
+ if (typeof v !== "string") continue;
11124
+ if (IDENTITY_PLACEHOLDER_WHOLE_RE.test(v)) continue;
11125
+ if (!SCOPE_VALUE_RE.test(v)) {
11126
+ ctx.addIssue({
11127
+ code: z.ZodIssueCode.custom,
11128
+ path: ["accessProfile", "identityOverrides", key],
11129
+ 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).`
11130
+ });
11131
+ }
11132
+ }
11133
+ }
11134
+ function lintScopeEntries(entries, path4, ctx) {
11135
+ if (!entries) return;
11136
+ const seenValueByNamespace = /* @__PURE__ */ new Map();
11137
+ for (let i = 0; i < entries.length; i++) {
11138
+ const raw = entries[i];
11139
+ if (typeof raw !== "string") continue;
11140
+ const sep = raw.indexOf(":");
11141
+ if (sep <= 0 || sep === raw.length - 1) {
11142
+ ctx.addIssue({
11143
+ code: z.ZodIssueCode.custom,
11144
+ path: [...path4, i],
11145
+ message: `'${raw}' must be '<namespace>:<value>' (e.g. 'org:6ba7...' or 'team:${"${{ identities.team }}"}')`
11146
+ });
11147
+ continue;
11148
+ }
11149
+ const namespace = raw.slice(0, sep);
11150
+ const val = raw.slice(sep + 1);
11151
+ const nsError = scopeNamespaceGrammarError(namespace);
11152
+ if (nsError !== null) {
11153
+ ctx.addIssue({ code: z.ZodIssueCode.custom, path: [...path4, i], message: `'${raw}' ${nsError}` });
11154
+ } else {
11155
+ const priorValue = seenValueByNamespace.get(namespace);
11156
+ if (priorValue !== void 0 && priorValue !== val) {
11157
+ ctx.addIssue({
11158
+ code: z.ZodIssueCode.custom,
11159
+ path: [...path4, i],
11160
+ message: `'${namespace}' appears more than once with different values ('${priorValue}' and '${val}') \u2014 an item carries at most one value per namespace`
11161
+ });
11162
+ }
11163
+ seenValueByNamespace.set(namespace, val);
11164
+ }
11165
+ if (IDENTITY_PLACEHOLDER_WHOLE_RE.test(val)) continue;
11166
+ if (!SCOPE_VALUE_RE.test(val)) {
11167
+ ctx.addIssue({
11168
+ code: z.ZodIssueCode.custom,
11169
+ path: [...path4, i],
11170
+ 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).`
11171
+ });
11172
+ }
11173
+ }
11174
+ }
11175
+ function lintNamespacedScopeArrays(value, ctx) {
11176
+ const bp = value;
11177
+ (bp.schemas ?? []).forEach((s, i) => lintScopeEntries(s.scopes, ["schemas", i, "scopes"], ctx));
11178
+ (bp.seed ?? []).forEach((s, i) => lintScopeEntries(s.scopes, ["seed", i, "scopes"], ctx));
11179
+ }
11180
+ function lintDataScopeKey(key, path4, ctx) {
11181
+ if (key === "userId") return;
11182
+ const ns = key.startsWith("scope:") ? key.slice("scope:".length) : null;
11183
+ const nsError = ns === null ? null : scopeNamespaceGrammarError(ns);
11184
+ if (ns === null || nsError !== null) {
11185
+ ctx.addIssue({
11186
+ code: z.ZodIssueCode.custom,
11187
+ path: path4,
11188
+ 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}`
11189
+ });
11190
+ }
11191
+ }
11192
+ function lintDataScopeKeys(value, ctx) {
11193
+ const bp = value;
11194
+ for (const key of Object.keys(bp.accessProfile?.dataScope ?? {})) {
11195
+ lintDataScopeKey(key, ["accessProfile", "dataScope", key], ctx);
11196
+ }
11197
+ for (const [roleId, clauses] of Object.entries(bp.roles ?? {})) {
11198
+ clauses.forEach((clause, i) => {
11199
+ for (const key of Object.keys(clause.dataScope ?? {})) {
11200
+ lintDataScopeKey(key, ["roles", roleId, i, "dataScope", key], ctx);
11201
+ }
11202
+ });
11203
+ }
11204
+ }
10616
11205
  function lintSeedSurfaces(value, ctx) {
10617
11206
  const bp = value;
10618
11207
  if (!Array.isArray(bp.seed)) return;
@@ -10630,6 +11219,14 @@ function lintSeedSurfaces(value, ctx) {
10630
11219
  }
10631
11220
  });
10632
11221
  }
11222
+ var BLUEPRINT_FIELD_PHASES = Object.freeze({
11223
+ servicePrincipal: "bootstrap",
11224
+ issuers: "bootstrap",
11225
+ schemas: "in-context",
11226
+ accessProfile: "in-context",
11227
+ roles: "in-context",
11228
+ seed: "in-context"
11229
+ });
10633
11230
  var BlueprintSchema = z.object({
10634
11231
  /** Stable blueprint id (the `--blueprint <name>` selector + idempotency key). */
10635
11232
  name: z.string().min(1),
@@ -10648,10 +11245,20 @@ var BlueprintSchema = z.object({
10648
11245
  /** Optional multi-clause roles, bound to principals via `access grant --role`. */
10649
11246
  roles: BlueprintRolesSchema.optional(),
10650
11247
  /** Optional principals ensured-exist at apply; referenced via ${{ identities.* }}. */
10651
- identities: IdentitiesDeclSchema.optional()
11248
+ identities: IdentitiesDeclSchema.optional(),
11249
+ /**
11250
+ * Optional trusted third-party IdP issuers to register (BYO-IdP token
11251
+ * exchange). Applied in the BOOTSTRAP-token phase, alongside app-context/
11252
+ * service-principal creation — NOT the in-context load (see
11253
+ * {@link BlueprintIssuerSchema}).
11254
+ */
11255
+ issuers: z.array(BlueprintIssuerSchema).optional()
10652
11256
  }).strict().superRefine((bp, ctx) => {
10653
11257
  lintSelfTokenPlacement(bp, ctx);
10654
- lintIdentityRefsDeclared(bp, ctx);
11258
+ lintIdentityRefs(bp, ctx);
11259
+ lintIdentityOverrides(bp, ctx);
11260
+ lintNamespacedScopeArrays(bp, ctx);
11261
+ lintDataScopeKeys(bp, ctx);
10655
11262
  lintSeedSurfaces(bp, ctx);
10656
11263
  });
10657
11264
  function formatIssuePath(path4) {
@@ -10807,7 +11414,9 @@ function deriveSuffix(contextId) {
10807
11414
  var WHOLE_TOKEN_RE = /^\$\{\{\s*([A-Za-z_]\w*)\.([A-Za-z_]\w*)\s*\}\}$/;
10808
11415
  var TOKEN_SCAN_RE = /\$\$\{\{|\$\{\{([\s\S]*?)\}\}|\$\{\{/g;
10809
11416
  var INNER_REF_RE = /^([A-Za-z_]\w*)\.([A-Za-z_]\w*)$/;
10810
- var DEFERRED_NAMESPACES = /* @__PURE__ */ new Set(["self", "identities"]);
11417
+ var DEFERRED_NAMESPACES = /* @__PURE__ */ new Set(["self", "identities", "under"]);
11418
+ var DEFERRED_NS_ALTERNATION = [...DEFERRED_NAMESPACES].join("|");
11419
+ var DEFERRED_INNER_RE = new RegExp(`^(?:${DEFERRED_NS_ALTERNATION})(?:\\.[A-Za-z_]\\w*)+$`);
10811
11420
  function resolveRef(ns, name, ctx, path4) {
10812
11421
  if (ns === "inputs") {
10813
11422
  if (!(name in ctx.declared)) {
@@ -10865,14 +11474,17 @@ function interpolate(str, ctx, path4) {
10865
11474
  });
10866
11475
  } else {
10867
11476
  const inner = (m[1] ?? "").trim();
11477
+ if (DEFERRED_INNER_RE.test(inner)) {
11478
+ out += m[0];
11479
+ last = TOKEN_SCAN_RE.lastIndex;
11480
+ continue;
11481
+ }
10868
11482
  const ref = inner.match(INNER_REF_RE);
10869
11483
  if (!ref) {
10870
11484
  ctx.issues.push({
10871
11485
  path: path4,
10872
11486
  message: `malformed reference '\${{ ${inner} }}' \u2014 expected '\${{ namespace.name }}' (escape a literal with '$\${{')`
10873
11487
  });
10874
- } else if (DEFERRED_NAMESPACES.has(ref[1])) {
10875
- out += m[0];
10876
11488
  } else {
10877
11489
  const v = resolveRef(ref[1], ref[2], ctx, path4);
10878
11490
  if (v !== void 0) out += String(v);
@@ -11399,7 +12011,7 @@ var MEMBER_SHARED_READ_ACTIONS = [
11399
12011
  ];
11400
12012
  var agenticSdlc = {
11401
12013
  name: "agentic-sdlc",
11402
- version: "1.6.0",
12014
+ version: "1.9.0",
11403
12015
  description: "A whole-SDLC system of record for an AI development team \u2014 decisions, designs, references, runbooks, post-mortems (as documents) plus controls, conventions, gotchas, and a glossary (as records), cross-linked and recalled by meaning.",
11404
12016
  contextId: "agentic-sdlc",
11405
12017
  contextName: "Agentic SDLC Knowledge Base",
@@ -12265,6 +12877,233 @@ var agenticSdlc = {
12265
12877
  { fieldName: "updatedOn", rangeEnabled: true },
12266
12878
  { fieldName: "priority", rangeEnabled: true }
12267
12879
  ]
12880
+ },
12881
+ {
12882
+ // A PROPOSED memory, waiting on a verification step — the staging area in
12883
+ // front of the `memory` type above. An agent that distills its own working
12884
+ // sessions produces claims faster than it can check them, and an unchecked
12885
+ // claim is worse than no claim: it is a plausible sentence that will be
12886
+ // recalled as fact. So a proposal lands here, an agent later verifies it,
12887
+ // and only a verified one is written to `memory`.
12888
+ //
12889
+ // ⚠ indexMode NONE IS THE LOAD-BEARING DECISION, not a tuning choice.
12890
+ // Store-only means these records are never indexed, so a search or a
12891
+ // grounded answer CANNOT return them under any query — the separation
12892
+ // between unverified proposals and trusted knowledge is enforced by the
12893
+ // platform rather than by every caller remembering to filter. Make this
12894
+ // HYBRID and the corpus of unchecked claims starts competing with the
12895
+ // curated KB for retrieval slots, which is the one failure this type
12896
+ // exists to prevent. They stay reachable by id and by the two lookups
12897
+ // below, which is all a disposition workflow needs.
12898
+ //
12899
+ // Note the consequence for the fields: `filterable` narrows SEARCH, and
12900
+ // nothing here is searchable, so no field declares it. Query paths are the
12901
+ // lookups, deliberately — a flag that does nothing would just read as
12902
+ // capability that isn't there.
12903
+ typeName: "candidate",
12904
+ displayName: "Memory candidate",
12905
+ indexMode: "NONE",
12906
+ fields: [
12907
+ {
12908
+ fieldId: "title",
12909
+ fieldType: "string",
12910
+ required: true,
12911
+ validation: { minLength: 1, maxLength: 200 },
12912
+ description: "One line naming the claim \u2014 what a reviewer reads first.",
12913
+ renderHints: { label: "Title", widget: "text", order: 1, displayField: true }
12914
+ },
12915
+ {
12916
+ fieldId: "body",
12917
+ fieldType: "string",
12918
+ description: "The proposed fact in full, as the distiller stated it \u2014 the thing to verify.",
12919
+ renderHints: { label: "Body", widget: "textarea", order: 2 }
12920
+ },
12921
+ {
12922
+ // Mirrors `memory.kind` exactly, so a verified candidate maps 1:1 onto
12923
+ // the memory it becomes with no vocabulary translation in between.
12924
+ fieldId: "kind",
12925
+ fieldType: "enum",
12926
+ enumValues: ["user", "feedback", "project", "reference", "observation"],
12927
+ description: "Which flavour of memory this would become \u2014 the same vocabulary as `memory.kind`.",
12928
+ renderHints: { label: "Kind", widget: "select", order: 3 }
12929
+ },
12930
+ {
12931
+ // The distiller's ROUTING suggestion, and the one field here with no
12932
+ // counterpart on `memory` — because it stops existing the moment the
12933
+ // question it asks is answered. A verified claim either becomes a
12934
+ // memory record or becomes a paragraph in a document; this is the
12935
+ // proposer's guess about which, and the verifier is free to overrule
12936
+ // it (the verdict lands in `disposition`, not here).
12937
+ fieldId: "dest",
12938
+ fieldType: "enum",
12939
+ enumValues: ["memory", "doc"],
12940
+ description: "Where the proposer thinks this belongs if it survives review \u2014 a suggestion, not the verdict.",
12941
+ renderHints: { label: "Suggested destination", widget: "select", order: 4 }
12942
+ },
12943
+ {
12944
+ // `area`, `tags` and `sourceRef` below mirror `memory`'s exactly, for
12945
+ // the same reason `kind` does: a verified candidate is COPIED into a
12946
+ // memory, and any field that exists on only one side has to be
12947
+ // re-derived or dropped at exactly the moment a human is deciding
12948
+ // whether the claim is true.
12949
+ fieldId: "area",
12950
+ fieldType: "string",
12951
+ description: 'Subsystem/topic the claim applies to (e.g. "auth", "search") \u2014 the shared area vocabulary.',
12952
+ renderHints: { label: "Area", widget: "text", order: 5 }
12953
+ },
12954
+ {
12955
+ fieldId: "tags",
12956
+ fieldType: "array",
12957
+ description: "Freeform labels carried over from the proposal.",
12958
+ renderHints: { label: "Tags", order: 6 }
12959
+ },
12960
+ {
12961
+ // Provenance the VERIFIER needs most: checking a claim starts with the
12962
+ // file or issue it is about. A candidate whose sourceRef was dropped
12963
+ // in transit costs the reviewer the search that the proposer had
12964
+ // already done.
12965
+ fieldId: "sourceRef",
12966
+ fieldType: "string",
12967
+ description: "Provenance: the file path / issue / URL the claim is about \u2014 where a verifier starts.",
12968
+ renderHints: { label: "Source ref", widget: "text", order: 7 }
12969
+ },
12970
+ {
12971
+ fieldId: "sessionId",
12972
+ fieldType: "string",
12973
+ description: "The conversation that proposed it \u2014 your runtime's session identifier.",
12974
+ renderHints: { label: "Session", widget: "text", order: 8 }
12975
+ },
12976
+ {
12977
+ // Also the stable ordering key: a display ordinal is derived by sorting
12978
+ // on this, so proposals keep the numbering a reviewer saw. Nothing is
12979
+ // ever hard-deleted here, which is what keeps that ordering stable.
12980
+ fieldId: "proposedAt",
12981
+ fieldType: "date",
12982
+ description: "ISO-8601 \u2014 when it was proposed. Also the stable ordering key for display.",
12983
+ renderHints: { label: "Proposed at", widget: "date", order: 9 }
12984
+ },
12985
+ {
12986
+ // THE VERDICT, and deliberately a plain payload field rather than the
12987
+ // record's own ACTIVE/ARCHIVED status. Settling a candidate is
12988
+ // REVERSIBLE — a wrong dismissal has to be undoable — and a write-side
12989
+ // visibility flip is the wrong tool for a reversible state: it is
12990
+ // asynchronous, it moves the record out from under readers, and it
12991
+ // leaves the index status of a once-indexed row stranded. A filterable
12992
+ // payload field flips synchronously and reads the same for everyone.
12993
+ fieldId: "disposition",
12994
+ fieldType: "enum",
12995
+ enumValues: ["pending", "stored", "documented", "ignored"],
12996
+ description: "The verdict: still pending, written to memory, captured in a doc, or dismissed.",
12997
+ renderHints: { label: "Disposition", widget: "select", order: 10 }
12998
+ },
12999
+ {
13000
+ fieldId: "ref",
13001
+ fieldType: "string",
13002
+ description: "The citation given when settling it \u2014 the record id, document path, or reason.",
13003
+ renderHints: { label: "Ref", widget: "text", order: 11 }
13004
+ },
13005
+ {
13006
+ // What the settling step actually CHECKED, which is not always what the
13007
+ // agent typed. Keeping both makes a bad citation auditable after the
13008
+ // fact instead of indistinguishable from a good one.
13009
+ fieldId: "resolved",
13010
+ fieldType: "string",
13011
+ description: "What was verified when it was settled \u2014 kept alongside `ref`, which is what was claimed.",
13012
+ renderHints: { label: "Resolved", widget: "text", order: 12 }
13013
+ },
13014
+ {
13015
+ fieldId: "origin",
13016
+ fieldType: "enum",
13017
+ enumValues: ["session", "recovered"],
13018
+ description: "Proposed by a live session, or recovered later from one that ended without settling.",
13019
+ renderHints: { label: "Origin", widget: "select", order: 13 }
13020
+ },
13021
+ {
13022
+ fieldId: "reopenedWhy",
13023
+ fieldType: "string",
13024
+ description: "Why a settled candidate was reopened \u2014 the audit trail for undoing a verdict.",
13025
+ renderHints: { label: "Reopened why", widget: "textarea", order: 14 }
13026
+ },
13027
+ {
13028
+ // The forward half of a correction: this candidate restates an earlier
13029
+ // one whose claim was wrong. Written at create, so it costs nothing.
13030
+ fieldId: "revises",
13031
+ fieldType: "reference",
13032
+ targetTypeName: "candidate",
13033
+ targetSurface: "record",
13034
+ targetField: "externalId",
13035
+ cardinality: "one",
13036
+ description: "The candidate this one corrects (by externalId).",
13037
+ renderHints: { label: "Revises", order: 15 }
13038
+ },
13039
+ {
13040
+ // The reverse half, and NOT redundant: "is this still open?" has to be
13041
+ // answerable without asking whether some OTHER record points here, and
13042
+ // there is no reverse-reference read. So the superseded candidate is
13043
+ // marked directly.
13044
+ //
13045
+ // Kept SEPARATE from `disposition` on purpose. Superseded and dismissed
13046
+ // are different facts: a dismissal may be undone, while resurrecting a
13047
+ // claim that a later run already corrected would re-offer the version
13048
+ // known to be wrong. Folding both into one enum makes that distinction
13049
+ // unrepresentable exactly when it matters.
13050
+ fieldId: "supersededBy",
13051
+ fieldType: "reference",
13052
+ targetTypeName: "candidate",
13053
+ targetSurface: "record",
13054
+ targetField: "externalId",
13055
+ cardinality: "one",
13056
+ description: "The candidate that corrected this one (by externalId) \u2014 set on the older record.",
13057
+ renderHints: { label: "Superseded by", order: 16 }
13058
+ }
13059
+ ],
13060
+ // FOUR lookups, because this type is asked three genuinely different
13061
+ // questions and one of them is a conjunction:
13062
+ //
13063
+ // `disposition` — "what is waiting ANYWHERE?" The cross-session
13064
+ // review queue. Low cardinality, so its
13065
+ // partitions are large; that is inherent to the
13066
+ // question and the pending set is small.
13067
+ // `sessionId` — "everything belonging to this conversation",
13068
+ // settled candidates included.
13069
+ // (sessionId, disposition) — "what is still waiting IN this conversation".
13070
+ // `proposedAt` (range) — worked by age: "what has been sitting
13071
+ // longest", "what came in this week". A range
13072
+ // read, not a filter over everything ever
13073
+ // proposed.
13074
+ //
13075
+ // The composite earns its slot because that third question is the HOTTEST
13076
+ // read in the system — it is what an agent is shown as it works, so it runs
13077
+ // far more often than the queue-wide scan does. Without it the answer is the
13078
+ // more selective single lookup plus a filter in the caller.
13079
+ //
13080
+ // `sessionId` LEADS, and the order is not cosmetic: the leading field
13081
+ // becomes the partition key. Leading with `disposition` would sort every
13082
+ // candidate ever proposed into four partitions; leading with `sessionId`
13083
+ // gives one small partition per conversation. Order is also
13084
+ // migration-locked, so it cannot be corrected later.
13085
+ //
13086
+ // A composite matches a LEADING RUN of its fields, so `(sessionId,
13087
+ // disposition)` also answers `sessionId` alone. The plain `sessionId`
13088
+ // lookup is kept anyway: under a partial tuple the unspecified field
13089
+ // becomes part of the ordering, so results come back grouped by
13090
+ // `disposition` rather than in one sequence — right for a filtered view,
13091
+ // wrong for "this conversation's candidates, oldest first", which is the
13092
+ // read the review surface and the queue/record agreement check both make.
13093
+ // One declaration out of ten is a cheap price for not depending on that
13094
+ // subtlety in the two places that must not be wrong.
13095
+ //
13096
+ // No `sortBy`: it defaults to the record's creation time, which is already
13097
+ // the primary key the review queue orders by. Naming `proposedAt` instead
13098
+ // would order by the WORKER's clock, and that is stamped when a proposal is
13099
+ // queued rather than when it is stored — the two disagree by exactly the
13100
+ // outage a durable queue exists to survive.
13101
+ lookupFields: [
13102
+ "disposition",
13103
+ "sessionId",
13104
+ { fieldNames: ["sessionId", "disposition"] },
13105
+ { fieldName: "proposedAt", rangeEnabled: true }
13106
+ ]
12268
13107
  }
12269
13108
  ],
12270
13109
  // Least-privilege, data-plane only. The scope of the `ssk_*` key the bootstrap
@@ -12318,7 +13157,12 @@ var agenticSdlc = {
12318
13157
  // grant (`records:r:memory`) and the inference capability travel in the
12319
13158
  // SAME clause, so grounding works regardless of cross-clause admission
12320
13159
  // semantics. Still self-fenced by dataScope: never another principal's.
12321
- allowedActions: ["records:cru:memory", "search:r", "inference:r"],
13160
+ // `candidate` is READ-only here, unlike `memory`'s full CRU. A candidate's
13161
+ // verdict is the output of a verification step, so it is written by the
13162
+ // agent runtime that performed the check — a human editing the field
13163
+ // directly would record a verdict nothing actually verified. Reading them
13164
+ // is the point: the queue, the verdicts and the corrections are browsable.
13165
+ allowedActions: ["records:cru:memory", "records:r:candidate", "search:r", "inference:r"],
12322
13166
  dataScope: { userId: ["${{ self.userId }}"] }
12323
13167
  }
12324
13168
  ]
@@ -13164,7 +14008,17 @@ async function resolveAuthContext(opts) {
13164
14008
  bearer: bridge.token,
13165
14009
  cognitoToken,
13166
14010
  baseUrl,
13167
- env: opts.env,
14011
+ /**
14012
+ * Derived from the REQUEST'S actual `baseUrl`, not echoed from `opts.env` — the two
14013
+ * diverge whenever `VECTROS_API_BASE_URL`/`--base-url` alone selects the environment
14014
+ * (the common shape when targeting staging without also passing `--env staging`,
14015
+ * since `envToBaseUrl` lets an explicit override win over `opts.env`'s default). Every
14016
+ * display surface that shows "on {env}" (blueprint-test's banner, `whoami`, etc.) reads
14017
+ * this field, so it must reflect where the request actually went, not which hidden flag
14018
+ * defaulted to what. `deriveEnvironment` falls through to `opts.env` only for a base URL
14019
+ * it doesn't recognize (a non-standard override, e.g. a future custom/self-hosted host).
14020
+ */
14021
+ env: deriveEnvironment(baseUrl) ?? opts.env,
13168
14022
  identity,
13169
14023
  tenantId: typeof identity.tenantId === "string" ? identity.tenantId : void 0,
13170
14024
  tenant
@@ -13248,7 +14102,8 @@ var DATA_PLANE_ALLOWLIST = [
13248
14102
  "search",
13249
14103
  "documents",
13250
14104
  "folders",
13251
- "inference"
14105
+ "inference",
14106
+ "entities"
13252
14107
  ];
13253
14108
  var KNOWN_CONTROL_PLANE_RESOURCES = [
13254
14109
  "keys",
@@ -13257,12 +14112,13 @@ var KNOWN_CONTROL_PLANE_RESOURCES = [
13257
14112
  "users",
13258
14113
  "billing",
13259
14114
  "admin",
13260
- // Identity/compartment provisioning. The flat `orgs`/`clients` resources were
13261
- // folded into the generic `entities` (per-namespace) + `namespaces` surfaces,
13262
- // which is what the API now scopes on — so those are the current spellings a
13263
- // pack could name. (Cosmetic: any non-data-plane resource hard-rejects whether
13264
- // or not it is listed here; this list only sharpens the error message.)
13265
- "entities",
14115
+ // The namespace REGISTRY (as opposed to `entities`, which creates/reads a
14116
+ // ROW within an already-registered namespace — see the data-plane-allowlist
14117
+ // note above). Registering a new namespace TYPE is a tenant-config action,
14118
+ // root-key-gated regardless of any scope a token carries — genuinely
14119
+ // control-plane, unlike per-namespace entity CRUD. (Cosmetic: any
14120
+ // non-data-plane resource hard-rejects whether or not it is listed here;
14121
+ // this list only sharpens the error message.)
13266
14122
  "namespaces"
13267
14123
  ];
13268
14124
  var VALID_OPS = /* @__PURE__ */ new Set(["c", "r", "u", "d"]);
@@ -13576,6 +14432,32 @@ async function runLoader(client, input) {
13576
14432
  throw new Error("Service-principal create returned no user id; cannot bind AccessProfile/key.");
13577
14433
  }
13578
14434
  log.debug({ component: "loader", externalId: blueprint.servicePrincipal.externalId, userId }, "service principal ensured");
14435
+ const issuerActions = [];
14436
+ for (const issuer of blueprint.issuers ?? []) {
14437
+ const issuerResp = await client.auth.registerIssuer({
14438
+ issuerId: issuer.issuerId,
14439
+ issuer: issuer.issuer,
14440
+ jwksUri: issuer.jwksUri,
14441
+ audience: issuer.audience,
14442
+ contextId: issuer.contextId,
14443
+ subClaim: issuer.subClaim,
14444
+ emailClaim: issuer.emailClaim,
14445
+ selfSignupPolicies: issuer.selfSignupPolicies?.map((p) => ({
14446
+ signup_type: p.signupType,
14447
+ role_id: p.roleId
14448
+ }))
14449
+ });
14450
+ const action = actionOf(issuerResp.created);
14451
+ issuerActions.push({ issuerId: issuer.issuerId, action });
14452
+ if (action === "reconciled") {
14453
+ log.warn(
14454
+ { component: "loader", issuerId: issuer.issuerId },
14455
+ "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"
14456
+ );
14457
+ } else {
14458
+ log.debug({ component: "loader", issuerId: issuer.issuerId, action }, "issuer registered");
14459
+ }
14460
+ }
13579
14461
  const ctxClient = input.remintForContext ? await input.remintForContext(blueprint.contextId) : client;
13580
14462
  log.debug({ component: "loader", contextId: blueprint.contextId }, "re-minted per-context client");
13581
14463
  const schemaIds = {};
@@ -13757,6 +14639,7 @@ async function runLoader(client, input) {
13757
14639
  profileAction,
13758
14640
  schemaActions,
13759
14641
  roleActions,
14642
+ issuerActions,
13760
14643
  rawKey,
13761
14644
  keyId,
13762
14645
  revokedKeyId,
@@ -13775,6 +14658,13 @@ function buildPlan(blueprint, opts = {}) {
13775
14658
  kind: "principal",
13776
14659
  summary: `principal '${blueprint.servicePrincipal.externalId}' (SERVICE)`
13777
14660
  });
14661
+ for (const iss of blueprint.issuers ?? []) {
14662
+ const selfSignup = iss.selfSignupPolicies?.length ? `, self-signup: ${iss.selfSignupPolicies.length} polic${iss.selfSignupPolicies.length === 1 ? "y" : "ies"}` : "";
14663
+ steps.push({
14664
+ kind: "issuer",
14665
+ summary: `issuer '${iss.issuerId}' (${iss.issuer} \u2192 context '${iss.contextId}'${selfSignup}) [${BLUEPRINT_FIELD_PHASES.issuers}-token phase]`
14666
+ });
14667
+ }
13778
14668
  for (const s of blueprint.schemas) {
13779
14669
  const lookups = toLookupDefs(s.lookupFields)?.map((l) => l.fieldNames ? `(${l.fieldNames.join("+")})` : l.fieldName) ?? [];
13780
14670
  const lookupNote = lookups.length ? ` lookup: [${lookups.join(", ")}]` : "";
@@ -13815,8 +14705,17 @@ function buildPlan(blueprint, opts = {}) {
13815
14705
  }
13816
14706
  function renderPlan(blueprint, steps) {
13817
14707
  const header = ` Plan for blueprint '${blueprint.name}' (v${blueprint.version}) \u2192 context '${blueprint.contextId}':`;
13818
- const legend = ` Apply is a non-destructive reconcile \u2014 each item is created if absent, else
13819
- updated in place to match. Nothing is dropped or recreated.`;
14708
+ const legendLines = [
14709
+ ` Apply is a non-destructive reconcile \u2014 each item is created if absent, else`,
14710
+ ` updated in place to match. Nothing is dropped or recreated.`
14711
+ ];
14712
+ if (steps.some((s) => s.kind === "issuer")) {
14713
+ legendLines.push(
14714
+ ` Exception: issuers cannot be updated once registered \u2014 an existing`,
14715
+ ` issuer is left unchanged, not reconciled to match.`
14716
+ );
14717
+ }
14718
+ const legend = legendLines.join("\n");
13820
14719
  const lines = steps.map((s) => ` + ${s.summary}`);
13821
14720
  return [header, legend, "", ...lines].join("\n");
13822
14721
  }
@@ -15671,6 +16570,14 @@ function revokeWarning(targets) {
15671
16570
  }
15672
16571
 
15673
16572
  // src/minted-key-store.ts
16573
+ function keyringAliasFor(alias, tenant) {
16574
+ if (tenant === "live" && alias.endsWith(".test")) {
16575
+ throw new Error(
16576
+ `'${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.`
16577
+ );
16578
+ }
16579
+ return tenant === "test" ? `${alias}.test` : alias;
16580
+ }
15674
16581
  function nowIso() {
15675
16582
  return (/* @__PURE__ */ new Date()).toISOString();
15676
16583
  }
@@ -15787,6 +16694,7 @@ var BLANK_SLATE = {
15787
16694
  };
15788
16695
  function parseBootstrapArgs(argv) {
15789
16696
  const args = {
16697
+ set: [],
15790
16698
  env: "production",
15791
16699
  yes: false,
15792
16700
  noMerge: false,
@@ -15804,6 +16712,12 @@ function parseBootstrapArgs(argv) {
15804
16712
  case "--blueprint":
15805
16713
  args.blueprint = argv[++i];
15806
16714
  break;
16715
+ case "--set":
16716
+ args.set.push(argv[++i]);
16717
+ break;
16718
+ case "--values":
16719
+ args.values = argv[++i];
16720
+ break;
15807
16721
  case "--env": {
15808
16722
  const v = argv[++i];
15809
16723
  if (v !== "staging" && v !== "production") {
@@ -15901,10 +16815,20 @@ function reconcileTally(items, noun) {
15901
16815
  if (reconciled) seg.push(`${reconciled} reconciled`);
15902
16816
  return `${items.length} ${noun}${items.length === 1 ? "" : "s"} (${seg.join(", ")})`;
15903
16817
  }
16818
+ function issuerTally(items) {
16819
+ if (items.length === 0) return null;
16820
+ const created = items.filter((i) => i.action === "created").length;
16821
+ const unchanged = items.length - created;
16822
+ const seg = [];
16823
+ if (created) seg.push(`${created} created`);
16824
+ if (unchanged) seg.push(`${unchanged} already registered \u2014 unchanged (issuers cannot be updated once registered)`);
16825
+ return `${items.length} issuer${items.length === 1 ? "" : "s"} (${seg.join(", ")})`;
16826
+ }
15904
16827
  function applyOutcomeSummary(result) {
15905
16828
  const parts = [
15906
16829
  reconcileTally(result.schemaActions, "schema"),
15907
16830
  reconcileTally(result.roleActions, "role"),
16831
+ issuerTally(result.issuerActions),
15908
16832
  `profile ${result.profileAction}`
15909
16833
  ].filter((p) => p !== null);
15910
16834
  const detail = parts.length ? ` \u2014 ${parts.join("; ")}` : "";
@@ -15915,6 +16839,7 @@ var BOOTSTRAP_HELP = `vectros bootstrap \u2014 provision a least-privilege MCP c
15915
16839
 
15916
16840
  Usage:
15917
16841
  vectros bootstrap [--blueprint <name> | --blank] [--no-seed]
16842
+ [--set name=value ...] [--values <file>]
15918
16843
  [--tenant test|live | --test] [--client desktop|code]
15919
16844
  [--yes] [--no-merge] [--rotate]
15920
16845
  [--token <t> | env VECTROS_BOOTSTRAP_TOKEN]
@@ -15924,6 +16849,10 @@ Options:
15924
16849
  --blueprint <name|file> A bundled blueprint (${BLUEPRINT_NAMES.join(", ")}),
15925
16850
  OR a path to your own blueprint YAML/JSON (./my.yaml) \u2014 the "copy &
15926
16851
  own" path. File blueprints get the identical scope gate (no override).
16852
+ --set name=value Set an install-time input value (repeatable) for a file blueprint's
16853
+ 'inputs:' block. Beats --values + the declared default. (Same flag as
16854
+ 'vectros blueprint validate/plan'.)
16855
+ --values <file> A YAML/JSON map of input values. (File blueprints only.)
15927
16856
  --blank Credential only (read-only data-plane profile, no schemas).
15928
16857
  --no-seed Provision the blueprint's schemas, profile and key but SKIP its
15929
16858
  sample seed records \u2014 the right choice for a production context
@@ -15958,10 +16887,21 @@ Options:
15958
16887
 
15959
16888
  No --blueprint/--blank in a TTY \u2192 interactive menu.`;
15960
16889
  async function resolveBlueprint(args, prompt, isTty) {
15961
- if (args.blankSlate) return BLANK_SLATE;
16890
+ if (args.blankSlate) {
16891
+ if (args.set.length > 0 || args.values) {
16892
+ throw new Error(`--set/--values apply only to file blueprints; --blank has no inputs to set.`);
16893
+ }
16894
+ return BLANK_SLATE;
16895
+ }
15962
16896
  if (args.blueprint) {
15963
16897
  if (looksLikePath(args.blueprint)) {
15964
- return await loadBlueprintFile(args.blueprint);
16898
+ const supplied = await buildInputSupply(args.set, args.values);
16899
+ return await loadBlueprintFile(args.blueprint, supplied);
16900
+ }
16901
+ if (args.set.length > 0 || args.values) {
16902
+ throw new Error(
16903
+ `--set/--values apply only to file blueprints; '${args.blueprint}' is a bundled blueprint (no inputs to set).`
16904
+ );
15965
16905
  }
15966
16906
  const b = getBlueprint(args.blueprint);
15967
16907
  if (!b) {
@@ -15974,6 +16914,11 @@ async function resolveBlueprint(args, prompt, isTty) {
15974
16914
  if (!isTty) {
15975
16915
  throw new Error("No --blueprint or --blank specified and not a TTY (cannot show the menu).");
15976
16916
  }
16917
+ if (args.set.length > 0 || args.values) {
16918
+ throw new Error(
16919
+ `--set/--values need an explicit --blueprint <file>; the interactive menu only offers bundled blueprints and blank slate, neither of which has inputs to set.`
16920
+ );
16921
+ }
15977
16922
  const options = [
15978
16923
  { label: "Blank slate (credential only)", blueprint: BLANK_SLATE },
15979
16924
  ...BUNDLED_BLUEPRINTS.map((b) => ({ label: `${b.name} \u2014 ${b.description}`, blueprint: b }))
@@ -16139,10 +17084,11 @@ ${renderPlan(blueprint, planSteps)}
16139
17084
  let storedFreshInKeyring = false;
16140
17085
  const readExistingKey = async (contextId) => {
16141
17086
  if (!keyring) return void 0;
17087
+ const alias = keyringAliasFor(contextId, args.tenant);
16142
17088
  try {
16143
- const existing = await keyring.get(contextId);
17089
+ const existing = await keyring.get(alias);
16144
17090
  if (!existing?.keyId) return void 0;
16145
- const rawKey = await keyring.resolveSecret(contextId);
17091
+ const rawKey = await keyring.resolveSecret(alias);
16146
17092
  return rawKey ? { keyId: existing.keyId, rawKey } : void 0;
16147
17093
  } catch {
16148
17094
  return void 0;
@@ -16159,16 +17105,27 @@ ${renderPlan(blueprint, planSteps)}
16159
17105
  );
16160
17106
  return;
16161
17107
  }
17108
+ const alias = keyringAliasFor(contextId, args.tenant);
16162
17109
  try {
16163
- warnReplacingAlias(await keyring.get(contextId), contextId, record.revokedKeyId);
17110
+ warnReplacingAlias(await keyring.get(alias), alias, record.revokedKeyId);
16164
17111
  await storeMintedKey(
16165
17112
  keyring,
16166
- { alias: contextId, context: contextId, principalId: record.principalId, keyId: record.keyId, env: args.env },
17113
+ {
17114
+ alias,
17115
+ context: contextId,
17116
+ principalId: record.principalId,
17117
+ keyId: record.keyId,
17118
+ // Derived from the REQUEST's actual baseUrl, not echoed from args.env — the two
17119
+ // diverge whenever VECTROS_API_BASE_URL/--base-url alone selected the environment.
17120
+ // `keyring list`/`keyring doctor` display this field; a stale echo would show
17121
+ // "env: production" next to a key this run actually minted against staging.
17122
+ env: deriveEnvironment(baseUrl) ?? args.env
17123
+ },
16167
17124
  record.rawKey,
16168
17125
  { activate: true, revokedKeyId: record.revokedKeyId }
16169
17126
  );
16170
17127
  if (record.revokedKeyId) {
16171
- await warnOtherAliasesNaming(keyring, record.revokedKeyId, contextId);
17128
+ await warnOtherAliasesNaming(keyring, record.revokedKeyId, alias);
16172
17129
  }
16173
17130
  storedFreshInKeyring = true;
16174
17131
  } catch (err) {
@@ -16213,7 +17170,7 @@ ${renderPlan(blueprint, planSteps)}
16213
17170
  );
16214
17171
  entry = buildVectrosServerEntry({ apiKey: result.rawKey, apiBaseUrl: baseUrl });
16215
17172
  } else if (storedFreshInKeyring || result.keyAction === "reused") {
16216
- entry = buildVectrosServerEntry({ alias: blueprint.contextId, apiBaseUrl: baseUrl });
17173
+ entry = buildVectrosServerEntry({ alias: keyringAliasFor(blueprint.contextId, args.tenant), apiBaseUrl: baseUrl });
16217
17174
  } else {
16218
17175
  entry = buildVectrosServerEntry({ apiKey: result.rawKey, apiBaseUrl: baseUrl });
16219
17176
  }
@@ -16296,7 +17253,7 @@ ${formatCliError(err, { action: "Bootstrap failed" })}`);
16296
17253
  import { writeFile as writeFile2, access } from "fs/promises";
16297
17254
 
16298
17255
  // src/scope-grammar.ts
16299
- var FORBIDDEN_SCOPE_NAMESPACES = /* @__PURE__ */ new Set([
17256
+ var FORBIDDEN_SCOPE_NAMESPACES2 = /* @__PURE__ */ new Set([
16300
17257
  "user",
16301
17258
  "record",
16302
17259
  "document",
@@ -16308,23 +17265,23 @@ var FORBIDDEN_SCOPE_NAMESPACES = /* @__PURE__ */ new Set([
16308
17265
  "versions",
16309
17266
  "lookup"
16310
17267
  ]);
16311
- var SCOPE_NAMESPACE_RE = /^[a-z][a-z0-9_-]{1,31}$/;
17268
+ var SCOPE_NAMESPACE_RE2 = /^[a-z][a-z0-9_-]{1,31}$/;
16312
17269
  var SCOPE_NAMESPACE_MIN_LENGTH = 2;
16313
17270
  var SCOPE_NAMESPACE_MAX_LENGTH = 32;
16314
- var SCOPE_VALUE_RE = /^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$/;
17271
+ var SCOPE_VALUE_RE2 = /^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$/;
16315
17272
  var SCOPE_VALUE_MIN_LENGTH = 1;
16316
17273
  var SCOPE_VALUE_MAX_LENGTH = 128;
16317
17274
  function valueGrammarError(value) {
16318
- if (!SCOPE_VALUE_RE.test(value)) {
17275
+ if (!SCOPE_VALUE_RE2.test(value)) {
16319
17276
  return `'${value}' is not a valid scope value \u2014 ${SCOPE_VALUE_MIN_LENGTH}-${SCOPE_VALUE_MAX_LENGTH} characters, starting with a letter or digit and continuing with letters, digits, '_' or '-'. A value may not contain a colon.`;
16320
17277
  }
16321
17278
  return null;
16322
17279
  }
16323
17280
  function namespaceGrammarError(ns) {
16324
- if (FORBIDDEN_SCOPE_NAMESPACES.has(ns)) {
17281
+ if (FORBIDDEN_SCOPE_NAMESPACES2.has(ns)) {
16325
17282
  return `'${ns}' is a reserved namespace and cannot be used as an ownership scope`;
16326
17283
  }
16327
- if (!SCOPE_NAMESPACE_RE.test(ns)) {
17284
+ if (!SCOPE_NAMESPACE_RE2.test(ns)) {
16328
17285
  return `'${ns}' is not a valid namespace \u2014 lowercase, ${SCOPE_NAMESPACE_MIN_LENGTH}-${SCOPE_NAMESPACE_MAX_LENGTH} characters, starting with a letter and continuing with letters, digits, '_' or '-'`;
16329
17286
  }
16330
17287
  return null;
@@ -16521,6 +17478,17 @@ function lintBlueprint(bp) {
16521
17478
  }
16522
17479
  });
16523
17480
  }
17481
+ for (const issuer of bp.issuers ?? []) {
17482
+ for (const policy of issuer.selfSignupPolicies ?? []) {
17483
+ if (!Object.prototype.hasOwnProperty.call(bp.roles ?? {}, policy.roleId)) {
17484
+ findings.push({
17485
+ severity: "warning",
17486
+ rule: "self-signup-role-unknown",
17487
+ 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.`
17488
+ });
17489
+ }
17490
+ }
17491
+ }
16524
17492
  return findings;
16525
17493
  }
16526
17494
  var ROLE_ID_RE = /^[a-z][a-z0-9-]{2,30}$/;
@@ -16584,8 +17552,8 @@ schemas:
16584
17552
  lookupFields: [title] # extra O(1) exact-match lookup indexes (max 10)
16585
17553
 
16586
17554
  accessProfile:
16587
- # SCOPE GATE: data-plane only (records/schemas/search/documents/folders/inference).
16588
- # Anything else (keys/profiles/admin/*) is HARD-REJECTED \u2014 by design, no override.
17555
+ # SCOPE GATE: data-plane only (records/schemas/search/documents/folders/inference/entities).
17556
+ # Anything else (keys/profiles/users/admin/namespaces/*) is HARD-REJECTED \u2014 by design, no override.
16589
17557
  # Ops: c|r|u|d, combinable (records:cru). Prefer the narrowest set (least privilege).
16590
17558
  allowedActions: [records:r, records:c, records:u, search:r, schemas:r]
16591
17559
  # dataScope binds the key to specific owners. Include null to ALSO see
@@ -17365,9 +18333,25 @@ async function runKeyRotate(opts, deps = {}) {
17365
18333
  return 1;
17366
18334
  }
17367
18335
  }
17368
- const existing = (await drainPages(() => client.auth.listScopedKeys())).find(
17369
- (k) => matchesTuple(k, opts.context, userId, keyName)
17370
- );
18336
+ const activeKeys = await drainPages(() => client.auth.listScopedKeys());
18337
+ const existing = activeKeys.find((k) => matchesTuple(k, opts.context, userId, keyName));
18338
+ if (!existing && keyName === DEFAULT_KEY_NAME && !opts.forceNew) {
18339
+ const others = activeKeys.filter(
18340
+ (k) => k.contextId === opts.context && k.userId === userId && k.status === "active"
18341
+ );
18342
+ if (others.length > 0) {
18343
+ const list = others.map((k) => ` \u2022 ${k.keyId} (name: '${k.keyName}')`).join("\n");
18344
+ process.stderr.write(
18345
+ ` \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):
18346
+ ${list}
18347
+ If one of these is it, re-run with --name '<its name>'.
18348
+ If you really want a new key alongside them, re-run with --force-new.
18349
+ Nothing was revoked or minted.
18350
+ `
18351
+ );
18352
+ return 1;
18353
+ }
18354
+ }
17371
18355
  if (existing?.keyId) {
17372
18356
  await client.auth.revokeScopedKey({ keyId: existing.keyId });
17373
18357
  process.stdout.write(` \u2714 Revoked prior key ${existing.keyId}
@@ -17800,7 +18784,7 @@ function isRecognizedOwnershipKey(key) {
17800
18784
  if (key === "userId" || key === "partnerUserId") return true;
17801
18785
  if (!key.startsWith("scope:")) return false;
17802
18786
  const ns = key.slice("scope:".length);
17803
- return !FORBIDDEN_SCOPE_NAMESPACES.has(ns) && SCOPE_NAMESPACE_RE.test(ns);
18787
+ return !FORBIDDEN_SCOPE_NAMESPACES2.has(ns) && SCOPE_NAMESPACE_RE2.test(ns);
17804
18788
  }
17805
18789
  var DIM_WILDCARD = "*";
17806
18790
  var ANY_TOKEN_RE = /^\$\{\{\s*any\s*\}\}$/;
@@ -18547,7 +19531,24 @@ async function captureSnapshot(client, blueprint) {
18547
19531
  }
18548
19532
  return { existingSchemaTypes, existingProfilePrincipals, existingRoleIds, failed };
18549
19533
  }
18550
- async function runAssertions(client, deps, blueprint, tenantId, result) {
19534
+ async function captureIssuerSnapshot(bootstrapClient) {
19535
+ const existingIssuerIds = /* @__PURE__ */ new Set();
19536
+ let failed = false;
19537
+ try {
19538
+ let startFrom;
19539
+ for (; ; ) {
19540
+ const page = await bootstrapClient.auth.listIssuers({ limit: SNAPSHOT_PAGE_SIZE, startFrom });
19541
+ for (const i of page.data ?? []) if (i.issuerId) existingIssuerIds.add(i.issuerId);
19542
+ const next = page.nextCursor;
19543
+ if (!next) break;
19544
+ startFrom = next;
19545
+ }
19546
+ } catch {
19547
+ failed = true;
19548
+ }
19549
+ return { existingIssuerIds, failed };
19550
+ }
19551
+ async function runAssertions(client, bootstrapClient, deps, blueprint, tenantId, result) {
18551
19552
  const assertions = [];
18552
19553
  try {
18553
19554
  const ping = await deps.pingWithKey(result.rawKey);
@@ -18632,9 +19633,22 @@ async function runAssertions(client, deps, blueprint, tenantId, result) {
18632
19633
  }
18633
19634
  }
18634
19635
  }
19636
+ for (const { issuerId } of result.issuerActions) {
19637
+ try {
19638
+ const issuer = await bootstrapClient.auth.getIssuer({ issuerId });
19639
+ const ok = issuer.issuerId === issuerId;
19640
+ assertions.push({
19641
+ name: `issuer:${issuerId}`,
19642
+ ok,
19643
+ detail: ok ? `issuer '${issuerId}' registered + readable` : `getIssuer(${issuerId}) returned issuerId '${issuer.issuerId ?? "(none)"}'`
19644
+ });
19645
+ } catch (e) {
19646
+ assertions.push({ name: `issuer:${issuerId}`, ok: false, detail: `getIssuer(${issuerId}) failed: ${errMsg(e)}` });
19647
+ }
19648
+ }
18635
19649
  return assertions;
18636
19650
  }
18637
- async function runTeardown(client, blueprint, machineId, snapshot, result) {
19651
+ async function runTeardown(client, bootstrapClient, blueprint, machineId, snapshot, result) {
18638
19652
  const steps = [];
18639
19653
  const keyName = result?.keyName ?? keyNameFor(blueprint.name, machineId);
18640
19654
  try {
@@ -18728,6 +19742,22 @@ async function runTeardown(client, blueprint, machineId, snapshot, result) {
18728
19742
  for (const [typeName] of toDelete) {
18729
19743
  steps.push({ step: `delete schema ${typeName}`, ok: false, error: lastError.get(typeName) ?? "could not delete" });
18730
19744
  }
19745
+ for (const { issuerId } of result.issuerActions) {
19746
+ if (snapshot.failed) {
19747
+ steps.push({ step: `delete issuer ${issuerId}`, ok: true, skipped: true, error: "skipped \u2014 pre-apply snapshot failed; cannot prove ownership" });
19748
+ continue;
19749
+ }
19750
+ if (snapshot.existingIssuerIds.has(issuerId)) {
19751
+ steps.push({ step: `delete issuer ${issuerId}`, ok: true, skipped: true, error: "skipped \u2014 pre-existed this run" });
19752
+ continue;
19753
+ }
19754
+ try {
19755
+ await bootstrapClient.auth.deleteIssuer({ issuerId });
19756
+ steps.push({ step: `delete issuer ${issuerId}`, ok: true });
19757
+ } catch (e) {
19758
+ steps.push({ step: `delete issuer ${issuerId}`, ok: false, error: errMsg(e) });
19759
+ }
19760
+ }
18731
19761
  return steps;
18732
19762
  }
18733
19763
  async function runBlueprintHarness(client, input, deps) {
@@ -18741,12 +19771,12 @@ async function runBlueprintHarness(client, input, deps) {
18741
19771
  log
18742
19772
  );
18743
19773
  const remint = remintForContext ?? (async () => client);
18744
- let snapshot;
19774
+ let contextSnapshot;
18745
19775
  if (remintForContext) {
18746
19776
  try {
18747
- snapshot = await captureSnapshot(await remintForContext(blueprint.contextId), blueprint);
19777
+ contextSnapshot = await captureSnapshot(await remintForContext(blueprint.contextId), blueprint);
18748
19778
  } catch (e) {
18749
- snapshot = {
19779
+ contextSnapshot = {
18750
19780
  existingSchemaTypes: /* @__PURE__ */ new Set(),
18751
19781
  existingProfilePrincipals: /* @__PURE__ */ new Set(),
18752
19782
  existingRoleIds: /* @__PURE__ */ new Set(),
@@ -18754,15 +19784,21 @@ async function runBlueprintHarness(client, input, deps) {
18754
19784
  };
18755
19785
  }
18756
19786
  } else {
18757
- snapshot = await captureSnapshot(client, blueprint);
19787
+ contextSnapshot = await captureSnapshot(client, blueprint);
18758
19788
  }
19789
+ const issuerSnapshot = await captureIssuerSnapshot(client);
19790
+ const snapshot = {
19791
+ ...contextSnapshot,
19792
+ existingIssuerIds: issuerSnapshot.existingIssuerIds,
19793
+ failed: contextSnapshot.failed || issuerSnapshot.failed
19794
+ };
18759
19795
  let result;
18760
19796
  let assertions = [];
18761
19797
  let applyError;
18762
19798
  try {
18763
19799
  result = await runLoader(client, { blueprint, tenantId, machineId, readExistingKey, persistMintedKey, log, remintForContext });
18764
19800
  log.debug({ component: "harness", contextId: result.contextId, keyName: result.keyName }, "applied");
18765
- assertions = await runAssertions(await remint(blueprint.contextId), deps, blueprint, tenantId, result);
19801
+ assertions = await runAssertions(await remint(blueprint.contextId), client, deps, blueprint, tenantId, result);
18766
19802
  } catch (e) {
18767
19803
  applyError = errMsg(e);
18768
19804
  log.warn({ component: "harness", err: applyError }, "apply phase failed");
@@ -18774,7 +19810,7 @@ async function runBlueprintHarness(client, input, deps) {
18774
19810
  tdClient = await remint(blueprint.contextId);
18775
19811
  } catch {
18776
19812
  }
18777
- teardown = await runTeardown(tdClient, blueprint, machineId, snapshot, result);
19813
+ teardown = await runTeardown(tdClient, client, blueprint, machineId, snapshot, result);
18778
19814
  }
18779
19815
  const passed = applyError === void 0 && assertions.length > 0 && assertions.every((a) => a.ok);
18780
19816
  return {
@@ -18966,7 +20002,14 @@ async function runJoin(opts, deps = {}) {
18966
20002
  return 2;
18967
20003
  }
18968
20004
  const context = opts.contextArg;
18969
- const alias = opts.alias ?? context;
20005
+ let alias;
20006
+ try {
20007
+ alias = keyringAliasFor(opts.alias ?? context, opts.tenant ?? "live");
20008
+ } catch (err) {
20009
+ process.stderr.write(` \u2716 ${formatError(err)}
20010
+ `);
20011
+ return 2;
20012
+ }
18970
20013
  let keyring;
18971
20014
  try {
18972
20015
  keyring = deps.keyring ?? await Keyring.open();
@@ -19041,9 +20084,9 @@ function emitJoined(alias, context, principalId, role, keyId, reused) {
19041
20084
 
19042
20085
  // src/build-info.ts
19043
20086
  var BUILD_INFO = {
19044
- cli: true ? "0.14.0" : "dev",
19045
- sdk: true ? "0.38.0" : "dev",
19046
- blueprints: true ? "0.11.0" : "dev"
20087
+ cli: true ? "0.15.0" : "dev",
20088
+ sdk: true ? "0.39.0" : "dev",
20089
+ blueprints: true ? "0.12.0" : "dev"
19047
20090
  };
19048
20091
  function formatBuildInfo() {
19049
20092
  return `vectros ${BUILD_INFO.cli} (sdk ${BUILD_INFO.sdk}, blueprints ${BUILD_INFO.blueprints})`;
@@ -19427,7 +20470,10 @@ function registerKeyCommands(program, deps) {
19427
20470
  process.exit(await runKeyRevoke(keyId, { ...toCommonOptions(flags), context: flags.context }, deps));
19428
20471
  });
19429
20472
  const rotate = addKeyDestinationOptions(
19430
- key.command("rotate").description("Revoke the matching active key and mint a fresh secret.").requiredOption("--principal <p>", "principal (usr_<id> or <id>)").requiredOption("--context <c>", "app context").option("--name <name>", "key name to rotate", DEFAULT_KEY_NAME)
20473
+ key.command("rotate").description("Revoke the matching active key and mint a fresh secret.").requiredOption("--principal <p>", "principal (usr_<id> or <id>)").requiredOption("--context <c>", "app context").option("--name <name>", "key name to rotate", DEFAULT_KEY_NAME).option(
20474
+ "--force-new",
20475
+ "mint a new key even though other active keys already exist for this principal/context under a different name (bypasses the default-name-mismatch guard)"
20476
+ )
19431
20477
  );
19432
20478
  addAuthOptions(rotate).action(async (flags) => {
19433
20479
  process.exit(
@@ -19436,6 +20482,7 @@ function registerKeyCommands(program, deps) {
19436
20482
  principal: flags.principal,
19437
20483
  context: flags.context,
19438
20484
  name: flags.name,
20485
+ forceNew: flags.forceNew,
19439
20486
  ...toKeyDestination(flags)
19440
20487
  }, deps)
19441
20488
  );