@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.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.
@@ -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:
@@ -5586,6 +5598,254 @@ 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.
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. 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.
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.TooManyRequestsError}
5668
+ *
5669
+ * @example
5670
+ * await client.auth.deleteIssuer({
5671
+ * issuerId: "auth0-prod"
5672
+ * })
5673
+ */
5674
+ deleteIssuer(request, requestOptions) {
5675
+ return HttpResponsePromise.fromPromise(this.__deleteIssuer(request, requestOptions));
5676
+ }
5677
+ async __deleteIssuer(request, requestOptions) {
5678
+ const { issuerId } = request;
5679
+ const _authRequest = await this._options.authProvider.getAuthRequest();
5680
+ const _headers = mergeHeaders(
5681
+ _authRequest.headers,
5682
+ this._options?.headers,
5683
+ requestOptions?.headers
5684
+ );
5685
+ const _response = await fetcher({
5686
+ url: url_exports.join(
5687
+ await Supplier.get(this._options.baseUrl) ?? await Supplier.get(this._options.environment),
5688
+ `v1/auth/issuers/${url_exports.encodePathParam(issuerId)}`
5689
+ ),
5690
+ method: "DELETE",
5691
+ headers: _headers,
5692
+ queryString: url_exports.queryBuilder().mergeAdditional(requestOptions?.queryParams).build(),
5693
+ timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1e3,
5694
+ maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries,
5695
+ abortSignal: requestOptions?.abortSignal,
5696
+ fetchFn: this._options?.fetch,
5697
+ logging: this._options.logging
5698
+ });
5699
+ if (_response.ok) {
5700
+ return { data: void 0, rawResponse: _response.rawResponse };
5701
+ }
5702
+ if (_response.error.reason === "status-code") {
5703
+ switch (_response.error.statusCode) {
5704
+ case 403:
5705
+ throw new ForbiddenError(_response.error.body, _response.rawResponse);
5706
+ case 404:
5707
+ throw new NotFoundError(_response.error.body, _response.rawResponse);
5708
+ case 429:
5709
+ throw new TooManyRequestsError(_response.error.body, _response.rawResponse);
5710
+ default:
5711
+ throw new VectrosError({
5712
+ statusCode: _response.error.statusCode,
5713
+ body: _response.error.body,
5714
+ rawResponse: _response.rawResponse
5715
+ });
5716
+ }
5717
+ }
5718
+ return handleNonStatusCodeError(
5719
+ _response.error,
5720
+ _response.rawResponse,
5721
+ "DELETE",
5722
+ "/v1/auth/issuers/{issuerId}"
5723
+ );
5724
+ }
5725
+ /**
5726
+ * Returns the issuers registered in your tenant. Requires a root API key or the bootstrap's provisioning capability. Returns a `{data, nextCursor}` envelope.
5727
+ *
5728
+ * @param {Vectros.ListIssuersRequest} request
5729
+ * @param {AuthClient.RequestOptions} requestOptions - Request-specific configuration.
5730
+ *
5731
+ * @throws {@link Vectros.ForbiddenError}
5732
+ *
5733
+ * @example
5734
+ * await client.auth.listIssuers()
5735
+ */
5736
+ listIssuers(request = {}, requestOptions) {
5737
+ return HttpResponsePromise.fromPromise(this.__listIssuers(request, requestOptions));
5738
+ }
5739
+ async __listIssuers(request = {}, requestOptions) {
5740
+ const { startFrom, limit } = request;
5741
+ const _queryParams = {
5742
+ startFrom,
5743
+ limit
5744
+ };
5745
+ const _authRequest = await this._options.authProvider.getAuthRequest();
5746
+ const _headers = mergeHeaders(
5747
+ _authRequest.headers,
5748
+ this._options?.headers,
5749
+ requestOptions?.headers
5750
+ );
5751
+ const _response = await fetcher({
5752
+ url: url_exports.join(
5753
+ await Supplier.get(this._options.baseUrl) ?? await Supplier.get(this._options.environment),
5754
+ "v1/auth/issuers"
5755
+ ),
5756
+ method: "GET",
5757
+ headers: _headers,
5758
+ queryString: url_exports.queryBuilder().addMany(_queryParams).mergeAdditional(requestOptions?.queryParams).build(),
5759
+ timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1e3,
5760
+ maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries,
5761
+ abortSignal: requestOptions?.abortSignal,
5762
+ fetchFn: this._options?.fetch,
5763
+ logging: this._options.logging
5764
+ });
5765
+ if (_response.ok) {
5766
+ return { data: _response.body, rawResponse: _response.rawResponse };
5767
+ }
5768
+ if (_response.error.reason === "status-code") {
5769
+ switch (_response.error.statusCode) {
5770
+ case 403:
5771
+ throw new ForbiddenError(_response.error.body, _response.rawResponse);
5772
+ default:
5773
+ throw new VectrosError({
5774
+ statusCode: _response.error.statusCode,
5775
+ body: _response.error.body,
5776
+ rawResponse: _response.rawResponse
5777
+ });
5778
+ }
5779
+ }
5780
+ return handleNonStatusCodeError(_response.error, _response.rawResponse, "GET", "/v1/auth/issuers");
5781
+ }
5782
+ /**
5783
+ * Registers a trusted third-party IdP issuer that BYO-IdP token exchange (`POST /v1/auth/token/exchange`) may accept a `subject_token` from. Requires a root API key or the CLI bootstrap's provisioning capability — never an ordinary partner-grantable scope. Idempotent by `issuerId` within your tenant; the `(issuer, audience)` pair must not already be registered by a different issuerId/tenant.
5784
+ *
5785
+ * @param {Vectros.IssuerRequest} request
5786
+ * @param {AuthClient.RequestOptions} requestOptions - Request-specific configuration.
5787
+ *
5788
+ * @throws {@link Vectros.BadRequestError}
5789
+ * @throws {@link Vectros.ForbiddenError}
5790
+ * @throws {@link Vectros.TooManyRequestsError}
5791
+ *
5792
+ * @example
5793
+ * await client.auth.registerIssuer({
5794
+ * issuerId: "auth0-prod",
5795
+ * issuer: "https://your-tenant.us.auth0.com/",
5796
+ * jwksUri: "https://your-tenant.us.auth0.com/.well-known/jwks.json",
5797
+ * audience: "https://api.your-app.example.com",
5798
+ * contextId: "casework"
5799
+ * })
5800
+ */
5801
+ registerIssuer(request, requestOptions) {
5802
+ return HttpResponsePromise.fromPromise(this.__registerIssuer(request, requestOptions));
5803
+ }
5804
+ async __registerIssuer(request, requestOptions) {
5805
+ const _authRequest = await this._options.authProvider.getAuthRequest();
5806
+ const _headers = mergeHeaders(
5807
+ _authRequest.headers,
5808
+ this._options?.headers,
5809
+ requestOptions?.headers
5810
+ );
5811
+ const _response = await fetcher({
5812
+ url: url_exports.join(
5813
+ await Supplier.get(this._options.baseUrl) ?? await Supplier.get(this._options.environment),
5814
+ "v1/auth/issuers"
5815
+ ),
5816
+ method: "POST",
5817
+ headers: _headers,
5818
+ contentType: "application/json",
5819
+ queryString: url_exports.queryBuilder().mergeAdditional(requestOptions?.queryParams).build(),
5820
+ requestType: "json",
5821
+ body: request,
5822
+ timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1e3,
5823
+ maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries,
5824
+ abortSignal: requestOptions?.abortSignal,
5825
+ fetchFn: this._options?.fetch,
5826
+ logging: this._options.logging
5827
+ });
5828
+ if (_response.ok) {
5829
+ return { data: _response.body, rawResponse: _response.rawResponse };
5830
+ }
5831
+ if (_response.error.reason === "status-code") {
5832
+ switch (_response.error.statusCode) {
5833
+ case 400:
5834
+ throw new BadRequestError(_response.error.body, _response.rawResponse);
5835
+ case 403:
5836
+ throw new ForbiddenError(_response.error.body, _response.rawResponse);
5837
+ case 429:
5838
+ throw new TooManyRequestsError(_response.error.body, _response.rawResponse);
5839
+ default:
5840
+ throw new VectrosError({
5841
+ statusCode: _response.error.statusCode,
5842
+ body: _response.error.body,
5843
+ rawResponse: _response.rawResponse
5844
+ });
5845
+ }
5846
+ }
5847
+ return handleNonStatusCodeError(_response.error, _response.rawResponse, "POST", "/v1/auth/issuers");
5848
+ }
5589
5849
  /**
5590
5850
  * 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
5851
  *
@@ -5909,6 +6169,72 @@ var AuthClient = class {
5909
6169
  }
5910
6170
  return handleNonStatusCodeError(_response.error, _response.rawResponse, "POST", "/v1/users/invite/resend");
5911
6171
  }
6172
+ /**
6173
+ * RFC 8693 OAuth 2.0 Token Exchange. Trades a JWT issued by a third-party identity provider you've registered (`POST /v1/auth/issuers`) for a Vectros `st_*` scoped bearer token — no Vectros credential required to call this endpoint. The exchanged token's scope is resolved entirely server-side from the matched user's access profile; this endpoint accepts no caller-supplied scope, resource, or audience parameter (RFC 8693 §2.1's `resource`/`audience`/`scope` are not used in v1 — the registered `(issuer, audience)` pair alone pins the target tenant and app context). On a first-time login (no existing Vectros identity for this subject), two opt-in binding paths exist: `invite_token` (a `PENDING` sub-user invitation), and — if the registration declares one or more self-signup policies — `signup_type` (a brand-new user is created and bound to the policy's configured role). If `invite_token` is present at all, it is the ONLY path tried — a failed invite never falls through to self-signup. Neither field is required for a subject with an existing identity. Uses the OAuth-standard error envelope (`{"error":..., "error_description":...}`, RFC 6749 §5.2), NOT this API's usual `{"message":...}` shape — its client is generic OAuth tooling, not the Vectros SDK.
6174
+ *
6175
+ * @param {Vectros.TokenExchangeRequest} request
6176
+ * @param {AuthClient.RequestOptions} requestOptions - Request-specific configuration.
6177
+ *
6178
+ * @throws {@link Vectros.BadRequestError}
6179
+ * @throws {@link Vectros.UnauthorizedError}
6180
+ * @throws {@link Vectros.ForbiddenError}
6181
+ * @throws {@link Vectros.NotFoundError}
6182
+ * @throws {@link Vectros.TooManyRequestsError}
6183
+ *
6184
+ * @example
6185
+ * await client.auth.exchangeToken({
6186
+ * grant_type: "urn:ietf:params:oauth:grant-type:token-exchange",
6187
+ * subject_token: "subject_token",
6188
+ * subject_token_type: "urn:ietf:params:oauth:token-type:jwt"
6189
+ * })
6190
+ */
6191
+ exchangeToken(request, requestOptions) {
6192
+ return HttpResponsePromise.fromPromise(this.__exchangeToken(request, requestOptions));
6193
+ }
6194
+ async __exchangeToken(request, requestOptions) {
6195
+ const _headers = mergeHeaders(this._options?.headers, requestOptions?.headers);
6196
+ const _response = await fetcher({
6197
+ url: url_exports.join(
6198
+ await Supplier.get(this._options.baseUrl) ?? await Supplier.get(this._options.environment),
6199
+ "v1/auth/token/exchange"
6200
+ ),
6201
+ method: "POST",
6202
+ headers: _headers,
6203
+ contentType: "application/json",
6204
+ queryString: url_exports.queryBuilder().mergeAdditional(requestOptions?.queryParams).build(),
6205
+ requestType: "json",
6206
+ body: request,
6207
+ timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1e3,
6208
+ maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries,
6209
+ abortSignal: requestOptions?.abortSignal,
6210
+ fetchFn: this._options?.fetch,
6211
+ logging: this._options.logging
6212
+ });
6213
+ if (_response.ok) {
6214
+ return { data: _response.body, rawResponse: _response.rawResponse };
6215
+ }
6216
+ if (_response.error.reason === "status-code") {
6217
+ switch (_response.error.statusCode) {
6218
+ case 400:
6219
+ throw new BadRequestError(_response.error.body, _response.rawResponse);
6220
+ case 401:
6221
+ throw new UnauthorizedError(_response.error.body, _response.rawResponse);
6222
+ case 403:
6223
+ throw new ForbiddenError(_response.error.body, _response.rawResponse);
6224
+ case 404:
6225
+ throw new NotFoundError(_response.error.body, _response.rawResponse);
6226
+ case 429:
6227
+ throw new TooManyRequestsError(_response.error.body, _response.rawResponse);
6228
+ default:
6229
+ throw new VectrosError({
6230
+ statusCode: _response.error.statusCode,
6231
+ body: _response.error.body,
6232
+ rawResponse: _response.rawResponse
6233
+ });
6234
+ }
6235
+ }
6236
+ return handleNonStatusCodeError(_response.error, _response.rawResponse, "POST", "/v1/auth/token/exchange");
6237
+ }
5912
6238
  };
