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