@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/AUTHORING.md +18 -1
- package/CHANGELOG.md +62 -0
- package/README.md +4 -3
- package/dist/cli.js +1108 -61
- package/dist/cli.js.map +1 -1
- package/dist/cli.mjs +1108 -61
- package/dist/cli.mjs.map +1 -1
- package/package.json +3 -3
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
|
-
//
|
|
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(
|
|
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
|
|
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\.[
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
13261
|
-
//
|
|
13262
|
-
//
|
|
13263
|
-
//
|
|
13264
|
-
//
|
|
13265
|
-
|
|
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
|
|
13819
|
-
|
|
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)
|
|
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
|
-
|
|
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(
|
|
17089
|
+
const existing = await keyring.get(alias);
|
|
16144
17090
|
if (!existing?.keyId) return void 0;
|
|
16145
|
-
const rawKey = await keyring.resolveSecret(
|
|
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(
|
|
17110
|
+
warnReplacingAlias(await keyring.get(alias), alias, record.revokedKeyId);
|
|
16164
17111
|
await storeMintedKey(
|
|
16165
17112
|
keyring,
|
|
16166
|
-
{
|
|
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,
|
|
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
|
|
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
|
|
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
|
|
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 (!
|
|
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 (
|
|
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 (!
|
|
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
|
|
17369
|
-
|
|
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 !
|
|
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
|
|
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
|
|
19774
|
+
let contextSnapshot;
|
|
18745
19775
|
if (remintForContext) {
|
|
18746
19776
|
try {
|
|
18747
|
-
|
|
19777
|
+
contextSnapshot = await captureSnapshot(await remintForContext(blueprint.contextId), blueprint);
|
|
18748
19778
|
} catch (e) {
|
|
18749
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
19045
|
-
sdk: true ? "0.
|
|
19046
|
-
blueprints: true ? "0.
|
|
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
|
);
|