5913
6239
  var ComplianceClient = class {
5914
6240
  constructor(options) {
@@ -8516,6 +8842,69 @@ var IdentityClient = class {
8516
8842
  }
8517
8843
  return handleNonStatusCodeError(_response.error, _response.rawResponse, "POST", "/v1/users/lookup");
8518
8844
  }
8845
+ /**
8846
+ * 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.
8847
+ *
8848
+ * @param {Vectros.UserExistsByEmailRequest} request
8849
+ * @param {IdentityClient.RequestOptions} requestOptions - Request-specific configuration.
8850
+ *
8851
+ * @throws {@link Vectros.BadRequestError}
8852
+ * @throws {@link Vectros.ForbiddenError}
8853
+ *
8854
+ * @example
8855
+ * await client.identity.userExistsByEmail({
8856
+ * email: "user@example.com",
8857
+ * contextId: "myapp"
8858
+ * })
8859
+ */
8860
+ userExistsByEmail(request = {}, requestOptions) {
8861
+ return HttpResponsePromise.fromPromise(this.__userExistsByEmail(request, requestOptions));
8862
+ }
8863
+ async __userExistsByEmail(request = {}, requestOptions) {
8864
+ const { email, contextId } = request;
8865
+ const _queryParams = {
8866
+ email,
8867
+ contextId
8868
+ };
8869
+ const _authRequest = await this._options.authProvider.getAuthRequest();
8870
+ const _headers = mergeHeaders(
8871
+ _authRequest.headers,
8872
+ this._options?.headers,
8873
+ requestOptions?.headers
8874
+ );
8875
+ const _response = await fetcher({
8876
+ url: url_exports.join(
8877
+ await Supplier.get(this._options.baseUrl) ?? await Supplier.get(this._options.environment),
8878
+ "v1/users/exists-by-email"
8879
+ ),
8880
+ method: "GET",
8881
+ headers: _headers,
8882
+ queryString: url_exports.queryBuilder().addMany(_queryParams).mergeAdditional(requestOptions?.queryParams).build(),
8883
+ timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1e3,
8884
+ maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries,
8885
+ abortSignal: requestOptions?.abortSignal,
8886
+ fetchFn: this._options?.fetch,
8887
+ logging: this._options.logging
8888
+ });
8889
+ if (_response.ok) {
8890
+ return { data: _response.body, rawResponse: _response.rawResponse };
8891
+ }
8892
+ if (_response.error.reason === "status-code") {
8893
+ switch (_response.error.statusCode) {
8894
+ case 400:
8895
+ throw new BadRequestError(_response.error.body, _response.rawResponse);
8896
+ case 403:
8897
+ throw new ForbiddenError(_response.error.body, _response.rawResponse);
8898
+ default:
8899
+ throw new VectrosError({
8900
+ statusCode: _response.error.statusCode,
8901
+ body: _response.error.body,
8902
+ rawResponse: _response.rawResponse
8903
+ });
8904
+ }
8905
+ }
8906
+ return handleNonStatusCodeError(_response.error, _response.rawResponse, "GET", "/v1/users/exists-by-email");
8907
+ }
8519
8908
  /**
8520
8909
  * 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
8910
  *
@@ -10233,10 +10622,11 @@ var logging;
10233
10622
  logging2.ConsoleLogger = ConsoleLogger;
10234
10623
  })(logging || (logging = {}));
10235
10624
 
10236
- // node_modules/@vectros-ai/blueprints/dist/index.mjs
10625
+ // ../blueprints/dist/index.mjs
10237
10626
  var import_zod = require("zod");
10238
10627
  var import_zod2 = require("zod");
10239
10628
  var CONTEXT_ID_RE = /^[a-z][a-z0-9-]{2,30}$/;
10629
+ var MAX_SCOPE_DIMENSIONS = 2;
10240
10630
  var ValidationRulesSchema = import_zod.z.object({
10241
10631
  required: import_zod.z.boolean().optional(),
10242
10632
  minLength: import_zod.z.number().int().optional(),
@@ -10431,11 +10821,14 @@ var BlueprintSchemaSchema = import_zod.z.object({
10431
10821
  // Schema-level ownership defaults, matching `SchemaRequest`: the principal
10432
10822
  // `userId` plus `scopes` — namespaced parent edges as `<namespace>:<value>`
10433
10823
  // (`org:...`, `client:...`, or a namespace you registered), at most two
10434
- // namespaces. The flat `orgId`/`clientId` fields fold into `scopes`.
10824
+ // namespaces. The flat `orgId`/`clientId` fields fold into `scopes`. Each
10825
+ // entry's namespace + value are validated below
10826
+ // (`lintNamespacedScopeArrays`) — this `.max(MAX_SCOPE_DIMENSIONS)` is
10827
+ // only the shape-level cap, not the grammar check.
10435
10828
  // With a scoped token these must be consistent with the profile's dataScope
10436
10829
  // (a cross-consistency lint is deferred to the lint slice).
10437
10830
  userId: import_zod.z.string().min(1).optional(),
10438
- scopes: import_zod.z.array(import_zod.z.string().min(1)).max(2).optional(),
10831
+ scopes: import_zod.z.array(import_zod.z.string().min(1)).max(MAX_SCOPE_DIMENSIONS).optional(),
10439
10832
  // The id of an existing schema this one is a CUSTOMIZATION of, when a schema
10440
10833
  // named `typeName` already exists in this context — required in that case,
10441
10834
  // and must be omitted when this create is the first schema under that name
@@ -10473,7 +10866,9 @@ var BlueprintAccessProfileSchema = import_zod.z.object({
10473
10866
  // ...) — the flat `orgId`/`clientId` keys are gone. e.g.
10474
10867
  // `{ "scope:org": ["org_x", null] }` = "org_x's records AND tenant-shared
10475
10868
  // records". Omitting null restricts to the listed owners ONLY (the key will
10476
- // NOT see tenant-level/seed records).
10869
+ // NOT see tenant-level/seed records). Every KEY is validated below
10870
+ // (`lintDataScopeKeys`) — `userId` or a grammar-valid `scope:<ns>`, no
10871
+ // other shape.
10477
10872
  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
10873
  // Optional identity overrides for the service principal's profile — scope
10479
10874
  // values its key STAMPS onto everything it writes (a credential can only
@@ -10482,6 +10877,11 @@ var BlueprintAccessProfileSchema = import_zod.z.object({
10482
10877
  // `orgId`/`clientId` keys are gone); values may be `${{ identities.* }}`
10483
10878
  // tokens (substituted at apply time). e.g. `{ "scope:org": '${{ identities.team }}' }`
10484
10879
  // makes every record the service key writes org/team-owned.
10880
+ //
10881
+ // Every KEY, the ≤2-dimension cap, and every LITERAL value are validated
10882
+ // below (`lintIdentityOverrides`, placeholder-aware — see that function's
10883
+ // docstring); this `.min(1)` is only the shape-level "non-blank" guard on
10884
+ // the value, not the grammar check.
10485
10885
  identityOverrides: import_zod.z.record(import_zod.z.string().min(1)).optional()
10486
10886
  }).strict();
10487
10887
  var BlueprintRoleClauseSchema = import_zod.z.object({
@@ -10511,11 +10911,61 @@ var IdentityDeclSchema = import_zod.z.object({
10511
10911
  displayName: import_zod.z.string().min(1).optional(),
10512
10912
  metadata: import_zod.z.record(import_zod.z.unknown()).optional()
10513
10913
  }).strict();
10514
- var IdentitiesDeclSchema = import_zod.z.record(IdentityDeclSchema);
10914
+ var IDENTITY_NAME_RE = /^[A-Za-z_]\w*$/;
10915
+ var IdentitiesDeclSchema = import_zod.z.record(
10916
+ import_zod.z.string().regex(IDENTITY_NAME_RE, {
10917
+ 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"
10918
+ }),
10919
+ IdentityDeclSchema
10920
+ );
10515
10921
  var BlueprintServicePrincipalSchema = import_zod.z.object({
10516
10922
  externalId: import_zod.z.string().min(1),
10517
10923
  displayName: import_zod.z.string().min(1)
10518
10924
  }).strict();
10925
+ var ISSUER_ID_RE = CONTEXT_ID_RE;
10926
+ var BlueprintIssuerSchema = import_zod.z.object({
10927
+ /** Short slug identifying this issuer within your tenant. Immutable once registered. */
10928
+ issuerId: import_zod.z.string().regex(ISSUER_ID_RE, {
10929
+ message: "issuerId must be 3-31 chars, start with a lowercase letter, then lowercase letters/digits/dashes (e.g. 'auth0-prod')"
10930
+ }),
10931
+ /** The IdP's `iss` claim value, exactly as it appears in tokens it issues. */
10932
+ issuer: import_zod.z.string().min(1),
10933
+ /** The IdP's remote JWKS endpoint, used to verify presented tokens' signatures. */
10934
+ jwksUri: import_zod.z.string().min(1),
10935
+ /**
10936
+ * The `aud` claim value a presented subject_token must carry. Must be globally
10937
+ * unique in combination with `issuer` — use a distinct audience per
10938
+ * environment/context sharing one IdP account (most OIDC providers support
10939
+ * this as an ordinary per-API/application default).
10940
+ */
10941
+ audience: import_zod.z.string().min(1),
10942
+ /** Which app context an exchanged token targets. Must be an existing app context. */
10943
+ contextId: import_zod.z.string().regex(CONTEXT_ID_RE, {
10944
+ message: "contextId must be 3-31 chars, start with a lowercase letter, then lowercase letters/digits/dashes (e.g. 'casework')"
10945
+ }),
10946
+ /** The claim carrying the subject identifier. Platform defaults to 'sub' when omitted. */
10947
+ subClaim: import_zod.z.string().min(1).optional(),
10948
+ /** The claim carrying the subject's email (first-login invite matching). Platform defaults to 'email' when omitted. */
10949
+ emailClaim: import_zod.z.string().min(1).optional(),
10950
+ /**
10951
+ * Opt-in self-service signup: a list of {signupType, roleId} pairs. When a first-time exchange
10952
+ * caller presents no invite token but names a signupType matching one of these (or omits it and
10953
+ * exactly one entry exists), a brand-new user is created and bound to that entry's role, no
10954
+ * invite required. Every entry must be something you're willing to grant to ANY caller who can
10955
+ * present a token from this issuer — a role carrying elevated (provisioning or wildcard) scope
10956
+ * is rejected. Omit entirely to leave self-signup disabled (the default).
10957
+ */
10958
+ selfSignupPolicies: import_zod.z.array(
10959
+ import_zod.z.object({
10960
+ signupType: import_zod.z.string().regex(ISSUER_ID_RE, {
10961
+ message: "signupType must be 3-31 chars, start with a lowercase letter, then lowercase letters/digits/dashes (e.g. 'practitioner')"
10962
+ }),
10963
+ roleId: import_zod.z.string().regex(ISSUER_ID_RE, {
10964
+ message: "roleId must be 3-31 chars, start with a lowercase letter, then lowercase letters/digits/dashes (e.g. 'practitioner-member')"
10965
+ })
10966
+ }).strict()
10967
+ ).optional()
10968
+ }).strict();
10519
10969
  var SeedCommonShape = {
10520
10970
  /** The schema typeName this seed instantiates (should match a declared schema). */
10521
10971
  typeName: import_zod.z.string().min(1),
@@ -10529,9 +10979,12 @@ var SeedCommonShape = {
10529
10979
  * Optional scope ownership for the seeded item, as `namespace:value` entries
10530
10980
  * (≤2, e.g. `['org:${{ identities.team }}']`; tokens substitute at apply
10531
10981
  * time). `[]` = a private, user-owned item. Omitted = the seeding
10532
- * credential's full identity (the default).
10982
+ * credential's full identity (the default). Each entry's namespace + value
10983
+ * are validated below (`lintNamespacedScopeArrays`) — this
10984
+ * `.max(MAX_SCOPE_DIMENSIONS)` is only the shape-level cap, not the grammar
10985
+ * check.
10533
10986
  */
10534
- scopes: import_zod.z.array(import_zod.z.string()).optional()
10987
+ scopes: import_zod.z.array(import_zod.z.string()).max(MAX_SCOPE_DIMENSIONS).optional()
10535
10988
  };
10536
10989
  var RecordSeedSchema = import_zod.z.object({
10537
10990
  surface: import_zod.z.literal("record"),
@@ -10553,7 +11006,7 @@ var BlueprintSeedRecordSchema = import_zod.z.discriminatedUnion("surface", [
10553
11006
  RecordSeedSchema,
10554
11007
  DocumentSeedSchema
10555
11008
  ]);
10556
- var SELF_TOKEN_RE = /\$\{\{\s*self\.[A-Za-z_]\w*\s*\}\}/;
11009
+ var SELF_TOKEN_RE = /\$\{\{\s*(?:self|under)\.[^}]*?\s*\}\}/;
10557
11010
  function lintSelfTokenPlacement(value, ctx) {
10558
11011
  const walk = (node, path4, inRoleDataScope) => {
10559
11012
  if (typeof node === "string") {
@@ -10580,12 +11033,20 @@ function lintSelfTokenPlacement(value, ctx) {
10580
11033
  walk(value, [], false);
10581
11034
  }
10582
11035
  var IDENTITY_REF_RE = /\$\{\{\s*identities\.([A-Za-z_]\w*)\s*\}\}/g;
10583
- function lintIdentityRefsDeclared(value, ctx) {
11036
+ var IDENTITY_REF_LOOSE_RE = /\$\{\{\s*identities\.([^}]*?)\s*\}\}/g;
11037
+ function lintIdentityRefs(value, ctx) {
10584
11038
  const declared = new Set(
10585
11039
  value && typeof value === "object" && "identities" in value && value.identities ? Object.keys(value.identities) : []
10586
11040
  );
10587
11041
  const walk = (node, path4) => {
10588
11042
  if (typeof node === "string") {
11043
+ for (const m of node.matchAll(IDENTITY_REF_LOOSE_RE)) {
11044
+ const captured = m[1];
11045
+ if (IDENTITY_NAME_RE.test(captured)) continue;
11046
+ const dotIndex = captured.indexOf(".");
11047
+ 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)} }}'`;
11048
+ ctx.addIssue({ code: import_zod.z.ZodIssueCode.custom, path: path4, message });
11049
+ }
10589
11050
  for (const m of node.matchAll(IDENTITY_REF_RE)) {
10590
11051
  if (!declared.has(m[1])) {
10591
11052
  ctx.addIssue({
@@ -10608,6 +11069,134 @@ function lintIdentityRefsDeclared(value, ctx) {
10608
11069
  };
10609
11070
  walk(value, []);
10610
11071
  }
11072
+ var SCOPE_VALUE_RE = /^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$/;
11073
+ var SCOPE_NAMESPACE_RE = /^[a-z][a-z0-9_-]{1,31}$/;
11074
+ var FORBIDDEN_SCOPE_NAMESPACES = /* @__PURE__ */ new Set([
11075
+ "user",
11076
+ "record",
11077
+ "document",
11078
+ "entity",
11079
+ "self",
11080
+ "tenant",
11081
+ "context",
11082
+ "scope",
11083
+ "versions",
11084
+ "lookup"
11085
+ ]);
11086
+ function scopeNamespaceGrammarError(namespace) {
11087
+ if (namespace === "") return "scope namespace must not be blank";
11088
+ if (FORBIDDEN_SCOPE_NAMESPACES.has(namespace)) {
11089
+ return `'${namespace}' is a reserved namespace and cannot be used as an ownership scope`;
11090
+ }
11091
+ if (!SCOPE_NAMESPACE_RE.test(namespace)) {
11092
+ return `'${namespace}' is not a valid namespace \u2014 2-32 chars, lowercase letter first, then lowercase letters, digits, '_' or '-'`;
11093
+ }
11094
+ return null;
11095
+ }
11096
+ var IDENTITY_PLACEHOLDER_WHOLE_RE = new RegExp(`^${IDENTITY_REF_LOOSE_RE.source}$`);
11097
+ function lintIdentityOverrides(value, ctx) {
11098
+ const overrides = value.accessProfile?.identityOverrides;
11099
+ if (!overrides) return;
11100
+ const entries = Object.entries(overrides);
11101
+ if (entries.length > MAX_SCOPE_DIMENSIONS) {
11102
+ ctx.addIssue({
11103
+ code: import_zod.z.ZodIssueCode.custom,
11104
+ path: ["accessProfile", "identityOverrides"],
11105
+ 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}.`
11106
+ });
11107
+ }
11108
+ for (const [key, v] of entries) {
11109
+ const ns = key.startsWith("scope:") ? key.slice("scope:".length) : null;
11110
+ const nsError = ns === null ? null : scopeNamespaceGrammarError(ns);
11111
+ if (ns === null || nsError !== null) {
11112
+ ctx.addIssue({
11113
+ code: import_zod.z.ZodIssueCode.custom,
11114
+ path: ["accessProfile", "identityOverrides", key],
11115
+ 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}`
11116
+ });
11117
+ }
11118
+ if (typeof v !== "string") continue;
11119
+ if (IDENTITY_PLACEHOLDER_WHOLE_RE.test(v)) continue;
11120
+ if (!SCOPE_VALUE_RE.test(v)) {
11121
+ ctx.addIssue({
11122
+ code: import_zod.z.ZodIssueCode.custom,
11123
+ path: ["accessProfile", "identityOverrides", key],
11124
+ 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).`
11125
+ });
11126
+ }
11127
+ }
11128
+ }
11129
+ function lintScopeEntries(entries, path4, ctx) {
11130
+ if (!entries) return;
11131
+ const seenValueByNamespace = /* @__PURE__ */ new Map();
11132
+ for (let i = 0; i < entries.length; i++) {
11133
+ const raw = entries[i];
11134
+ if (typeof raw !== "string") continue;
11135
+ const sep = raw.indexOf(":");
11136
+ if (sep <= 0 || sep === raw.length - 1) {
11137
+ ctx.addIssue({
11138
+ code: import_zod.z.ZodIssueCode.custom,
11139
+ path: [...path4, i],
11140
+ message: `'${raw}' must be '<namespace>:<value>' (e.g. 'org:6ba7...' or 'team:${"${{ identities.team }}"}')`
11141
+ });
11142
+ continue;
11143
+ }
11144
+ const namespace = raw.slice(0, sep);
11145
+ const val = raw.slice(sep + 1);
11146
+ const nsError = scopeNamespaceGrammarError(namespace);
11147
+ if (nsError !== null) {
11148
+ ctx.addIssue({ code: import_zod.z.ZodIssueCode.custom, path: [...path4, i], message: `'${raw}' ${nsError}` });
11149
+ } else {
11150
+ const priorValue = seenValueByNamespace.get(namespace);
11151
+ if (priorValue !== void 0 && priorValue !== val) {
11152
+ ctx.addIssue({
11153
+ code: import_zod.z.ZodIssueCode.custom,
11154
+ path: [...path4, i],
11155
+ message: `'${namespace}' appears more than once with different values ('${priorValue}' and '${val}') \u2014 an item carries at most one value per namespace`
11156
+ });
11157
+ }
11158
+ seenValueByNamespace.set(namespace, val);
11159
+ }
11160
+ if (IDENTITY_PLACEHOLDER_WHOLE_RE.test(val)) continue;
11161
+ if (!SCOPE_VALUE_RE.test(val)) {
11162
+ ctx.addIssue({
11163
+ code: import_zod.z.ZodIssueCode.custom,
11164
+ path: [...path4, i],
11165
+ 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).`
11166
+ });
11167
+ }
11168
+ }
11169
+ }
11170
+ function lintNamespacedScopeArrays(value, ctx) {
11171
+ const bp = value;
11172
+ (bp.schemas ?? []).forEach((s, i) => lintScopeEntries(s.scopes, ["schemas", i, "scopes"], ctx));
11173
+ (bp.seed ?? []).forEach((s, i) => lintScopeEntries(s.scopes, ["seed", i, "scopes"], ctx));
11174
+ }
11175
+ function lintDataScopeKey(key, path4, ctx) {
11176
+ if (key === "userId") return;
11177
+ const ns = key.startsWith("scope:") ? key.slice("scope:".length) : null;
11178
+ const nsError = ns === null ? null : scopeNamespaceGrammarError(ns);
11179
+ if (ns === null || nsError !== null) {
11180
+ ctx.addIssue({
11181
+ code: import_zod.z.ZodIssueCode.custom,
11182
+ path: path4,
11183
+ 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}`
11184
+ });
11185
+ }
11186
+ }
11187
+ function lintDataScopeKeys(value, ctx) {
11188
+ const bp = value;
11189
+ for (const key of Object.keys(bp.accessProfile?.dataScope ?? {})) {
11190
+ lintDataScopeKey(key, ["accessProfile", "dataScope", key], ctx);
11191
+ }
11192
+ for (const [roleId, clauses] of Object.entries(bp.roles ?? {})) {
11193
+ clauses.forEach((clause, i) => {
11194
+ for (const key of Object.keys(clause.dataScope ?? {})) {
11195
+ lintDataScopeKey(key, ["roles", roleId, i, "dataScope", key], ctx);
11196
+ }
11197
+ });
11198
+ }
11199
+ }
10611
11200
  function lintSeedSurfaces(value, ctx) {
10612
11201
  const bp = value;
10613
11202
  if (!Array.isArray(bp.seed)) return;
@@ -10625,6 +11214,14 @@ function lintSeedSurfaces(value, ctx) {
10625
11214
  }
10626
11215
  });
10627
11216
  }
11217
+ var BLUEPRINT_FIELD_PHASES = Object.freeze({
11218
+ servicePrincipal: "bootstrap",
11219
+ issuers: "bootstrap",
11220
+ schemas: "in-context",
11221
+ accessProfile: "in-context",
11222
+ roles: "in-context",
11223
+ seed: "in-context"
11224
+ });
10628
11225
  var BlueprintSchema = import_zod.z.object({
10629
11226
  /** Stable blueprint id (the `--blueprint <name>` selector + idempotency key). */
10630
11227
  name: import_zod.z.string().min(1),
@@ -10643,10 +11240,20 @@ var BlueprintSchema = import_zod.z.object({
10643
11240
  /** Optional multi-clause roles, bound to principals via `access grant --role`. */
10644
11241
  roles: BlueprintRolesSchema.optional(),
10645
11242
  /** Optional principals ensured-exist at apply; referenced via ${{ identities.* }}. */
10646
- identities: IdentitiesDeclSchema.optional()
11243
+ identities: IdentitiesDeclSchema.optional(),
11244
+ /**
11245
+ * Optional trusted third-party IdP issuers to register (BYO-IdP token
11246
+ * exchange). Applied in the BOOTSTRAP-token phase, alongside app-context/
11247
+ * service-principal creation — NOT the in-context load (see
11248
+ * {@link BlueprintIssuerSchema}).
11249
+ */
11250
+ issuers: import_zod.z.array(BlueprintIssuerSchema).optional()
10647
11251
  }).strict().superRefine((bp, ctx) => {
10648
11252
  lintSelfTokenPlacement(bp, ctx);
10649
- lintIdentityRefsDeclared(bp, ctx);
11253
+ lintIdentityRefs(bp, ctx);
11254
+ lintIdentityOverrides(bp, ctx);
11255
+ lintNamespacedScopeArrays(bp, ctx);
11256
+ lintDataScopeKeys(bp, ctx);
10650
11257
  lintSeedSurfaces(bp, ctx);
10651
11258
  });
10652
11259
  function formatIssuePath(path4) {
@@ -10802,7 +11409,9 @@ function deriveSuffix(contextId) {
10802
11409
  var WHOLE_TOKEN_RE = /^\$\{\{\s*([A-Za-z_]\w*)\.([A-Za-z_]\w*)\s*\}\}$/;
10803
11410
  var TOKEN_SCAN_RE = /\$\$\{\{|\$\{\{([\s\S]*?)\}\}|\$\{\{/g;
10804
11411
  var INNER_REF_RE = /^([A-Za-z_]\w*)\.([A-Za-z_]\w*)$/;
10805
- var DEFERRED_NAMESPACES = /* @__PURE__ */ new Set(["self", "identities"]);
11412
+ var DEFERRED_NAMESPACES = /* @__PURE__ */ new Set(["self", "identities", "under"]);
11413
+ var DEFERRED_NS_ALTERNATION = [...DEFERRED_NAMESPACES].join("|");
11414
+ var DEFERRED_INNER_RE = new RegExp(`^(?:${DEFERRED_NS_ALTERNATION})(?:\\.[A-Za-z_]\\w*)+$`);
10806
11415
  function resolveRef(ns, name, ctx, path4) {
10807
11416
  if (ns === "inputs") {
10808
11417
  if (!(name in ctx.declared)) {
@@ -10860,14 +11469,17 @@ function interpolate(str, ctx, path4) {
10860
11469
  });
10861
11470
  } else {
10862
11471
  const inner = (m[1] ?? "").trim();
11472
+ if (DEFERRED_INNER_RE.test(inner)) {
11473
+ out += m[0];
11474
+ last = TOKEN_SCAN_RE.lastIndex;
11475
+ continue;
11476
+ }
10863
11477
  const ref = inner.match(INNER_REF_RE);
10864
11478
  if (!ref) {
10865
11479
  ctx.issues.push({
10866
11480
  path: path4,
10867
11481
  message: `malformed reference '\${{ ${inner} }}' \u2014 expected '\${{ namespace.name }}' (escape a literal with '$\${{')`
10868
11482
  });
10869
- } else if (DEFERRED_NAMESPACES.has(ref[1])) {
10870
- out += m[0];
10871
11483
  } else {
10872
11484
  const v = resolveRef(ref[1], ref[2], ctx, path4);
10873
11485
  if (v !== void 0) out += String(v);
@@ -11394,7 +12006,7 @@ var MEMBER_SHARED_READ_ACTIONS = [
11394
12006
  ];
11395
12007
  var agenticSdlc = {
11396
12008
  name: "agentic-sdlc",
11397
- version: "1.6.0",
12009
+ version: "1.9.0",
11398
12010
  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
12011
  contextId: "agentic-sdlc",
11400
12012
  contextName: "Agentic SDLC Knowledge Base",
@@ -12260,6 +12872,233 @@ var agenticSdlc = {
12260
12872
  { fieldName: "updatedOn", rangeEnabled: true },
12261
12873
  { fieldName: "priority", rangeEnabled: true }
12262
12874
  ]
12875
+ },
12876
+ {
12877
+ // A PROPOSED memory, waiting on a verification step — the staging area in
12878
+ // front of the `memory` type above. An agent that distills its own working
12879
+ // sessions produces claims faster than it can check them, and an unchecked
12880
+ // claim is worse than no claim: it is a plausible sentence that will be
12881
+ // recalled as fact. So a proposal lands here, an agent later verifies it,
12882
+ // and only a verified one is written to `memory`.
12883
+ //
12884
+ // ⚠ indexMode NONE IS THE LOAD-BEARING DECISION, not a tuning choice.
12885
+ // Store-only means these records are never indexed, so a search or a
12886
+ // grounded answer CANNOT return them under any query — the separation
12887
+ // between unverified proposals and trusted knowledge is enforced by the
12888
+ // platform rather than by every caller remembering to filter. Make this
12889
+ // HYBRID and the corpus of unchecked claims starts competing with the
12890
+ // curated KB for retrieval slots, which is the one failure this type
12891
+ // exists to prevent. They stay reachable by id and by the two lookups
12892
+ // below, which is all a disposition workflow needs.
12893
+ //
12894
+ // Note the consequence for the fields: `filterable` narrows SEARCH, and
12895
+ // nothing here is searchable, so no field declares it. Query paths are the
12896
+ // lookups, deliberately — a flag that does nothing would just read as
12897
+ // capability that isn't there.
12898
+ typeName: "candidate",
12899
+ displayName: "Memory candidate",
12900
+ indexMode: "NONE",
12901
+ fields: [
12902
+ {
12903
+ fieldId: "title",
12904
+ fieldType: "string",
12905
+ required: true,
12906
+ validation: { minLength: 1, maxLength: 200 },
12907
+ description: "One line naming the claim \u2014 what a reviewer reads first.",
12908
+ renderHints: { label: "Title", widget: "text", order: 1, displayField: true }
12909
+ },
12910
+ {
12911
+ fieldId: "body",
12912
+ fieldType: "string",
12913
+ description: "The proposed fact in full, as the distiller stated it \u2014 the thing to verify.",
12914
+ renderHints: { label: "Body", widget: "textarea", order: 2 }
12915
+ },
12916
+ {
12917
+ // Mirrors `memory.kind` exactly, so a verified candidate maps 1:1 onto
12918
+ // the memory it becomes with no vocabulary translation in between.
12919
+ fieldId: "kind",
12920
+ fieldType: "enum",
12921
+ enumValues: ["user", "feedback", "project", "reference", "observation"],
12922
+ description: "Which flavour of memory this would become \u2014 the same vocabulary as `memory.kind`.",
12923
+ renderHints: { label: "Kind", widget: "select", order: 3 }
12924
+ },
12925
+ {
12926
+ // The distiller's ROUTING suggestion, and the one field here with no
12927
+ // counterpart on `memory` — because it stops existing the moment the
12928
+ // question it asks is answered. A verified claim either becomes a
12929
+ // memory record or becomes a paragraph in a document; this is the
12930
+ // proposer's guess about which, and the verifier is free to overrule
12931
+ // it (the verdict lands in `disposition`, not here).
12932
+ fieldId: "dest",
12933
+ fieldType: "enum",
12934
+ enumValues: ["memory", "doc"],
12935
+ description: "Where the proposer thinks this belongs if it survives review \u2014 a suggestion, not the verdict.",
12936
+ renderHints: { label: "Suggested destination", widget: "select", order: 4 }
12937
+ },
12938
+ {
12939
+ // `area`, `tags` and `sourceRef` below mirror `memory`'s exactly, for
12940
+ // the same reason `kind` does: a verified candidate is COPIED into a
12941
+ // memory, and any field that exists on only one side has to be
12942
+ // re-derived or dropped at exactly the moment a human is deciding
12943
+ // whether the claim is true.
12944
+ fieldId: "area",
12945
+ fieldType: "string",
12946
+ description: 'Subsystem/topic the claim applies to (e.g. "auth", "search") \u2014 the shared area vocabulary.',
12947
+ renderHints: { label: "Area", widget: "text", order: 5 }
12948
+ },
12949
+ {
12950
+ fieldId: "tags",
12951
+ fieldType: "array",
12952
+ description: "Freeform labels carried over from the proposal.",
12953
+ renderHints: { label: "Tags", order: 6 }
12954
+ },
12955
+ {
12956
+ // Provenance the VERIFIER needs most: checking a claim starts with the
12957
+ // file or issue it is about. A candidate whose sourceRef was dropped
12958
+ // in transit costs the reviewer the search that the proposer had
12959
+ // already done.
12960
+ fieldId: "sourceRef",
12961
+ fieldType: "string",
12962
+ description: "Provenance: the file path / issue / URL the claim is about \u2014 where a verifier starts.",
12963
+ renderHints: { label: "Source ref", widget: "text", order: 7 }
12964
+ },
12965
+ {
12966
+ fieldId: "sessionId",
12967
+ fieldType: "string",
12968
+ description: "The conversation that proposed it \u2014 your runtime's session identifier.",
12969
+ renderHints: { label: "Session", widget: "text", order: 8 }
12970
+ },
12971
+ {
12972
+ // Also the stable ordering key: a display ordinal is derived by sorting
12973
+ // on this, so proposals keep the numbering a reviewer saw. Nothing is
12974
+ // ever hard-deleted here, which is what keeps that ordering stable.
12975
+ fieldId: "proposedAt",
12976
+ fieldType: "date",
12977
+ description: "ISO-8601 \u2014 when it was proposed. Also the stable ordering key for display.",
12978
+ renderHints: { label: "Proposed at", widget: "date", order: 9 }
12979
+ },
12980
+ {
12981
+ // THE VERDICT, and deliberately a plain payload field rather than the
12982
+ // record's own ACTIVE/ARCHIVED status. Settling a candidate is
12983
+ // REVERSIBLE — a wrong dismissal has to be undoable — and a write-side
12984
+ // visibility flip is the wrong tool for a reversible state: it is
12985
+ // asynchronous, it moves the record out from under readers, and it
12986
+ // leaves the index status of a once-indexed row stranded. A filterable
12987
+ // payload field flips synchronously and reads the same for everyone.
12988
+ fieldId: "disposition",
12989
+ fieldType: "enum",
12990
+ enumValues: ["pending", "stored", "documented", "ignored"],
12991
+ description: "The verdict: still pending, written to memory, captured in a doc, or dismissed.",
12992
+ renderHints: { label: "Disposition", widget: "select", order: 10 }
12993
+ },
12994
+ {
12995
+ fieldId: "ref",
12996
+ fieldType: "string",
12997
+ description: "The citation given when settling it \u2014 the record id, document path, or reason.",
12998
+ renderHints: { label: "Ref", widget: "text", order: 11 }
12999
+ },
13000
+ {
13001
+ // What the settling step actually CHECKED, which is not always what the
13002
+ // agent typed. Keeping both makes a bad citation auditable after the
13003
+ // fact instead of indistinguishable from a good one.
13004
+ fieldId: "resolved",
13005
+ fieldType: "string",
13006
+ description: "What was verified when it was settled \u2014 kept alongside `ref`, which is what was claimed.",
13007
+ renderHints: { label: "Resolved", widget: "text", order: 12 }
13008
+ },
13009
+ {
13010
+ fieldId: "origin",
13011
+ fieldType: "enum",
13012
+ enumValues: ["session", "recovered"],
13013
+ description: "Proposed by a live session, or recovered later from one that ended without settling.",
13014
+ renderHints: { label: "Origin", widget: "select", order: 13 }
13015
+ },
13016
+ {
13017
+ fieldId: "reopenedWhy",
13018
+ fieldType: "string",
13019
+ description: "Why a settled candidate was reopened \u2014 the audit trail for undoing a verdict.",
13020
+ renderHints: { label: "Reopened why", widget: "textarea", order: 14 }
13021
+ },
13022
+ {
13023
+ // The forward half of a correction: this candidate restates an earlier
13024
+ // one whose claim was wrong. Written at create, so it costs nothing.
13025
+ fieldId: "revises",
13026
+ fieldType: "reference",
13027
+ targetTypeName: "candidate",
13028
+ targetSurface: "record",
13029
+ targetField: "externalId",
13030
+ cardinality: "one",
13031
+ description: "The candidate this one corrects (by externalId).",
13032
+ renderHints: { label: "Revises", order: 15 }
13033
+ },
13034
+ {
13035
+ // The reverse half, and NOT redundant: "is this still open?" has to be
13036
+ // answerable without asking whether some OTHER record points here, and
13037
+ // there is no reverse-reference read. So the superseded candidate is
13038
+ // marked directly.
13039
+ //
13040
+ // Kept SEPARATE from `disposition` on purpose. Superseded and dismissed
13041
+ // are different facts: a dismissal may be undone, while resurrecting a
13042
+ // claim that a later run already corrected would re-offer the version
13043
+ // known to be wrong. Folding both into one enum makes that distinction
13044
+ // unrepresentable exactly when it matters.
13045
+ fieldId: "supersededBy",
13046
+ fieldType: "reference",
13047
+ targetTypeName: "candidate",
13048
+ targetSurface: "record",
13049
+ targetField: "externalId",
13050
+ cardinality: "one",
13051
+ description: "The candidate that corrected this one (by externalId) \u2014 set on the older record.",
13052
+ renderHints: { label: "Superseded by", order: 16 }
13053
+ }
13054
+ ],
13055
+ // FOUR lookups, because this type is asked three genuinely different
13056
+ // questions and one of them is a conjunction:
13057
+ //
13058
+ // `disposition` — "what is waiting ANYWHERE?" The cross-session
13059
+ // review queue. Low cardinality, so its
13060
+ // partitions are large; that is inherent to the
13061
+ // question and the pending set is small.
13062
+ // `sessionId` — "everything belonging to this conversation",
13063
+ // settled candidates included.
13064
+ // (sessionId, disposition) — "what is still waiting IN this conversation".
13065
+ // `proposedAt` (range) — worked by age: "what has been sitting
13066
+ // longest", "what came in this week". A range
13067
+ // read, not a filter over everything ever
13068
+ // proposed.
13069
+ //
13070
+ // The composite earns its slot because that third question is the HOTTEST
13071
+ // read in the system — it is what an agent is shown as it works, so it runs
13072
+ // far more often than the queue-wide scan does. Without it the answer is the
13073
+ // more selective single lookup plus a filter in the caller.
13074
+ //
13075
+ // `sessionId` LEADS, and the order is not cosmetic: the leading field
13076
+ // becomes the partition key. Leading with `disposition` would sort every
13077
+ // candidate ever proposed into four partitions; leading with `sessionId`
13078
+ // gives one small partition per conversation. Order is also
13079
+ // migration-locked, so it cannot be corrected later.
13080
+ //
13081
+ // A composite matches a LEADING RUN of its fields, so `(sessionId,
13082
+ // disposition)` also answers `sessionId` alone. The plain `sessionId`
13083
+ // lookup is kept anyway: under a partial tuple the unspecified field
13084
+ // becomes part of the ordering, so results come back grouped by
13085
+ // `disposition` rather than in one sequence — right for a filtered view,
13086
+ // wrong for "this conversation's candidates, oldest first", which is the
13087
+ // read the review surface and the queue/record agreement check both make.
13088
+ // One declaration out of ten is a cheap price for not depending on that
13089
+ // subtlety in the two places that must not be wrong.
13090
+ //
13091
+ // No `sortBy`: it defaults to the record's creation time, which is already
13092
+ // the primary key the review queue orders by. Naming `proposedAt` instead
13093
+ // would order by the WORKER's clock, and that is stamped when a proposal is
13094
+ // queued rather than when it is stored — the two disagree by exactly the
13095
+ // outage a durable queue exists to survive.
13096
+ lookupFields: [
13097
+ "disposition",
13098
+ "sessionId",
13099
+ { fieldNames: ["sessionId", "disposition"] },
13100
+ { fieldName: "proposedAt", rangeEnabled: true }
13101
+ ]
12263
13102
  }
12264
13103
  ],
12265
13104
  // Least-privilege, data-plane only. The scope of the `ssk_*` key the bootstrap
@@ -12313,7 +13152,12 @@ var agenticSdlc = {
12313
13152
  // grant (`records:r:memory`) and the inference capability travel in the
12314
13153
  // SAME clause, so grounding works regardless of cross-clause admission
12315
13154
  // semantics. Still self-fenced by dataScope: never another principal's.
12316
- allowedActions: ["records:cru:memory", "search:r", "inference:r"],
13155
+ // `candidate` is READ-only here, unlike `memory`'s full CRU. A candidate's
13156
+ // verdict is the output of a verification step, so it is written by the
13157
+ // agent runtime that performed the check — a human editing the field
13158
+ // directly would record a verdict nothing actually verified. Reading them
13159
+ // is the point: the queue, the verdicts and the corrections are browsable.
13160
+ allowedActions: ["records:cru:memory", "records:r:candidate", "search:r", "inference:r"],
12317
13161
  dataScope: { userId: ["${{ self.userId }}"] }
12318
13162
  }
12319
13163
  ]
@@ -13159,7 +14003,17 @@ async function resolveAuthContext(opts) {
13159
14003
  bearer: bridge.token,
13160
14004
  cognitoToken,
13161
14005
  baseUrl,
13162
- env: opts.env,
14006
+ /**
14007
+ * Derived from the REQUEST'S actual `baseUrl`, not echoed from `opts.env` — the two
14008
+ * diverge whenever `VECTROS_API_BASE_URL`/`--base-url` alone selects the environment
14009
+ * (the common shape when targeting staging without also passing `--env staging`,
14010
+ * since `envToBaseUrl` lets an explicit override win over `opts.env`'s default). Every
14011
+ * display surface that shows "on {env}" (blueprint-test's banner, `whoami`, etc.) reads
14012
+ * this field, so it must reflect where the request actually went, not which hidden flag
14013
+ * defaulted to what. `deriveEnvironment` falls through to `opts.env` only for a base URL
14014
+ * it doesn't recognize (a non-standard override, e.g. a future custom/self-hosted host).
14015
+ */
14016
+ env: deriveEnvironment(baseUrl) ?? opts.env,
13163
14017
  identity,
13164
14018
  tenantId: typeof identity.tenantId === "string" ? identity.tenantId : void 0,
13165
14019
  tenant
@@ -13243,7 +14097,8 @@ var DATA_PLANE_ALLOWLIST = [
13243
14097
  "search",
13244
14098
  "documents",
13245
14099
  "folders",
13246
- "inference"
14100
+ "inference",
14101
+ "entities"
13247
14102
  ];
13248
14103
  var KNOWN_CONTROL_PLANE_RESOURCES = [
13249
14104
  "keys",
@@ -13252,12 +14107,13 @@ var KNOWN_CONTROL_PLANE_RESOURCES = [
13252
14107
  "users",
13253
14108
  "billing",
13254
14109
  "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",
14110
+ // The namespace REGISTRY (as opposed to `entities`, which creates/reads a
14111
+ // ROW within an already-registered namespace — see the data-plane-allowlist
14112
+ // note above). Registering a new namespace TYPE is a tenant-config action,
14113
+ // root-key-gated regardless of any scope a token carries — genuinely
14114
+ // control-plane, unlike per-namespace entity CRUD. (Cosmetic: any
14115
+ // non-data-plane resource hard-rejects whether or not it is listed here;
14116
+ // this list only sharpens the error message.)
13261
14117
  "namespaces"
13262
14118
  ];
13263
14119
  var VALID_OPS = /* @__PURE__ */ new Set(["c", "r", "u", "d"]);
@@ -13571,6 +14427,32 @@ async function runLoader(client, input) {
13571
14427
  throw new Error("Service-principal create returned no user id; cannot bind AccessProfile/key.");
13572
14428
  }
13573
14429
  log.debug({ component: "loader", externalId: blueprint.servicePrincipal.externalId, userId }, "service principal ensured");
14430
+ const issuerActions = [];
14431
+ for (const issuer of blueprint.issuers ?? []) {
14432
+ const issuerResp = await client.auth.registerIssuer({
14433
+ issuerId: issuer.issuerId,
14434
+ issuer: issuer.issuer,
14435
+ jwksUri: issuer.jwksUri,
14436
+ audience: issuer.audience,
14437
+ contextId: issuer.contextId,
14438
+ subClaim: issuer.subClaim,
14439
+ emailClaim: issuer.emailClaim,
14440
+ selfSignupPolicies: issuer.selfSignupPolicies?.map((p) => ({
14441
+ signup_type: p.signupType,
14442
+ role_id: p.roleId
14443
+ }))
14444
+ });
14445
+ const action = actionOf(issuerResp.created);
14446
+ issuerActions.push({ issuerId: issuer.issuerId, action });
14447
+ if (action === "reconciled") {
14448
+ log.warn(
14449
+ { component: "loader", issuerId: issuer.issuerId },
14450
+ "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"
14451
+ );
14452
+ } else {
14453
+ log.debug({ component: "loader", issuerId: issuer.issuerId, action }, "issuer registered");
14454
+ }
14455
+ }
13574
14456
  const ctxClient = input.remintForContext ? await input.remintForContext(blueprint.contextId) : client;
13575
14457
  log.debug({ component: "loader", contextId: blueprint.contextId }, "re-minted per-context client");
13576
14458
  const schemaIds = {};
@@ -13752,6 +14634,7 @@ async function runLoader(client, input) {
13752
14634
  profileAction,
13753
14635
  schemaActions,
13754
14636
  roleActions,
14637
+ issuerActions,
13755
14638
  rawKey,
13756
14639
  keyId,
13757
14640
  revokedKeyId,
@@ -13770,6 +14653,13 @@ function buildPlan(blueprint, opts = {}) {
13770
14653
  kind: "principal",
13771
14654
  summary: `principal '${blueprint.servicePrincipal.externalId}' (SERVICE)`
13772
14655
  });
14656
+ for (const iss of blueprint.issuers ?? []) {
14657
+ const selfSignup = iss.selfSignupPolicies?.length ? `, self-signup: ${iss.selfSignupPolicies.length} polic${iss.selfSignupPolicies.length === 1 ? "y" : "ies"}` : "";
14658
+ steps.push({
14659
+ kind: "issuer",
14660
+ summary: `issuer '${iss.issuerId}' (${iss.issuer} \u2192 context '${iss.contextId}'${selfSignup}) [${BLUEPRINT_FIELD_PHASES.issuers}-token phase]`
14661
+ });
14662
+ }
13773
14663
  for (const s of blueprint.schemas) {
13774
14664
  const lookups = toLookupDefs(s.lookupFields)?.map((l) => l.fieldNames ? `(${l.fieldNames.join("+")})` : l.fieldName) ?? [];
13775
14665
  const lookupNote = lookups.length ? ` lookup: [${lookups.join(", ")}]` : "";
@@ -13810,8 +14700,17 @@ function buildPlan(blueprint, opts = {}) {
13810
14700
  }
13811
14701
  function renderPlan(blueprint, steps) {
13812
14702
  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.`;
14703
+ const legendLines = [
14704
+ ` Apply is a non-destructive reconcile \u2014 each item is created if absent, else`,
14705
+ ` updated in place to match. Nothing is dropped or recreated.`
14706
+ ];
14707
+ if (steps.some((s) => s.kind === "issuer")) {
14708
+ legendLines.push(
14709
+ ` Exception: issuers cannot be updated once registered \u2014 an existing`,
14710
+ ` issuer is left unchanged, not reconciled to match.`
14711
+ );
14712
+ }
14713
+ const legend = legendLines.join("\n");
13815
14714
  const lines = steps.map((s) => ` + ${s.summary}`);
13816
14715
  return [header, legend, "", ...lines].join("\n");
13817
14716
  }
@@ -15666,6 +16565,14 @@ function revokeWarning(targets) {
15666
16565
  }
15667
16566
 
15668
16567
  // src/minted-key-store.ts
16568
+ function keyringAliasFor(alias, tenant) {
16569
+ if (tenant === "live" && alias.endsWith(".test")) {
16570
+ throw new Error(
16571
+ `'${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.`
16572
+ );
16573
+ }
16574
+ return tenant === "test" ? `${alias}.test` : alias;
16575
+ }
15669
16576
  function nowIso() {
15670
16577
  return (/* @__PURE__ */ new Date()).toISOString();
15671
16578
  }
@@ -15782,6 +16689,7 @@ var BLANK_SLATE = {
15782
16689
  };
15783
16690
  function parseBootstrapArgs(argv) {
15784
16691
  const args = {
16692
+ set: [],
15785
16693
  env: "production",
15786
16694
  yes: false,
15787
16695
  noMerge: false,
@@ -15799,6 +16707,12 @@ function parseBootstrapArgs(argv) {
15799
16707
  case "--blueprint":
15800
16708
  args.blueprint = argv[++i];
15801
16709
  break;
16710
+ case "--set":
16711
+ args.set.push(argv[++i]);
16712
+ break;
16713
+ case "--values":
16714
+ args.values = argv[++i];
16715
+ break;
15802
16716
  case "--env": {
15803
16717
  const v = argv[++i];
15804
16718
  if (v !== "staging" && v !== "production") {
@@ -15896,10 +16810,20 @@ function reconcileTally(items, noun) {
15896
16810
  if (reconciled) seg.push(`${reconciled} reconciled`);
15897
16811
  return `${items.length} ${noun}${items.length === 1 ? "" : "s"} (${seg.join(", ")})`;
15898
16812
  }
16813
+ function issuerTally(items) {
16814
+ if (items.length === 0) return null;
16815
+ const created = items.filter((i) => i.action === "created").length;
16816
+ const unchanged = items.length - created;
16817
+ const seg = [];
16818
+ if (created) seg.push(`${created} created`);
16819
+ if (unchanged) seg.push(`${unchanged} already registered \u2014 unchanged (issuers cannot be updated once registered)`);
16820
+ return `${items.length} issuer${items.length === 1 ? "" : "s"} (${seg.join(", ")})`;
16821
+ }
15899
16822
  function applyOutcomeSummary(result) {
15900
16823
  const parts = [
15901
16824
  reconcileTally(result.schemaActions, "schema"),
15902
16825
  reconcileTally(result.roleActions, "role"),
16826
+ issuerTally(result.issuerActions),
15903
16827
  `profile ${result.profileAction}`
15904
16828
  ].filter((p) => p !== null);
15905
16829
  const detail = parts.length ? ` \u2014 ${parts.join("; ")}` : "";
@@ -15910,6 +16834,7 @@ var BOOTSTRAP_HELP = `vectros bootstrap \u2014 provision a least-privilege MCP c
15910
16834
 
15911
16835
  Usage:
15912
16836
  vectros bootstrap [--blueprint <name> | --blank] [--no-seed]
16837
+ [--set name=value ...] [--values <file>]
15913
16838
  [--tenant test|live | --test] [--client desktop|code]
15914
16839
  [--yes] [--no-merge] [--rotate]
15915
16840
  [--token <t> | env VECTROS_BOOTSTRAP_TOKEN]
@@ -15919,6 +16844,10 @@ Options:
15919
16844
  --blueprint <name|file> A bundled blueprint (${BLUEPRINT_NAMES.join(", ")}),
15920
16845
  OR a path to your own blueprint YAML/JSON (./my.yaml) \u2014 the "copy &
15921
16846
  own" path. File blueprints get the identical scope gate (no override).
16847
+ --set name=value Set an install-time input value (repeatable) for a file blueprint's
16848
+ 'inputs:' block. Beats --values + the declared default. (Same flag as
16849
+ 'vectros blueprint validate/plan'.)
16850
+ --values <file> A YAML/JSON map of input values. (File blueprints only.)
15922
16851
  --blank Credential only (read-only data-plane profile, no schemas).
15923
16852
  --no-seed Provision the blueprint's schemas, profile and key but SKIP its
15924
16853
  sample seed records \u2014 the right choice for a production context
@@ -15953,10 +16882,21 @@ Options:
15953
16882
 
15954
16883
  No --blueprint/--blank in a TTY \u2192 interactive menu.`;
15955
16884
  async function resolveBlueprint(args, prompt, isTty) {
15956
- if (args.blankSlate) return BLANK_SLATE;
16885
+ if (args.blankSlate) {
16886
+ if (args.set.length > 0 || args.values) {
16887
+ throw new Error(`--set/--values apply only to file blueprints; --blank has no inputs to set.`);
16888
+ }
16889
+ return BLANK_SLATE;
16890
+ }
15957
16891
  if (args.blueprint) {
15958
16892
  if (looksLikePath(args.blueprint)) {
15959
- return await loadBlueprintFile(args.blueprint);
16893
+ const supplied = await buildInputSupply(args.set, args.values);
16894
+ return await loadBlueprintFile(args.blueprint, supplied);
16895
+ }
16896
+ if (args.set.length > 0 || args.values) {
16897
+ throw new Error(
16898
+ `--set/--values apply only to file blueprints; '${args.blueprint}' is a bundled blueprint (no inputs to set).`
16899
+ );
15960
16900
  }
15961
16901
  const b = getBlueprint(args.blueprint);
15962
16902
  if (!b) {
@@ -15969,6 +16909,11 @@ async function resolveBlueprint(args, prompt, isTty) {
15969
16909
  if (!isTty) {
15970
16910
  throw new Error("No --blueprint or --blank specified and not a TTY (cannot show the menu).");
15971
16911
  }
16912
+ if (args.set.length > 0 || args.values) {
16913
+ throw new Error(
16914
+ `--set/--values need an explicit --blueprint <file>; the interactive menu only offers bundled blueprints and blank slate, neither of which has inputs to set.`
16915
+ );
16916
+ }
15972
16917
  const options = [
15973
16918
  { label: "Blank slate (credential only)", blueprint: BLANK_SLATE },
15974
16919
  ...BUNDLED_BLUEPRINTS.map((b) => ({ label: `${b.name} \u2014 ${b.description}`, blueprint: b }))
@@ -16134,10 +17079,11 @@ ${renderPlan(blueprint, planSteps)}
16134
17079
  let storedFreshInKeyring = false;
16135
17080
  const readExistingKey = async (contextId) => {
16136
17081
  if (!keyring) return void 0;
17082
+ const alias = keyringAliasFor(contextId, args.tenant);
16137
17083
  try {
16138
- const existing = await keyring.get(contextId);
17084
+ const existing = await keyring.get(alias);
16139
17085
  if (!existing?.keyId) return void 0;
16140
- const rawKey = await keyring.resolveSecret(contextId);
17086
+ const rawKey = await keyring.resolveSecret(alias);
16141
17087
  return rawKey ? { keyId: existing.keyId, rawKey } : void 0;
16142
17088
  } catch {
16143
17089
  return void 0;
@@ -16154,16 +17100,27 @@ ${renderPlan(blueprint, planSteps)}
16154
17100
  );
16155
17101
  return;
16156
17102
  }
17103
+ const alias = keyringAliasFor(contextId, args.tenant);
16157
17104
  try {
16158
- warnReplacingAlias(await keyring.get(contextId), contextId, record.revokedKeyId);
17105
+ warnReplacingAlias(await keyring.get(alias), alias, record.revokedKeyId);
16159
17106
  await storeMintedKey(
16160
17107
  keyring,
16161
- { alias: contextId, context: contextId, principalId: record.principalId, keyId: record.keyId, env: args.env },
17108
+ {
17109
+ alias,
17110
+ context: contextId,
17111
+ principalId: record.principalId,
17112
+ keyId: record.keyId,
17113
+ // Derived from the REQUEST's actual baseUrl, not echoed from args.env — the two
17114
+ // diverge whenever VECTROS_API_BASE_URL/--base-url alone selected the environment.
17115
+ // `keyring list`/`keyring doctor` display this field; a stale echo would show
17116
+ // "env: production" next to a key this run actually minted against staging.
17117
+ env: deriveEnvironment(baseUrl) ?? args.env
17118
+ },
16162
17119
  record.rawKey,
16163
17120
  { activate: true, revokedKeyId: record.revokedKeyId }
16164
17121
  );
16165
17122
  if (record.revokedKeyId) {
16166
- await warnOtherAliasesNaming(keyring, record.revokedKeyId, contextId);
17123
+ await warnOtherAliasesNaming(keyring, record.revokedKeyId, alias);
16167
17124
  }
16168
17125
  storedFreshInKeyring = true;
16169
17126
  } catch (err) {
@@ -16208,7 +17165,7 @@ ${renderPlan(blueprint, planSteps)}
16208
17165
  );
16209
17166
  entry = buildVectrosServerEntry({ apiKey: result.rawKey, apiBaseUrl: baseUrl });
16210
17167
  } else if (storedFreshInKeyring || result.keyAction === "reused") {
16211
- entry = buildVectrosServerEntry({ alias: blueprint.contextId, apiBaseUrl: baseUrl });
17168
+ entry = buildVectrosServerEntry({ alias: keyringAliasFor(blueprint.contextId, args.tenant), apiBaseUrl: baseUrl });
16212
17169
  } else {
16213
17170
  entry = buildVectrosServerEntry({ apiKey: result.rawKey, apiBaseUrl: baseUrl });
16214
17171
  }
@@ -16291,7 +17248,7 @@ ${formatCliError(err, { action: "Bootstrap failed" })}`);
16291
17248
  var import_promises3 = require("fs/promises");
16292
17249
 
16293
17250
  // src/scope-grammar.ts
16294
- var FORBIDDEN_SCOPE_NAMESPACES = /* @__PURE__ */ new Set([
17251
+ var FORBIDDEN_SCOPE_NAMESPACES2 = /* @__PURE__ */ new Set([
16295
17252
  "user",
16296
17253
  "record",
16297
17254
  "document",
@@ -16303,23 +17260,23 @@ var FORBIDDEN_SCOPE_NAMESPACES = /* @__PURE__ */ new Set([
16303
17260
  "versions",
16304
17261
  "lookup"
16305
17262
  ]);
16306
- var SCOPE_NAMESPACE_RE = /^[a-z][a-z0-9_-]{1,31}$/;
17263
+ var SCOPE_NAMESPACE_RE2 = /^[a-z][a-z0-9_-]{1,31}$/;
16307
17264
  var SCOPE_NAMESPACE_MIN_LENGTH = 2;
16308
17265
  var SCOPE_NAMESPACE_MAX_LENGTH = 32;
16309
- var SCOPE_VALUE_RE = /^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$/;
17266
+ var SCOPE_VALUE_RE2 = /^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$/;
16310
17267
  var SCOPE_VALUE_MIN_LENGTH = 1;
16311
17268
  var SCOPE_VALUE_MAX_LENGTH = 128;
16312
17269
  function valueGrammarError(value) {
16313
- if (!SCOPE_VALUE_RE.test(value)) {
17270
+ if (!SCOPE_VALUE_RE2.test(value)) {
16314
17271
  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
17272
  }
16316
17273
  return null;
16317
17274
  }
16318
17275
  function namespaceGrammarError(ns) {
16319
- if (FORBIDDEN_SCOPE_NAMESPACES.has(ns)) {
17276
+ if (FORBIDDEN_SCOPE_NAMESPACES2.has(ns)) {
16320
17277
  return `'${ns}' is a reserved namespace and cannot be used as an ownership scope`;
16321
17278
  }
16322
- if (!SCOPE_NAMESPACE_RE.test(ns)) {
17279
+ if (!SCOPE_NAMESPACE_RE2.test(ns)) {
16323
17280
  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
17281
  }
16325
17282
  return null;
@@ -16516,6 +17473,17 @@ function lintBlueprint(bp) {
16516
17473
  }
16517
17474
  });
16518
17475
  }
17476
+ for (const issuer of bp.issuers ?? []) {
17477
+ for (const policy of issuer.selfSignupPolicies ?? []) {
17478
+ if (!Object.prototype.hasOwnProperty.call(bp.roles ?? {}, policy.roleId)) {
17479
+ findings.push({
17480
+ severity: "warning",
17481
+ rule: "self-signup-role-unknown",
17482
+ 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.`
17483
+ });
17484
+ }
17485
+ }
17486
+ }
16519
17487
  return findings;
16520
17488
  }
16521
17489
  var ROLE_ID_RE = /^[a-z][a-z0-9-]{2,30}$/;
@@ -16579,8 +17547,8 @@ schemas:
16579
17547
  lookupFields: [title] # extra O(1) exact-match lookup indexes (max 10)
16580
17548
 
16581
17549
  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.
17550
+ # SCOPE GATE: data-plane only (records/schemas/search/documents/folders/inference/entities).
17551
+ # Anything else (keys/profiles/users/admin/namespaces/*) is HARD-REJECTED \u2014 by design, no override.
16584
17552
  # Ops: c|r|u|d, combinable (records:cru). Prefer the narrowest set (least privilege).
16585
17553
  allowedActions: [records:r, records:c, records:u, search:r, schemas:r]
16586
17554
  # dataScope binds the key to specific owners. Include null to ALSO see
@@ -17360,9 +18328,25 @@ async function runKeyRotate(opts, deps = {}) {
17360
18328
  return 1;
17361
18329
  }
17362
18330
  }
17363
- const existing = (await drainPages(() => client.auth.listScopedKeys())).find(
17364
- (k) => matchesTuple(k, opts.context, userId, keyName)
17365
- );
18331
+ const activeKeys = await drainPages(() => client.auth.listScopedKeys());
18332
+ const existing = activeKeys.find((k) => matchesTuple(k, opts.context, userId, keyName));
18333
+ if (!existing && keyName === DEFAULT_KEY_NAME && !opts.forceNew) {
18334
+ const others = activeKeys.filter(
18335
+ (k) => k.contextId === opts.context && k.userId === userId && k.status === "active"
18336
+ );
18337
+ if (others.length > 0) {
18338
+ const list = others.map((k) => ` \u2022 ${k.keyId} (name: '${k.keyName}')`).join("\n");
18339
+ process.stderr.write(
18340
+ ` \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):
18341
+ ${list}
18342
+ If one of these is it, re-run with --name '<its name>'.
18343
+ If you really want a new key alongside them, re-run with --force-new.
18344
+ Nothing was revoked or minted.
18345
+ `
18346
+ );
18347
+ return 1;
18348
+ }
18349
+ }
17366
18350
  if (existing?.keyId) {
17367
18351
  await client.auth.revokeScopedKey({ keyId: existing.keyId });
17368
18352
  process.stdout.write(` \u2714 Revoked prior key ${existing.keyId}
@@ -17795,7 +18779,7 @@ function isRecognizedOwnershipKey(key) {
17795
18779
  if (key === "userId" || key === "partnerUserId") return true;
17796
18780
  if (!key.startsWith("scope:")) return false;
17797
18781
  const ns = key.slice("scope:".length);
17798
- return !FORBIDDEN_SCOPE_NAMESPACES.has(ns) && SCOPE_NAMESPACE_RE.test(ns);
18782
+ return !FORBIDDEN_SCOPE_NAMESPACES2.has(ns) && SCOPE_NAMESPACE_RE2.test(ns);
17799
18783
  }
17800
18784
  var DIM_WILDCARD = "*";
17801
18785
  var ANY_TOKEN_RE = /^\$\{\{\s*any\s*\}\}$/;
@@ -18542,7 +19526,24 @@ async function captureSnapshot(client, blueprint) {
18542
19526
  }
18543
19527
  return { existingSchemaTypes, existingProfilePrincipals, existingRoleIds, failed };
18544
19528
  }
18545
- async function runAssertions(client, deps, blueprint, tenantId, result) {
19529
+ async function captureIssuerSnapshot(bootstrapClient) {
19530
+ const existingIssuerIds = /* @__PURE__ */ new Set();
19531
+ let failed = false;
19532
+ try {
19533
+ let startFrom;
19534
+ for (; ; ) {
19535
+ const page = await bootstrapClient.auth.listIssuers({ limit: SNAPSHOT_PAGE_SIZE, startFrom });
19536
+ for (const i of page.data ?? []) if (i.issuerId) existingIssuerIds.add(i.issuerId);
19537
+ const next = page.nextCursor;
19538
+ if (!next) break;
19539
+ startFrom = next;
19540
+ }
19541
+ } catch {
19542
+ failed = true;
19543
+ }
19544
+ return { existingIssuerIds, failed };
19545
+ }
19546
+ async function runAssertions(client, bootstrapClient, deps, blueprint, tenantId, result) {
18546
19547
  const assertions = [];
18547
19548
  try {
18548
19549
  const ping = await deps.pingWithKey(result.rawKey);
@@ -18627,9 +19628,22 @@ async function runAssertions(client, deps, blueprint, tenantId, result) {
18627
19628
  }
18628
19629
  }
18629
19630
  }
19631
+ for (const { issuerId } of result.issuerActions) {
19632
+ try {
19633
+ const issuer = await bootstrapClient.auth.getIssuer({ issuerId });
19634
+ const ok = issuer.issuerId === issuerId;
19635
+ assertions.push({
19636
+ name: `issuer:${issuerId}`,
19637
+ ok,
19638
+ detail: ok ? `issuer '${issuerId}' registered + readable` : `getIssuer(${issuerId}) returned issuerId '${issuer.issuerId ?? "(none)"}'`
19639
+ });
19640
+ } catch (e) {
19641
+ assertions.push({ name: `issuer:${issuerId}`, ok: false, detail: `getIssuer(${issuerId}) failed: ${errMsg(e)}` });
19642
+ }
19643
+ }
18630
19644
  return assertions;
18631
19645
  }
18632
- async function runTeardown(client, blueprint, machineId, snapshot, result) {
19646
+ async function runTeardown(client, bootstrapClient, blueprint, machineId, snapshot, result) {
18633
19647
  const steps = [];
18634
19648
  const keyName = result?.keyName ?? keyNameFor(blueprint.name, machineId);
18635
19649
  try {
@@ -18723,6 +19737,22 @@ async function runTeardown(client, blueprint, machineId, snapshot, result) {
18723
19737
  for (const [typeName] of toDelete) {
18724
19738
  steps.push({ step: `delete schema ${typeName}`, ok: false, error: lastError.get(typeName) ?? "could not delete" });
18725
19739
  }
19740
+ for (const { issuerId } of result.issuerActions) {
19741
+ if (snapshot.failed) {
19742
+ steps.push({ step: `delete issuer ${issuerId}`, ok: true, skipped: true, error: "skipped \u2014 pre-apply snapshot failed; cannot prove ownership" });
19743
+ continue;
19744
+ }
19745
+ if (snapshot.existingIssuerIds.has(issuerId)) {
19746
+ steps.push({ step: `delete issuer ${issuerId}`, ok: true, skipped: true, error: "skipped \u2014 pre-existed this run" });
19747
+ continue;
19748
+ }
19749
+ try {
19750
+ await bootstrapClient.auth.deleteIssuer({ issuerId });
19751
+ steps.push({ step: `delete issuer ${issuerId}`, ok: true });
19752
+ } catch (e) {
19753
+ steps.push({ step: `delete issuer ${issuerId}`, ok: false, error: errMsg(e) });
19754
+ }
19755
+ }
18726
19756
  return steps;
18727
19757
  }
18728
19758
  async function runBlueprintHarness(client, input, deps) {
@@ -18736,12 +19766,12 @@ async function runBlueprintHarness(client, input, deps) {
18736
19766
  log
18737
19767
  );
18738
19768
  const remint = remintForContext ?? (async () => client);
18739
- let snapshot;
19769
+ let contextSnapshot;
18740
19770
  if (remintForContext) {
18741
19771
  try {
18742
- snapshot = await captureSnapshot(await remintForContext(blueprint.contextId), blueprint);
19772
+ contextSnapshot = await captureSnapshot(await remintForContext(blueprint.contextId), blueprint);
18743
19773
  } catch (e) {
18744
- snapshot = {
19774
+ contextSnapshot = {
18745
19775
  existingSchemaTypes: /* @__PURE__ */ new Set(),
18746
19776
  existingProfilePrincipals: /* @__PURE__ */ new Set(),
18747
19777
  existingRoleIds: /* @__PURE__ */ new Set(),
@@ -18749,15 +19779,21 @@ async function runBlueprintHarness(client, input, deps) {
18749
19779
  };
18750
19780
  }
18751
19781
  } else {
18752
- snapshot = await captureSnapshot(client, blueprint);
19782
+ contextSnapshot = await captureSnapshot(client, blueprint);
18753
19783
  }
19784
+ const issuerSnapshot = await captureIssuerSnapshot(client);
19785
+ const snapshot = {
19786
+ ...contextSnapshot,
19787
+ existingIssuerIds: issuerSnapshot.existingIssuerIds,
19788
+ failed: contextSnapshot.failed || issuerSnapshot.failed
19789
+ };
18754
19790
  let result;
18755
19791
  let assertions = [];
18756
19792
  let applyError;
18757
19793
  try {
18758
19794
  result = await runLoader(client, { blueprint, tenantId, machineId, readExistingKey, persistMintedKey, log, remintForContext });
18759
19795
  log.debug({ component: "harness", contextId: result.contextId, keyName: result.keyName }, "applied");
18760
- assertions = await runAssertions(await remint(blueprint.contextId), deps, blueprint, tenantId, result);
19796
+ assertions = await runAssertions(await remint(blueprint.contextId), client, deps, blueprint, tenantId, result);
18761
19797
  } catch (e) {
18762
19798
  applyError = errMsg(e);
18763
19799
  log.warn({ component: "harness", err: applyError }, "apply phase failed");
@@ -18769,7 +19805,7 @@ async function runBlueprintHarness(client, input, deps) {
18769
19805
  tdClient = await remint(blueprint.contextId);
18770
19806
  } catch {
18771
19807
  }
18772
- teardown = await runTeardown(tdClient, blueprint, machineId, snapshot, result);
19808
+ teardown = await runTeardown(tdClient, client, blueprint, machineId, snapshot, result);
18773
19809
  }
18774
19810
  const passed = applyError === void 0 && assertions.length > 0 && assertions.every((a) => a.ok);
18775
19811
  return {
@@ -18961,7 +19997,14 @@ async function runJoin(opts, deps = {}) {
18961
19997
  return 2;
18962
19998
  }
18963
19999
  const context = opts.contextArg;
18964
- const alias = opts.alias ?? context;
20000
+ let alias;
20001
+ try {
20002
+ alias = keyringAliasFor(opts.alias ?? context, opts.tenant ?? "live");
20003
+ } catch (err) {
20004
+ process.stderr.write(` \u2716 ${formatError(err)}
20005
+ `);
20006
+ return 2;
20007
+ }
18965
20008
  let keyring;
18966
20009
  try {
18967
20010
  keyring = deps.keyring ?? await Keyring.open();
@@ -19036,9 +20079,9 @@ function emitJoined(alias, context, principalId, role, keyId, reused) {
19036
20079
 
19037
20080
  // src/build-info.ts
19038
20081
  var BUILD_INFO = {
19039
- cli: true ? "0.14.0" : "dev",
19040
- sdk: true ? "0.38.0" : "dev",
19041
- blueprints: true ? "0.11.0" : "dev"
20082
+ cli: true ? "0.15.0" : "dev",
20083
+ sdk: true ? "0.39.0" : "dev",
20084
+ blueprints: true ? "0.12.0" : "dev"
19042
20085
  };
19043
20086
  function formatBuildInfo() {
19044
20087
  return `vectros ${BUILD_INFO.cli} (sdk ${BUILD_INFO.sdk}, blueprints ${BUILD_INFO.blueprints})`;
@@ -19422,7 +20465,10 @@ function registerKeyCommands(program, deps) {
19422
20465
  process.exit(await runKeyRevoke(keyId, { ...toCommonOptions(flags), context: flags.context }, deps));
19423
20466
  });
19424
20467
  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)
20468
+ key.command("rotate").description("Revoke the matching active key and mint a fresh secret.").requiredOption("--principal <p>", "principal (usr_<id> or <id>)").requiredOption("--context <c>", "app context").option("--name <name>", "key name to rotate", DEFAULT_KEY_NAME).option(
20469
+ "--force-new",
20470
+ "mint a new key even though other active keys already exist for this principal/context under a different name (bypasses the default-name-mismatch guard)"
20471
+ )
19426
20472
  );
19427
20473
  addAuthOptions(rotate).action(async (flags) => {
19428
20474
  process.exit(
@@ -19431,6 +20477,7 @@ function registerKeyCommands(program, deps) {
19431
20477
  principal: flags.principal,
19432
20478
  context: flags.context,
19433
20479
  name: flags.name,
20480
+ forceNew: flags.forceNew,
19434
20481
  ...toKeyDestination(flags)
19435
20482
  }, deps)
19436
20483
  );