@vectros-ai/cli 0.14.0 → 0.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AUTHORING.md +151 -8
- package/CHANGELOG.md +210 -0
- package/README.md +196 -3
- package/dist/cli.js +2306 -545
- package/dist/cli.js.map +1 -1
- package/dist/cli.mjs +2306 -545
- package/dist/cli.mjs.map +1 -1
- package/package.json +4 -4
package/dist/cli.mjs
CHANGED
|
@@ -1680,6 +1680,7 @@ __export(api_exports, {
|
|
|
1680
1680
|
TooManyRequestsError: () => TooManyRequestsError,
|
|
1681
1681
|
TruncationWarningEvent: () => TruncationWarningEvent,
|
|
1682
1682
|
UnauthorizedError: () => UnauthorizedError,
|
|
1683
|
+
UserExistsResponse: () => UserExistsResponse,
|
|
1683
1684
|
UserRequest: () => UserRequest,
|
|
1684
1685
|
UserResponse: () => UserResponse,
|
|
1685
1686
|
auth: () => auth_exports,
|
|
@@ -2356,6 +2357,14 @@ var TruncationWarningEvent;
|
|
|
2356
2357
|
ContextWindowBudget: "context_window_budget"
|
|
2357
2358
|
};
|
|
2358
2359
|
})(TruncationWarningEvent || (TruncationWarningEvent = {}));
|
|
2360
|
+
var UserExistsResponse;
|
|
2361
|
+
((UserExistsResponse2) => {
|
|
2362
|
+
UserExistsResponse2.Status = {
|
|
2363
|
+
Active: "ACTIVE",
|
|
2364
|
+
Suspended: "SUSPENDED",
|
|
2365
|
+
Pending: "PENDING"
|
|
2366
|
+
};
|
|
2367
|
+
})(UserExistsResponse || (UserExistsResponse = {}));
|
|
2359
2368
|
var UserRequest;
|
|
2360
2369
|
((UserRequest2) => {
|
|
2361
2370
|
UserRequest2.Status = {
|
|
@@ -4099,7 +4108,7 @@ var AuthClient = class {
|
|
|
4099
4108
|
return handleNonStatusCodeError(_response.error, _response.rawResponse, "GET", "/v1/admin/keys/scoped");
|
|
4100
4109
|
}
|
|
4101
4110
|
/**
|
|
4102
|
-
* Creates a scoped API key (an `ssk_*` secret) that inherits its permissions from an existing access profile in your account. The call is idempotent on the combination of tenant, context, user, and key name: re-issuing the same request returns the existing key WITHOUT re-disclosing its raw secret. The raw key is returned ONLY in this response — store it securely, as it cannot be retrieved again. Requires the `keys:c` scope.
|
|
4111
|
+
* Creates a scoped API key (an `ssk_*` secret) that inherits its permissions from an existing access profile in your account. The call is idempotent on the combination of tenant, context, user, and key name: re-issuing the same request returns the existing key WITHOUT re-disclosing its raw secret. The raw key is returned ONLY in this response — store it securely, as it cannot be retrieved again. Requires the `keys:c` scope. If you use a scoped credential, `keys:c` alone is not sufficient: because the minted key is durably bound to the profile you name, the profile's effective scopes may not exceed your own, and you may only mint against a profile whose `identityOverrides` values your own identity holds. A root API key (`sk_`) is exempt from both bounds.
|
|
4103
4112
|
*
|
|
4104
4113
|
* @param {Vectros.CreateScopedKeyRequest} request
|
|
4105
4114
|
* @param {AuthClient.RequestOptions} requestOptions - Request-specific configuration.
|
|
@@ -4431,7 +4440,7 @@ var AuthClient = class {
|
|
|
4431
4440
|
);
|
|
4432
4441
|
}
|
|
4433
4442
|
/**
|
|
4434
|
-
* Creates a new access profile under the given app context. This call is idempotent by `principalId`: if a profile with the same `principalId` already exists, the existing profile is returned (with status 200) instead of creating a duplicate. The response's `created` field (and the HTTP status — 201 when created, 200 when an existing profile was returned) tells the two apart. To overwrite an existing profile's `scopes`/`roleId`, `identityOverrides`, and `status` instead of returning it unchanged, set `?upsert=true` (this also requires the `profiles:u` scope, and applies the same `identityOverrides` bounds the update endpoint documents — a scoped credential may not repoint or clear an identity value it does not itself hold). You must provide exactly one of `scopes` (an inline list of scopes) or `roleId` (a reference to a role); supplying both, or neither, is rejected. `identityOverrides` is keyed by ownership namespace in `scope:<namespace>` form — `scope:org` and `scope:client` for the reserved namespaces, or any namespace you have registered — and may name at most two; any other key (including the account identifier or `userId`) is rejected. If you use a scoped credential, the profile's effective scopes may not exceed your own; a root API key (`sk_`) is exempt. Requires the `profiles:c` scope.
|
|
4443
|
+
* Creates a new access profile under the given app context. This call is idempotent by `principalId`: if a profile with the same `principalId` already exists, the existing profile is returned (with status 200) instead of creating a duplicate. The response's `created` field (and the HTTP status — 201 when created, 200 when an existing profile was returned) tells the two apart. To overwrite an existing profile's `scopes`/`roleId`, `identityOverrides`, and `status` instead of returning it unchanged, set `?upsert=true` (this also requires the `profiles:u` scope, and applies the same `identityOverrides` bounds the update endpoint documents — a scoped credential may not repoint or clear an identity value it does not itself hold). The `principalId` must name a principal that already exists: a `usr_` principal must be a live user in your tenant, so create the user before granting it a profile. A `usr_` id that names no such user is rejected, and no profile is created. `key_` principals are not checked this way. You must provide exactly one of `scopes` (an inline list of scopes) or `roleId` (a reference to a role); supplying both, or neither, is rejected. `identityOverrides` is keyed by ownership namespace in `scope:<namespace>` form — `scope:org` and `scope:client` for the reserved namespaces, or any namespace you have registered — and may name at most two; any other key (including the account identifier or `userId`) is rejected. If you use a scoped credential, the profile's effective scopes may not exceed your own; a root API key (`sk_`) is exempt. Requires the `profiles:c` scope.
|
|
4435
4444
|
*
|
|
4436
4445
|
* @param {Vectros.CreateAccessProfileRequest} request
|
|
4437
4446
|
* @param {AuthClient.RequestOptions} requestOptions - Request-specific configuration.
|
|
@@ -4849,11 +4858,12 @@ var AuthClient = class {
|
|
|
4849
4858
|
);
|
|
4850
4859
|
}
|
|
4851
4860
|
/**
|
|
4852
|
-
* Updates an access profile. This is a partial update: any field you omit (or send as null) keeps its existing value. A profile must reference either inline `scopes` or a `roleId`, never both — so setting `scopes` clears any `roleId`, and setting `roleId` clears any inline `scopes`. The `contextId` and `principalId` are immutable. Status changes (for example active to suspended) take effect within about five minutes. If you use a scoped credential, the profile's effective scopes may not exceed your own, and its `identityOverrides` are bounded twice: you may only set a value your own identity holds, and you may only change or clear a value the profile already holds if that value is yours as well. Repointing or clearing another principal's established identity therefore returns 403. A root API key (`sk_`) is exempt. Requires the `profiles:u` scope.
|
|
4861
|
+
* Updates an access profile. This is a partial update: any field you omit (or send as null) keeps its existing value. A profile must reference either inline `scopes` or a `roleId`, never both — so setting `scopes` clears any `roleId`, and setting `roleId` clears any inline `scopes`. The `contextId` and `principalId` are immutable. Status changes (for example active to suspended) take effect within about five minutes. If you use a scoped credential, the profile's effective scopes may not exceed your own, and its `identityOverrides` are bounded twice: you may only set a value your own identity holds, and you may only change or clear a value the profile already holds if that value is yours as well. Repointing or clearing another principal's established identity therefore returns 403. A root API key (`sk_`) is exempt. If you set `roleId`, it must reference a role that already exists in this context. Requires the `profiles:u` scope.
|
|
4853
4862
|
*
|
|
4854
4863
|
* @param {Vectros.UpdateAccessProfileRequest} request
|
|
4855
4864
|
* @param {AuthClient.RequestOptions} requestOptions - Request-specific configuration.
|
|
4856
4865
|
*
|
|
4866
|
+
* @throws {@link Vectros.BadRequestError}
|
|
4857
4867
|
* @throws {@link Vectros.ForbiddenError}
|
|
4858
4868
|
* @throws {@link Vectros.NotFoundError}
|
|
4859
4869
|
* @throws {@link Vectros.TooManyRequestsError}
|
|
@@ -4900,6 +4910,8 @@ var AuthClient = class {
|
|
|
4900
4910
|
}
|
|
4901
4911
|
if (_response.error.reason === "status-code") {
|
|
4902
4912
|
switch (_response.error.statusCode) {
|
|
4913
|
+
case 400:
|
|
4914
|
+
throw new BadRequestError(_response.error.body, _response.rawResponse);
|
|
4903
4915
|
case 403:
|
|
4904
4916
|
throw new ForbiddenError(_response.error.body, _response.rawResponse);
|
|
4905
4917
|
case 404:
|
|
@@ -5529,7 +5541,7 @@ var AuthClient = class {
|
|
|
5529
5541
|
);
|
|
5530
5542
|
}
|
|
5531
5543
|
/**
|
|
5532
|
-
* Returns full usage detail for the requested calendar month, broken down by category (search, documents, and records) with per-category credit estimates and a split between your live and test environments. Defaults to the current month when `year` and `month` are omitted. Requires the `billing:r` scope on scoped tokens; API keys always have access.
|
|
5544
|
+
* Returns full usage detail for the requested calendar month, broken down by category (search, documents, and records) with per-category credit estimates and a split between your live and test environments. Defaults to the current month when `year` and `month` are omitted. Requires the `billing:r` scope on scoped tokens; API keys always have access. **A token confined to a single app context sees only that context's usage**: totals, the environment split, and the `contexts` breakdown narrow to it, and the environment your context is not bound to is omitted (`null`), not zeroed. Only a token with cross-context reach sees your full account-wide totals. Two exceptions to the narrowing, since they have no per-context breakdown to narrow to: `reads.calls.used`/`reads.dataOut.bytes` (metered per account, not per context) read as `0` for a confined token rather than a narrowed figure — the corresponding overage-credit charge fields narrow correctly; and `credits.limit` stays your whole plan's ceiling, so `credits.remaining` may overstate the account's true remaining room.
|
|
5533
5545
|
*
|
|
5534
5546
|
* @param {Vectros.GetUsageRequest} request
|
|
5535
5547
|
* @param {AuthClient.RequestOptions} requestOptions - Request-specific configuration.
|
|
@@ -5591,6 +5603,257 @@ var AuthClient = class {
|
|
|
5591
5603
|
}
|
|
5592
5604
|
return handleNonStatusCodeError(_response.error, _response.rawResponse, "GET", "/v1/usage");
|
|
5593
5605
|
}
|
|
5606
|
+
/**
|
|
5607
|
+
* Retrieves a single registered issuer by issuerId. Requires a root API key or the bootstrap's provisioning capability. A credential confined to one app context sees only an issuer registered in that context; naming one registered in another context returns 404, identically to a nonexistent issuerId. A root API key sees every context.
|
|
5608
|
+
*
|
|
5609
|
+
* @param {Vectros.GetIssuerRequest} request
|
|
5610
|
+
* @param {AuthClient.RequestOptions} requestOptions - Request-specific configuration.
|
|
5611
|
+
*
|
|
5612
|
+
* @throws {@link Vectros.ForbiddenError}
|
|
5613
|
+
* @throws {@link Vectros.NotFoundError}
|
|
5614
|
+
*
|
|
5615
|
+
* @example
|
|
5616
|
+
* await client.auth.getIssuer({
|
|
5617
|
+
* issuerId: "auth0-prod"
|
|
5618
|
+
* })
|
|
5619
|
+
*/
|
|
5620
|
+
getIssuer(request, requestOptions) {
|
|
5621
|
+
return HttpResponsePromise.fromPromise(this.__getIssuer(request, requestOptions));
|
|
5622
|
+
}
|
|
5623
|
+
async __getIssuer(request, requestOptions) {
|
|
5624
|
+
const { issuerId } = request;
|
|
5625
|
+
const _authRequest = await this._options.authProvider.getAuthRequest();
|
|
5626
|
+
const _headers = mergeHeaders(
|
|
5627
|
+
_authRequest.headers,
|
|
5628
|
+
this._options?.headers,
|
|
5629
|
+
requestOptions?.headers
|
|
5630
|
+
);
|
|
5631
|
+
const _response = await fetcher({
|
|
5632
|
+
url: url_exports.join(
|
|
5633
|
+
await Supplier.get(this._options.baseUrl) ?? await Supplier.get(this._options.environment),
|
|
5634
|
+
`v1/auth/issuers/${url_exports.encodePathParam(issuerId)}`
|
|
5635
|
+
),
|
|
5636
|
+
method: "GET",
|
|
5637
|
+
headers: _headers,
|
|
5638
|
+
queryString: url_exports.queryBuilder().mergeAdditional(requestOptions?.queryParams).build(),
|
|
5639
|
+
timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1e3,
|
|
5640
|
+
maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries,
|
|
5641
|
+
abortSignal: requestOptions?.abortSignal,
|
|
5642
|
+
fetchFn: this._options?.fetch,
|
|
5643
|
+
logging: this._options.logging
|
|
5644
|
+
});
|
|
5645
|
+
if (_response.ok) {
|
|
5646
|
+
return { data: _response.body, rawResponse: _response.rawResponse };
|
|
5647
|
+
}
|
|
5648
|
+
if (_response.error.reason === "status-code") {
|
|
5649
|
+
switch (_response.error.statusCode) {
|
|
5650
|
+
case 403:
|
|
5651
|
+
throw new ForbiddenError(_response.error.body, _response.rawResponse);
|
|
5652
|
+
case 404:
|
|
5653
|
+
throw new NotFoundError(_response.error.body, _response.rawResponse);
|
|
5654
|
+
default:
|
|
5655
|
+
throw new VectrosError({
|
|
5656
|
+
statusCode: _response.error.statusCode,
|
|
5657
|
+
body: _response.error.body,
|
|
5658
|
+
rawResponse: _response.rawResponse
|
|
5659
|
+
});
|
|
5660
|
+
}
|
|
5661
|
+
}
|
|
5662
|
+
return handleNonStatusCodeError(_response.error, _response.rawResponse, "GET", "/v1/auth/issuers/{issuerId}");
|
|
5663
|
+
}
|
|
5664
|
+
/**
|
|
5665
|
+
* Deregisters a trusted third-party IdP issuer. Requires a root API key or the bootstrap's provisioning capability. A credential confined to one app context may only deregister an issuer registered in that context; naming one registered in another context returns 404, identically to a nonexistent issuerId. A root API key may deregister any issuer. Refused if any user account was ever created or matched via this issuer (by a prior self-signup or accepted invite, through `POST /v1/auth/token/exchange`) — that access cannot be silently orphaned. Deactivate the affected users first if you intend to cut off their access, or register a replacement issuer before removing this one. An issuer that has never been used for an exchange (no bound users yet) can always be deregistered.
|
|
5666
|
+
*
|
|
5667
|
+
* @param {Vectros.DeleteIssuerRequest} request
|
|
5668
|
+
* @param {AuthClient.RequestOptions} requestOptions - Request-specific configuration.
|
|
5669
|
+
*
|
|
5670
|
+
* @throws {@link Vectros.ForbiddenError}
|
|
5671
|
+
* @throws {@link Vectros.NotFoundError}
|
|
5672
|
+
* @throws {@link Vectros.ConflictError}
|
|
5673
|
+
* @throws {@link Vectros.TooManyRequestsError}
|
|
5674
|
+
*
|
|
5675
|
+
* @example
|
|
5676
|
+
* await client.auth.deleteIssuer({
|
|
5677
|
+
* issuerId: "auth0-prod"
|
|
5678
|
+
* })
|
|
5679
|
+
*/
|
|
5680
|
+
deleteIssuer(request, requestOptions) {
|
|
5681
|
+
return HttpResponsePromise.fromPromise(this.__deleteIssuer(request, requestOptions));
|
|
5682
|
+
}
|
|
5683
|
+
async __deleteIssuer(request, requestOptions) {
|
|
5684
|
+
const { issuerId } = request;
|
|
5685
|
+
const _authRequest = await this._options.authProvider.getAuthRequest();
|
|
5686
|
+
const _headers = mergeHeaders(
|
|
5687
|
+
_authRequest.headers,
|
|
5688
|
+
this._options?.headers,
|
|
5689
|
+
requestOptions?.headers
|
|
5690
|
+
);
|
|
5691
|
+
const _response = await fetcher({
|
|
5692
|
+
url: url_exports.join(
|
|
5693
|
+
await Supplier.get(this._options.baseUrl) ?? await Supplier.get(this._options.environment),
|
|
5694
|
+
`v1/auth/issuers/${url_exports.encodePathParam(issuerId)}`
|
|
5695
|
+
),
|
|
5696
|
+
method: "DELETE",
|
|
5697
|
+
headers: _headers,
|
|
5698
|
+
queryString: url_exports.queryBuilder().mergeAdditional(requestOptions?.queryParams).build(),
|
|
5699
|
+
timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1e3,
|
|
5700
|
+
maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries,
|
|
5701
|
+
abortSignal: requestOptions?.abortSignal,
|
|
5702
|
+
fetchFn: this._options?.fetch,
|
|
5703
|
+
logging: this._options.logging
|
|
5704
|
+
});
|
|
5705
|
+
if (_response.ok) {
|
|
5706
|
+
return { data: void 0, rawResponse: _response.rawResponse };
|
|
5707
|
+
}
|
|
5708
|
+
if (_response.error.reason === "status-code") {
|
|
5709
|
+
switch (_response.error.statusCode) {
|
|
5710
|
+
case 403:
|
|
5711
|
+
throw new ForbiddenError(_response.error.body, _response.rawResponse);
|
|
5712
|
+
case 404:
|
|
5713
|
+
throw new NotFoundError(_response.error.body, _response.rawResponse);
|
|
5714
|
+
case 409:
|
|
5715
|
+
throw new ConflictError(_response.error.body, _response.rawResponse);
|
|
5716
|
+
case 429:
|
|
5717
|
+
throw new TooManyRequestsError(_response.error.body, _response.rawResponse);
|
|
5718
|
+
default:
|
|
5719
|
+
throw new VectrosError({
|
|
5720
|
+
statusCode: _response.error.statusCode,
|
|
5721
|
+
body: _response.error.body,
|
|
5722
|
+
rawResponse: _response.rawResponse
|
|
5723
|
+
});
|
|
5724
|
+
}
|
|
5725
|
+
}
|
|
5726
|
+
return handleNonStatusCodeError(
|
|
5727
|
+
_response.error,
|
|
5728
|
+
_response.rawResponse,
|
|
5729
|
+
"DELETE",
|
|
5730
|
+
"/v1/auth/issuers/{issuerId}"
|
|
5731
|
+
);
|
|
5732
|
+
}
|
|
5733
|
+
/**
|
|
5734
|
+
* Returns the issuers registered in your tenant. Requires a root API key or the bootstrap's provisioning capability. A credential confined to one app context sees only the issuers registered in that context; a root API key sees every context. Returns a `{data, nextCursor}` envelope.
|
|
5735
|
+
*
|
|
5736
|
+
* @param {Vectros.ListIssuersRequest} request
|
|
5737
|
+
* @param {AuthClient.RequestOptions} requestOptions - Request-specific configuration.
|
|
5738
|
+
*
|
|
5739
|
+
* @throws {@link Vectros.ForbiddenError}
|
|
5740
|
+
*
|
|
5741
|
+
* @example
|
|
5742
|
+
* await client.auth.listIssuers()
|
|
5743
|
+
*/
|
|
5744
|
+
listIssuers(request = {}, requestOptions) {
|
|
5745
|
+
return HttpResponsePromise.fromPromise(this.__listIssuers(request, requestOptions));
|
|
5746
|
+
}
|
|
5747
|
+
async __listIssuers(request = {}, requestOptions) {
|
|
5748
|
+
const { startFrom, limit } = request;
|
|
5749
|
+
const _queryParams = {
|
|
5750
|
+
startFrom,
|
|
5751
|
+
limit
|
|
5752
|
+
};
|
|
5753
|
+
const _authRequest = await this._options.authProvider.getAuthRequest();
|
|
5754
|
+
const _headers = mergeHeaders(
|
|
5755
|
+
_authRequest.headers,
|
|
5756
|
+
this._options?.headers,
|
|
5757
|
+
requestOptions?.headers
|
|
5758
|
+
);
|
|
5759
|
+
const _response = await fetcher({
|
|
5760
|
+
url: url_exports.join(
|
|
5761
|
+
await Supplier.get(this._options.baseUrl) ?? await Supplier.get(this._options.environment),
|
|
5762
|
+
"v1/auth/issuers"
|
|
5763
|
+
),
|
|
5764
|
+
method: "GET",
|
|
5765
|
+
headers: _headers,
|
|
5766
|
+
queryString: url_exports.queryBuilder().addMany(_queryParams).mergeAdditional(requestOptions?.queryParams).build(),
|
|
5767
|
+
timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1e3,
|
|
5768
|
+
maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries,
|
|
5769
|
+
abortSignal: requestOptions?.abortSignal,
|
|
5770
|
+
fetchFn: this._options?.fetch,
|
|
5771
|
+
logging: this._options.logging
|
|
5772
|
+
});
|
|
5773
|
+
if (_response.ok) {
|
|
5774
|
+
return { data: _response.body, rawResponse: _response.rawResponse };
|
|
5775
|
+
}
|
|
5776
|
+
if (_response.error.reason === "status-code") {
|
|
5777
|
+
switch (_response.error.statusCode) {
|
|
5778
|
+
case 403:
|
|
5779
|
+
throw new ForbiddenError(_response.error.body, _response.rawResponse);
|
|
5780
|
+
default:
|
|
5781
|
+
throw new VectrosError({
|
|
5782
|
+
statusCode: _response.error.statusCode,
|
|
5783
|
+
body: _response.error.body,
|
|
5784
|
+
rawResponse: _response.rawResponse
|
|
5785
|
+
});
|
|
5786
|
+
}
|
|
5787
|
+
}
|
|
5788
|
+
return handleNonStatusCodeError(_response.error, _response.rawResponse, "GET", "/v1/auth/issuers");
|
|
5789
|
+
}
|
|
5790
|
+
/**
|
|
5791
|
+
* Registers a trusted third-party IdP issuer that BYO-IdP token exchange (`POST /v1/auth/token/exchange`) may accept a `subject_token` from. Requires a root API key or the CLI bootstrap's provisioning capability — never an ordinary partner-grantable scope. Idempotent by `issuerId` within your tenant; the `(issuer, audience)` pair must not already be registered by a different issuerId/tenant. An app context may have at most one active issuer — deregister the existing one first if you need to replace it. One issuer MAY serve several contexts today, each via its own registration row with a distinct `audience`.
|
|
5792
|
+
*
|
|
5793
|
+
* @param {Vectros.IssuerRequest} request
|
|
5794
|
+
* @param {AuthClient.RequestOptions} requestOptions - Request-specific configuration.
|
|
5795
|
+
*
|
|
5796
|
+
* @throws {@link Vectros.BadRequestError}
|
|
5797
|
+
* @throws {@link Vectros.ForbiddenError}
|
|
5798
|
+
* @throws {@link Vectros.TooManyRequestsError}
|
|
5799
|
+
*
|
|
5800
|
+
* @example
|
|
5801
|
+
* await client.auth.registerIssuer({
|
|
5802
|
+
* issuerId: "auth0-prod",
|
|
5803
|
+
* issuer: "https://your-tenant.us.auth0.com/",
|
|
5804
|
+
* jwksUri: "https://your-tenant.us.auth0.com/.well-known/jwks.json",
|
|
5805
|
+
* audience: "https://api.your-app.example.com",
|
|
5806
|
+
* contextId: "casework"
|
|
5807
|
+
* })
|
|
5808
|
+
*/
|
|
5809
|
+
registerIssuer(request, requestOptions) {
|
|
5810
|
+
return HttpResponsePromise.fromPromise(this.__registerIssuer(request, requestOptions));
|
|
5811
|
+
}
|
|
5812
|
+
async __registerIssuer(request, requestOptions) {
|
|
5813
|
+
const _authRequest = await this._options.authProvider.getAuthRequest();
|
|
5814
|
+
const _headers = mergeHeaders(
|
|
5815
|
+
_authRequest.headers,
|
|
5816
|
+
this._options?.headers,
|
|
5817
|
+
requestOptions?.headers
|
|
5818
|
+
);
|
|
5819
|
+
const _response = await fetcher({
|
|
5820
|
+
url: url_exports.join(
|
|
5821
|
+
await Supplier.get(this._options.baseUrl) ?? await Supplier.get(this._options.environment),
|
|
5822
|
+
"v1/auth/issuers"
|
|
5823
|
+
),
|
|
5824
|
+
method: "POST",
|
|
5825
|
+
headers: _headers,
|
|
5826
|
+
contentType: "application/json",
|
|
5827
|
+
queryString: url_exports.queryBuilder().mergeAdditional(requestOptions?.queryParams).build(),
|
|
5828
|
+
requestType: "json",
|
|
5829
|
+
body: request,
|
|
5830
|
+
timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1e3,
|
|
5831
|
+
maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries,
|
|
5832
|
+
abortSignal: requestOptions?.abortSignal,
|
|
5833
|
+
fetchFn: this._options?.fetch,
|
|
5834
|
+
logging: this._options.logging
|
|
5835
|
+
});
|
|
5836
|
+
if (_response.ok) {
|
|
5837
|
+
return { data: _response.body, rawResponse: _response.rawResponse };
|
|
5838
|
+
}
|
|
5839
|
+
if (_response.error.reason === "status-code") {
|
|
5840
|
+
switch (_response.error.statusCode) {
|
|
5841
|
+
case 400:
|
|
5842
|
+
throw new BadRequestError(_response.error.body, _response.rawResponse);
|
|
5843
|
+
case 403:
|
|
5844
|
+
throw new ForbiddenError(_response.error.body, _response.rawResponse);
|
|
5845
|
+
case 429:
|
|
5846
|
+
throw new TooManyRequestsError(_response.error.body, _response.rawResponse);
|
|
5847
|
+
default:
|
|
5848
|
+
throw new VectrosError({
|
|
5849
|
+
statusCode: _response.error.statusCode,
|
|
5850
|
+
body: _response.error.body,
|
|
5851
|
+
rawResponse: _response.rawResponse
|
|
5852
|
+
});
|
|
5853
|
+
}
|
|
5854
|
+
}
|
|
5855
|
+
return handleNonStatusCodeError(_response.error, _response.rawResponse, "POST", "/v1/auth/issuers");
|
|
5856
|
+
}
|
|
5594
5857
|
/**
|
|
5595
5858
|
* Returns the identity bound to your credential — your account, principal type, key id, and scope details — so you can confirm who you are authenticated as and that the credential is valid. MCP clients use this to render "signed in as ..." in a chat UI without a separate identity endpoint.
|
|
5596
5859
|
*
|
|
@@ -5636,7 +5899,7 @@ var AuthClient = class {
|
|
|
5636
5899
|
return handleNonStatusCodeError(_response.error, _response.rawResponse, "GET", "/v1/ping");
|
|
5637
5900
|
}
|
|
5638
5901
|
/**
|
|
5639
|
-
* Returns the access profiles for the given principal across
|
|
5902
|
+
* Returns the access profiles for the given principal. Looking up your OWN principal — or holding the `context-directory-read` capability — returns the profiles across ALL of your contexts, letting you answer questions like "which apps does this user have access to?". A context-bound credential looking up a DIFFERENT principal instead sees only that principal's profile in your credential's own context (at most one result), never across contexts it has no authority over. Results are always confined to your account. Requires the `profiles:r` scope.
|
|
5640
5903
|
*
|
|
5641
5904
|
* @param {Vectros.ListProfilesForPrincipalRequest} request
|
|
5642
5905
|
* @param {AuthClient.RequestOptions} requestOptions - Request-specific configuration.
|
|
@@ -5914,40 +6177,34 @@ var AuthClient = class {
|
|
|
5914
6177
|
}
|
|
5915
6178
|
return handleNonStatusCodeError(_response.error, _response.rawResponse, "POST", "/v1/users/invite/resend");
|
|
5916
6179
|
}
|
|
5917
|
-
};
|
|
5918
|
-
var ComplianceClient = class {
|
|
5919
|
-
constructor(options) {
|
|
5920
|
-
this._options = normalizeClientOptionsWithAuth(options);
|
|
5921
|
-
}
|
|
5922
6180
|
/**
|
|
5923
|
-
*
|
|
6181
|
+
* RFC 8693 OAuth 2.0 Token Exchange. Trades a JWT issued by a third-party identity provider you've registered (`POST /v1/auth/issuers`) for a Vectros `st_*` scoped bearer token — no Vectros credential required to call this endpoint. The exchanged token's scope is resolved entirely server-side from the matched user's access profile; this endpoint accepts no caller-supplied scope, resource, or audience parameter (RFC 8693 §2.1's `resource`/`audience`/`scope` are not used in v1 — the registered `(issuer, audience)` pair alone pins the target tenant and app context). On a first-time login (no existing Vectros identity for this subject), two opt-in binding paths exist: `invite_token` (a `PENDING` sub-user invitation), and — if the registration declares one or more self-signup policies — `signup_type` (a brand-new user is created and bound to the policy's configured role). If `invite_token` is present at all, it is the ONLY path tried — a failed invite never falls through to self-signup. Neither field is required for a subject with an existing identity. If your issuer is registered against more than one app context (each via its own audience), `context_id` selects which one to target; omit it when your token's `aud` claim matches only one registered context — the common case, unaffected by this field. Uses the OAuth-standard error envelope (`{"error":..., "error_description":...}`, RFC 6749 §5.2), NOT this API's usual `{"message":...}` shape — its client is generic OAuth tooling, not the Vectros SDK.
|
|
5924
6182
|
*
|
|
5925
|
-
* @param {Vectros.
|
|
5926
|
-
* @param {
|
|
6183
|
+
* @param {Vectros.TokenExchangeRequest} request
|
|
6184
|
+
* @param {AuthClient.RequestOptions} requestOptions - Request-specific configuration.
|
|
5927
6185
|
*
|
|
6186
|
+
* @throws {@link Vectros.BadRequestError}
|
|
6187
|
+
* @throws {@link Vectros.UnauthorizedError}
|
|
5928
6188
|
* @throws {@link Vectros.ForbiddenError}
|
|
6189
|
+
* @throws {@link Vectros.NotFoundError}
|
|
5929
6190
|
* @throws {@link Vectros.TooManyRequestsError}
|
|
5930
|
-
* @throws {@link Vectros.NotImplementedError}
|
|
5931
6191
|
*
|
|
5932
6192
|
* @example
|
|
5933
|
-
* await client.
|
|
5934
|
-
*
|
|
6193
|
+
* await client.auth.exchangeToken({
|
|
6194
|
+
* grant_type: "urn:ietf:params:oauth:grant-type:token-exchange",
|
|
6195
|
+
* subject_token: "subject_token",
|
|
6196
|
+
* subject_token_type: "urn:ietf:params:oauth:token-type:jwt"
|
|
5935
6197
|
* })
|
|
5936
6198
|
*/
|
|
5937
|
-
|
|
5938
|
-
return HttpResponsePromise.fromPromise(this.
|
|
6199
|
+
exchangeToken(request, requestOptions) {
|
|
6200
|
+
return HttpResponsePromise.fromPromise(this.__exchangeToken(request, requestOptions));
|
|
5939
6201
|
}
|
|
5940
|
-
async
|
|
5941
|
-
const
|
|
5942
|
-
const _headers = mergeHeaders(
|
|
5943
|
-
_authRequest.headers,
|
|
5944
|
-
this._options?.headers,
|
|
5945
|
-
requestOptions?.headers
|
|
5946
|
-
);
|
|
6202
|
+
async __exchangeToken(request, requestOptions) {
|
|
6203
|
+
const _headers = mergeHeaders(this._options?.headers, requestOptions?.headers);
|
|
5947
6204
|
const _response = await fetcher({
|
|
5948
6205
|
url: url_exports.join(
|
|
5949
6206
|
await Supplier.get(this._options.baseUrl) ?? await Supplier.get(this._options.environment),
|
|
5950
|
-
"v1/
|
|
6207
|
+
"v1/auth/token/exchange"
|
|
5951
6208
|
),
|
|
5952
6209
|
method: "POST",
|
|
5953
6210
|
headers: _headers,
|
|
@@ -5966,12 +6223,16 @@ var ComplianceClient = class {
|
|
|
5966
6223
|
}
|
|
5967
6224
|
if (_response.error.reason === "status-code") {
|
|
5968
6225
|
switch (_response.error.statusCode) {
|
|
6226
|
+
case 400:
|
|
6227
|
+
throw new BadRequestError(_response.error.body, _response.rawResponse);
|
|
6228
|
+
case 401:
|
|
6229
|
+
throw new UnauthorizedError(_response.error.body, _response.rawResponse);
|
|
5969
6230
|
case 403:
|
|
5970
6231
|
throw new ForbiddenError(_response.error.body, _response.rawResponse);
|
|
6232
|
+
case 404:
|
|
6233
|
+
throw new NotFoundError(_response.error.body, _response.rawResponse);
|
|
5971
6234
|
case 429:
|
|
5972
6235
|
throw new TooManyRequestsError(_response.error.body, _response.rawResponse);
|
|
5973
|
-
case 501:
|
|
5974
|
-
throw new NotImplementedError(_response.error.body, _response.rawResponse);
|
|
5975
6236
|
default:
|
|
5976
6237
|
throw new VectrosError({
|
|
5977
6238
|
statusCode: _response.error.statusCode,
|
|
@@ -5980,10 +6241,78 @@ var ComplianceClient = class {
|
|
|
5980
6241
|
});
|
|
5981
6242
|
}
|
|
5982
6243
|
}
|
|
5983
|
-
return handleNonStatusCodeError(_response.error, _response.rawResponse, "POST", "/v1/
|
|
6244
|
+
return handleNonStatusCodeError(_response.error, _response.rawResponse, "POST", "/v1/auth/token/exchange");
|
|
6245
|
+
}
|
|
6246
|
+
};
|
|
6247
|
+
var ComplianceClient = class {
|
|
6248
|
+
constructor(options) {
|
|
6249
|
+
this._options = normalizeClientOptionsWithAuth(options);
|
|
5984
6250
|
}
|
|
5985
6251
|
/**
|
|
5986
|
-
*
|
|
6252
|
+
* Submits a right-to-erasure request for a single end-subject (a user, or an identity entity in any namespace). Erasure removes exactly the data the subject solely owns across the declared contexts, plus the subject's identity and lookup rows. It never touches another account's data and never cascades into another subject's data. The request is asynchronous: it returns 202 with a `requestId`; poll `GET /v1/erasure-requests/{id}` until the job completes to obtain the completion certificate. Requires a root API key — a scoped credential is rejected with 403.
|
|
6253
|
+
*
|
|
6254
|
+
* @param {Vectros.ErasureRequest} request
|
|
6255
|
+
* @param {ComplianceClient.RequestOptions} requestOptions - Request-specific configuration.
|
|
6256
|
+
*
|
|
6257
|
+
* @throws {@link Vectros.ForbiddenError}
|
|
6258
|
+
* @throws {@link Vectros.TooManyRequestsError}
|
|
6259
|
+
* @throws {@link Vectros.NotImplementedError}
|
|
6260
|
+
*
|
|
6261
|
+
* @example
|
|
6262
|
+
* await client.compliance.createErasureRequest({
|
|
6263
|
+
* subjectType: "user"
|
|
6264
|
+
* })
|
|
6265
|
+
*/
|
|
6266
|
+
createErasureRequest(request, requestOptions) {
|
|
6267
|
+
return HttpResponsePromise.fromPromise(this.__createErasureRequest(request, requestOptions));
|
|
6268
|
+
}
|
|
6269
|
+
async __createErasureRequest(request, requestOptions) {
|
|
6270
|
+
const _authRequest = await this._options.authProvider.getAuthRequest();
|
|
6271
|
+
const _headers = mergeHeaders(
|
|
6272
|
+
_authRequest.headers,
|
|
6273
|
+
this._options?.headers,
|
|
6274
|
+
requestOptions?.headers
|
|
6275
|
+
);
|
|
6276
|
+
const _response = await fetcher({
|
|
6277
|
+
url: url_exports.join(
|
|
6278
|
+
await Supplier.get(this._options.baseUrl) ?? await Supplier.get(this._options.environment),
|
|
6279
|
+
"v1/erasure-requests"
|
|
6280
|
+
),
|
|
6281
|
+
method: "POST",
|
|
6282
|
+
headers: _headers,
|
|
6283
|
+
contentType: "application/json",
|
|
6284
|
+
queryString: url_exports.queryBuilder().mergeAdditional(requestOptions?.queryParams).build(),
|
|
6285
|
+
requestType: "json",
|
|
6286
|
+
body: request,
|
|
6287
|
+
timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1e3,
|
|
6288
|
+
maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries,
|
|
6289
|
+
abortSignal: requestOptions?.abortSignal,
|
|
6290
|
+
fetchFn: this._options?.fetch,
|
|
6291
|
+
logging: this._options.logging
|
|
6292
|
+
});
|
|
6293
|
+
if (_response.ok) {
|
|
6294
|
+
return { data: _response.body, rawResponse: _response.rawResponse };
|
|
6295
|
+
}
|
|
6296
|
+
if (_response.error.reason === "status-code") {
|
|
6297
|
+
switch (_response.error.statusCode) {
|
|
6298
|
+
case 403:
|
|
6299
|
+
throw new ForbiddenError(_response.error.body, _response.rawResponse);
|
|
6300
|
+
case 429:
|
|
6301
|
+
throw new TooManyRequestsError(_response.error.body, _response.rawResponse);
|
|
6302
|
+
case 501:
|
|
6303
|
+
throw new NotImplementedError(_response.error.body, _response.rawResponse);
|
|
6304
|
+
default:
|
|
6305
|
+
throw new VectrosError({
|
|
6306
|
+
statusCode: _response.error.statusCode,
|
|
6307
|
+
body: _response.error.body,
|
|
6308
|
+
rawResponse: _response.rawResponse
|
|
6309
|
+
});
|
|
6310
|
+
}
|
|
6311
|
+
}
|
|
6312
|
+
return handleNonStatusCodeError(_response.error, _response.rawResponse, "POST", "/v1/erasure-requests");
|
|
6313
|
+
}
|
|
6314
|
+
/**
|
|
6315
|
+
* Polls an erasure request by id. While the job is still running this returns its status only; once it completes, the response also includes the verifiable completion certificate (which contexts were swept, per-context deletion counts, and reports of dangling references and shared rows that were left intact). Requires a root API key.
|
|
5987
6316
|
*
|
|
5988
6317
|
* @param {Vectros.GetErasureRequestRequest} request
|
|
5989
6318
|
* @param {ComplianceClient.RequestOptions} requestOptions - Request-specific configuration.
|
|
@@ -7391,6 +7720,7 @@ var IdentityClient = class {
|
|
|
7391
7720
|
* @example
|
|
7392
7721
|
* await client.identity.listEntities({
|
|
7393
7722
|
* namespace: "team",
|
|
7723
|
+
* contextId: "myapp",
|
|
7394
7724
|
* startFrom: "b3BhcXVlLWN1cnNvci1mcm9tLXRoZS1wcmV2aW91cy1wYWdl"
|
|
7395
7725
|
* })
|
|
7396
7726
|
*/
|
|
@@ -7400,6 +7730,7 @@ var IdentityClient = class {
|
|
|
7400
7730
|
async __listEntities(request, requestOptions) {
|
|
7401
7731
|
const {
|
|
7402
7732
|
namespace,
|
|
7733
|
+
contextId,
|
|
7403
7734
|
userId,
|
|
7404
7735
|
externalId,
|
|
7405
7736
|
scope,
|
|
@@ -7414,6 +7745,7 @@ var IdentityClient = class {
|
|
|
7414
7745
|
limit
|
|
7415
7746
|
} = request;
|
|
7416
7747
|
const _queryParams = {
|
|
7748
|
+
contextId,
|
|
7417
7749
|
userId,
|
|
7418
7750
|
externalId,
|
|
7419
7751
|
scope,
|
|
@@ -7472,6 +7804,7 @@ var IdentityClient = class {
|
|
|
7472
7804
|
* @example
|
|
7473
7805
|
* await client.identity.createEntity({
|
|
7474
7806
|
* namespace: "team",
|
|
7807
|
+
* contextId: "myapp",
|
|
7475
7808
|
* body: {
|
|
7476
7809
|
* externalId: "team_eng_platform"
|
|
7477
7810
|
* }
|
|
@@ -7481,9 +7814,10 @@ var IdentityClient = class {
|
|
|
7481
7814
|
return HttpResponsePromise.fromPromise(this.__createEntity(request, requestOptions));
|
|
7482
7815
|
}
|
|
7483
7816
|
async __createEntity(request, requestOptions) {
|
|
7484
|
-
const { namespace, upsert, body: _body } = request;
|
|
7817
|
+
const { namespace, upsert, contextId, body: _body } = request;
|
|
7485
7818
|
const _queryParams = {
|
|
7486
|
-
upsert
|
|
7819
|
+
upsert,
|
|
7820
|
+
contextId
|
|
7487
7821
|
};
|
|
7488
7822
|
const _authRequest = await this._options.authProvider.getAuthRequest();
|
|
7489
7823
|
const _headers = mergeHeaders(
|
|
@@ -7540,14 +7874,18 @@ var IdentityClient = class {
|
|
|
7540
7874
|
* @example
|
|
7541
7875
|
* await client.identity.getEntity({
|
|
7542
7876
|
* namespace: "team",
|
|
7543
|
-
* id: "6ba7b810-9dad-11d1-80b4-00c04fd430c8"
|
|
7877
|
+
* id: "6ba7b810-9dad-11d1-80b4-00c04fd430c8",
|
|
7878
|
+
* contextId: "myapp"
|
|
7544
7879
|
* })
|
|
7545
7880
|
*/
|
|
7546
7881
|
getEntity(request, requestOptions) {
|
|
7547
7882
|
return HttpResponsePromise.fromPromise(this.__getEntity(request, requestOptions));
|
|
7548
7883
|
}
|
|
7549
7884
|
async __getEntity(request, requestOptions) {
|
|
7550
|
-
const { namespace, id } = request;
|
|
7885
|
+
const { namespace, id, contextId } = request;
|
|
7886
|
+
const _queryParams = {
|
|
7887
|
+
contextId
|
|
7888
|
+
};
|
|
7551
7889
|
const _authRequest = await this._options.authProvider.getAuthRequest();
|
|
7552
7890
|
const _headers = mergeHeaders(
|
|
7553
7891
|
_authRequest.headers,
|
|
@@ -7561,7 +7899,7 @@ var IdentityClient = class {
|
|
|
7561
7899
|
),
|
|
7562
7900
|
method: "GET",
|
|
7563
7901
|
headers: _headers,
|
|
7564
|
-
queryString: url_exports.queryBuilder().mergeAdditional(requestOptions?.queryParams).build(),
|
|
7902
|
+
queryString: url_exports.queryBuilder().addMany(_queryParams).mergeAdditional(requestOptions?.queryParams).build(),
|
|
7565
7903
|
timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1e3,
|
|
7566
7904
|
maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries,
|
|
7567
7905
|
abortSignal: requestOptions?.abortSignal,
|
|
@@ -7598,6 +7936,7 @@ var IdentityClient = class {
|
|
|
7598
7936
|
* await client.identity.updateEntity({
|
|
7599
7937
|
* namespace: "team",
|
|
7600
7938
|
* id: "id",
|
|
7939
|
+
* contextId: "myapp",
|
|
7601
7940
|
* body: {
|
|
7602
7941
|
* externalId: "team_eng_platform"
|
|
7603
7942
|
* }
|
|
@@ -7607,7 +7946,10 @@ var IdentityClient = class {
|
|
|
7607
7946
|
return HttpResponsePromise.fromPromise(this.__updateEntity(request, requestOptions));
|
|
7608
7947
|
}
|
|
7609
7948
|
async __updateEntity(request, requestOptions) {
|
|
7610
|
-
const { namespace, id, body: _body } = request;
|
|
7949
|
+
const { namespace, id, contextId, body: _body } = request;
|
|
7950
|
+
const _queryParams = {
|
|
7951
|
+
contextId
|
|
7952
|
+
};
|
|
7611
7953
|
const _authRequest = await this._options.authProvider.getAuthRequest();
|
|
7612
7954
|
const _headers = mergeHeaders(
|
|
7613
7955
|
_authRequest.headers,
|
|
@@ -7622,7 +7964,7 @@ var IdentityClient = class {
|
|
|
7622
7964
|
method: "PUT",
|
|
7623
7965
|
headers: _headers,
|
|
7624
7966
|
contentType: "application/json",
|
|
7625
|
-
queryString: url_exports.queryBuilder().mergeAdditional(requestOptions?.queryParams).build(),
|
|
7967
|
+
queryString: url_exports.queryBuilder().addMany(_queryParams).mergeAdditional(requestOptions?.queryParams).build(),
|
|
7626
7968
|
requestType: "json",
|
|
7627
7969
|
body: _body,
|
|
7628
7970
|
timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1e3,
|
|
@@ -7662,14 +8004,18 @@ var IdentityClient = class {
|
|
|
7662
8004
|
* @example
|
|
7663
8005
|
* await client.identity.deleteEntity({
|
|
7664
8006
|
* namespace: "team",
|
|
7665
|
-
* id: "id"
|
|
8007
|
+
* id: "id",
|
|
8008
|
+
* contextId: "myapp"
|
|
7666
8009
|
* })
|
|
7667
8010
|
*/
|
|
7668
8011
|
deleteEntity(request, requestOptions) {
|
|
7669
8012
|
return HttpResponsePromise.fromPromise(this.__deleteEntity(request, requestOptions));
|
|
7670
8013
|
}
|
|
7671
8014
|
async __deleteEntity(request, requestOptions) {
|
|
7672
|
-
const { namespace, id } = request;
|
|
8015
|
+
const { namespace, id, contextId } = request;
|
|
8016
|
+
const _queryParams = {
|
|
8017
|
+
contextId
|
|
8018
|
+
};
|
|
7673
8019
|
const _authRequest = await this._options.authProvider.getAuthRequest();
|
|
7674
8020
|
const _headers = mergeHeaders(
|
|
7675
8021
|
_authRequest.headers,
|
|
@@ -7683,7 +8029,7 @@ var IdentityClient = class {
|
|
|
7683
8029
|
),
|
|
7684
8030
|
method: "DELETE",
|
|
7685
8031
|
headers: _headers,
|
|
7686
|
-
queryString: url_exports.queryBuilder().mergeAdditional(requestOptions?.queryParams).build(),
|
|
8032
|
+
queryString: url_exports.queryBuilder().addMany(_queryParams).mergeAdditional(requestOptions?.queryParams).build(),
|
|
7687
8033
|
timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1e3,
|
|
7688
8034
|
maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries,
|
|
7689
8035
|
abortSignal: requestOptions?.abortSignal,
|
|
@@ -7726,6 +8072,7 @@ var IdentityClient = class {
|
|
|
7726
8072
|
* @example
|
|
7727
8073
|
* await client.identity.lookupEntities({
|
|
7728
8074
|
* namespace: "team",
|
|
8075
|
+
* contextId: "myapp",
|
|
7729
8076
|
* body: {
|
|
7730
8077
|
* type: "person_v1",
|
|
7731
8078
|
* field: "ssn"
|
|
@@ -7736,7 +8083,10 @@ var IdentityClient = class {
|
|
|
7736
8083
|
return HttpResponsePromise.fromPromise(this.__lookupEntities(request, requestOptions));
|
|
7737
8084
|
}
|
|
7738
8085
|
async __lookupEntities(request, requestOptions) {
|
|
7739
|
-
const { namespace, body: _body } = request;
|
|
8086
|
+
const { namespace, contextId, body: _body } = request;
|
|
8087
|
+
const _queryParams = {
|
|
8088
|
+
contextId
|
|
8089
|
+
};
|
|
7740
8090
|
const _authRequest = await this._options.authProvider.getAuthRequest();
|
|
7741
8091
|
const _headers = mergeHeaders(
|
|
7742
8092
|
_authRequest.headers,
|
|
@@ -7751,7 +8101,7 @@ var IdentityClient = class {
|
|
|
7751
8101
|
method: "POST",
|
|
7752
8102
|
headers: _headers,
|
|
7753
8103
|
contentType: "application/json",
|
|
7754
|
-
queryString: url_exports.queryBuilder().mergeAdditional(requestOptions?.queryParams).build(),
|
|
8104
|
+
queryString: url_exports.queryBuilder().addMany(_queryParams).mergeAdditional(requestOptions?.queryParams).build(),
|
|
7755
8105
|
requestType: "json",
|
|
7756
8106
|
body: _body,
|
|
7757
8107
|
timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1e3,
|
|
@@ -7795,15 +8145,17 @@ var IdentityClient = class {
|
|
|
7795
8145
|
* @example
|
|
7796
8146
|
* await client.identity.getEntityVersions({
|
|
7797
8147
|
* namespace: "team",
|
|
7798
|
-
* id: "id"
|
|
8148
|
+
* id: "id",
|
|
8149
|
+
* contextId: "myapp"
|
|
7799
8150
|
* })
|
|
7800
8151
|
*/
|
|
7801
8152
|
getEntityVersions(request, requestOptions) {
|
|
7802
8153
|
return HttpResponsePromise.fromPromise(this.__getEntityVersions(request, requestOptions));
|
|
7803
8154
|
}
|
|
7804
8155
|
async __getEntityVersions(request, requestOptions) {
|
|
7805
|
-
const { namespace, id, startFrom } = request;
|
|
8156
|
+
const { namespace, id, contextId, startFrom } = request;
|
|
7806
8157
|
const _queryParams = {
|
|
8158
|
+
contextId,
|
|
7807
8159
|
startFrom
|
|
7808
8160
|
};
|
|
7809
8161
|
const _authRequest = await this._options.authProvider.getAuthRequest();
|
|
@@ -7865,7 +8217,10 @@ var IdentityClient = class {
|
|
|
7865
8217
|
return HttpResponsePromise.fromPromise(this.__getNamespace(request, requestOptions));
|
|
7866
8218
|
}
|
|
7867
8219
|
async __getNamespace(request, requestOptions) {
|
|
7868
|
-
const { namespace } = request;
|
|
8220
|
+
const { namespace, contextId } = request;
|
|
8221
|
+
const _queryParams = {
|
|
8222
|
+
contextId
|
|
8223
|
+
};
|
|
7869
8224
|
const _authRequest = await this._options.authProvider.getAuthRequest();
|
|
7870
8225
|
const _headers = mergeHeaders(
|
|
7871
8226
|
_authRequest.headers,
|
|
@@ -7879,7 +8234,7 @@ var IdentityClient = class {
|
|
|
7879
8234
|
),
|
|
7880
8235
|
method: "GET",
|
|
7881
8236
|
headers: _headers,
|
|
7882
|
-
queryString: url_exports.queryBuilder().mergeAdditional(requestOptions?.queryParams).build(),
|
|
8237
|
+
queryString: url_exports.queryBuilder().addMany(_queryParams).mergeAdditional(requestOptions?.queryParams).build(),
|
|
7883
8238
|
timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1e3,
|
|
7884
8239
|
maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries,
|
|
7885
8240
|
abortSignal: requestOptions?.abortSignal,
|
|
@@ -7904,7 +8259,7 @@ var IdentityClient = class {
|
|
|
7904
8259
|
return handleNonStatusCodeError(_response.error, _response.rawResponse, "GET", "/v1/namespaces/{namespace}");
|
|
7905
8260
|
}
|
|
7906
8261
|
/**
|
|
7907
|
-
* Updates the mutable fields (`entityBacked`, `defaultSchemaId`, `specificityRank`) of a registered namespace. The namespace name
|
|
8262
|
+
* Updates the mutable fields (`entityBacked`, `defaultSchemaId`, `specificityRank`) of a registered namespace. The namespace name and its `contextId` (which row is selected) are both immutable. Requires a root API key. `org` and `client` are updatable like any other namespace — there is no reserved-built-in exception.
|
|
7908
8263
|
*
|
|
7909
8264
|
* @param {Vectros.UpdateNamespaceRequest} request
|
|
7910
8265
|
* @param {IdentityClient.RequestOptions} requestOptions - Request-specific configuration.
|
|
@@ -7926,7 +8281,10 @@ var IdentityClient = class {
|
|
|
7926
8281
|
return HttpResponsePromise.fromPromise(this.__updateNamespace(request, requestOptions));
|
|
7927
8282
|
}
|
|
7928
8283
|
async __updateNamespace(request, requestOptions) {
|
|
7929
|
-
const { namespace, body: _body } = request;
|
|
8284
|
+
const { namespace, contextId, body: _body } = request;
|
|
8285
|
+
const _queryParams = {
|
|
8286
|
+
contextId
|
|
8287
|
+
};
|
|
7930
8288
|
const _authRequest = await this._options.authProvider.getAuthRequest();
|
|
7931
8289
|
const _headers = mergeHeaders(
|
|
7932
8290
|
_authRequest.headers,
|
|
@@ -7941,7 +8299,7 @@ var IdentityClient = class {
|
|
|
7941
8299
|
method: "PUT",
|
|
7942
8300
|
headers: _headers,
|
|
7943
8301
|
contentType: "application/json",
|
|
7944
|
-
queryString: url_exports.queryBuilder().mergeAdditional(requestOptions?.queryParams).build(),
|
|
8302
|
+
queryString: url_exports.queryBuilder().addMany(_queryParams).mergeAdditional(requestOptions?.queryParams).build(),
|
|
7945
8303
|
requestType: "json",
|
|
7946
8304
|
body: _body,
|
|
7947
8305
|
timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1e3,
|
|
@@ -7972,7 +8330,7 @@ var IdentityClient = class {
|
|
|
7972
8330
|
return handleNonStatusCodeError(_response.error, _response.rawResponse, "PUT", "/v1/namespaces/{namespace}");
|
|
7973
8331
|
}
|
|
7974
8332
|
/**
|
|
7975
|
-
* Deletes a registered scope namespace. Requires a root API key.
|
|
8333
|
+
* Deletes a registered scope namespace. Requires a root API key. `org` and `client` are deletable like any other namespace — there is no reserved-built-in exception. A namespace that still has entities cannot be deleted (409) — delete its entities first; this keeps them reachable by the account-teardown and erasure sweeps.
|
|
7976
8334
|
*
|
|
7977
8335
|
* @param {Vectros.DeleteNamespaceRequest} request
|
|
7978
8336
|
* @param {IdentityClient.RequestOptions} requestOptions - Request-specific configuration.
|
|
@@ -7990,7 +8348,10 @@ var IdentityClient = class {
|
|
|
7990
8348
|
return HttpResponsePromise.fromPromise(this.__deleteNamespace(request, requestOptions));
|
|
7991
8349
|
}
|
|
7992
8350
|
async __deleteNamespace(request, requestOptions) {
|
|
7993
|
-
const { namespace } = request;
|
|
8351
|
+
const { namespace, contextId } = request;
|
|
8352
|
+
const _queryParams = {
|
|
8353
|
+
contextId
|
|
8354
|
+
};
|
|
7994
8355
|
const _authRequest = await this._options.authProvider.getAuthRequest();
|
|
7995
8356
|
const _headers = mergeHeaders(
|
|
7996
8357
|
_authRequest.headers,
|
|
@@ -8004,7 +8365,7 @@ var IdentityClient = class {
|
|
|
8004
8365
|
),
|
|
8005
8366
|
method: "DELETE",
|
|
8006
8367
|
headers: _headers,
|
|
8007
|
-
queryString: url_exports.queryBuilder().mergeAdditional(requestOptions?.queryParams).build(),
|
|
8368
|
+
queryString: url_exports.queryBuilder().addMany(_queryParams).mergeAdditional(requestOptions?.queryParams).build(),
|
|
8008
8369
|
timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1e3,
|
|
8009
8370
|
maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries,
|
|
8010
8371
|
abortSignal: requestOptions?.abortSignal,
|
|
@@ -8033,7 +8394,7 @@ var IdentityClient = class {
|
|
|
8033
8394
|
return handleNonStatusCodeError(_response.error, _response.rawResponse, "DELETE", "/v1/namespaces/{namespace}");
|
|
8034
8395
|
}
|
|
8035
8396
|
/**
|
|
8036
|
-
* Returns the scope namespaces
|
|
8397
|
+
* Returns the scope namespaces in your account, including `org` and `client`, which are provisioned for you when the account is created. Returns a `{data, nextCursor}` envelope.
|
|
8037
8398
|
*
|
|
8038
8399
|
* @param {Vectros.ListNamespacesRequest} request
|
|
8039
8400
|
* @param {IdentityClient.RequestOptions} requestOptions - Request-specific configuration.
|
|
@@ -8045,10 +8406,11 @@ var IdentityClient = class {
|
|
|
8045
8406
|
return HttpResponsePromise.fromPromise(this.__listNamespaces(request, requestOptions));
|
|
8046
8407
|
}
|
|
8047
8408
|
async __listNamespaces(request = {}, requestOptions) {
|
|
8048
|
-
const { startFrom, limit } = request;
|
|
8409
|
+
const { startFrom, limit, contextId } = request;
|
|
8049
8410
|
const _queryParams = {
|
|
8050
8411
|
startFrom,
|
|
8051
|
-
limit
|
|
8412
|
+
limit,
|
|
8413
|
+
contextId
|
|
8052
8414
|
};
|
|
8053
8415
|
const _authRequest = await this._options.authProvider.getAuthRequest();
|
|
8054
8416
|
const _headers = mergeHeaders(
|
|
@@ -8083,9 +8445,9 @@ var IdentityClient = class {
|
|
|
8083
8445
|
return handleNonStatusCodeError(_response.error, _response.rawResponse, "GET", "/v1/namespaces");
|
|
8084
8446
|
}
|
|
8085
8447
|
/**
|
|
8086
|
-
* Registers a new scope namespace and declares whether its values resolve to identity entities (`entityBacked`). Also requires `specificityRank`, an explicit, account-unique position in the specificity order used to break recordType schema-resolution ties. Requires a root API key
|
|
8448
|
+
* Registers a new scope namespace and declares whether its values resolve to identity entities (`entityBacked`). Also requires `specificityRank`, an explicit, account-unique position in the specificity order used to break recordType schema-resolution ties. Requires a root API key or the CLI bootstrap's provisioning capability — never an ordinary partner-grantable scope. `org` and `client` are provisioned when your account is created, so registering either returns the already-registered error.
|
|
8087
8449
|
*
|
|
8088
|
-
* @param {Vectros.
|
|
8450
|
+
* @param {Vectros.RegisterNamespaceRequest} request
|
|
8089
8451
|
* @param {IdentityClient.RequestOptions} requestOptions - Request-specific configuration.
|
|
8090
8452
|
*
|
|
8091
8453
|
* @throws {@link Vectros.BadRequestError}
|
|
@@ -8094,14 +8456,20 @@ var IdentityClient = class {
|
|
|
8094
8456
|
*
|
|
8095
8457
|
* @example
|
|
8096
8458
|
* await client.identity.registerNamespace({
|
|
8097
|
-
*
|
|
8098
|
-
*
|
|
8459
|
+
* body: {
|
|
8460
|
+
* namespace: "team",
|
|
8461
|
+
* specificityRank: 1500
|
|
8462
|
+
* }
|
|
8099
8463
|
* })
|
|
8100
8464
|
*/
|
|
8101
8465
|
registerNamespace(request, requestOptions) {
|
|
8102
8466
|
return HttpResponsePromise.fromPromise(this.__registerNamespace(request, requestOptions));
|
|
8103
8467
|
}
|
|
8104
8468
|
async __registerNamespace(request, requestOptions) {
|
|
8469
|
+
const { contextId, body: _body } = request;
|
|
8470
|
+
const _queryParams = {
|
|
8471
|
+
contextId
|
|
8472
|
+
};
|
|
8105
8473
|
const _authRequest = await this._options.authProvider.getAuthRequest();
|
|
8106
8474
|
const _headers = mergeHeaders(
|
|
8107
8475
|
_authRequest.headers,
|
|
@@ -8116,9 +8484,9 @@ var IdentityClient = class {
|
|
|
8116
8484
|
method: "POST",
|
|
8117
8485
|
headers: _headers,
|
|
8118
8486
|
contentType: "application/json",
|
|
8119
|
-
queryString: url_exports.queryBuilder().mergeAdditional(requestOptions?.queryParams).build(),
|
|
8487
|
+
queryString: url_exports.queryBuilder().addMany(_queryParams).mergeAdditional(requestOptions?.queryParams).build(),
|
|
8120
8488
|
requestType: "json",
|
|
8121
|
-
body:
|
|
8489
|
+
body: _body,
|
|
8122
8490
|
timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1e3,
|
|
8123
8491
|
maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries,
|
|
8124
8492
|
abortSignal: requestOptions?.abortSignal,
|
|
@@ -8403,12 +8771,13 @@ var IdentityClient = class {
|
|
|
8403
8771
|
return handleNonStatusCodeError(_response.error, _response.rawResponse, "PUT", "/v1/users/{id}");
|
|
8404
8772
|
}
|
|
8405
8773
|
/**
|
|
8406
|
-
* Permanently deletes a user identity. This cannot be undone. If the user is a pending invitation, the associated access profile created for that invitation is also removed. Requires the `users:d` scope.
|
|
8774
|
+
* Permanently deletes a user identity. This cannot be undone. If the user is a pending invitation, the associated access profile created for that invitation is also removed. Requires the `users:d` scope. Deleting your account's last OWNER is refused (409) — an account must always retain at least one owner.
|
|
8407
8775
|
*
|
|
8408
8776
|
* @param {Vectros.DeleteUserRequest} request
|
|
8409
8777
|
* @param {IdentityClient.RequestOptions} requestOptions - Request-specific configuration.
|
|
8410
8778
|
*
|
|
8411
8779
|
* @throws {@link Vectros.NotFoundError}
|
|
8780
|
+
* @throws {@link Vectros.ConflictError}
|
|
8412
8781
|
* @throws {@link Vectros.TooManyRequestsError}
|
|
8413
8782
|
*
|
|
8414
8783
|
* @example
|
|
@@ -8448,6 +8817,8 @@ var IdentityClient = class {
|
|
|
8448
8817
|
switch (_response.error.statusCode) {
|
|
8449
8818
|
case 404:
|
|
8450
8819
|
throw new NotFoundError(_response.error.body, _response.rawResponse);
|
|
8820
|
+
case 409:
|
|
8821
|
+
throw new ConflictError(_response.error.body, _response.rawResponse);
|
|
8451
8822
|
case 429:
|
|
8452
8823
|
throw new TooManyRequestsError(_response.error.body, _response.rawResponse);
|
|
8453
8824
|
default:
|
|
@@ -8521,6 +8892,69 @@ var IdentityClient = class {
|
|
|
8521
8892
|
}
|
|
8522
8893
|
return handleNonStatusCodeError(_response.error, _response.rawResponse, "POST", "/v1/users/lookup");
|
|
8523
8894
|
}
|
|
8895
|
+
/**
|
|
8896
|
+
* 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.
|
|
8897
|
+
*
|
|
8898
|
+
* @param {Vectros.UserExistsByEmailRequest} request
|
|
8899
|
+
* @param {IdentityClient.RequestOptions} requestOptions - Request-specific configuration.
|
|
8900
|
+
*
|
|
8901
|
+
* @throws {@link Vectros.BadRequestError}
|
|
8902
|
+
* @throws {@link Vectros.ForbiddenError}
|
|
8903
|
+
*
|
|
8904
|
+
* @example
|
|
8905
|
+
* await client.identity.userExistsByEmail({
|
|
8906
|
+
* email: "user@example.com",
|
|
8907
|
+
* contextId: "myapp"
|
|
8908
|
+
* })
|
|
8909
|
+
*/
|
|
8910
|
+
userExistsByEmail(request = {}, requestOptions) {
|
|
8911
|
+
return HttpResponsePromise.fromPromise(this.__userExistsByEmail(request, requestOptions));
|
|
8912
|
+
}
|
|
8913
|
+
async __userExistsByEmail(request = {}, requestOptions) {
|
|
8914
|
+
const { email, contextId } = request;
|
|
8915
|
+
const _queryParams = {
|
|
8916
|
+
email,
|
|
8917
|
+
contextId
|
|
8918
|
+
};
|
|
8919
|
+
const _authRequest = await this._options.authProvider.getAuthRequest();
|
|
8920
|
+
const _headers = mergeHeaders(
|
|
8921
|
+
_authRequest.headers,
|
|
8922
|
+
this._options?.headers,
|
|
8923
|
+
requestOptions?.headers
|
|
8924
|
+
);
|
|
8925
|
+
const _response = await fetcher({
|
|
8926
|
+
url: url_exports.join(
|
|
8927
|
+
await Supplier.get(this._options.baseUrl) ?? await Supplier.get(this._options.environment),
|
|
8928
|
+
"v1/users/exists-by-email"
|
|
8929
|
+
),
|
|
8930
|
+
method: "GET",
|
|
8931
|
+
headers: _headers,
|
|
8932
|
+
queryString: url_exports.queryBuilder().addMany(_queryParams).mergeAdditional(requestOptions?.queryParams).build(),
|
|
8933
|
+
timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1e3,
|
|
8934
|
+
maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries,
|
|
8935
|
+
abortSignal: requestOptions?.abortSignal,
|
|
8936
|
+
fetchFn: this._options?.fetch,
|
|
8937
|
+
logging: this._options.logging
|
|
8938
|
+
});
|
|
8939
|
+
if (_response.ok) {
|
|
8940
|
+
return { data: _response.body, rawResponse: _response.rawResponse };
|
|
8941
|
+
}
|
|
8942
|
+
if (_response.error.reason === "status-code") {
|
|
8943
|
+
switch (_response.error.statusCode) {
|
|
8944
|
+
case 400:
|
|
8945
|
+
throw new BadRequestError(_response.error.body, _response.rawResponse);
|
|
8946
|
+
case 403:
|
|
8947
|
+
throw new ForbiddenError(_response.error.body, _response.rawResponse);
|
|
8948
|
+
default:
|
|
8949
|
+
throw new VectrosError({
|
|
8950
|
+
statusCode: _response.error.statusCode,
|
|
8951
|
+
body: _response.error.body,
|
|
8952
|
+
rawResponse: _response.rawResponse
|
|
8953
|
+
});
|
|
8954
|
+
}
|
|
8955
|
+
}
|
|
8956
|
+
return handleNonStatusCodeError(_response.error, _response.rawResponse, "GET", "/v1/users/exists-by-email");
|
|
8957
|
+
}
|
|
8524
8958
|
/**
|
|
8525
8959
|
* Returns the audit trail of changes to a user, most recent first. Identity history is always recorded and always available. Sensitive field values are redacted in every historical version. Returns a page in the `{data, nextCursor}` envelope. Requires the `users:r` scope.
|
|
8526
8960
|
*
|
|
@@ -10238,10 +10672,11 @@ var logging;
|
|
|
10238
10672
|
logging2.ConsoleLogger = ConsoleLogger;
|
|
10239
10673
|
})(logging || (logging = {}));
|
|
10240
10674
|
|
|
10241
|
-
//
|
|
10675
|
+
// ../blueprints/dist/index.mjs
|
|
10242
10676
|
import { z } from "zod";
|
|
10243
10677
|
import { z as z2 } from "zod";
|
|
10244
10678
|
var CONTEXT_ID_RE = /^[a-z][a-z0-9-]{2,30}$/;
|
|
10679
|
+
var MAX_SCOPE_DIMENSIONS = 2;
|
|
10245
10680
|
var ValidationRulesSchema = z.object({
|
|
10246
10681
|
required: z.boolean().optional(),
|
|
10247
10682
|
minLength: z.number().int().optional(),
|
|
@@ -10436,11 +10871,14 @@ var BlueprintSchemaSchema = z.object({
|
|
|
10436
10871
|
// Schema-level ownership defaults, matching `SchemaRequest`: the principal
|
|
10437
10872
|
// `userId` plus `scopes` — namespaced parent edges as `<namespace>:<value>`
|
|
10438
10873
|
// (`org:...`, `client:...`, or a namespace you registered), at most two
|
|
10439
|
-
// namespaces. The flat `orgId`/`clientId` fields fold into `scopes`.
|
|
10874
|
+
// namespaces. The flat `orgId`/`clientId` fields fold into `scopes`. Each
|
|
10875
|
+
// entry's namespace + value are validated below
|
|
10876
|
+
// (`lintNamespacedScopeArrays`) — this `.max(MAX_SCOPE_DIMENSIONS)` is
|
|
10877
|
+
// only the shape-level cap, not the grammar check.
|
|
10440
10878
|
// With a scoped token these must be consistent with the profile's dataScope
|
|
10441
10879
|
// (a cross-consistency lint is deferred to the lint slice).
|
|
10442
10880
|
userId: z.string().min(1).optional(),
|
|
10443
|
-
scopes: z.array(z.string().min(1)).max(
|
|
10881
|
+
scopes: z.array(z.string().min(1)).max(MAX_SCOPE_DIMENSIONS).optional(),
|
|
10444
10882
|
// The id of an existing schema this one is a CUSTOMIZATION of, when a schema
|
|
10445
10883
|
// named `typeName` already exists in this context — required in that case,
|
|
10446
10884
|
// and must be omitted when this create is the first schema under that name
|
|
@@ -10463,6 +10901,35 @@ var BlueprintSchemaSchema = z.object({
|
|
|
10463
10901
|
});
|
|
10464
10902
|
}
|
|
10465
10903
|
});
|
|
10904
|
+
var CAPABILITY_NAME_RE = /^[a-z][a-z0-9-]*$/;
|
|
10905
|
+
var GrantedCapabilitiesSchema = z.array(z.string().min(1)).optional().superRefine((caps, ctx) => {
|
|
10906
|
+
if (!caps) return;
|
|
10907
|
+
const seen = /* @__PURE__ */ new Set();
|
|
10908
|
+
caps.forEach((name, i) => {
|
|
10909
|
+
if (name === "") return;
|
|
10910
|
+
if (name === "*") {
|
|
10911
|
+
ctx.addIssue({
|
|
10912
|
+
code: z.ZodIssueCode.custom,
|
|
10913
|
+
path: [i],
|
|
10914
|
+
message: "'*' is not a capability \u2014 granted_capabilities has no wildcard, and a '*' in allowedActions confers zero capabilities. Name each capability explicitly."
|
|
10915
|
+
});
|
|
10916
|
+
} else if (!CAPABILITY_NAME_RE.test(name)) {
|
|
10917
|
+
ctx.addIssue({
|
|
10918
|
+
code: z.ZodIssueCode.custom,
|
|
10919
|
+
path: [i],
|
|
10920
|
+
message: `'${name}' is not a valid capability name (expected lowercase letters, digits and hyphens, starting with a letter, and no ':' \u2014 a capability is not an action)`
|
|
10921
|
+
});
|
|
10922
|
+
}
|
|
10923
|
+
if (seen.has(name)) {
|
|
10924
|
+
ctx.addIssue({
|
|
10925
|
+
code: z.ZodIssueCode.custom,
|
|
10926
|
+
path: [i],
|
|
10927
|
+
message: `'${name}' is declared more than once`
|
|
10928
|
+
});
|
|
10929
|
+
}
|
|
10930
|
+
seen.add(name);
|
|
10931
|
+
});
|
|
10932
|
+
});
|
|
10466
10933
|
var BlueprintAccessProfileSchema = z.object({
|
|
10467
10934
|
// Validated structurally here; the SCOPE GATE (in @vectros-ai/cli) is
|
|
10468
10935
|
// what enforces the data-plane-only security boundary.
|
|
@@ -10478,7 +10945,9 @@ var BlueprintAccessProfileSchema = z.object({
|
|
|
10478
10945
|
// ...) — the flat `orgId`/`clientId` keys are gone. e.g.
|
|
10479
10946
|
// `{ "scope:org": ["org_x", null] }` = "org_x's records AND tenant-shared
|
|
10480
10947
|
// records". Omitting null restricts to the listed owners ONLY (the key will
|
|
10481
|
-
// NOT see tenant-level/seed records).
|
|
10948
|
+
// NOT see tenant-level/seed records). Every KEY is validated below
|
|
10949
|
+
// (`lintDataScopeKeys`) — `userId` or a grammar-valid `scope:<ns>`, no
|
|
10950
|
+
// other shape.
|
|
10482
10951
|
dataScope: z.record(z.array(z.union([z.string().min(1), z.null()]))).optional(),
|
|
10483
10952
|
// Optional identity overrides for the service principal's profile — scope
|
|
10484
10953
|
// values its key STAMPS onto everything it writes (a credential can only
|
|
@@ -10487,11 +10956,36 @@ var BlueprintAccessProfileSchema = z.object({
|
|
|
10487
10956
|
// `orgId`/`clientId` keys are gone); values may be `${{ identities.* }}`
|
|
10488
10957
|
// tokens (substituted at apply time). e.g. `{ "scope:org": '${{ identities.team }}' }`
|
|
10489
10958
|
// makes every record the service key writes org/team-owned.
|
|
10490
|
-
|
|
10959
|
+
//
|
|
10960
|
+
// Every KEY, the ≤2-dimension cap, and every LITERAL value are validated
|
|
10961
|
+
// below (`lintIdentityOverrides`, placeholder-aware — see that function's
|
|
10962
|
+
// docstring); this `.min(1)` is only the shape-level "non-blank" guard on
|
|
10963
|
+
// the value, not the grammar check.
|
|
10964
|
+
identityOverrides: z.record(z.string().min(1)).optional(),
|
|
10965
|
+
// Optional capabilities granted to the blueprint's own service-principal
|
|
10966
|
+
// profile. DECISION: allowed here, not role-clause-only. On the platform,
|
|
10967
|
+
// an access profile's scopes are themselves a list of clauses — the exact
|
|
10968
|
+
// same wire shape a role clause carries — and the loader already builds
|
|
10969
|
+
// this profile as ONE such clause. Refusing `capabilities` here while
|
|
10970
|
+
// allowing it on a role clause would be an asymmetry this FORMAT package
|
|
10971
|
+
// has no basis to invent: nothing on the platform side distinguishes the
|
|
10972
|
+
// two, and whether granting a capability to a blueprint's own service key
|
|
10973
|
+
// is sound POLICY is exactly the question the CLI's scope gate exists to
|
|
10974
|
+
// answer (a separate package, out of scope for this format layer) — the
|
|
10975
|
+
// same posture this package already takes with `allowedActions` above.
|
|
10976
|
+
//
|
|
10977
|
+
// Worth naming for whoever writes that gate: this profile is a long-lived,
|
|
10978
|
+
// non-human-attributable machine credential (typically stored in CI/CD
|
|
10979
|
+
// secrets), not an ephemeral human session — a materially different
|
|
10980
|
+
// blast-radius question from granting the same capability to a role that
|
|
10981
|
+
// a person is bound to. The gate should weigh the two separately rather
|
|
10982
|
+
// than reuse one policy for both.
|
|
10983
|
+
capabilities: GrantedCapabilitiesSchema
|
|
10491
10984
|
}).strict();
|
|
10492
10985
|
var BlueprintRoleClauseSchema = z.object({
|
|
10493
10986
|
allowedActions: z.array(z.string().min(1)).min(1),
|
|
10494
|
-
dataScope: z.record(z.array(z.union([z.string().min(1), z.null()]))).optional()
|
|
10987
|
+
dataScope: z.record(z.array(z.union([z.string().min(1), z.null()]))).optional(),
|
|
10988
|
+
capabilities: GrantedCapabilitiesSchema
|
|
10495
10989
|
}).strict();
|
|
10496
10990
|
var BlueprintRolesSchema = z.record(z.array(BlueprintRoleClauseSchema).min(1));
|
|
10497
10991
|
var IDENTITY_NAMESPACE_RE = /^[a-z][a-z0-9_-]{1,31}$/;
|
|
@@ -10516,57 +11010,260 @@ var IdentityDeclSchema = z.object({
|
|
|
10516
11010
|
displayName: z.string().min(1).optional(),
|
|
10517
11011
|
metadata: z.record(z.unknown()).optional()
|
|
10518
11012
|
}).strict();
|
|
10519
|
-
var
|
|
11013
|
+
var IDENTITY_NAME_RE = /^[A-Za-z_]\w*$/;
|
|
11014
|
+
var IdentitiesDeclSchema = z.record(
|
|
11015
|
+
z.string().regex(IDENTITY_NAME_RE, {
|
|
11016
|
+
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"
|
|
11017
|
+
}),
|
|
11018
|
+
IdentityDeclSchema
|
|
11019
|
+
);
|
|
10520
11020
|
var BlueprintServicePrincipalSchema = z.object({
|
|
10521
11021
|
externalId: z.string().min(1),
|
|
10522
11022
|
displayName: z.string().min(1)
|
|
10523
11023
|
}).strict();
|
|
10524
|
-
var
|
|
10525
|
-
|
|
10526
|
-
|
|
11024
|
+
var ISSUER_ID_RE = CONTEXT_ID_RE;
|
|
11025
|
+
var BlueprintIssuerSchema = z.object({
|
|
11026
|
+
/** Short slug identifying this issuer within your account. Immutable once registered. */
|
|
11027
|
+
issuerId: z.string().regex(ISSUER_ID_RE, {
|
|
11028
|
+
message: "issuerId must be 3-31 chars, start with a lowercase letter, then lowercase letters/digits/dashes (e.g. 'auth0-prod')"
|
|
11029
|
+
}),
|
|
11030
|
+
/** The IdP's `iss` claim value, exactly as it appears in tokens it issues. */
|
|
11031
|
+
issuer: z.string().min(1),
|
|
11032
|
+
/** The IdP's remote JWKS endpoint, used to verify presented tokens' signatures. */
|
|
11033
|
+
jwksUri: z.string().min(1),
|
|
10527
11034
|
/**
|
|
10528
|
-
*
|
|
10529
|
-
*
|
|
10530
|
-
*
|
|
11035
|
+
* The `aud` claim value a presented subject_token must carry. Must be globally
|
|
11036
|
+
* unique in combination with `issuer` — use a distinct audience per
|
|
11037
|
+
* environment/context sharing one IdP account (most OIDC providers support
|
|
11038
|
+
* this as an ordinary per-API/application default).
|
|
10531
11039
|
*/
|
|
10532
|
-
|
|
11040
|
+
audience: z.string().min(1),
|
|
10533
11041
|
/**
|
|
10534
|
-
*
|
|
10535
|
-
*
|
|
10536
|
-
*
|
|
10537
|
-
*
|
|
11042
|
+
* Which app context an exchanged token targets. **Must equal the blueprint's own
|
|
11043
|
+
* `contextId`** — an issuer is a trust anchor, so a blueprint may only attach one to the
|
|
11044
|
+
* context it actually provisions. One IdP account serving several contexts needs one entry
|
|
11045
|
+
* per context, each in that context's own blueprint.
|
|
10538
11046
|
*/
|
|
10539
|
-
|
|
10540
|
-
|
|
10541
|
-
|
|
10542
|
-
|
|
10543
|
-
|
|
10544
|
-
/** The
|
|
10545
|
-
|
|
11047
|
+
contextId: z.string().regex(CONTEXT_ID_RE, {
|
|
11048
|
+
message: "contextId must be 3-31 chars, start with a lowercase letter, then lowercase letters/digits/dashes (e.g. 'casework')"
|
|
11049
|
+
}),
|
|
11050
|
+
/** The claim carrying the subject identifier. Platform defaults to 'sub' when omitted. */
|
|
11051
|
+
subClaim: z.string().min(1).optional(),
|
|
11052
|
+
/** The claim carrying the subject's email (first-login invite matching). Platform defaults to 'email' when omitted. */
|
|
11053
|
+
emailClaim: z.string().min(1).optional(),
|
|
11054
|
+
/**
|
|
11055
|
+
* Opt-in self-service signup: a list of {signupType, roleId} pairs. When a first-time exchange
|
|
11056
|
+
* caller presents no invite token but names a signupType matching one of these (or omits it and
|
|
11057
|
+
* exactly one entry exists), a brand-new user is created and bound to that entry's role, no
|
|
11058
|
+
* invite required. Every entry must be something you're willing to grant to ANY caller who can
|
|
11059
|
+
* present a token from this issuer — a role carrying elevated (provisioning or wildcard) scope
|
|
11060
|
+
* is rejected. Omit entirely to leave self-signup disabled (the default).
|
|
11061
|
+
*/
|
|
11062
|
+
selfSignupPolicies: z.array(
|
|
11063
|
+
z.object({
|
|
11064
|
+
signupType: z.string().regex(ISSUER_ID_RE, {
|
|
11065
|
+
message: "signupType must be 3-31 chars, start with a lowercase letter, then lowercase letters/digits/dashes (e.g. 'practitioner')"
|
|
11066
|
+
}),
|
|
11067
|
+
roleId: z.string().regex(ISSUER_ID_RE, {
|
|
11068
|
+
message: "roleId must be 3-31 chars, start with a lowercase letter, then lowercase letters/digits/dashes (e.g. 'practitioner-member')"
|
|
11069
|
+
})
|
|
11070
|
+
}).strict()
|
|
11071
|
+
).optional()
|
|
10546
11072
|
}).strict();
|
|
10547
|
-
var
|
|
10548
|
-
|
|
10549
|
-
|
|
10550
|
-
/**
|
|
10551
|
-
|
|
10552
|
-
|
|
10553
|
-
|
|
10554
|
-
|
|
10555
|
-
|
|
11073
|
+
var BlueprintNamespaceSchema = z.object({
|
|
11074
|
+
/** The namespace to register — same grammar as a `scope:<namespace>` key. */
|
|
11075
|
+
namespace: z.string().min(1),
|
|
11076
|
+
/**
|
|
11077
|
+
* When `true`, every value in this namespace must resolve to an existing
|
|
11078
|
+
* identity entity (create via `entities:c` at the in-context phase, or a
|
|
11079
|
+
* declared `identities:` entry). When `false`/omitted, values are
|
|
11080
|
+
* free-form strings validated by grammar only.
|
|
11081
|
+
*/
|
|
11082
|
+
entityBacked: z.boolean().optional(),
|
|
11083
|
+
/**
|
|
11084
|
+
* This namespace's position in your account's specificity order (used to
|
|
11085
|
+
* break a tie when a caller holds two scope dimensions at once). Required
|
|
11086
|
+
* by the platform on registration; MUST be unique across every namespace
|
|
11087
|
+
* in the account, including `org` (1000) and `client` (2000) — pick a
|
|
11088
|
+
* value outside that pair, and outside any other namespace this same
|
|
11089
|
+
* account might register (its own, or another blueprint's).
|
|
11090
|
+
*/
|
|
11091
|
+
specificityRank: z.number().int().min(0).max(1e6),
|
|
11092
|
+
/**
|
|
11093
|
+
* Optional membership backing — declares which record type + field hold
|
|
11094
|
+
* grants of THIS namespace's values. Supply together with
|
|
11095
|
+
* `membershipTargetField`, or omit both. Declaring this grants nobody
|
|
11096
|
+
* anything on its own: a role opts in explicitly with
|
|
11097
|
+
* `${{ member.scope.<namespace> }}` in its `dataScope`.
|
|
11098
|
+
*/
|
|
11099
|
+
membershipRecordType: z.string().min(1).optional(),
|
|
11100
|
+
/** The field on `membershipRecordType` naming the user a grant is FOR. Required alongside `membershipRecordType`. */
|
|
11101
|
+
membershipTargetField: z.string().min(1).optional(),
|
|
11102
|
+
/**
|
|
11103
|
+
* The field on `membershipRecordType` naming a grant's LEVEL, so the same
|
|
11104
|
+
* user can hold different levels in different values of this namespace.
|
|
11105
|
+
* Supply together with `membershipLevels`, or omit both for plain
|
|
11106
|
+
* in-or-out membership.
|
|
11107
|
+
*/
|
|
11108
|
+
membershipLevelField: z.string().min(1).optional(),
|
|
11109
|
+
/** The complete set of level labels this namespace allows. Required alongside `membershipLevelField`. */
|
|
11110
|
+
membershipLevels: z.array(z.string().min(1)).optional()
|
|
10556
11111
|
}).strict();
|
|
10557
|
-
var
|
|
10558
|
-
|
|
10559
|
-
|
|
10560
|
-
|
|
10561
|
-
|
|
10562
|
-
|
|
10563
|
-
const
|
|
10564
|
-
|
|
10565
|
-
|
|
11112
|
+
var MEMBERSHIP_FIELD_NAME_RE = /^[A-Za-z0-9_-]{1,64}$/;
|
|
11113
|
+
function lintNamespaces(value, ctx) {
|
|
11114
|
+
if (!value || typeof value !== "object" || !("namespaces" in value)) return;
|
|
11115
|
+
const namespaces = value.namespaces;
|
|
11116
|
+
if (!Array.isArray(namespaces)) return;
|
|
11117
|
+
const rawSchemas = value.schemas;
|
|
11118
|
+
const declaredSchemaTypes = new Set(
|
|
11119
|
+
(Array.isArray(rawSchemas) ? rawSchemas : []).map((s) => s && typeof s === "object" && "typeName" in s ? s.typeName : void 0).filter((t) => typeof t === "string")
|
|
11120
|
+
);
|
|
11121
|
+
const seenNames = /* @__PURE__ */ new Set();
|
|
11122
|
+
const seenRanks = /* @__PURE__ */ new Map();
|
|
11123
|
+
namespaces.forEach((entry, i) => {
|
|
11124
|
+
if (!entry || typeof entry !== "object") return;
|
|
11125
|
+
const ns = entry;
|
|
11126
|
+
const path4 = ["namespaces", i];
|
|
11127
|
+
if (typeof ns.namespace === "string") {
|
|
11128
|
+
const err = scopeNamespaceGrammarError(ns.namespace);
|
|
11129
|
+
if (err) ctx.addIssue({ code: z.ZodIssueCode.custom, path: [...path4, "namespace"], message: err });
|
|
11130
|
+
if (seenNames.has(ns.namespace)) {
|
|
11131
|
+
ctx.addIssue({
|
|
11132
|
+
code: z.ZodIssueCode.custom,
|
|
11133
|
+
path: [...path4, "namespace"],
|
|
11134
|
+
message: `'${ns.namespace}' is declared more than once in namespaces[]`
|
|
11135
|
+
});
|
|
11136
|
+
}
|
|
11137
|
+
seenNames.add(ns.namespace);
|
|
11138
|
+
}
|
|
11139
|
+
if (typeof ns.specificityRank === "number") {
|
|
11140
|
+
const prior = seenRanks.get(ns.specificityRank);
|
|
11141
|
+
if (prior !== void 0) {
|
|
11142
|
+
ctx.addIssue({
|
|
11143
|
+
code: z.ZodIssueCode.custom,
|
|
11144
|
+
path: [...path4, "specificityRank"],
|
|
11145
|
+
message: `specificityRank ${ns.specificityRank} is already used by namespaces[${prior}] \u2014 every namespace's rank must be unique`
|
|
11146
|
+
});
|
|
11147
|
+
}
|
|
11148
|
+
seenRanks.set(ns.specificityRank, i);
|
|
11149
|
+
}
|
|
11150
|
+
const hasRecordType = ns.membershipRecordType !== void 0;
|
|
11151
|
+
const hasTargetField = ns.membershipTargetField !== void 0;
|
|
11152
|
+
if (hasRecordType !== hasTargetField) {
|
|
11153
|
+
ctx.addIssue({
|
|
11154
|
+
code: z.ZodIssueCode.custom,
|
|
11155
|
+
path: [...path4, "membershipRecordType"],
|
|
11156
|
+
message: "membershipRecordType and membershipTargetField must be declared together, or neither"
|
|
11157
|
+
});
|
|
11158
|
+
}
|
|
11159
|
+
if (typeof ns.membershipRecordType === "string" && !declaredSchemaTypes.has(ns.membershipRecordType)) {
|
|
11160
|
+
ctx.addIssue({
|
|
11161
|
+
code: z.ZodIssueCode.custom,
|
|
11162
|
+
path: [...path4, "membershipRecordType"],
|
|
11163
|
+
message: `membershipRecordType '${ns.membershipRecordType}' must name a typeName this blueprint declares in schemas[] \u2014 a namespace's membership can only resolve over a record type the blueprint itself ships, never a type that merely happens to already exist in the target context.`
|
|
11164
|
+
});
|
|
11165
|
+
}
|
|
11166
|
+
for (const [field, value2] of [
|
|
11167
|
+
["membershipRecordType", ns.membershipRecordType],
|
|
11168
|
+
["membershipTargetField", ns.membershipTargetField],
|
|
11169
|
+
["membershipLevelField", ns.membershipLevelField]
|
|
11170
|
+
]) {
|
|
11171
|
+
if (typeof value2 === "string" && !MEMBERSHIP_FIELD_NAME_RE.test(value2)) {
|
|
11172
|
+
ctx.addIssue({
|
|
11173
|
+
code: z.ZodIssueCode.custom,
|
|
11174
|
+
path: [...path4, field],
|
|
11175
|
+
message: `${field} '${value2}' is invalid \u2014 must be 1-64 characters of letters, digits, underscores, or hyphens`
|
|
11176
|
+
});
|
|
11177
|
+
}
|
|
11178
|
+
}
|
|
11179
|
+
const hasLevelField = ns.membershipLevelField !== void 0;
|
|
11180
|
+
const hasLevels = Array.isArray(ns.membershipLevels) && ns.membershipLevels.length > 0;
|
|
11181
|
+
if (hasLevelField !== hasLevels) {
|
|
11182
|
+
ctx.addIssue({
|
|
11183
|
+
code: z.ZodIssueCode.custom,
|
|
11184
|
+
path: [...path4, "membershipLevelField"],
|
|
11185
|
+
message: "membershipLevelField and a non-empty membershipLevels must be declared together, or neither"
|
|
11186
|
+
});
|
|
11187
|
+
}
|
|
11188
|
+
if ((hasLevelField || hasLevels) && !hasRecordType) {
|
|
11189
|
+
ctx.addIssue({
|
|
11190
|
+
code: z.ZodIssueCode.custom,
|
|
11191
|
+
path: [...path4, "membershipLevelField"],
|
|
11192
|
+
message: "membershipLevelField/membershipLevels require membershipRecordType + membershipTargetField too"
|
|
11193
|
+
});
|
|
11194
|
+
}
|
|
11195
|
+
if (Array.isArray(ns.membershipLevels)) {
|
|
11196
|
+
const seenLevels = /* @__PURE__ */ new Set();
|
|
11197
|
+
for (const level of ns.membershipLevels) {
|
|
11198
|
+
if (typeof level !== "string") continue;
|
|
11199
|
+
const err = scopeNamespaceGrammarError(level);
|
|
11200
|
+
if (err) {
|
|
11201
|
+
ctx.addIssue({
|
|
11202
|
+
code: z.ZodIssueCode.custom,
|
|
11203
|
+
path: [...path4, "membershipLevels"],
|
|
11204
|
+
message: `level '${level}': ${err}`
|
|
11205
|
+
});
|
|
11206
|
+
} else if (seenLevels.has(level)) {
|
|
11207
|
+
ctx.addIssue({
|
|
11208
|
+
code: z.ZodIssueCode.custom,
|
|
11209
|
+
path: [...path4, "membershipLevels"],
|
|
11210
|
+
message: `membershipLevels contains '${level}' twice`
|
|
11211
|
+
});
|
|
11212
|
+
}
|
|
11213
|
+
seenLevels.add(level);
|
|
11214
|
+
}
|
|
11215
|
+
}
|
|
11216
|
+
});
|
|
11217
|
+
}
|
|
11218
|
+
var SeedCommonShape = {
|
|
11219
|
+
/** The schema typeName this seed instantiates (should match a declared schema). */
|
|
11220
|
+
typeName: z.string().min(1),
|
|
11221
|
+
/**
|
|
11222
|
+
* Stable, caller-supplied id — the loader's idempotency key AND the value other
|
|
11223
|
+
* seeds resolve a `reference` against (across surfaces: a record seed may
|
|
11224
|
+
* reference a document seed by its externalId, and vice versa).
|
|
11225
|
+
*/
|
|
11226
|
+
externalId: z.string().min(1),
|
|
11227
|
+
/**
|
|
11228
|
+
* Optional scope ownership for the seeded item, as `namespace:value` entries
|
|
11229
|
+
* (≤2, e.g. `['org:${{ identities.team }}']`; tokens substitute at apply
|
|
11230
|
+
* time). `[]` = a private, user-owned item. Omitted = the seeding
|
|
11231
|
+
* credential's full identity (the default). Each entry's namespace + value
|
|
11232
|
+
* are validated below (`lintNamespacedScopeArrays`) — this
|
|
11233
|
+
* `.max(MAX_SCOPE_DIMENSIONS)` is only the shape-level cap, not the grammar
|
|
11234
|
+
* check.
|
|
11235
|
+
*/
|
|
11236
|
+
scopes: z.array(z.string()).max(MAX_SCOPE_DIMENSIONS).optional()
|
|
11237
|
+
};
|
|
11238
|
+
var RecordSeedSchema = z.object({
|
|
11239
|
+
surface: z.literal("record"),
|
|
11240
|
+
...SeedCommonShape,
|
|
11241
|
+
/** The record payload, validated against the bound schema. */
|
|
11242
|
+
fields: z.record(z.unknown())
|
|
11243
|
+
}).strict();
|
|
11244
|
+
var DocumentSeedSchema = z.object({
|
|
11245
|
+
surface: z.literal("document"),
|
|
11246
|
+
...SeedCommonShape,
|
|
11247
|
+
/** Human-readable document title — REQUIRED by the text-ingest path. */
|
|
11248
|
+
title: z.string().min(1),
|
|
11249
|
+
/** Raw text content to ingest + index — REQUIRED and non-empty (the platform rejects a blank ingest). */
|
|
11250
|
+
text: z.string().min(1),
|
|
11251
|
+
/** Optional structured payload bound to the schema (the document's `fields`). */
|
|
11252
|
+
fields: z.record(z.unknown()).optional()
|
|
11253
|
+
}).strict();
|
|
11254
|
+
var BlueprintSeedRecordSchema = z.discriminatedUnion("surface", [
|
|
11255
|
+
RecordSeedSchema,
|
|
11256
|
+
DocumentSeedSchema
|
|
11257
|
+
]);
|
|
11258
|
+
var SELF_TOKEN_RE = /\$\{\{\s*(?:self|under|member)\.[^}]*?\s*\}\}/;
|
|
11259
|
+
function lintSelfTokenPlacement(value, ctx) {
|
|
11260
|
+
const walk = (node, path4, inRoleDataScope) => {
|
|
11261
|
+
if (typeof node === "string") {
|
|
11262
|
+
if (!inRoleDataScope && SELF_TOKEN_RE.test(node)) {
|
|
10566
11263
|
ctx.addIssue({
|
|
10567
11264
|
code: z.ZodIssueCode.custom,
|
|
10568
11265
|
path: path4,
|
|
10569
|
-
message: "'${{ self.* }}' is a runtime
|
|
11266
|
+
message: "'${{ self.* }}' / '${{ member.* }}' is a runtime placeholder \u2014 it is only valid inside a roles[].dataScope value"
|
|
10570
11267
|
});
|
|
10571
11268
|
}
|
|
10572
11269
|
return;
|
|
@@ -10585,12 +11282,20 @@ function lintSelfTokenPlacement(value, ctx) {
|
|
|
10585
11282
|
walk(value, [], false);
|
|
10586
11283
|
}
|
|
10587
11284
|
var IDENTITY_REF_RE = /\$\{\{\s*identities\.([A-Za-z_]\w*)\s*\}\}/g;
|
|
10588
|
-
|
|
11285
|
+
var IDENTITY_REF_LOOSE_RE = /\$\{\{\s*identities\.([^}]*?)\s*\}\}/g;
|
|
11286
|
+
function lintIdentityRefs(value, ctx) {
|
|
10589
11287
|
const declared = new Set(
|
|
10590
11288
|
value && typeof value === "object" && "identities" in value && value.identities ? Object.keys(value.identities) : []
|
|
10591
11289
|
);
|
|
10592
11290
|
const walk = (node, path4) => {
|
|
10593
11291
|
if (typeof node === "string") {
|
|
11292
|
+
for (const m of node.matchAll(IDENTITY_REF_LOOSE_RE)) {
|
|
11293
|
+
const captured = m[1];
|
|
11294
|
+
if (IDENTITY_NAME_RE.test(captured)) continue;
|
|
11295
|
+
const dotIndex = captured.indexOf(".");
|
|
11296
|
+
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)} }}'`;
|
|
11297
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, path: path4, message });
|
|
11298
|
+
}
|
|
10594
11299
|
for (const m of node.matchAll(IDENTITY_REF_RE)) {
|
|
10595
11300
|
if (!declared.has(m[1])) {
|
|
10596
11301
|
ctx.addIssue({
|
|
@@ -10613,6 +11318,134 @@ function lintIdentityRefsDeclared(value, ctx) {
|
|
|
10613
11318
|
};
|
|
10614
11319
|
walk(value, []);
|
|
10615
11320
|
}
|
|
11321
|
+
var SCOPE_VALUE_RE = /^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$/;
|
|
11322
|
+
var SCOPE_NAMESPACE_RE = /^[a-z][a-z0-9_-]{1,31}$/;
|
|
11323
|
+
var FORBIDDEN_SCOPE_NAMESPACES = /* @__PURE__ */ new Set([
|
|
11324
|
+
"user",
|
|
11325
|
+
"record",
|
|
11326
|
+
"document",
|
|
11327
|
+
"entity",
|
|
11328
|
+
"self",
|
|
11329
|
+
"tenant",
|
|
11330
|
+
"context",
|
|
11331
|
+
"scope",
|
|
11332
|
+
"versions",
|
|
11333
|
+
"lookup"
|
|
11334
|
+
]);
|
|
11335
|
+
function scopeNamespaceGrammarError(namespace) {
|
|
11336
|
+
if (namespace === "") return "scope namespace must not be blank";
|
|
11337
|
+
if (FORBIDDEN_SCOPE_NAMESPACES.has(namespace)) {
|
|
11338
|
+
return `'${namespace}' is a reserved namespace and cannot be used as an ownership scope`;
|
|
11339
|
+
}
|
|
11340
|
+
if (!SCOPE_NAMESPACE_RE.test(namespace)) {
|
|
11341
|
+
return `'${namespace}' is not a valid namespace \u2014 2-32 chars, lowercase letter first, then lowercase letters, digits, '_' or '-'`;
|
|
11342
|
+
}
|
|
11343
|
+
return null;
|
|
11344
|
+
}
|
|
11345
|
+
var IDENTITY_PLACEHOLDER_WHOLE_RE = new RegExp(`^${IDENTITY_REF_LOOSE_RE.source}$`);
|
|
11346
|
+
function lintIdentityOverrides(value, ctx) {
|
|
11347
|
+
const overrides = value.accessProfile?.identityOverrides;
|
|
11348
|
+
if (!overrides) return;
|
|
11349
|
+
const entries = Object.entries(overrides);
|
|
11350
|
+
if (entries.length > MAX_SCOPE_DIMENSIONS) {
|
|
11351
|
+
ctx.addIssue({
|
|
11352
|
+
code: z.ZodIssueCode.custom,
|
|
11353
|
+
path: ["accessProfile", "identityOverrides"],
|
|
11354
|
+
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}.`
|
|
11355
|
+
});
|
|
11356
|
+
}
|
|
11357
|
+
for (const [key, v] of entries) {
|
|
11358
|
+
const ns = key.startsWith("scope:") ? key.slice("scope:".length) : null;
|
|
11359
|
+
const nsError = ns === null ? null : scopeNamespaceGrammarError(ns);
|
|
11360
|
+
if (ns === null || nsError !== null) {
|
|
11361
|
+
ctx.addIssue({
|
|
11362
|
+
code: z.ZodIssueCode.custom,
|
|
11363
|
+
path: ["accessProfile", "identityOverrides", key],
|
|
11364
|
+
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}`
|
|
11365
|
+
});
|
|
11366
|
+
}
|
|
11367
|
+
if (typeof v !== "string") continue;
|
|
11368
|
+
if (IDENTITY_PLACEHOLDER_WHOLE_RE.test(v)) continue;
|
|
11369
|
+
if (!SCOPE_VALUE_RE.test(v)) {
|
|
11370
|
+
ctx.addIssue({
|
|
11371
|
+
code: z.ZodIssueCode.custom,
|
|
11372
|
+
path: ["accessProfile", "identityOverrides", key],
|
|
11373
|
+
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).`
|
|
11374
|
+
});
|
|
11375
|
+
}
|
|
11376
|
+
}
|
|
11377
|
+
}
|
|
11378
|
+
function lintScopeEntries(entries, path4, ctx) {
|
|
11379
|
+
if (!entries) return;
|
|
11380
|
+
const seenValueByNamespace = /* @__PURE__ */ new Map();
|
|
11381
|
+
for (let i = 0; i < entries.length; i++) {
|
|
11382
|
+
const raw = entries[i];
|
|
11383
|
+
if (typeof raw !== "string") continue;
|
|
11384
|
+
const sep = raw.indexOf(":");
|
|
11385
|
+
if (sep <= 0 || sep === raw.length - 1) {
|
|
11386
|
+
ctx.addIssue({
|
|
11387
|
+
code: z.ZodIssueCode.custom,
|
|
11388
|
+
path: [...path4, i],
|
|
11389
|
+
message: `'${raw}' must be '<namespace>:<value>' (e.g. 'org:6ba7...' or 'team:${"${{ identities.team }}"}')`
|
|
11390
|
+
});
|
|
11391
|
+
continue;
|
|
11392
|
+
}
|
|
11393
|
+
const namespace = raw.slice(0, sep);
|
|
11394
|
+
const val = raw.slice(sep + 1);
|
|
11395
|
+
const nsError = scopeNamespaceGrammarError(namespace);
|
|
11396
|
+
if (nsError !== null) {
|
|
11397
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, path: [...path4, i], message: `'${raw}' ${nsError}` });
|
|
11398
|
+
} else {
|
|
11399
|
+
const priorValue = seenValueByNamespace.get(namespace);
|
|
11400
|
+
if (priorValue !== void 0 && priorValue !== val) {
|
|
11401
|
+
ctx.addIssue({
|
|
11402
|
+
code: z.ZodIssueCode.custom,
|
|
11403
|
+
path: [...path4, i],
|
|
11404
|
+
message: `'${namespace}' appears more than once with different values ('${priorValue}' and '${val}') \u2014 an item carries at most one value per namespace`
|
|
11405
|
+
});
|
|
11406
|
+
}
|
|
11407
|
+
seenValueByNamespace.set(namespace, val);
|
|
11408
|
+
}
|
|
11409
|
+
if (IDENTITY_PLACEHOLDER_WHOLE_RE.test(val)) continue;
|
|
11410
|
+
if (!SCOPE_VALUE_RE.test(val)) {
|
|
11411
|
+
ctx.addIssue({
|
|
11412
|
+
code: z.ZodIssueCode.custom,
|
|
11413
|
+
path: [...path4, i],
|
|
11414
|
+
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).`
|
|
11415
|
+
});
|
|
11416
|
+
}
|
|
11417
|
+
}
|
|
11418
|
+
}
|
|
11419
|
+
function lintNamespacedScopeArrays(value, ctx) {
|
|
11420
|
+
const bp = value;
|
|
11421
|
+
(bp.schemas ?? []).forEach((s, i) => lintScopeEntries(s.scopes, ["schemas", i, "scopes"], ctx));
|
|
11422
|
+
(bp.seed ?? []).forEach((s, i) => lintScopeEntries(s.scopes, ["seed", i, "scopes"], ctx));
|
|
11423
|
+
}
|
|
11424
|
+
function lintDataScopeKey(key, path4, ctx) {
|
|
11425
|
+
if (key === "userId") return;
|
|
11426
|
+
const ns = key.startsWith("scope:") ? key.slice("scope:".length) : null;
|
|
11427
|
+
const nsError = ns === null ? null : scopeNamespaceGrammarError(ns);
|
|
11428
|
+
if (ns === null || nsError !== null) {
|
|
11429
|
+
ctx.addIssue({
|
|
11430
|
+
code: z.ZodIssueCode.custom,
|
|
11431
|
+
path: path4,
|
|
11432
|
+
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}`
|
|
11433
|
+
});
|
|
11434
|
+
}
|
|
11435
|
+
}
|
|
11436
|
+
function lintDataScopeKeys(value, ctx) {
|
|
11437
|
+
const bp = value;
|
|
11438
|
+
for (const key of Object.keys(bp.accessProfile?.dataScope ?? {})) {
|
|
11439
|
+
lintDataScopeKey(key, ["accessProfile", "dataScope", key], ctx);
|
|
11440
|
+
}
|
|
11441
|
+
for (const [roleId, clauses] of Object.entries(bp.roles ?? {})) {
|
|
11442
|
+
clauses.forEach((clause, i) => {
|
|
11443
|
+
for (const key of Object.keys(clause.dataScope ?? {})) {
|
|
11444
|
+
lintDataScopeKey(key, ["roles", roleId, i, "dataScope", key], ctx);
|
|
11445
|
+
}
|
|
11446
|
+
});
|
|
11447
|
+
}
|
|
11448
|
+
}
|
|
10616
11449
|
function lintSeedSurfaces(value, ctx) {
|
|
10617
11450
|
const bp = value;
|
|
10618
11451
|
if (!Array.isArray(bp.seed)) return;
|
|
@@ -10630,6 +11463,41 @@ function lintSeedSurfaces(value, ctx) {
|
|
|
10630
11463
|
}
|
|
10631
11464
|
});
|
|
10632
11465
|
}
|
|
11466
|
+
function lintIdentitySurfaces(value, ctx) {
|
|
11467
|
+
const bp = value;
|
|
11468
|
+
if (!Array.isArray(bp.schemas)) return;
|
|
11469
|
+
bp.schemas.forEach((schema, i) => {
|
|
11470
|
+
if (!schema.allowedSurfaces?.includes("user")) return;
|
|
11471
|
+
ctx.addIssue({
|
|
11472
|
+
code: z.ZodIssueCode.custom,
|
|
11473
|
+
path: ["schemas", i, "allowedSurfaces"],
|
|
11474
|
+
message: `schema '${schema.typeName}': allowedSurfaces cannot include 'user' \u2014 a user-surfaced schema is account-global and requires a root API key, which no blueprint credential ever holds, so this would fail at apply with a 403. Model the data as a 'record' scoped to the user, or as an 'entity' schema (which a blueprint CAN provision, in its own app context).`
|
|
11475
|
+
});
|
|
11476
|
+
});
|
|
11477
|
+
}
|
|
11478
|
+
function lintIssuerContext(value, ctx) {
|
|
11479
|
+
const bp = value;
|
|
11480
|
+
if (!Array.isArray(bp.issuers)) return;
|
|
11481
|
+
bp.issuers.forEach((issuer, i) => {
|
|
11482
|
+
if (issuer.contextId === void 0 || issuer.contextId === bp.contextId) return;
|
|
11483
|
+
ctx.addIssue({
|
|
11484
|
+
code: z.ZodIssueCode.custom,
|
|
11485
|
+
path: ["issuers", i, "contextId"],
|
|
11486
|
+
message: `issuer '${issuer.issuerId}' targets app context '${issuer.contextId}', but this blueprint provisions '${bp.contextId}'. An issuer must target the blueprint's own context \u2014 everything else a blueprint creates does, and an issuer that points elsewhere attaches an identity provider to an app context the blueprint never mentions. Set it to '${bp.contextId}', or move this issuer into a blueprint for '${issuer.contextId}'.`
|
|
11487
|
+
});
|
|
11488
|
+
});
|
|
11489
|
+
}
|
|
11490
|
+
var BLUEPRINT_FIELD_PHASES = Object.freeze({
|
|
11491
|
+
servicePrincipal: "bootstrap",
|
|
11492
|
+
issuers: "bootstrap",
|
|
11493
|
+
// Registration requires the platform's provisioning:c capability, which
|
|
11494
|
+
// only the bootstrap credential carries — same reasoning as issuers.
|
|
11495
|
+
namespaces: "bootstrap",
|
|
11496
|
+
schemas: "in-context",
|
|
11497
|
+
accessProfile: "in-context",
|
|
11498
|
+
roles: "in-context",
|
|
11499
|
+
seed: "in-context"
|
|
11500
|
+
});
|
|
10633
11501
|
var BlueprintSchema = z.object({
|
|
10634
11502
|
/** Stable blueprint id (the `--blueprint <name>` selector + idempotency key). */
|
|
10635
11503
|
name: z.string().min(1),
|
|
@@ -10648,11 +11516,33 @@ var BlueprintSchema = z.object({
|
|
|
10648
11516
|
/** Optional multi-clause roles, bound to principals via `access grant --role`. */
|
|
10649
11517
|
roles: BlueprintRolesSchema.optional(),
|
|
10650
11518
|
/** Optional principals ensured-exist at apply; referenced via ${{ identities.* }}. */
|
|
10651
|
-
identities: IdentitiesDeclSchema.optional()
|
|
11519
|
+
identities: IdentitiesDeclSchema.optional(),
|
|
11520
|
+
/**
|
|
11521
|
+
* Optional trusted third-party IdP issuers to register (BYO-IdP token
|
|
11522
|
+
* exchange). Applied in the BOOTSTRAP-token phase, alongside app-context/
|
|
11523
|
+
* service-principal creation — NOT the in-context load (see
|
|
11524
|
+
* {@link BlueprintIssuerSchema}).
|
|
11525
|
+
*/
|
|
11526
|
+
issuers: z.array(BlueprintIssuerSchema).optional(),
|
|
11527
|
+
/**
|
|
11528
|
+
* Optional namespaces to register before anything else needs them — see
|
|
11529
|
+
* {@link BlueprintNamespaceSchema}. Applied in the BOOTSTRAP-token phase,
|
|
11530
|
+
* alongside app-context/service-principal creation and issuers, BEFORE
|
|
11531
|
+
* schemas/seed (an `entityBacked` namespace's entities, and an
|
|
11532
|
+
* `allowedSurfaces: ['entity']` schema, both need the registration to
|
|
11533
|
+
* already exist). `org`/`client` need no entry — they exist already.
|
|
11534
|
+
*/
|
|
11535
|
+
namespaces: z.array(BlueprintNamespaceSchema).optional()
|
|
10652
11536
|
}).strict().superRefine((bp, ctx) => {
|
|
10653
11537
|
lintSelfTokenPlacement(bp, ctx);
|
|
10654
|
-
|
|
11538
|
+
lintIdentityRefs(bp, ctx);
|
|
11539
|
+
lintIdentityOverrides(bp, ctx);
|
|
11540
|
+
lintNamespacedScopeArrays(bp, ctx);
|
|
11541
|
+
lintDataScopeKeys(bp, ctx);
|
|
10655
11542
|
lintSeedSurfaces(bp, ctx);
|
|
11543
|
+
lintIdentitySurfaces(bp, ctx);
|
|
11544
|
+
lintIssuerContext(bp, ctx);
|
|
11545
|
+
lintNamespaces(bp, ctx);
|
|
10656
11546
|
});
|
|
10657
11547
|
function formatIssuePath(path4) {
|
|
10658
11548
|
let out = "";
|
|
@@ -10807,7 +11697,11 @@ function deriveSuffix(contextId) {
|
|
|
10807
11697
|
var WHOLE_TOKEN_RE = /^\$\{\{\s*([A-Za-z_]\w*)\.([A-Za-z_]\w*)\s*\}\}$/;
|
|
10808
11698
|
var TOKEN_SCAN_RE = /\$\$\{\{|\$\{\{([\s\S]*?)\}\}|\$\{\{/g;
|
|
10809
11699
|
var INNER_REF_RE = /^([A-Za-z_]\w*)\.([A-Za-z_]\w*)$/;
|
|
10810
|
-
var DEFERRED_NAMESPACES = /* @__PURE__ */ new Set(["self", "identities"]);
|
|
11700
|
+
var DEFERRED_NAMESPACES = /* @__PURE__ */ new Set(["self", "identities", "under", "member"]);
|
|
11701
|
+
var DEFERRED_NS_ALTERNATION = [...DEFERRED_NAMESPACES].join("|");
|
|
11702
|
+
var DEFERRED_INNER_RE = new RegExp(
|
|
11703
|
+
`^(?:${DEFERRED_NS_ALTERNATION})(?:\\.[A-Za-z_]\\w*)+(?::[A-Za-z_]\\w*)?$`
|
|
11704
|
+
);
|
|
10811
11705
|
function resolveRef(ns, name, ctx, path4) {
|
|
10812
11706
|
if (ns === "inputs") {
|
|
10813
11707
|
if (!(name in ctx.declared)) {
|
|
@@ -10865,14 +11759,17 @@ function interpolate(str, ctx, path4) {
|
|
|
10865
11759
|
});
|
|
10866
11760
|
} else {
|
|
10867
11761
|
const inner = (m[1] ?? "").trim();
|
|
11762
|
+
if (DEFERRED_INNER_RE.test(inner)) {
|
|
11763
|
+
out += m[0];
|
|
11764
|
+
last = TOKEN_SCAN_RE.lastIndex;
|
|
11765
|
+
continue;
|
|
11766
|
+
}
|
|
10868
11767
|
const ref = inner.match(INNER_REF_RE);
|
|
10869
11768
|
if (!ref) {
|
|
10870
11769
|
ctx.issues.push({
|
|
10871
11770
|
path: path4,
|
|
10872
11771
|
message: `malformed reference '\${{ ${inner} }}' \u2014 expected '\${{ namespace.name }}' (escape a literal with '$\${{')`
|
|
10873
11772
|
});
|
|
10874
|
-
} else if (DEFERRED_NAMESPACES.has(ref[1])) {
|
|
10875
|
-
out += m[0];
|
|
10876
11773
|
} else {
|
|
10877
11774
|
const v = resolveRef(ref[1], ref[2], ctx, path4);
|
|
10878
11775
|
if (v !== void 0) out += String(v);
|
|
@@ -11054,324 +11951,82 @@ var taskManagement = {
|
|
|
11054
11951
|
// Equality, NOT range: the values are an ordinal vocabulary but they sort
|
|
11055
11952
|
// LEXICALLY (high < low < medium < urgent), not by severity — a range
|
|
11056
11953
|
// index here would be permanently wrong. Filter by exact priority instead.
|
|
11057
|
-
renderHints: { label: "Priority", widget: "select", order: 4, section: "Tracking" }
|
|
11058
|
-
},
|
|
11059
|
-
{
|
|
11060
|
-
fieldId: "assignee",
|
|
11061
|
-
fieldType: "string",
|
|
11062
|
-
filterable: true,
|
|
11063
|
-
description: "userId or display name.",
|
|
11064
|
-
renderHints: { label: "Assignee", widget: "text", order: 5, section: "Tracking" }
|
|
11065
|
-
},
|
|
11066
|
-
{
|
|
11067
|
-
fieldId: "project",
|
|
11068
|
-
fieldType: "string",
|
|
11069
|
-
filterable: true,
|
|
11070
|
-
description: "Grouping key for cross-session continuity.",
|
|
11071
|
-
renderHints: { label: "Project", widget: "text", order: 6, section: "Tracking" }
|
|
11072
|
-
},
|
|
11073
|
-
{
|
|
11074
|
-
fieldId: "dueDate",
|
|
11075
|
-
fieldType: "date",
|
|
11076
|
-
description: "ISO-8601. Range-queryable (see lookupFields) \u2014 list tasks due in a window.",
|
|
11077
|
-
renderHints: { label: "Due date", widget: "date", order: 7, section: "Tracking" }
|
|
11078
|
-
},
|
|
11079
|
-
{
|
|
11080
|
-
fieldId: "tags",
|
|
11081
|
-
fieldType: "array",
|
|
11082
|
-
filterable: true,
|
|
11083
|
-
renderHints: { label: "Tags", order: 8, section: "Tracking" }
|
|
11084
|
-
}
|
|
11085
|
-
// externalId is the record's FIRST-CLASS identifier (the dedup/lookup key + the
|
|
11086
|
-
// value a `reference` resolves against) — not a payload field; the loader sends
|
|
11087
|
-
// it top-level on the RecordRequest.
|
|
11088
|
-
],
|
|
11089
|
-
// Two lookup shapes, chosen deliberately because the equality-vs-range choice
|
|
11090
|
-
// per field is MIGRATION-LOCKED once the schema is live (you cannot flip a
|
|
11091
|
-
// field slot↔range later, even by removing and re-adding it):
|
|
11092
|
-
// • `project` — EQUALITY (a grouping key, not ordered): enumerate one
|
|
11093
|
-
// project's tasks directly, no search. Each equality lookup uses 1 of the
|
|
11094
|
-
// schema's 7 fast index slots.
|
|
11095
|
-
// • `dueDate` — RANGE: ordered `from`/`to`/`prefix` queries ("tasks due this
|
|
11096
|
-
// week", "due in 2026-06"). Range lookups use a relationship row, not a
|
|
11097
|
-
// slot, and are billed at the range rate. ISO-8601 sorts chronologically,
|
|
11098
|
-
// so the order is correct.
|
|
11099
|
-
// `externalId` has a built-in first-class finder and must NOT be redeclared here.
|
|
11100
|
-
lookupFields: ["project", { fieldName: "dueDate", rangeEnabled: true }]
|
|
11101
|
-
}
|
|
11102
|
-
],
|
|
11103
|
-
// Least-privilege profile — MUST pass the CLI scope gate.
|
|
11104
|
-
// r/c/u + search + schema discovery. NOT records:d (least privilege).
|
|
11105
|
-
accessProfile: {
|
|
11106
|
-
allowedActions: ["records:r", "records:c", "records:u", "search:r", "schemas:r"]
|
|
11107
|
-
// dataScope omitted → tenant-level shared tracker. Add a `scope:org`
|
|
11108
|
-
// entry for per-org isolation.
|
|
11109
|
-
},
|
|
11110
|
-
servicePrincipal: {
|
|
11111
|
-
externalId: "task-management",
|
|
11112
|
-
displayName: "Task Management"
|
|
11113
|
-
},
|
|
11114
|
-
seed: [
|
|
11115
|
-
{
|
|
11116
|
-
surface: "record",
|
|
11117
|
-
typeName: "task",
|
|
11118
|
-
externalId: "seed-welcome",
|
|
11119
|
-
fields: {
|
|
11120
|
-
title: "Welcome to your Vectros task tracker",
|
|
11121
|
-
description: "Created by the bootstrap loader. Ask your agent to add tasks.",
|
|
11122
|
-
status: "todo",
|
|
11123
|
-
priority: "low",
|
|
11124
|
-
project: "getting-started",
|
|
11125
|
-
dueDate: "2026-06-30"
|
|
11126
|
-
}
|
|
11127
|
-
}
|
|
11128
|
-
]
|
|
11129
|
-
};
|
|
11130
|
-
var task_management_default = taskManagement;
|
|
11131
|
-
var codingAgentMemory = {
|
|
11132
|
-
name: "coding-agent-memory",
|
|
11133
|
-
version: "1.0.0",
|
|
11134
|
-
description: "Persistent project memory for a coding agent \u2014 decisions, conventions, and gotchas that survive across sessions.",
|
|
11135
|
-
contextId: "coding-memory",
|
|
11136
|
-
contextName: "Coding Agent \u2014 Project Memory",
|
|
11137
|
-
schemas: [
|
|
11138
|
-
{
|
|
11139
|
-
// A durable architectural/product decision: what was decided, and WHY.
|
|
11140
|
-
// The rationale is the high-value, RAG-able body — "why did we do X?"
|
|
11141
|
-
// is the question a cold-context agent most needs answered.
|
|
11142
|
-
typeName: "decision",
|
|
11143
|
-
displayName: "Decision",
|
|
11144
|
-
indexMode: "HYBRID",
|
|
11145
|
-
// keyword on titles + semantic on statement/rationale
|
|
11146
|
-
fields: [
|
|
11147
|
-
{
|
|
11148
|
-
fieldId: "title",
|
|
11149
|
-
fieldType: "string",
|
|
11150
|
-
required: true,
|
|
11151
|
-
searchable: true,
|
|
11152
|
-
validation: { minLength: 1, maxLength: 200 },
|
|
11153
|
-
renderHints: { label: "Title", widget: "text", order: 1, displayField: true }
|
|
11154
|
-
},
|
|
11155
|
-
{
|
|
11156
|
-
fieldId: "statement",
|
|
11157
|
-
fieldType: "string",
|
|
11158
|
-
searchable: true,
|
|
11159
|
-
description: "What was decided, in one or two sentences.",
|
|
11160
|
-
renderHints: { label: "Statement", widget: "textarea", order: 2 }
|
|
11161
|
-
},
|
|
11162
|
-
{
|
|
11163
|
-
fieldId: "rationale",
|
|
11164
|
-
fieldType: "string",
|
|
11165
|
-
searchable: true,
|
|
11166
|
-
description: "Why \u2014 the trade-offs and the context. The most-recalled field.",
|
|
11167
|
-
renderHints: { label: "Rationale", widget: "textarea", order: 3 }
|
|
11168
|
-
},
|
|
11169
|
-
{
|
|
11170
|
-
fieldId: "status",
|
|
11171
|
-
fieldType: "enum",
|
|
11172
|
-
filterable: true,
|
|
11173
|
-
enumValues: ["proposed", "active", "superseded"],
|
|
11174
|
-
renderHints: { label: "Status", widget: "select", order: 4 }
|
|
11175
|
-
},
|
|
11176
|
-
{
|
|
11177
|
-
fieldId: "area",
|
|
11178
|
-
fieldType: "string",
|
|
11179
|
-
filterable: true,
|
|
11180
|
-
description: 'Subsystem / module the decision applies to (e.g. "auth", "search").',
|
|
11181
|
-
renderHints: { label: "Area", widget: "text", order: 5 }
|
|
11182
|
-
},
|
|
11183
|
-
{
|
|
11184
|
-
fieldId: "decidedOn",
|
|
11185
|
-
fieldType: "date",
|
|
11186
|
-
description: 'ISO-8601 date. Range-queryable \u2014 "decisions made this quarter".',
|
|
11187
|
-
renderHints: { label: "Decided on", widget: "date", order: 6 }
|
|
11188
|
-
}
|
|
11189
|
-
// externalId is the record's FIRST-CLASS identifier (the dedup/upsert key + the
|
|
11190
|
-
// value a `reference` resolves against) — it is NOT a payload field, so it is not
|
|
11191
|
-
// declared here; the loader sends it top-level on the RecordRequest.
|
|
11192
|
-
],
|
|
11193
|
-
// `externalId` is a first-class identifier with its own finder (exact
|
|
11194
|
-
// get/upsert) — look it up directly, never redeclare it as a schema lookup.
|
|
11195
|
-
// The choice below is MIGRATION-LOCKED once the schema is live:
|
|
11196
|
-
// • `area`/`status` — EQUALITY (categorical): "list active decisions",
|
|
11197
|
-
// "all decisions in the auth area". 2 of the 7 fast index slots.
|
|
11198
|
-
// • `decidedOn` — RANGE: ordered `from`/`to`/`prefix` over the date.
|
|
11199
|
-
lookupFields: ["area", "status", { fieldName: "decidedOn", rangeEnabled: true }]
|
|
11200
|
-
},
|
|
11201
|
-
{
|
|
11202
|
-
// A coding convention the team follows — the agent reads these before it
|
|
11203
|
-
// writes code so it matches the surrounding style instead of inventing one.
|
|
11204
|
-
typeName: "convention",
|
|
11205
|
-
displayName: "Convention",
|
|
11206
|
-
indexMode: "HYBRID",
|
|
11207
|
-
fields: [
|
|
11208
|
-
{
|
|
11209
|
-
fieldId: "name",
|
|
11210
|
-
fieldType: "string",
|
|
11211
|
-
required: true,
|
|
11212
|
-
searchable: true,
|
|
11213
|
-
validation: { minLength: 1, maxLength: 200 },
|
|
11214
|
-
renderHints: { label: "Name", widget: "text", order: 1, displayField: true }
|
|
11215
|
-
},
|
|
11216
|
-
{
|
|
11217
|
-
fieldId: "rule",
|
|
11218
|
-
fieldType: "string",
|
|
11219
|
-
searchable: true,
|
|
11220
|
-
description: "The convention itself, stated as an imperative.",
|
|
11221
|
-
renderHints: { label: "Rule", widget: "textarea", order: 2 }
|
|
11222
|
-
},
|
|
11223
|
-
{
|
|
11224
|
-
fieldId: "area",
|
|
11225
|
-
fieldType: "string",
|
|
11226
|
-
filterable: true,
|
|
11227
|
-
renderHints: { label: "Area", widget: "text", order: 3 }
|
|
11228
|
-
},
|
|
11229
|
-
{
|
|
11230
|
-
// Parallels `decision.status` so a convention can be RETIRED rather than
|
|
11231
|
-
// deleted (the blueprint omits records:d — memory is superseded, not lost).
|
|
11232
|
-
fieldId: "status",
|
|
11233
|
-
fieldType: "enum",
|
|
11234
|
-
filterable: true,
|
|
11235
|
-
enumValues: ["active", "retired"],
|
|
11236
|
-
renderHints: { label: "Status", widget: "select", order: 4 }
|
|
11237
|
-
},
|
|
11238
|
-
{
|
|
11239
|
-
// A typed REFERENCE to the decision that established this convention — a
|
|
11240
|
-
// foreign-key link between record types (the knowledge-graph edge). The
|
|
11241
|
-
// platform requires both the target type AND its surface; the value is the
|
|
11242
|
-
// target decision's externalId (the default `targetField`). Provenance:
|
|
11243
|
-
// "why does this convention exist? → open the decision behind it." Not
|
|
11244
|
-
// searchable (a foreign-key id is search noise); declared as an equality
|
|
11245
|
-
// lookup below so you can also enumerate "conventions from decision X".
|
|
11246
|
-
fieldId: "establishedByDecision",
|
|
11247
|
-
fieldType: "reference",
|
|
11248
|
-
targetTypeName: "decision",
|
|
11249
|
-
targetSurface: "record",
|
|
11250
|
-
targetField: "externalId",
|
|
11251
|
-
cardinality: "one",
|
|
11252
|
-
renderHints: { label: "Established by (decision)", order: 5 }
|
|
11253
|
-
},
|
|
11254
|
-
{
|
|
11255
|
-
fieldId: "adoptedOn",
|
|
11256
|
-
fieldType: "date",
|
|
11257
|
-
description: "ISO-8601 \u2014 when this convention was adopted. Range-queryable.",
|
|
11258
|
-
renderHints: { label: "Adopted on", widget: "date", order: 6 }
|
|
11259
|
-
}
|
|
11260
|
-
// externalId: first-class identifier, not a payload field (see `decision`).
|
|
11261
|
-
],
|
|
11262
|
-
// externalId has its own first-class finder for exact get — look it up
|
|
11263
|
-
// directly, not as a schema lookup. Locked equality-vs-range choices:
|
|
11264
|
-
// • `area`/`status` — EQUALITY: "active conventions for auth".
|
|
11265
|
-
// • `establishedByDecision` — EQUALITY on the reference value: enumerate
|
|
11266
|
-
// "conventions established by decision X" (a forward link query; the
|
|
11267
|
-
// platform has no reverse-reference index on this surface).
|
|
11268
|
-
// • `adoptedOn` — RANGE over the adoption date.
|
|
11269
|
-
// 3 equality lookups (of 7 fast slots) + 1 range row.
|
|
11270
|
-
lookupFields: ["area", "status", "establishedByDecision", { fieldName: "adoptedOn", rangeEnabled: true }]
|
|
11271
|
-
},
|
|
11272
|
-
{
|
|
11273
|
-
// A gotcha / sharp edge: a symptom, its cause, and the fix. Saves the
|
|
11274
|
-
// agent (and the human) from re-discovering the same trap.
|
|
11275
|
-
typeName: "gotcha",
|
|
11276
|
-
displayName: "Gotcha",
|
|
11277
|
-
indexMode: "HYBRID",
|
|
11278
|
-
fields: [
|
|
11279
|
-
{
|
|
11280
|
-
fieldId: "symptom",
|
|
11281
|
-
fieldType: "string",
|
|
11282
|
-
required: true,
|
|
11283
|
-
searchable: true,
|
|
11284
|
-
validation: { minLength: 1, maxLength: 500 },
|
|
11285
|
-
renderHints: { label: "Symptom", widget: "textarea", order: 1, displayField: true }
|
|
11286
|
-
},
|
|
11287
|
-
{
|
|
11288
|
-
fieldId: "cause",
|
|
11289
|
-
fieldType: "string",
|
|
11290
|
-
searchable: true,
|
|
11291
|
-
renderHints: { label: "Cause", widget: "textarea", order: 2 }
|
|
11954
|
+
renderHints: { label: "Priority", widget: "select", order: 4, section: "Tracking" }
|
|
11292
11955
|
},
|
|
11293
11956
|
{
|
|
11294
|
-
fieldId: "
|
|
11957
|
+
fieldId: "assignee",
|
|
11295
11958
|
fieldType: "string",
|
|
11296
|
-
|
|
11297
|
-
|
|
11959
|
+
filterable: true,
|
|
11960
|
+
description: "userId or display name.",
|
|
11961
|
+
renderHints: { label: "Assignee", widget: "text", order: 5, section: "Tracking" }
|
|
11298
11962
|
},
|
|
11299
11963
|
{
|
|
11300
|
-
fieldId: "
|
|
11964
|
+
fieldId: "project",
|
|
11301
11965
|
fieldType: "string",
|
|
11302
11966
|
filterable: true,
|
|
11303
|
-
|
|
11967
|
+
description: "Grouping key for cross-session continuity.",
|
|
11968
|
+
renderHints: { label: "Project", widget: "text", order: 6, section: "Tracking" }
|
|
11304
11969
|
},
|
|
11305
11970
|
{
|
|
11306
|
-
|
|
11307
|
-
|
|
11308
|
-
|
|
11309
|
-
|
|
11310
|
-
filterable: true,
|
|
11311
|
-
enumValues: ["active", "retired"],
|
|
11312
|
-
renderHints: { label: "Status", widget: "select", order: 5 }
|
|
11971
|
+
fieldId: "dueDate",
|
|
11972
|
+
fieldType: "date",
|
|
11973
|
+
description: "ISO-8601. Range-queryable (see lookupFields) \u2014 list tasks due in a window.",
|
|
11974
|
+
renderHints: { label: "Due date", widget: "date", order: 7, section: "Tracking" }
|
|
11313
11975
|
},
|
|
11314
11976
|
{
|
|
11315
|
-
fieldId: "
|
|
11316
|
-
fieldType: "
|
|
11317
|
-
|
|
11318
|
-
renderHints: { label: "
|
|
11977
|
+
fieldId: "tags",
|
|
11978
|
+
fieldType: "array",
|
|
11979
|
+
filterable: true,
|
|
11980
|
+
renderHints: { label: "Tags", order: 8, section: "Tracking" }
|
|
11319
11981
|
}
|
|
11320
|
-
// externalId
|
|
11982
|
+
// externalId is the record's FIRST-CLASS identifier (the dedup/lookup key + the
|
|
11983
|
+
// value a `reference` resolves against) — not a payload field; the loader sends
|
|
11984
|
+
// it top-level on the RecordRequest.
|
|
11321
11985
|
],
|
|
11322
|
-
//
|
|
11323
|
-
//
|
|
11324
|
-
//
|
|
11325
|
-
|
|
11986
|
+
// Two lookup shapes, chosen deliberately because the equality-vs-range choice
|
|
11987
|
+
// per field is MIGRATION-LOCKED once the schema is live (you cannot flip a
|
|
11988
|
+
// field slot↔range later, even by removing and re-adding it):
|
|
11989
|
+
// • `project` — EQUALITY (a grouping key, not ordered): enumerate one
|
|
11990
|
+
// project's tasks directly, no search. Each equality lookup uses 1 of the
|
|
11991
|
+
// schema's 7 fast index slots.
|
|
11992
|
+
// • `dueDate` — RANGE: ordered `from`/`to`/`prefix` queries ("tasks due this
|
|
11993
|
+
// week", "due in 2026-06"). Range lookups use a relationship row, not a
|
|
11994
|
+
// slot, and are billed at the range rate. ISO-8601 sorts chronologically,
|
|
11995
|
+
// so the order is correct.
|
|
11996
|
+
// `externalId` has a built-in first-class finder and must NOT be redeclared here.
|
|
11997
|
+
lookupFields: ["project", { fieldName: "dueDate", rangeEnabled: true }]
|
|
11326
11998
|
}
|
|
11327
11999
|
],
|
|
11328
12000
|
// Least-privilege profile — MUST pass the CLI scope gate.
|
|
11329
|
-
// r/c/u + search + schema discovery
|
|
11330
|
-
// recall over the captured rationale). NOT records:d: memory is superseded,
|
|
11331
|
-
// not deleted, so the project's decision history stays auditable.
|
|
12001
|
+
// r/c/u + search + schema discovery. NOT records:d (least privilege).
|
|
11332
12002
|
accessProfile: {
|
|
11333
|
-
allowedActions: ["records:r", "records:c", "records:u", "search:r", "schemas:r",
|
|
12003
|
+
allowedActions: ["records:r", "records:c", "records:u", "search:r", "schemas:r"],
|
|
12004
|
+
// dataScope: {} (present, empty) is the deliberate marker for a
|
|
12005
|
+
// tenant-level shared tracker — every task visible to every holder of
|
|
12006
|
+
// this credential. Add a `scope:org` entry instead for per-org isolation.
|
|
12007
|
+
dataScope: {}
|
|
11334
12008
|
},
|
|
11335
12009
|
servicePrincipal: {
|
|
11336
|
-
externalId: "
|
|
11337
|
-
displayName: "
|
|
12010
|
+
externalId: "task-management",
|
|
12011
|
+
displayName: "Task Management"
|
|
11338
12012
|
},
|
|
11339
12013
|
seed: [
|
|
11340
12014
|
{
|
|
11341
|
-
// Seeded FIRST so the convention below resolves its reference to it — the
|
|
11342
|
-
// loader creates seeds in array order, and a reference target must exist when
|
|
11343
|
-
// the referencing record is written.
|
|
11344
|
-
surface: "record",
|
|
11345
|
-
typeName: "decision",
|
|
11346
|
-
externalId: "seed-use-vectros-for-memory",
|
|
11347
|
-
fields: {
|
|
11348
|
-
title: "Use Vectros as the coding agent\u2019s project memory",
|
|
11349
|
-
statement: "Persist decisions, conventions, and gotchas as Vectros records so they survive across agent sessions.",
|
|
11350
|
-
rationale: "A coding agent loses context every cold start. A governed, searchable memory lets it recall prior decisions by meaning instead of re-asking \u2014 and the version history shows how the thinking evolved.",
|
|
11351
|
-
status: "active",
|
|
11352
|
-
area: "getting-started",
|
|
11353
|
-
decidedOn: "2026-06-14"
|
|
11354
|
-
}
|
|
11355
|
-
},
|
|
11356
|
-
{
|
|
11357
|
-
// Demonstrates a live typed link at bootstrap: this convention references the
|
|
11358
|
-
// decision above by its externalId, so "open the decision behind this rule"
|
|
11359
|
-
// works the moment the blueprint is applied.
|
|
11360
12015
|
surface: "record",
|
|
11361
|
-
typeName: "
|
|
11362
|
-
externalId: "seed-
|
|
12016
|
+
typeName: "task",
|
|
12017
|
+
externalId: "seed-welcome",
|
|
11363
12018
|
fields: {
|
|
11364
|
-
|
|
11365
|
-
|
|
11366
|
-
|
|
11367
|
-
|
|
11368
|
-
|
|
11369
|
-
|
|
12019
|
+
title: "Welcome to your Vectros task tracker",
|
|
12020
|
+
description: "Created by the bootstrap loader. Ask your agent to add tasks.",
|
|
12021
|
+
status: "todo",
|
|
12022
|
+
priority: "low",
|
|
12023
|
+
project: "getting-started",
|
|
12024
|
+
dueDate: "2026-06-30"
|
|
11370
12025
|
}
|
|
11371
12026
|
}
|
|
11372
12027
|
]
|
|
11373
12028
|
};
|
|
11374
|
-
var
|
|
12029
|
+
var task_management_default = taskManagement;
|
|
11375
12030
|
var DATA_PLANE_ACTIONS = [
|
|
11376
12031
|
"records:r",
|
|
11377
12032
|
"records:c",
|
|
@@ -11399,7 +12054,7 @@ var MEMBER_SHARED_READ_ACTIONS = [
|
|
|
11399
12054
|
];
|
|
11400
12055
|
var agenticSdlc = {
|
|
11401
12056
|
name: "agentic-sdlc",
|
|
11402
|
-
version: "1.
|
|
12057
|
+
version: "1.9.0",
|
|
11403
12058
|
description: "A whole-SDLC system of record for an AI development team \u2014 decisions, designs, references, runbooks, post-mortems (as documents) plus controls, conventions, gotchas, and a glossary (as records), cross-linked and recalled by meaning.",
|
|
11404
12059
|
contextId: "agentic-sdlc",
|
|
11405
12060
|
contextName: "Agentic SDLC Knowledge Base",
|
|
@@ -12265,13 +12920,244 @@ var agenticSdlc = {
|
|
|
12265
12920
|
{ fieldName: "updatedOn", rangeEnabled: true },
|
|
12266
12921
|
{ fieldName: "priority", rangeEnabled: true }
|
|
12267
12922
|
]
|
|
12923
|
+
},
|
|
12924
|
+
{
|
|
12925
|
+
// A PROPOSED memory, waiting on a verification step — the staging area in
|
|
12926
|
+
// front of the `memory` type above. An agent that distills its own working
|
|
12927
|
+
// sessions produces claims faster than it can check them, and an unchecked
|
|
12928
|
+
// claim is worse than no claim: it is a plausible sentence that will be
|
|
12929
|
+
// recalled as fact. So a proposal lands here, an agent later verifies it,
|
|
12930
|
+
// and only a verified one is written to `memory`.
|
|
12931
|
+
//
|
|
12932
|
+
// ⚠ indexMode NONE IS THE LOAD-BEARING DECISION, not a tuning choice.
|
|
12933
|
+
// Store-only means these records are never indexed, so a search or a
|
|
12934
|
+
// grounded answer CANNOT return them under any query — the separation
|
|
12935
|
+
// between unverified proposals and trusted knowledge is enforced by the
|
|
12936
|
+
// platform rather than by every caller remembering to filter. Make this
|
|
12937
|
+
// HYBRID and the corpus of unchecked claims starts competing with the
|
|
12938
|
+
// curated KB for retrieval slots, which is the one failure this type
|
|
12939
|
+
// exists to prevent. They stay reachable by id and by the two lookups
|
|
12940
|
+
// below, which is all a disposition workflow needs.
|
|
12941
|
+
//
|
|
12942
|
+
// Note the consequence for the fields: `filterable` narrows SEARCH, and
|
|
12943
|
+
// nothing here is searchable, so no field declares it. Query paths are the
|
|
12944
|
+
// lookups, deliberately — a flag that does nothing would just read as
|
|
12945
|
+
// capability that isn't there.
|
|
12946
|
+
typeName: "candidate",
|
|
12947
|
+
displayName: "Memory candidate",
|
|
12948
|
+
indexMode: "NONE",
|
|
12949
|
+
fields: [
|
|
12950
|
+
{
|
|
12951
|
+
fieldId: "title",
|
|
12952
|
+
fieldType: "string",
|
|
12953
|
+
required: true,
|
|
12954
|
+
validation: { minLength: 1, maxLength: 200 },
|
|
12955
|
+
description: "One line naming the claim \u2014 what a reviewer reads first.",
|
|
12956
|
+
renderHints: { label: "Title", widget: "text", order: 1, displayField: true }
|
|
12957
|
+
},
|
|
12958
|
+
{
|
|
12959
|
+
fieldId: "body",
|
|
12960
|
+
fieldType: "string",
|
|
12961
|
+
description: "The proposed fact in full, as the distiller stated it \u2014 the thing to verify.",
|
|
12962
|
+
renderHints: { label: "Body", widget: "textarea", order: 2 }
|
|
12963
|
+
},
|
|
12964
|
+
{
|
|
12965
|
+
// Mirrors `memory.kind` exactly, so a verified candidate maps 1:1 onto
|
|
12966
|
+
// the memory it becomes with no vocabulary translation in between.
|
|
12967
|
+
fieldId: "kind",
|
|
12968
|
+
fieldType: "enum",
|
|
12969
|
+
enumValues: ["user", "feedback", "project", "reference", "observation"],
|
|
12970
|
+
description: "Which flavour of memory this would become \u2014 the same vocabulary as `memory.kind`.",
|
|
12971
|
+
renderHints: { label: "Kind", widget: "select", order: 3 }
|
|
12972
|
+
},
|
|
12973
|
+
{
|
|
12974
|
+
// The distiller's ROUTING suggestion, and the one field here with no
|
|
12975
|
+
// counterpart on `memory` — because it stops existing the moment the
|
|
12976
|
+
// question it asks is answered. A verified claim either becomes a
|
|
12977
|
+
// memory record or becomes a paragraph in a document; this is the
|
|
12978
|
+
// proposer's guess about which, and the verifier is free to overrule
|
|
12979
|
+
// it (the verdict lands in `disposition`, not here).
|
|
12980
|
+
fieldId: "dest",
|
|
12981
|
+
fieldType: "enum",
|
|
12982
|
+
enumValues: ["memory", "doc"],
|
|
12983
|
+
description: "Where the proposer thinks this belongs if it survives review \u2014 a suggestion, not the verdict.",
|
|
12984
|
+
renderHints: { label: "Suggested destination", widget: "select", order: 4 }
|
|
12985
|
+
},
|
|
12986
|
+
{
|
|
12987
|
+
// `area`, `tags` and `sourceRef` below mirror `memory`'s exactly, for
|
|
12988
|
+
// the same reason `kind` does: a verified candidate is COPIED into a
|
|
12989
|
+
// memory, and any field that exists on only one side has to be
|
|
12990
|
+
// re-derived or dropped at exactly the moment a human is deciding
|
|
12991
|
+
// whether the claim is true.
|
|
12992
|
+
fieldId: "area",
|
|
12993
|
+
fieldType: "string",
|
|
12994
|
+
description: 'Subsystem/topic the claim applies to (e.g. "auth", "search") \u2014 the shared area vocabulary.',
|
|
12995
|
+
renderHints: { label: "Area", widget: "text", order: 5 }
|
|
12996
|
+
},
|
|
12997
|
+
{
|
|
12998
|
+
fieldId: "tags",
|
|
12999
|
+
fieldType: "array",
|
|
13000
|
+
description: "Freeform labels carried over from the proposal.",
|
|
13001
|
+
renderHints: { label: "Tags", order: 6 }
|
|
13002
|
+
},
|
|
13003
|
+
{
|
|
13004
|
+
// Provenance the VERIFIER needs most: checking a claim starts with the
|
|
13005
|
+
// file or issue it is about. A candidate whose sourceRef was dropped
|
|
13006
|
+
// in transit costs the reviewer the search that the proposer had
|
|
13007
|
+
// already done.
|
|
13008
|
+
fieldId: "sourceRef",
|
|
13009
|
+
fieldType: "string",
|
|
13010
|
+
description: "Provenance: the file path / issue / URL the claim is about \u2014 where a verifier starts.",
|
|
13011
|
+
renderHints: { label: "Source ref", widget: "text", order: 7 }
|
|
13012
|
+
},
|
|
13013
|
+
{
|
|
13014
|
+
fieldId: "sessionId",
|
|
13015
|
+
fieldType: "string",
|
|
13016
|
+
description: "The conversation that proposed it \u2014 your runtime's session identifier.",
|
|
13017
|
+
renderHints: { label: "Session", widget: "text", order: 8 }
|
|
13018
|
+
},
|
|
13019
|
+
{
|
|
13020
|
+
// Also the stable ordering key: a display ordinal is derived by sorting
|
|
13021
|
+
// on this, so proposals keep the numbering a reviewer saw. Nothing is
|
|
13022
|
+
// ever hard-deleted here, which is what keeps that ordering stable.
|
|
13023
|
+
fieldId: "proposedAt",
|
|
13024
|
+
fieldType: "date",
|
|
13025
|
+
description: "ISO-8601 \u2014 when it was proposed. Also the stable ordering key for display.",
|
|
13026
|
+
renderHints: { label: "Proposed at", widget: "date", order: 9 }
|
|
13027
|
+
},
|
|
13028
|
+
{
|
|
13029
|
+
// THE VERDICT, and deliberately a plain payload field rather than the
|
|
13030
|
+
// record's own ACTIVE/ARCHIVED status. Settling a candidate is
|
|
13031
|
+
// REVERSIBLE — a wrong dismissal has to be undoable — and a write-side
|
|
13032
|
+
// visibility flip is the wrong tool for a reversible state: it is
|
|
13033
|
+
// asynchronous, it moves the record out from under readers, and it
|
|
13034
|
+
// leaves the index status of a once-indexed row stranded. A filterable
|
|
13035
|
+
// payload field flips synchronously and reads the same for everyone.
|
|
13036
|
+
fieldId: "disposition",
|
|
13037
|
+
fieldType: "enum",
|
|
13038
|
+
enumValues: ["pending", "stored", "documented", "ignored"],
|
|
13039
|
+
description: "The verdict: still pending, written to memory, captured in a doc, or dismissed.",
|
|
13040
|
+
renderHints: { label: "Disposition", widget: "select", order: 10 }
|
|
13041
|
+
},
|
|
13042
|
+
{
|
|
13043
|
+
fieldId: "ref",
|
|
13044
|
+
fieldType: "string",
|
|
13045
|
+
description: "The citation given when settling it \u2014 the record id, document path, or reason.",
|
|
13046
|
+
renderHints: { label: "Ref", widget: "text", order: 11 }
|
|
13047
|
+
},
|
|
13048
|
+
{
|
|
13049
|
+
// What the settling step actually CHECKED, which is not always what the
|
|
13050
|
+
// agent typed. Keeping both makes a bad citation auditable after the
|
|
13051
|
+
// fact instead of indistinguishable from a good one.
|
|
13052
|
+
fieldId: "resolved",
|
|
13053
|
+
fieldType: "string",
|
|
13054
|
+
description: "What was verified when it was settled \u2014 kept alongside `ref`, which is what was claimed.",
|
|
13055
|
+
renderHints: { label: "Resolved", widget: "text", order: 12 }
|
|
13056
|
+
},
|
|
13057
|
+
{
|
|
13058
|
+
fieldId: "origin",
|
|
13059
|
+
fieldType: "enum",
|
|
13060
|
+
enumValues: ["session", "recovered"],
|
|
13061
|
+
description: "Proposed by a live session, or recovered later from one that ended without settling.",
|
|
13062
|
+
renderHints: { label: "Origin", widget: "select", order: 13 }
|
|
13063
|
+
},
|
|
13064
|
+
{
|
|
13065
|
+
fieldId: "reopenedWhy",
|
|
13066
|
+
fieldType: "string",
|
|
13067
|
+
description: "Why a settled candidate was reopened \u2014 the audit trail for undoing a verdict.",
|
|
13068
|
+
renderHints: { label: "Reopened why", widget: "textarea", order: 14 }
|
|
13069
|
+
},
|
|
13070
|
+
{
|
|
13071
|
+
// The forward half of a correction: this candidate restates an earlier
|
|
13072
|
+
// one whose claim was wrong. Written at create, so it costs nothing.
|
|
13073
|
+
fieldId: "revises",
|
|
13074
|
+
fieldType: "reference",
|
|
13075
|
+
targetTypeName: "candidate",
|
|
13076
|
+
targetSurface: "record",
|
|
13077
|
+
targetField: "externalId",
|
|
13078
|
+
cardinality: "one",
|
|
13079
|
+
description: "The candidate this one corrects (by externalId).",
|
|
13080
|
+
renderHints: { label: "Revises", order: 15 }
|
|
13081
|
+
},
|
|
13082
|
+
{
|
|
13083
|
+
// The reverse half, and NOT redundant: "is this still open?" has to be
|
|
13084
|
+
// answerable without asking whether some OTHER record points here, and
|
|
13085
|
+
// there is no reverse-reference read. So the superseded candidate is
|
|
13086
|
+
// marked directly.
|
|
13087
|
+
//
|
|
13088
|
+
// Kept SEPARATE from `disposition` on purpose. Superseded and dismissed
|
|
13089
|
+
// are different facts: a dismissal may be undone, while resurrecting a
|
|
13090
|
+
// claim that a later run already corrected would re-offer the version
|
|
13091
|
+
// known to be wrong. Folding both into one enum makes that distinction
|
|
13092
|
+
// unrepresentable exactly when it matters.
|
|
13093
|
+
fieldId: "supersededBy",
|
|
13094
|
+
fieldType: "reference",
|
|
13095
|
+
targetTypeName: "candidate",
|
|
13096
|
+
targetSurface: "record",
|
|
13097
|
+
targetField: "externalId",
|
|
13098
|
+
cardinality: "one",
|
|
13099
|
+
description: "The candidate that corrected this one (by externalId) \u2014 set on the older record.",
|
|
13100
|
+
renderHints: { label: "Superseded by", order: 16 }
|
|
13101
|
+
}
|
|
13102
|
+
],
|
|
13103
|
+
// FOUR lookups, because this type is asked three genuinely different
|
|
13104
|
+
// questions and one of them is a conjunction:
|
|
13105
|
+
//
|
|
13106
|
+
// `disposition` — "what is waiting ANYWHERE?" The cross-session
|
|
13107
|
+
// review queue. Low cardinality, so its
|
|
13108
|
+
// partitions are large; that is inherent to the
|
|
13109
|
+
// question and the pending set is small.
|
|
13110
|
+
// `sessionId` — "everything belonging to this conversation",
|
|
13111
|
+
// settled candidates included.
|
|
13112
|
+
// (sessionId, disposition) — "what is still waiting IN this conversation".
|
|
13113
|
+
// `proposedAt` (range) — worked by age: "what has been sitting
|
|
13114
|
+
// longest", "what came in this week". A range
|
|
13115
|
+
// read, not a filter over everything ever
|
|
13116
|
+
// proposed.
|
|
13117
|
+
//
|
|
13118
|
+
// The composite earns its slot because that third question is the HOTTEST
|
|
13119
|
+
// read in the system — it is what an agent is shown as it works, so it runs
|
|
13120
|
+
// far more often than the queue-wide scan does. Without it the answer is the
|
|
13121
|
+
// more selective single lookup plus a filter in the caller.
|
|
13122
|
+
//
|
|
13123
|
+
// `sessionId` LEADS, and the order is not cosmetic: the leading field
|
|
13124
|
+
// becomes the partition key. Leading with `disposition` would sort every
|
|
13125
|
+
// candidate ever proposed into four partitions; leading with `sessionId`
|
|
13126
|
+
// gives one small partition per conversation. Order is also
|
|
13127
|
+
// migration-locked, so it cannot be corrected later.
|
|
13128
|
+
//
|
|
13129
|
+
// A composite matches a LEADING RUN of its fields, so `(sessionId,
|
|
13130
|
+
// disposition)` also answers `sessionId` alone. The plain `sessionId`
|
|
13131
|
+
// lookup is kept anyway: under a partial tuple the unspecified field
|
|
13132
|
+
// becomes part of the ordering, so results come back grouped by
|
|
13133
|
+
// `disposition` rather than in one sequence — right for a filtered view,
|
|
13134
|
+
// wrong for "this conversation's candidates, oldest first", which is the
|
|
13135
|
+
// read the review surface and the queue/record agreement check both make.
|
|
13136
|
+
// One declaration out of ten is a cheap price for not depending on that
|
|
13137
|
+
// subtlety in the two places that must not be wrong.
|
|
13138
|
+
//
|
|
13139
|
+
// No `sortBy`: it defaults to the record's creation time, which is already
|
|
13140
|
+
// the primary key the review queue orders by. Naming `proposedAt` instead
|
|
13141
|
+
// would order by the WORKER's clock, and that is stamped when a proposal is
|
|
13142
|
+
// queued rather than when it is stored — the two disagree by exactly the
|
|
13143
|
+
// outage a durable queue exists to survive.
|
|
13144
|
+
lookupFields: [
|
|
13145
|
+
"disposition",
|
|
13146
|
+
"sessionId",
|
|
13147
|
+
{ fieldNames: ["sessionId", "disposition"] },
|
|
13148
|
+
{ fieldName: "proposedAt", rangeEnabled: true }
|
|
13149
|
+
]
|
|
12268
13150
|
}
|
|
12269
13151
|
],
|
|
12270
13152
|
// Least-privilege, data-plane only. The scope of the `ssk_*` key the bootstrap
|
|
12271
13153
|
// mints for THIS blueprint's service principal (the MCP/API runtime). See
|
|
12272
13154
|
// DATA_PLANE_ACTIONS above for the action set + rationale.
|
|
12273
13155
|
accessProfile: {
|
|
12274
|
-
allowedActions: DATA_PLANE_ACTIONS
|
|
13156
|
+
allowedActions: DATA_PLANE_ACTIONS,
|
|
13157
|
+
// dataScope: {} (present, empty) is the deliberate marker for tenant-wide
|
|
13158
|
+
// reach — this service key IS the runtime's own access to the whole KB,
|
|
13159
|
+
// not a multi-owner resource to isolate.
|
|
13160
|
+
dataScope: {}
|
|
12275
13161
|
},
|
|
12276
13162
|
// A reusable `editor` role for the HUMAN owner — DISTINCT from `accessProfile`
|
|
12277
13163
|
// (which scopes only the service-principal key). `bootstrap` provisions this
|
|
@@ -12291,7 +13177,9 @@ var agenticSdlc = {
|
|
|
12291
13177
|
// which includes members' private memory — the trusted-administrator tradeoff,
|
|
12292
13178
|
// called out in the bundled guide. Scope it down if your deployment wants
|
|
12293
13179
|
// curator access without private-memory visibility.
|
|
12294
|
-
|
|
13180
|
+
// dataScope: {} echoes the "deliberately UNSCOPED (whole-context)" call-out
|
|
13181
|
+
// above as an explicit marker, not just prose — see it for the tradeoff.
|
|
13182
|
+
editor: [{ allowedActions: EDITOR_ACTIONS, dataScope: {} }],
|
|
12295
13183
|
// A team member (or their agent) — TWO memory tiers as two UNIONed clauses
|
|
12296
13184
|
// (a record is accessible when ANY clause grants it; within one clause every
|
|
12297
13185
|
// dataScope dimension must match):
|
|
@@ -12311,14 +13199,21 @@ var agenticSdlc = {
|
|
|
12311
13199
|
// `vectros join agentic-sdlc --role member`; verify a binding with
|
|
12312
13200
|
// `vectros access explain --principal me --context agentic-sdlc`.
|
|
12313
13201
|
member: [
|
|
12314
|
-
|
|
13202
|
+
// dataScope: {} echoes clause 1's own "UNSCOPED, but safe" call-out above
|
|
13203
|
+
// as an explicit marker, not just prose.
|
|
13204
|
+
{ allowedActions: MEMBER_SHARED_READ_ACTIONS, dataScope: {} },
|
|
12315
13205
|
{
|
|
12316
13206
|
// `inference:r` is IN this self-scoped clause (not only clause 1) so a
|
|
12317
13207
|
// member's `rag_ask` can ground on its OWN private memory — the read
|
|
12318
13208
|
// grant (`records:r:memory`) and the inference capability travel in the
|
|
12319
13209
|
// SAME clause, so grounding works regardless of cross-clause admission
|
|
12320
13210
|
// semantics. Still self-fenced by dataScope: never another principal's.
|
|
12321
|
-
|
|
13211
|
+
// `candidate` is READ-only here, unlike `memory`'s full CRU. A candidate's
|
|
13212
|
+
// verdict is the output of a verification step, so it is written by the
|
|
13213
|
+
// agent runtime that performed the check — a human editing the field
|
|
13214
|
+
// directly would record a verdict nothing actually verified. Reading them
|
|
13215
|
+
// is the point: the queue, the verdicts and the corrections are browsable.
|
|
13216
|
+
allowedActions: ["records:cru:memory", "records:r:candidate", "search:r", "inference:r"],
|
|
12322
13217
|
dataScope: { userId: ["${{ self.userId }}"] }
|
|
12323
13218
|
}
|
|
12324
13219
|
]
|
|
@@ -12421,7 +13316,11 @@ var secondBrain = {
|
|
|
12421
13316
|
// (so the agent can `rag_ask` a grounded, cited question over your notes). No
|
|
12422
13317
|
// records:d: notes are archived (status → 'archived'), never deleted.
|
|
12423
13318
|
accessProfile: {
|
|
12424
|
-
allowedActions: ["records:r", "records:c", "records:u", "search:r", "schemas:r", "inference:r"]
|
|
13319
|
+
allowedActions: ["records:r", "records:c", "records:u", "search:r", "schemas:r", "inference:r"],
|
|
13320
|
+
// dataScope: {} (present, empty) is the deliberate marker for tenant-wide
|
|
13321
|
+
// reach — this is a single personal-knowledge-base credential with one
|
|
13322
|
+
// holder, not a multi-owner shared resource to isolate.
|
|
13323
|
+
dataScope: {}
|
|
12425
13324
|
},
|
|
12426
13325
|
servicePrincipal: {
|
|
12427
13326
|
externalId: "second-brain",
|
|
@@ -12553,7 +13452,11 @@ var clinicalIntake = {
|
|
|
12553
13452
|
// reveal qualifier (the demo key literally cannot un-redact the PHI fields),
|
|
12554
13453
|
// and NO inference:r (we never point a RAG capability at a PHI corpus here).
|
|
12555
13454
|
accessProfile: {
|
|
12556
|
-
allowedActions: ["records:r", "records:c", "records:u", "search:r", "schemas:r"]
|
|
13455
|
+
allowedActions: ["records:r", "records:c", "records:u", "search:r", "schemas:r"],
|
|
13456
|
+
// dataScope: {} (present, empty) is the deliberate marker for tenant-wide
|
|
13457
|
+
// reach, unchanged from this demo's original intent — a real clinical
|
|
13458
|
+
// deployment should scope this (e.g. `scope:org`) before handling real PHI.
|
|
13459
|
+
dataScope: {}
|
|
12557
13460
|
},
|
|
12558
13461
|
servicePrincipal: {
|
|
12559
13462
|
externalId: "clinical-intake",
|
|
@@ -12582,7 +13485,6 @@ var clinicalIntake = {
|
|
|
12582
13485
|
var clinical_intake_default = clinicalIntake;
|
|
12583
13486
|
var BUNDLED_BLUEPRINTS = [
|
|
12584
13487
|
task_management_default,
|
|
12585
|
-
coding_agent_memory_default,
|
|
12586
13488
|
agentic_sdlc_default,
|
|
12587
13489
|
second_brain_default,
|
|
12588
13490
|
clinical_intake_default
|
|
@@ -12881,18 +13783,17 @@ async function mintBridgeToken(opts) {
|
|
|
12881
13783
|
ttlSeconds = DEFAULT_BRIDGE_TTL_SECONDS,
|
|
12882
13784
|
tenant = "live",
|
|
12883
13785
|
useScopedToken = false,
|
|
13786
|
+
contextId,
|
|
12884
13787
|
fetchImpl = fetch
|
|
12885
13788
|
} = opts;
|
|
12886
13789
|
const base = apiBaseUrl.replace(/\/$/, "");
|
|
12887
13790
|
const path4 = bridgeTokenPath(useScopedToken);
|
|
12888
|
-
|
|
12889
|
-
|
|
12890
|
-
|
|
12891
|
-
|
|
12892
|
-
|
|
12893
|
-
|
|
12894
|
-
(status) => status === 404 && !useScopedToken ? ` (the dedicated ${CLI_BOOTSTRAP_TOKEN_PATH} endpoint may not be deployed yet \u2014 retry with --use-scoped-token for the fallback path)` : ""
|
|
12895
|
-
);
|
|
13791
|
+
let url = `${base}${path4}?ttl=${encodeURIComponent(String(ttlSeconds))}&tenant=${encodeURIComponent(tenant)}`;
|
|
13792
|
+
if (contextId) url += `&context=${encodeURIComponent(contextId)}`;
|
|
13793
|
+
return fetchStToken(url, cognitoToken, fetchImpl, path4, (status) => {
|
|
13794
|
+
if (status !== 404 || useScopedToken || contextId) return "";
|
|
13795
|
+
return ` (the dedicated ${CLI_BOOTSTRAP_TOKEN_PATH} endpoint may not be deployed yet \u2014 retry with --use-scoped-token for the fallback path)`;
|
|
13796
|
+
});
|
|
12896
13797
|
}
|
|
12897
13798
|
async function mintContextToken(opts) {
|
|
12898
13799
|
const {
|
|
@@ -13164,7 +14065,17 @@ async function resolveAuthContext(opts) {
|
|
|
13164
14065
|
bearer: bridge.token,
|
|
13165
14066
|
cognitoToken,
|
|
13166
14067
|
baseUrl,
|
|
13167
|
-
|
|
14068
|
+
/**
|
|
14069
|
+
* Derived from the REQUEST'S actual `baseUrl`, not echoed from `opts.env` — the two
|
|
14070
|
+
* diverge whenever `VECTROS_API_BASE_URL`/`--base-url` alone selects the environment
|
|
14071
|
+
* (the common shape when targeting staging without also passing `--env staging`,
|
|
14072
|
+
* since `envToBaseUrl` lets an explicit override win over `opts.env`'s default). Every
|
|
14073
|
+
* display surface that shows "on {env}" (blueprint-test's banner, `whoami`, etc.) reads
|
|
14074
|
+
* this field, so it must reflect where the request actually went, not which hidden flag
|
|
14075
|
+
* defaulted to what. `deriveEnvironment` falls through to `opts.env` only for a base URL
|
|
14076
|
+
* it doesn't recognize (a non-standard override, e.g. a future custom/self-hosted host).
|
|
14077
|
+
*/
|
|
14078
|
+
env: deriveEnvironment(baseUrl) ?? opts.env,
|
|
13168
14079
|
identity,
|
|
13169
14080
|
tenantId: typeof identity.tenantId === "string" ? identity.tenantId : void 0,
|
|
13170
14081
|
tenant
|
|
@@ -13248,7 +14159,8 @@ var DATA_PLANE_ALLOWLIST = [
|
|
|
13248
14159
|
"search",
|
|
13249
14160
|
"documents",
|
|
13250
14161
|
"folders",
|
|
13251
|
-
"inference"
|
|
14162
|
+
"inference",
|
|
14163
|
+
"entities"
|
|
13252
14164
|
];
|
|
13253
14165
|
var KNOWN_CONTROL_PLANE_RESOURCES = [
|
|
13254
14166
|
"keys",
|
|
@@ -13257,14 +14169,36 @@ var KNOWN_CONTROL_PLANE_RESOURCES = [
|
|
|
13257
14169
|
"users",
|
|
13258
14170
|
"billing",
|
|
13259
14171
|
"admin",
|
|
13260
|
-
//
|
|
13261
|
-
//
|
|
13262
|
-
//
|
|
13263
|
-
//
|
|
13264
|
-
//
|
|
13265
|
-
|
|
14172
|
+
// The namespace REGISTRY (as opposed to `entities`, which creates/reads a
|
|
14173
|
+
// ROW within an already-registered namespace — see the data-plane-allowlist
|
|
14174
|
+
// note above). Registering a new namespace TYPE is a tenant-config action,
|
|
14175
|
+
// root-key-gated regardless of any scope a token carries — genuinely
|
|
14176
|
+
// control-plane, unlike per-namespace entity CRUD. (Cosmetic: any
|
|
14177
|
+
// non-data-plane resource hard-rejects whether or not it is listed here;
|
|
14178
|
+
// this list only sharpens the error message.)
|
|
14179
|
+
//
|
|
14180
|
+
// ⚠️ This bars `namespaces` as a SCOPE — no partner-authored role can ever
|
|
14181
|
+
// be granted it. It does NOT bar a blueprint's own top-level `namespaces[]`
|
|
14182
|
+
// FORMAT field (@vectros-ai/blueprints), which deliberately DOES write this
|
|
14183
|
+
// same registry — the same "DISCRETIONARY vs UNCONDITIONAL" distinction
|
|
14184
|
+
// {@link PARTIALLY_ADMITTED_OPS} draws for `entities`, applied here: the
|
|
14185
|
+
// write rides `provisioning:c` (non-grantable — no partner role can ever
|
|
14186
|
+
// hold it, the same bearer `keys`/`app-contexts` writes already require)
|
|
14187
|
+
// and the platform confines it to the blueprint's OWN context
|
|
14188
|
+
// unconditionally, never tenant-wide, so the reach is bounded to what the
|
|
14189
|
+
// blueprint already provisions, never the pack's say-so alone. See
|
|
14190
|
+
// `@vectros-ai/blueprints`' `namespaces[]` doc for the full design.
|
|
13266
14191
|
"namespaces"
|
|
13267
14192
|
];
|
|
14193
|
+
var PARTIALLY_ADMITTED_OPS = /* @__PURE__ */ new Map([
|
|
14194
|
+
["profiles", /* @__PURE__ */ new Set(["r", "u", "d"])]
|
|
14195
|
+
]);
|
|
14196
|
+
var SELF_QUALIFIED_OPS = /* @__PURE__ */ new Map([
|
|
14197
|
+
["profiles", /* @__PURE__ */ new Set(["c"])]
|
|
14198
|
+
]);
|
|
14199
|
+
var UNQUALIFIABLE_WITH_ANY_TAIL = /* @__PURE__ */ new Map([
|
|
14200
|
+
["profiles", /* @__PURE__ */ new Set(["r"])]
|
|
14201
|
+
]);
|
|
13268
14202
|
var VALID_OPS = /* @__PURE__ */ new Set(["c", "r", "u", "d"]);
|
|
13269
14203
|
var DATA_PLANE_SET = new Set(DATA_PLANE_ALLOWLIST);
|
|
13270
14204
|
var CONTROL_PLANE_SET = new Set(KNOWN_CONTROL_PLANE_RESOURCES);
|
|
@@ -13281,6 +14215,59 @@ ${summary}`
|
|
|
13281
14215
|
this.rejected = rejected;
|
|
13282
14216
|
}
|
|
13283
14217
|
};
|
|
14218
|
+
var GRANTED_CAPABILITIES_ALLOWLIST = ["member-lifecycle"];
|
|
14219
|
+
var GRANTED_CAPABILITIES_SET = new Set(GRANTED_CAPABILITIES_ALLOWLIST);
|
|
14220
|
+
var CAPABILITIES_HINT = GRANTED_CAPABILITIES_ALLOWLIST.join("/");
|
|
14221
|
+
var CapabilityGateError = class extends Error {
|
|
14222
|
+
rejected;
|
|
14223
|
+
constructor(rejected) {
|
|
14224
|
+
const summary = rejected.map((r) => ` \u2022 ${r.action}: ${r.reason}`).join("\n");
|
|
14225
|
+
super(
|
|
14226
|
+
`Capability gate rejected ${rejected.length} requested capability(ies); the bootstrap loader grants (${CAPABILITIES_HINT}) only and mints nothing on reject:
|
|
14227
|
+
${summary}`
|
|
14228
|
+
);
|
|
14229
|
+
this.name = "CapabilityGateError";
|
|
14230
|
+
this.rejected = rejected;
|
|
14231
|
+
}
|
|
14232
|
+
};
|
|
14233
|
+
function evaluateCapabilityToken(name) {
|
|
14234
|
+
const token = name.trim();
|
|
14235
|
+
if (token.length === 0) {
|
|
14236
|
+
return { action: name, reason: "empty capability name" };
|
|
14237
|
+
}
|
|
14238
|
+
if (!GRANTED_CAPABILITIES_SET.has(token)) {
|
|
14239
|
+
return {
|
|
14240
|
+
action: name,
|
|
14241
|
+
reason: `requests capability '${token}', which the bootstrap loader does not grant (${CAPABILITIES_HINT} only). A capability outside this list must be requested deliberately in the developer portal, never via the bootstrap loader`
|
|
14242
|
+
};
|
|
14243
|
+
}
|
|
14244
|
+
return null;
|
|
14245
|
+
}
|
|
14246
|
+
function evaluateCapabilityGate(capabilities) {
|
|
14247
|
+
const accepted = [];
|
|
14248
|
+
const rejected = [];
|
|
14249
|
+
for (const name of capabilities ?? []) {
|
|
14250
|
+
const rejection = evaluateCapabilityToken(name);
|
|
14251
|
+
if (rejection) {
|
|
14252
|
+
rejected.push(rejection);
|
|
14253
|
+
} else {
|
|
14254
|
+
accepted.push(name);
|
|
14255
|
+
}
|
|
14256
|
+
}
|
|
14257
|
+
return { ok: rejected.length === 0, accepted, rejected };
|
|
14258
|
+
}
|
|
14259
|
+
function assertCapabilityGate(capabilities) {
|
|
14260
|
+
const result = evaluateCapabilityGate(capabilities);
|
|
14261
|
+
if (!result.ok) {
|
|
14262
|
+
throw new CapabilityGateError(result.rejected);
|
|
14263
|
+
}
|
|
14264
|
+
}
|
|
14265
|
+
function firstInvalidOp(ops) {
|
|
14266
|
+
for (const ch of ops) {
|
|
14267
|
+
if (!VALID_OPS.has(ch)) return ch;
|
|
14268
|
+
}
|
|
14269
|
+
return void 0;
|
|
14270
|
+
}
|
|
13284
14271
|
function evaluateScopeToken(action) {
|
|
13285
14272
|
const token = action.trim();
|
|
13286
14273
|
if (token.length === 0) {
|
|
@@ -13302,6 +14289,48 @@ function evaluateScopeToken(action) {
|
|
|
13302
14289
|
};
|
|
13303
14290
|
}
|
|
13304
14291
|
if (!DATA_PLANE_SET.has(resource)) {
|
|
14292
|
+
const admittedOps = PARTIALLY_ADMITTED_OPS.get(resource);
|
|
14293
|
+
const selfOnlyOps = SELF_QUALIFIED_OPS.get(resource);
|
|
14294
|
+
if (admittedOps || selfOnlyOps) {
|
|
14295
|
+
const admittedHint = [.../* @__PURE__ */ new Set([...admittedOps ?? [], ...selfOnlyOps ?? []])].sort().join("/");
|
|
14296
|
+
if (ops === "*") {
|
|
14297
|
+
return {
|
|
14298
|
+
action,
|
|
14299
|
+
reason: `requests '${resource}:*', which includes operation(s) outside the narrow allowlist (${admittedHint}) admitted for this control-plane resource. Request only the specific operations you need`
|
|
14300
|
+
};
|
|
14301
|
+
}
|
|
14302
|
+
const invalidOp2 = firstInvalidOp(ops);
|
|
14303
|
+
if (invalidOp2) {
|
|
14304
|
+
return {
|
|
14305
|
+
action,
|
|
14306
|
+
reason: `invalid operation character '${invalidOp2}' (valid operations are c, r, u, d, or '*')`
|
|
14307
|
+
};
|
|
14308
|
+
}
|
|
14309
|
+
const qualifier = parts.length > 2 ? parts.slice(2).join(":") : void 0;
|
|
14310
|
+
const isSelfQualified = qualifier === "self";
|
|
14311
|
+
const unqualifiableOps = UNQUALIFIABLE_WITH_ANY_TAIL.get(resource);
|
|
14312
|
+
for (const ch of ops) {
|
|
14313
|
+
if (qualifier !== void 0 && unqualifiableOps?.has(ch)) {
|
|
14314
|
+
return {
|
|
14315
|
+
action,
|
|
14316
|
+
reason: `requests '${resource}:${ch}' alongside a qualifier ('${qualifier}'), but the platform does not resolve a qualifier for '${ch}' on '${resource}' at all \u2014 it will reject the WHOLE entry as malformed, not just ignore the qualifier. Split into separate entries (e.g. '${resource}:${ch}' + the other op(s) with their qualifier).`
|
|
14317
|
+
};
|
|
14318
|
+
}
|
|
14319
|
+
if (admittedOps?.has(ch)) continue;
|
|
14320
|
+
if (selfOnlyOps?.has(ch)) {
|
|
14321
|
+
if (isSelfQualified) continue;
|
|
14322
|
+
return {
|
|
14323
|
+
action,
|
|
14324
|
+
reason: `requests control-plane scope '${resource}:${ch}' without the required ':self' qualifier. A bare or arbitrarily-qualified '${resource}:${ch}' is outside the narrow allowlist (${admittedHint}) admitted for this resource \u2014 only '${resource}:${ch}:self' is. That operation must otherwise be done deliberately in the developer portal, never via the bootstrap loader`
|
|
14325
|
+
};
|
|
14326
|
+
}
|
|
14327
|
+
return {
|
|
14328
|
+
action,
|
|
14329
|
+
reason: `requests control-plane scope '${resource}:${ch}', which is outside the narrow allowlist (${admittedHint}) admitted for this resource. That operation must be done deliberately in the developer portal, never via the bootstrap loader`
|
|
14330
|
+
};
|
|
14331
|
+
}
|
|
14332
|
+
return null;
|
|
14333
|
+
}
|
|
13305
14334
|
if (CONTROL_PLANE_SET.has(resource)) {
|
|
13306
14335
|
return {
|
|
13307
14336
|
action,
|
|
@@ -13316,13 +14345,12 @@ function evaluateScopeToken(action) {
|
|
|
13316
14345
|
if (ops === "*") {
|
|
13317
14346
|
return null;
|
|
13318
14347
|
}
|
|
13319
|
-
|
|
13320
|
-
|
|
13321
|
-
|
|
13322
|
-
|
|
13323
|
-
|
|
13324
|
-
|
|
13325
|
-
}
|
|
14348
|
+
const invalidOp = firstInvalidOp(ops);
|
|
14349
|
+
if (invalidOp) {
|
|
14350
|
+
return {
|
|
14351
|
+
action,
|
|
14352
|
+
reason: `invalid operation character '${invalidOp}' (valid operations are c, r, u, d, or '*')`
|
|
14353
|
+
};
|
|
13326
14354
|
}
|
|
13327
14355
|
return null;
|
|
13328
14356
|
}
|
|
@@ -13345,6 +14373,16 @@ function assertScopeGate(allowedActions) {
|
|
|
13345
14373
|
throw new ScopeGateError(result.rejected);
|
|
13346
14374
|
}
|
|
13347
14375
|
}
|
|
14376
|
+
function assertBlueprintGates(blueprint) {
|
|
14377
|
+
assertScopeGate(blueprint.accessProfile.allowedActions);
|
|
14378
|
+
assertCapabilityGate(blueprint.accessProfile.capabilities);
|
|
14379
|
+
for (const clauses of Object.values(blueprint.roles ?? {})) {
|
|
14380
|
+
for (const clause of clauses) {
|
|
14381
|
+
assertScopeGate(clause.allowedActions);
|
|
14382
|
+
assertCapabilityGate(clause.capabilities);
|
|
14383
|
+
}
|
|
14384
|
+
}
|
|
14385
|
+
}
|
|
13348
14386
|
|
|
13349
14387
|
// src/machine-id.ts
|
|
13350
14388
|
import { promises as fs } from "fs";
|
|
@@ -13449,6 +14487,68 @@ async function resolveApplyTimeIdentities(blueprint, client, log) {
|
|
|
13449
14487
|
function actionOf(created) {
|
|
13450
14488
|
return created ? "created" : "reconciled";
|
|
13451
14489
|
}
|
|
14490
|
+
async function registerNamespaceIdempotently(client, contextIdForRequest, decl) {
|
|
14491
|
+
const body = {
|
|
14492
|
+
namespace: decl.namespace,
|
|
14493
|
+
entityBacked: decl.entityBacked,
|
|
14494
|
+
specificityRank: decl.specificityRank,
|
|
14495
|
+
membershipRecordType: decl.membershipRecordType,
|
|
14496
|
+
membershipTargetField: decl.membershipTargetField,
|
|
14497
|
+
membershipLevelField: decl.membershipLevelField,
|
|
14498
|
+
membershipLevels: decl.membershipLevels
|
|
14499
|
+
};
|
|
14500
|
+
try {
|
|
14501
|
+
await client.registerNamespace({ contextId: contextIdForRequest, body });
|
|
14502
|
+
return "created";
|
|
14503
|
+
} catch (err) {
|
|
14504
|
+
const info = extractServerError(err);
|
|
14505
|
+
if (info.statusCode !== 400) throw err;
|
|
14506
|
+
let existing;
|
|
14507
|
+
try {
|
|
14508
|
+
existing = await client.getNamespace({ namespace: decl.namespace, contextId: contextIdForRequest });
|
|
14509
|
+
} catch {
|
|
14510
|
+
throw err;
|
|
14511
|
+
}
|
|
14512
|
+
if (existing.contextId !== contextIdForRequest) throw err;
|
|
14513
|
+
const mismatch = describeNamespaceMismatch(decl, existing);
|
|
14514
|
+
if (mismatch) {
|
|
14515
|
+
throw new Error(
|
|
14516
|
+
`namespace '${decl.namespace}' is already registered with different settings than this blueprint declares (${mismatch}) \u2014 this looks like a collision with another blueprint or a manual registration, not a benign re-apply. Resolve the disagreement (or rename this blueprint's namespace) before re-applying.`
|
|
14517
|
+
);
|
|
14518
|
+
}
|
|
14519
|
+
return "reconciled";
|
|
14520
|
+
}
|
|
14521
|
+
}
|
|
14522
|
+
function describeNamespaceMismatch(decl, existing) {
|
|
14523
|
+
const mismatches = [];
|
|
14524
|
+
const declaredEntityBacked = decl.entityBacked ?? false;
|
|
14525
|
+
const existingEntityBacked = existing.entityBacked ?? false;
|
|
14526
|
+
if (declaredEntityBacked !== existingEntityBacked) {
|
|
14527
|
+
mismatches.push(
|
|
14528
|
+
`entityBacked: declared ${JSON.stringify(declaredEntityBacked)}, registered ${JSON.stringify(existingEntityBacked)}`
|
|
14529
|
+
);
|
|
14530
|
+
}
|
|
14531
|
+
const fields = [
|
|
14532
|
+
"specificityRank",
|
|
14533
|
+
"membershipRecordType",
|
|
14534
|
+
"membershipTargetField",
|
|
14535
|
+
"membershipLevelField"
|
|
14536
|
+
];
|
|
14537
|
+
for (const field of fields) {
|
|
14538
|
+
const declared = decl[field];
|
|
14539
|
+
const existingValue = existing[field];
|
|
14540
|
+
if (declared === void 0 && (existingValue === void 0 || existingValue === null)) continue;
|
|
14541
|
+
if (declared === existingValue) continue;
|
|
14542
|
+
mismatches.push(`${field}: declared ${JSON.stringify(declared)}, registered ${JSON.stringify(existingValue)}`);
|
|
14543
|
+
}
|
|
14544
|
+
const declaredLevels = decl.membershipLevels;
|
|
14545
|
+
const existingLevels = existing.membershipLevels;
|
|
14546
|
+
const levelsDiffer = declaredLevels === void 0 ? existingLevels != null && existingLevels.length > 0 : JSON.stringify([...declaredLevels].sort()) !== JSON.stringify([...existingLevels ?? []].sort());
|
|
14547
|
+
if (levelsDiffer) {
|
|
14548
|
+
mismatches.push(`membershipLevels: declared ${JSON.stringify(declaredLevels)}, registered ${JSON.stringify(existingLevels)}`);
|
|
14549
|
+
}
|
|
14550
|
+
return mismatches.length > 0 ? mismatches.join("; ") : null;
|
|
14551
|
+
}
|
|
13452
14552
|
var LostKeyError = class extends Error {
|
|
13453
14553
|
constructor(keyName) {
|
|
13454
14554
|
super(
|
|
@@ -13459,6 +14559,20 @@ var LostKeyError = class extends Error {
|
|
|
13459
14559
|
}
|
|
13460
14560
|
keyName;
|
|
13461
14561
|
};
|
|
14562
|
+
var ExistingPrincipalNotConfirmedError = class extends Error {
|
|
14563
|
+
constructor(contextId, userId, externalId) {
|
|
14564
|
+
super(
|
|
14565
|
+
`About to bind a NEW scoped key to a principal that already existed in this tenant before this run (externalId '${externalId}', userId '${userId}', context '${contextId}'). This is either an intentional multi-machine setup for an integration you already own, or this blueprint's servicePrincipal.externalId collided with an unrelated existing identity \u2014 confirm before a new credential is minted bound to it.`
|
|
14566
|
+
);
|
|
14567
|
+
this.contextId = contextId;
|
|
14568
|
+
this.userId = userId;
|
|
14569
|
+
this.externalId = externalId;
|
|
14570
|
+
this.name = "ExistingPrincipalNotConfirmedError";
|
|
14571
|
+
}
|
|
14572
|
+
contextId;
|
|
14573
|
+
userId;
|
|
14574
|
+
externalId;
|
|
14575
|
+
};
|
|
13462
14576
|
function mapEnumValues(enumValues) {
|
|
13463
14577
|
if (!enumValues) return void 0;
|
|
13464
14578
|
return enumValues.map((value) => ({ value }));
|
|
@@ -13533,7 +14647,8 @@ function toCapabilities(capabilities) {
|
|
|
13533
14647
|
function toRoleScopeClauses(clauses) {
|
|
13534
14648
|
return clauses.map((c) => ({
|
|
13535
14649
|
allowed_actions: c.allowedActions,
|
|
13536
|
-
data_scope: c.dataScope
|
|
14650
|
+
data_scope: c.dataScope,
|
|
14651
|
+
granted_capabilities: c.capabilities
|
|
13537
14652
|
}));
|
|
13538
14653
|
}
|
|
13539
14654
|
var UnresolvedIdentitiesError = class extends Error {
|
|
@@ -13553,11 +14668,18 @@ function assertNoUnresolvedIdentities(blueprint) {
|
|
|
13553
14668
|
}
|
|
13554
14669
|
}
|
|
13555
14670
|
async function runLoader(client, input) {
|
|
13556
|
-
const {
|
|
13557
|
-
|
|
13558
|
-
|
|
13559
|
-
|
|
13560
|
-
|
|
14671
|
+
const {
|
|
14672
|
+
blueprint,
|
|
14673
|
+
tenantId,
|
|
14674
|
+
machineId,
|
|
14675
|
+
rotate = false,
|
|
14676
|
+
skipSeed = false,
|
|
14677
|
+
readExistingKey,
|
|
14678
|
+
persistMintedKey,
|
|
14679
|
+
confirmExistingPrincipal,
|
|
14680
|
+
log
|
|
14681
|
+
} = input;
|
|
14682
|
+
assertBlueprintGates(blueprint);
|
|
13561
14683
|
assertNoUnresolvedIdentities(blueprint);
|
|
13562
14684
|
const ctxResp = await client.auth.createAppContext({ body: { contextId: blueprint.contextId, name: contextNameOf(blueprint) } });
|
|
13563
14685
|
const contextAction = actionOf(ctxResp.created);
|
|
@@ -13575,7 +14697,65 @@ async function runLoader(client, input) {
|
|
|
13575
14697
|
if (!userId) {
|
|
13576
14698
|
throw new Error("Service-principal create returned no user id; cannot bind AccessProfile/key.");
|
|
13577
14699
|
}
|
|
13578
|
-
|
|
14700
|
+
const userAction = actionOf(user.created);
|
|
14701
|
+
log.debug(
|
|
14702
|
+
{ component: "loader", externalId: blueprint.servicePrincipal.externalId, userId, userAction },
|
|
14703
|
+
"service principal ensured"
|
|
14704
|
+
);
|
|
14705
|
+
const localKeyForContext = await readExistingKey(blueprint.contextId);
|
|
14706
|
+
const storedPrincipalId = localKeyForContext?.principalId;
|
|
14707
|
+
const localKeyNamesThisPrincipal = storedPrincipalId === `usr_${userId}` || storedPrincipalId === userId;
|
|
14708
|
+
if (userAction === "reconciled" && !localKeyNamesThisPrincipal) {
|
|
14709
|
+
const proceed = confirmExistingPrincipal ? await confirmExistingPrincipal({
|
|
14710
|
+
contextId: blueprint.contextId,
|
|
14711
|
+
userId,
|
|
14712
|
+
externalId: blueprint.servicePrincipal.externalId
|
|
14713
|
+
}) : true;
|
|
14714
|
+
if (!proceed) {
|
|
14715
|
+
throw new ExistingPrincipalNotConfirmedError(blueprint.contextId, userId, blueprint.servicePrincipal.externalId);
|
|
14716
|
+
}
|
|
14717
|
+
log.debug(
|
|
14718
|
+
{ component: "loader", contextId: blueprint.contextId, userId },
|
|
14719
|
+
"existing-principal collision confirmed (or unwired) \u2014 proceeding with any further write to this principal"
|
|
14720
|
+
);
|
|
14721
|
+
}
|
|
14722
|
+
const needsContextPinnedBootstrap = (blueprint.issuers ?? []).length > 0 || (blueprint.namespaces ?? []).length > 0;
|
|
14723
|
+
const ctxBootstrapClient = needsContextPinnedBootstrap ? input.remintBootstrapForContext ? await input.remintBootstrapForContext(blueprint.contextId) : client : void 0;
|
|
14724
|
+
if (ctxBootstrapClient && input.remintBootstrapForContext) {
|
|
14725
|
+
log.debug({ component: "loader", contextId: blueprint.contextId }, "re-minted context-pinned bootstrap client");
|
|
14726
|
+
}
|
|
14727
|
+
const namespaceActions = [];
|
|
14728
|
+
for (const ns of blueprint.namespaces ?? []) {
|
|
14729
|
+
const action = await registerNamespaceIdempotently(ctxBootstrapClient.identity, blueprint.contextId, ns);
|
|
14730
|
+
namespaceActions.push({ namespace: ns.namespace, action });
|
|
14731
|
+
log.debug({ component: "loader", namespace: ns.namespace, contextId: blueprint.contextId, action }, "namespace ensured");
|
|
14732
|
+
}
|
|
14733
|
+
const issuerActions = [];
|
|
14734
|
+
for (const issuer of blueprint.issuers ?? []) {
|
|
14735
|
+
const issuerResp = await ctxBootstrapClient.auth.registerIssuer({
|
|
14736
|
+
issuerId: issuer.issuerId,
|
|
14737
|
+
issuer: issuer.issuer,
|
|
14738
|
+
jwksUri: issuer.jwksUri,
|
|
14739
|
+
audience: issuer.audience,
|
|
14740
|
+
contextId: issuer.contextId,
|
|
14741
|
+
subClaim: issuer.subClaim,
|
|
14742
|
+
emailClaim: issuer.emailClaim,
|
|
14743
|
+
selfSignupPolicies: issuer.selfSignupPolicies?.map((p) => ({
|
|
14744
|
+
signup_type: p.signupType,
|
|
14745
|
+
role_id: p.roleId
|
|
14746
|
+
}))
|
|
14747
|
+
});
|
|
14748
|
+
const action = actionOf(issuerResp.created);
|
|
14749
|
+
issuerActions.push({ issuerId: issuer.issuerId, action });
|
|
14750
|
+
if (action === "reconciled") {
|
|
14751
|
+
log.warn(
|
|
14752
|
+
{ component: "loader", issuerId: issuer.issuerId },
|
|
14753
|
+
"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"
|
|
14754
|
+
);
|
|
14755
|
+
} else {
|
|
14756
|
+
log.debug({ component: "loader", issuerId: issuer.issuerId, action }, "issuer registered");
|
|
14757
|
+
}
|
|
14758
|
+
}
|
|
13579
14759
|
const ctxClient = input.remintForContext ? await input.remintForContext(blueprint.contextId) : client;
|
|
13580
14760
|
log.debug({ component: "loader", contextId: blueprint.contextId }, "re-minted per-context client");
|
|
13581
14761
|
const schemaIds = {};
|
|
@@ -13622,7 +14802,10 @@ async function runLoader(client, input) {
|
|
|
13622
14802
|
// literal JSON null, which is exactly what the platform's scope
|
|
13623
14803
|
// matcher tests via `allowedValues.contains(null)`. The cast bridges
|
|
13624
14804
|
// to the SDK's loosely-typed Record<string, Record<string,unknown>>.
|
|
13625
|
-
data_scope: blueprint.accessProfile.dataScope
|
|
14805
|
+
data_scope: blueprint.accessProfile.dataScope,
|
|
14806
|
+
// Already gate-validated (assertBlueprintGates, above this function's
|
|
14807
|
+
// call site) before this body is ever built.
|
|
14808
|
+
granted_capabilities: blueprint.accessProfile.capabilities
|
|
13626
14809
|
}
|
|
13627
14810
|
],
|
|
13628
14811
|
// Optional identity overrides — scope values the service key STAMPS onto
|
|
@@ -13680,7 +14863,7 @@ async function runLoader(client, input) {
|
|
|
13680
14863
|
keyId = keyResp.keyId ?? "";
|
|
13681
14864
|
rawKey = keyResp.rawKey;
|
|
13682
14865
|
if (keyId) {
|
|
13683
|
-
await persistMintedKey(blueprint.contextId, { keyId, rawKey, keyName, principalId: userId
|
|
14866
|
+
await persistMintedKey(blueprint.contextId, { keyId, rawKey, keyName, principalId: `usr_${userId}`, revokedKeyId });
|
|
13684
14867
|
}
|
|
13685
14868
|
if (keyAction !== "rotated") keyAction = "created";
|
|
13686
14869
|
} else {
|
|
@@ -13754,9 +14937,12 @@ async function runLoader(client, input) {
|
|
|
13754
14937
|
keyName,
|
|
13755
14938
|
keyAction,
|
|
13756
14939
|
contextAction,
|
|
14940
|
+
userAction,
|
|
13757
14941
|
profileAction,
|
|
13758
14942
|
schemaActions,
|
|
13759
14943
|
roleActions,
|
|
14944
|
+
issuerActions,
|
|
14945
|
+
namespaceActions,
|
|
13760
14946
|
rawKey,
|
|
13761
14947
|
keyId,
|
|
13762
14948
|
revokedKeyId,
|
|
@@ -13775,6 +14961,22 @@ function buildPlan(blueprint, opts = {}) {
|
|
|
13775
14961
|
kind: "principal",
|
|
13776
14962
|
summary: `principal '${blueprint.servicePrincipal.externalId}' (SERVICE)`
|
|
13777
14963
|
});
|
|
14964
|
+
for (const ns of blueprint.namespaces ?? []) {
|
|
14965
|
+
const ownerNote = `owned by '${blueprint.contextId}'`;
|
|
14966
|
+
const entityNote = ns.entityBacked ? ", entity-backed" : "";
|
|
14967
|
+
const membershipNote = ns.membershipRecordType ? `, membership: ${ns.membershipRecordType}.${ns.membershipTargetField}` + (ns.membershipLevelField ? ` (levels: ${ns.membershipLevels?.join("/")})` : "") : "";
|
|
14968
|
+
steps.push({
|
|
14969
|
+
kind: "namespace",
|
|
14970
|
+
summary: `namespace '${ns.namespace}' (${ownerNote}${entityNote}${membershipNote}) [${BLUEPRINT_FIELD_PHASES.namespaces}-token phase]`
|
|
14971
|
+
});
|
|
14972
|
+
}
|
|
14973
|
+
for (const iss of blueprint.issuers ?? []) {
|
|
14974
|
+
const selfSignup = iss.selfSignupPolicies?.length ? `, self-signup: ${iss.selfSignupPolicies.length} polic${iss.selfSignupPolicies.length === 1 ? "y" : "ies"}` : "";
|
|
14975
|
+
steps.push({
|
|
14976
|
+
kind: "issuer",
|
|
14977
|
+
summary: `issuer '${iss.issuerId}' (${iss.issuer} \u2192 context '${iss.contextId}'${selfSignup}) [${BLUEPRINT_FIELD_PHASES.issuers}-token phase]`
|
|
14978
|
+
});
|
|
14979
|
+
}
|
|
13778
14980
|
for (const s of blueprint.schemas) {
|
|
13779
14981
|
const lookups = toLookupDefs(s.lookupFields)?.map((l) => l.fieldNames ? `(${l.fieldNames.join("+")})` : l.fieldName) ?? [];
|
|
13780
14982
|
const lookupNote = lookups.length ? ` lookup: [${lookups.join(", ")}]` : "";
|
|
@@ -13790,15 +14992,18 @@ function buildPlan(blueprint, opts = {}) {
|
|
|
13790
14992
|
}
|
|
13791
14993
|
const actions = blueprint.accessProfile.allowedActions;
|
|
13792
14994
|
const scopeNote = blueprint.accessProfile.dataScope && Object.keys(blueprint.accessProfile.dataScope).length ? ` dataScope: ${Object.keys(blueprint.accessProfile.dataScope).join(", ")}` : "";
|
|
14995
|
+
const capabilityNote = blueprint.accessProfile.capabilities?.length ? ` capabilities: [${blueprint.accessProfile.capabilities.join(", ")}]` : "";
|
|
13793
14996
|
steps.push({
|
|
13794
14997
|
kind: "profile",
|
|
13795
|
-
summary: `profile scopes [${actions.join(", ")}] (data-plane \u2713)${scopeNote}`
|
|
14998
|
+
summary: `profile scopes [${actions.join(", ")}] (data-plane \u2713)${scopeNote}${capabilityNote}`
|
|
13796
14999
|
});
|
|
13797
15000
|
for (const [roleId, clauses] of Object.entries(blueprint.roles ?? {})) {
|
|
13798
15001
|
const actionSet = [...new Set(clauses.flatMap((c) => c.allowedActions))];
|
|
15002
|
+
const capSet = [...new Set(clauses.flatMap((c) => c.capabilities ?? []))];
|
|
15003
|
+
const roleCapabilityNote = capSet.length ? `, capabilities [${capSet.join(", ")}]` : "";
|
|
13799
15004
|
steps.push({
|
|
13800
15005
|
kind: "role",
|
|
13801
|
-
summary: `role '${roleId}' (${clauses.length} clause${clauses.length === 1 ? "" : "s"}, actions [${actionSet.join(", ")}])`
|
|
15006
|
+
summary: `role '${roleId}' (${clauses.length} clause${clauses.length === 1 ? "" : "s"}, actions [${actionSet.join(", ")}]${roleCapabilityNote})`
|
|
13802
15007
|
});
|
|
13803
15008
|
}
|
|
13804
15009
|
steps.push({ kind: "key", summary: `key ${blueprint.name}-<machine> (mint 1 scoped ssk_*)` });
|
|
@@ -13815,8 +15020,24 @@ function buildPlan(blueprint, opts = {}) {
|
|
|
13815
15020
|
}
|
|
13816
15021
|
function renderPlan(blueprint, steps) {
|
|
13817
15022
|
const header = ` Plan for blueprint '${blueprint.name}' (v${blueprint.version}) \u2192 context '${blueprint.contextId}':`;
|
|
13818
|
-
const
|
|
13819
|
-
|
|
15023
|
+
const legendLines = [
|
|
15024
|
+
` Apply is a non-destructive reconcile \u2014 each item is created if absent, else`,
|
|
15025
|
+
` updated in place to match. Nothing is dropped or recreated.`
|
|
15026
|
+
];
|
|
15027
|
+
if (steps.some((s) => s.kind === "issuer")) {
|
|
15028
|
+
legendLines.push(
|
|
15029
|
+
` Exception: issuers cannot be updated once registered \u2014 an existing`,
|
|
15030
|
+
` issuer is left unchanged, not reconciled to match.`
|
|
15031
|
+
);
|
|
15032
|
+
}
|
|
15033
|
+
if (steps.some((s) => s.kind === "namespace")) {
|
|
15034
|
+
legendLines.push(
|
|
15035
|
+
` Exception: namespace registration is not idempotent \u2014 a re-apply matching`,
|
|
15036
|
+
` an existing registration converges silently, but one that disagrees FAILS`,
|
|
15037
|
+
` the apply rather than reconciling it.`
|
|
15038
|
+
);
|
|
15039
|
+
}
|
|
15040
|
+
const legend = legendLines.join("\n");
|
|
13820
15041
|
const lines = steps.map((s) => ` + ${s.summary}`);
|
|
13821
15042
|
return [header, legend, "", ...lines].join("\n");
|
|
13822
15043
|
}
|
|
@@ -14064,6 +15285,15 @@ var OsKeychainSecretStore = class {
|
|
|
14064
15285
|
* against the real Windows store on every MR — the dev-machine probe is a point
|
|
14065
15286
|
* measurement, not a regression gate. (Tracked internally; this file ships to
|
|
14066
15287
|
* public npm, so no tracker number here.)
|
|
15288
|
+
*
|
|
15289
|
+
* ⚠️ THIS METHOD'S FAIL-CLOSED GUARANTEE IS AN UPSTREAM BEHAVIOURAL CONTRACT, NOT
|
|
15290
|
+
* SOMETHING THIS CODE ENFORCES. `@napi-rs/keyring` is pinned to an EXACT version
|
|
15291
|
+
* in `package.json` for exactly this reason: if a future release returned `[]`
|
|
15292
|
+
* from `findCredentials()` on a locked store instead of throwing, the `catch`
|
|
15293
|
+
* block above would never run — `!.some(...)` on an empty array reads as "gone",
|
|
15294
|
+
* `deleteSecret` returns `true`, and `remove`/`prune` drop the index row of a
|
|
15295
|
+
* still-live credential. A version bump of this dependency is a security review
|
|
15296
|
+
* of the paragraph above against the new release, not a routine dependency bump.
|
|
14067
15297
|
*/
|
|
14068
15298
|
async deleteSecret(ref) {
|
|
14069
15299
|
try {
|
|
@@ -14997,6 +16227,26 @@ The key itself WAS created server-side and is live \u2014 its id is ${entry.keyI
|
|
|
14997
16227
|
const entry = await this.getActive();
|
|
14998
16228
|
return entry ? this.readSecret(entry) : void 0;
|
|
14999
16229
|
}
|
|
16230
|
+
/**
|
|
16231
|
+
* The ACTIVE entry's tenant (test/live), or undefined if there is no active
|
|
16232
|
+
* entry or its tenant cannot be determined — WITHOUT migrating its secret.
|
|
16233
|
+
*
|
|
16234
|
+
* Deliberately NOT built on {@link resolveActiveSecret}/{@link readSecret}:
|
|
16235
|
+
* both migrate the secret to this machine's preferred store as a side effect
|
|
16236
|
+
* of reading it, and a caller that only wants to CLASSIFY the active identity
|
|
16237
|
+
* (e.g. "is it safe to activate a different one over it?") must not silently
|
|
16238
|
+
* relocate — and take the cross-process index lock for — a credential it has
|
|
16239
|
+
* no other reason to touch. Same non-migrating shape {@link assess} already
|
|
16240
|
+
* uses per entry (`store.getSecret` directly, never `readSecret`).
|
|
16241
|
+
*/
|
|
16242
|
+
async activeTenant() {
|
|
16243
|
+
const entry = await this.getActive();
|
|
16244
|
+
if (!entry) return void 0;
|
|
16245
|
+
const store = this.storeFor(entry);
|
|
16246
|
+
if (!store) return void 0;
|
|
16247
|
+
const secret = await store.getSecret(entry.secretRef);
|
|
16248
|
+
return secret !== void 0 ? keyTenant(secret) : void 0;
|
|
16249
|
+
}
|
|
15000
16250
|
/**
|
|
15001
16251
|
* Read one entry's secret, moving it to this machine's preferred store if it is
|
|
15002
16252
|
* somewhere else.
|
|
@@ -15123,26 +16373,31 @@ import * as readline from "readline/promises";
|
|
|
15123
16373
|
async function runKeyringList(opts, deps = {}) {
|
|
15124
16374
|
try {
|
|
15125
16375
|
const keyring = deps.keyring ?? await Keyring.open();
|
|
15126
|
-
const entries = await keyring.list();
|
|
15127
|
-
const active = await keyring.activeAlias();
|
|
15128
16376
|
if (opts.json) {
|
|
16377
|
+
const entries = await keyring.list();
|
|
16378
|
+
const active = await keyring.activeAlias();
|
|
15129
16379
|
process.stdout.write(`${formatJson({ active, entries })}
|
|
15130
16380
|
`);
|
|
15131
16381
|
return 0;
|
|
15132
16382
|
}
|
|
15133
|
-
|
|
16383
|
+
const { health } = await assessEntries(keyring);
|
|
16384
|
+
if (health.length === 0) {
|
|
15134
16385
|
process.stdout.write("\n (no keyring entries yet \u2014 run `vectros join <context>` to add one)\n");
|
|
15135
16386
|
return 0;
|
|
15136
16387
|
}
|
|
15137
16388
|
process.stdout.write(
|
|
15138
|
-
"\n" + formatTable(
|
|
15139
|
-
{ header: "", get: (
|
|
15140
|
-
{ header: "alias", get: (
|
|
15141
|
-
{ header: "context", get: (
|
|
15142
|
-
{ header: "principal", get: (
|
|
15143
|
-
{ header: "role", get: (
|
|
15144
|
-
|
|
15145
|
-
|
|
16389
|
+
"\n" + formatTable(health, [
|
|
16390
|
+
{ header: "", get: (h) => h.active ? "*" : "" },
|
|
16391
|
+
{ header: "alias", get: (h) => h.entry.alias },
|
|
16392
|
+
{ header: "context", get: (h) => h.entry.context },
|
|
16393
|
+
{ header: "principal", get: (h) => h.entry.principalId },
|
|
16394
|
+
{ header: "role", get: (h) => h.entry.role },
|
|
16395
|
+
// `tenant` (test/live) replaces the old `env` (staging/production) column
|
|
16396
|
+
// here: env is an internal-team concern an external developer's CLI never
|
|
16397
|
+
// sees, while tenant is the distinction every user routinely holds two
|
|
16398
|
+
// credentials side by side over.
|
|
16399
|
+
{ header: "tenant", get: (h) => tenantCell(h, { upper: true }) },
|
|
16400
|
+
{ header: "key id", get: (h) => h.entry.keyId }
|
|
15146
16401
|
]) + "\n"
|
|
15147
16402
|
);
|
|
15148
16403
|
return 0;
|
|
@@ -15324,6 +16579,10 @@ async function assessEntries(keyring) {
|
|
|
15324
16579
|
}))
|
|
15325
16580
|
};
|
|
15326
16581
|
}
|
|
16582
|
+
function tenantCell(h, opts) {
|
|
16583
|
+
if (h.status !== "ok") return void 0;
|
|
16584
|
+
return h.tenant ?? (opts.upper ? "UNKNOWN" : "unknown");
|
|
16585
|
+
}
|
|
15327
16586
|
function describeEntry(h) {
|
|
15328
16587
|
return `${h.entry.alias} (${h.entry.context}, ${h.entry.keyId})`;
|
|
15329
16588
|
}
|
|
@@ -15468,9 +16727,7 @@ async function runKeyringDoctor(opts, deps = {}) {
|
|
|
15468
16727
|
// `unknown` can escalate; one that sees a confident wrong answer cannot.
|
|
15469
16728
|
store: storeKindOf(h.entry) ?? "unknown",
|
|
15470
16729
|
secret: h.status,
|
|
15471
|
-
|
|
15472
|
-
// live key" from "we could not read the shape of this one".
|
|
15473
|
-
tenant: h.status === "ok" ? h.tenant ?? "unknown" : void 0
|
|
16730
|
+
tenant: tenantCell(h, { upper: false })
|
|
15474
16731
|
})),
|
|
15475
16732
|
liveKeys: liveKeys.map((h) => h.entry.alias),
|
|
15476
16733
|
findings
|
|
@@ -15485,7 +16742,7 @@ async function runKeyringDoctor(opts, deps = {}) {
|
|
|
15485
16742
|
{ header: "", get: (h) => h.active ? "*" : "" },
|
|
15486
16743
|
{ header: "alias", get: (h) => h.entry.alias },
|
|
15487
16744
|
{ header: "context", get: (h) => h.entry.context },
|
|
15488
|
-
{ header: "tenant", get: (h) => h
|
|
16745
|
+
{ header: "tenant", get: (h) => tenantCell(h, { upper: true }) },
|
|
15489
16746
|
{ header: "key id", get: (h) => h.entry.keyId },
|
|
15490
16747
|
{ header: "secret", get: (h) => h.status === "ok" ? "ok" : h.status.toUpperCase() }
|
|
15491
16748
|
]) + "\n"
|
|
@@ -15671,6 +16928,55 @@ function revokeWarning(targets) {
|
|
|
15671
16928
|
}
|
|
15672
16929
|
|
|
15673
16930
|
// src/minted-key-store.ts
|
|
16931
|
+
function keyringAliasFor(alias, tenant) {
|
|
16932
|
+
if (tenant === "live" && alias.endsWith(".test")) {
|
|
16933
|
+
throw new Error(
|
|
16934
|
+
`'${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.`
|
|
16935
|
+
);
|
|
16936
|
+
}
|
|
16937
|
+
return tenant === "test" ? `${alias}.test` : alias;
|
|
16938
|
+
}
|
|
16939
|
+
function resolveActivateDecision(opts) {
|
|
16940
|
+
if (opts.explicitActivate !== void 0) return opts.explicitActivate;
|
|
16941
|
+
if (opts.tenant === "live") return true;
|
|
16942
|
+
return opts.currentActiveTenant !== "live";
|
|
16943
|
+
}
|
|
16944
|
+
async function determineActivateDecision(keyring, opts) {
|
|
16945
|
+
if (opts.explicitActivate !== void 0 || opts.tenant === "live") {
|
|
16946
|
+
return resolveActivateDecision(opts);
|
|
16947
|
+
}
|
|
16948
|
+
let currentActiveTenant;
|
|
16949
|
+
try {
|
|
16950
|
+
if (opts.hint && opts.hint.alias === await keyring.activeAlias()) {
|
|
16951
|
+
currentActiveTenant = keyTenant(opts.hint.secret);
|
|
16952
|
+
} else {
|
|
16953
|
+
currentActiveTenant = await keyring.activeTenant();
|
|
16954
|
+
}
|
|
16955
|
+
} catch {
|
|
16956
|
+
}
|
|
16957
|
+
return resolveActivateDecision({ ...opts, currentActiveTenant });
|
|
16958
|
+
}
|
|
16959
|
+
function tenantActivateMessage(opts) {
|
|
16960
|
+
if (opts.willActivate) {
|
|
16961
|
+
if (opts.tenant !== "test") return "";
|
|
16962
|
+
return `
|
|
16963
|
+
\u26A0 '${opts.alias}' is now your ACTIVE identity \u2014 a TEST tenant credential. Anything
|
|
16964
|
+
resolving a key with no explicit --alias (including the MCP server) follows it.
|
|
16965
|
+
`;
|
|
16966
|
+
}
|
|
16967
|
+
if (opts.explicitlyDeclined) {
|
|
16968
|
+
return `
|
|
16969
|
+
\u26A0 Stored '${opts.alias}' (${opts.tenant} tenant) without activating it \u2014 you passed --no-activate.
|
|
16970
|
+
Run \`vectros switch ${opts.alias}\` when you're ready to use it.
|
|
16971
|
+
`;
|
|
16972
|
+
}
|
|
16973
|
+
return `
|
|
16974
|
+
\u26A0 Stored '${opts.alias}' (test tenant) WITHOUT activating it \u2014 your currently-active
|
|
16975
|
+
identity is a LIVE credential, and switching it out from under you silently is
|
|
16976
|
+
exactly the mistake this check exists to prevent. Run \`vectros switch ${opts.alias}\`
|
|
16977
|
+
to use the test key, or re-run with --activate to do so automatically.
|
|
16978
|
+
`;
|
|
16979
|
+
}
|
|
15674
16980
|
function nowIso() {
|
|
15675
16981
|
return (/* @__PURE__ */ new Date()).toISOString();
|
|
15676
16982
|
}
|
|
@@ -15767,10 +17073,7 @@ async function storeMintedKey(keyring, binding, secret, opts = {}) {
|
|
|
15767
17073
|
// src/cli-bootstrap.ts
|
|
15768
17074
|
function validateBlueprint(input) {
|
|
15769
17075
|
const blueprint = parseBlueprint(input);
|
|
15770
|
-
|
|
15771
|
-
for (const clauses of Object.values(blueprint.roles ?? {})) {
|
|
15772
|
-
for (const clause of clauses) assertScopeGate(clause.allowedActions);
|
|
15773
|
-
}
|
|
17076
|
+
assertBlueprintGates(blueprint);
|
|
15774
17077
|
return blueprint;
|
|
15775
17078
|
}
|
|
15776
17079
|
var BLANK_SLATE = {
|
|
@@ -15787,10 +17090,12 @@ var BLANK_SLATE = {
|
|
|
15787
17090
|
};
|
|
15788
17091
|
function parseBootstrapArgs(argv) {
|
|
15789
17092
|
const args = {
|
|
17093
|
+
set: [],
|
|
15790
17094
|
env: "production",
|
|
15791
17095
|
yes: false,
|
|
15792
17096
|
noMerge: false,
|
|
15793
17097
|
rotate: false,
|
|
17098
|
+
confirmExistingPrincipal: false,
|
|
15794
17099
|
useScopedToken: false,
|
|
15795
17100
|
blankSlate: false,
|
|
15796
17101
|
noSeed: false,
|
|
@@ -15804,6 +17109,12 @@ function parseBootstrapArgs(argv) {
|
|
|
15804
17109
|
case "--blueprint":
|
|
15805
17110
|
args.blueprint = argv[++i];
|
|
15806
17111
|
break;
|
|
17112
|
+
case "--set":
|
|
17113
|
+
args.set.push(argv[++i]);
|
|
17114
|
+
break;
|
|
17115
|
+
case "--values":
|
|
17116
|
+
args.values = argv[++i];
|
|
17117
|
+
break;
|
|
15807
17118
|
case "--env": {
|
|
15808
17119
|
const v = argv[++i];
|
|
15809
17120
|
if (v !== "staging" && v !== "production") {
|
|
@@ -15836,6 +17147,9 @@ function parseBootstrapArgs(argv) {
|
|
|
15836
17147
|
case "--rotate":
|
|
15837
17148
|
args.rotate = true;
|
|
15838
17149
|
break;
|
|
17150
|
+
case "--confirm-existing-principal":
|
|
17151
|
+
args.confirmExistingPrincipal = true;
|
|
17152
|
+
break;
|
|
15839
17153
|
case "--use-scoped-token":
|
|
15840
17154
|
args.useScopedToken = true;
|
|
15841
17155
|
break;
|
|
@@ -15857,6 +17171,12 @@ function parseBootstrapArgs(argv) {
|
|
|
15857
17171
|
case "--test":
|
|
15858
17172
|
args.tenant = "test";
|
|
15859
17173
|
break;
|
|
17174
|
+
case "--activate":
|
|
17175
|
+
args.activate = true;
|
|
17176
|
+
break;
|
|
17177
|
+
case "--no-activate":
|
|
17178
|
+
args.activate = false;
|
|
17179
|
+
break;
|
|
15860
17180
|
case "--help":
|
|
15861
17181
|
case "-h":
|
|
15862
17182
|
args.help = true;
|
|
@@ -15874,6 +17194,26 @@ function seedDeclined(answer) {
|
|
|
15874
17194
|
const a = answer.trim().toLowerCase();
|
|
15875
17195
|
return a === "n" || a === "no";
|
|
15876
17196
|
}
|
|
17197
|
+
function existingPrincipalConfirmed(answer) {
|
|
17198
|
+
const a = answer.trim().toLowerCase();
|
|
17199
|
+
return a === "y" || a === "yes";
|
|
17200
|
+
}
|
|
17201
|
+
function buildConfirmExistingPrincipal(args, prompt, isTty) {
|
|
17202
|
+
return async (details) => {
|
|
17203
|
+
if (!args.yes && isTty) {
|
|
17204
|
+
const ans = await prompt(
|
|
17205
|
+
`
|
|
17206
|
+
\u26A0 '${details.externalId}' already exists in your tenant (context '${details.contextId}') \u2014 this key will bind to that EXISTING identity, not a new one.
|
|
17207
|
+
Expected if you're re-applying a blueprint from a new machine, or setting up a second
|
|
17208
|
+
credential for an integration you already own. If you don't recognize this identity,
|
|
17209
|
+
do NOT proceed \u2014 this may be a name collision with something you don't control.
|
|
17210
|
+
Proceed and bind to the existing identity? [y/N]: `
|
|
17211
|
+
);
|
|
17212
|
+
return existingPrincipalConfirmed(ans);
|
|
17213
|
+
}
|
|
17214
|
+
return args.confirmExistingPrincipal;
|
|
17215
|
+
};
|
|
17216
|
+
}
|
|
15877
17217
|
function seedSummaryLine(opts) {
|
|
15878
17218
|
if (opts.noSeed) {
|
|
15879
17219
|
return opts.declinedInteractively ? " \u2714 Seeded 0 record(s) \u2014 skipped (you declined sample data)\n" : " \u2714 Seeded 0 record(s) \u2014 skipped (--no-seed)\n";
|
|
@@ -15901,22 +17241,45 @@ function reconcileTally(items, noun) {
|
|
|
15901
17241
|
if (reconciled) seg.push(`${reconciled} reconciled`);
|
|
15902
17242
|
return `${items.length} ${noun}${items.length === 1 ? "" : "s"} (${seg.join(", ")})`;
|
|
15903
17243
|
}
|
|
17244
|
+
function issuerTally(items) {
|
|
17245
|
+
if (items.length === 0) return null;
|
|
17246
|
+
const created = items.filter((i) => i.action === "created").length;
|
|
17247
|
+
const unchanged = items.length - created;
|
|
17248
|
+
const seg = [];
|
|
17249
|
+
if (created) seg.push(`${created} created`);
|
|
17250
|
+
if (unchanged) seg.push(`${unchanged} already registered \u2014 unchanged (issuers cannot be updated once registered)`);
|
|
17251
|
+
return `${items.length} issuer${items.length === 1 ? "" : "s"} (${seg.join(", ")})`;
|
|
17252
|
+
}
|
|
17253
|
+
function namespaceTally(items) {
|
|
17254
|
+
if (items.length === 0) return null;
|
|
17255
|
+
const created = items.filter((i) => i.action === "created").length;
|
|
17256
|
+
const matched = items.length - created;
|
|
17257
|
+
const seg = [];
|
|
17258
|
+
if (created) seg.push(`${created} created`);
|
|
17259
|
+
if (matched) seg.push(`${matched} already registered \u2014 confirmed matching`);
|
|
17260
|
+
return `${items.length} namespace${items.length === 1 ? "" : "s"} (${seg.join(", ")})`;
|
|
17261
|
+
}
|
|
15904
17262
|
function applyOutcomeSummary(result) {
|
|
15905
17263
|
const parts = [
|
|
15906
17264
|
reconcileTally(result.schemaActions, "schema"),
|
|
15907
17265
|
reconcileTally(result.roleActions, "role"),
|
|
17266
|
+
issuerTally(result.issuerActions),
|
|
17267
|
+
namespaceTally(result.namespaceActions),
|
|
15908
17268
|
`profile ${result.profileAction}`
|
|
15909
17269
|
].filter((p) => p !== null);
|
|
15910
17270
|
const detail = parts.length ? ` \u2014 ${parts.join("; ")}` : "";
|
|
17271
|
+
const principalNote = result.userAction === "reconciled" ? ` \u26A0 service principal already existed in this tenant before this run (not newly created)
|
|
17272
|
+
` : "";
|
|
15911
17273
|
return ` \u2714 context '${result.contextId}' ${result.contextAction}${detail}
|
|
15912
|
-
`;
|
|
17274
|
+
${principalNote}`;
|
|
15913
17275
|
}
|
|
15914
17276
|
var BOOTSTRAP_HELP = `vectros bootstrap \u2014 provision a least-privilege MCP credential.
|
|
15915
17277
|
|
|
15916
17278
|
Usage:
|
|
15917
17279
|
vectros bootstrap [--blueprint <name> | --blank] [--no-seed]
|
|
17280
|
+
[--set name=value ...] [--values <file>]
|
|
15918
17281
|
[--tenant test|live | --test] [--client desktop|code]
|
|
15919
|
-
[--yes] [--no-merge] [--rotate]
|
|
17282
|
+
[--yes] [--no-merge] [--rotate] [--confirm-existing-principal]
|
|
15920
17283
|
[--token <t> | env VECTROS_BOOTSTRAP_TOKEN]
|
|
15921
17284
|
[--base-url <url>] [--use-scoped-token]
|
|
15922
17285
|
|
|
@@ -15924,6 +17287,10 @@ Options:
|
|
|
15924
17287
|
--blueprint <name|file> A bundled blueprint (${BLUEPRINT_NAMES.join(", ")}),
|
|
15925
17288
|
OR a path to your own blueprint YAML/JSON (./my.yaml) \u2014 the "copy &
|
|
15926
17289
|
own" path. File blueprints get the identical scope gate (no override).
|
|
17290
|
+
--set name=value Set an install-time input value (repeatable) for a file blueprint's
|
|
17291
|
+
'inputs:' block. Beats --values + the declared default. (Same flag as
|
|
17292
|
+
'vectros blueprint validate/plan'.)
|
|
17293
|
+
--values <file> A YAML/JSON map of input values. (File blueprints only.)
|
|
15927
17294
|
--blank Credential only (read-only data-plane profile, no schemas).
|
|
15928
17295
|
--no-seed Provision the blueprint's schemas, profile and key but SKIP its
|
|
15929
17296
|
sample seed records \u2014 the right choice for a production context
|
|
@@ -15935,6 +17302,10 @@ Options:
|
|
|
15935
17302
|
The tenant rides the key (ssk_test_* vs ssk_live_*), so
|
|
15936
17303
|
the MCP/client config is otherwise identical.
|
|
15937
17304
|
--test Shorthand for --tenant test.
|
|
17305
|
+
--activate Force the freshly-stored key to become your active identity, even
|
|
17306
|
+
if that would switch away from a currently-active LIVE credential
|
|
17307
|
+
(the one case --tenant test does NOT do this automatically).
|
|
17308
|
+
--no-activate Store the key without activating it, regardless of tenant.
|
|
15938
17309
|
--base-url <url> Override the API base URL (beats VECTROS_API_BASE_URL).
|
|
15939
17310
|
--client <desktop|code> MCP host to configure (default: desktop). 'desktop' merges
|
|
15940
17311
|
claude_desktop_config.json (also works for Cursor/Cline);
|
|
@@ -15949,6 +17320,13 @@ Options:
|
|
|
15949
17320
|
print the config snippet instead of merging it into your client's
|
|
15950
17321
|
config file. Not a dry run; see 'blueprint plan' for that.
|
|
15951
17322
|
--rotate Revoke + re-mint THIS machine's key.
|
|
17323
|
+
--confirm-existing-principal
|
|
17324
|
+
Required (with --yes, or with no TTY) to mint a NEW key bound to a
|
|
17325
|
+
service principal that already existed in your tenant before this run
|
|
17326
|
+
\u2014 e.g. a deliberate second machine for an integration you already own.
|
|
17327
|
+
Refused by default so an unexpected collision halts a CI run loudly
|
|
17328
|
+
instead of silently minting a credential bound to the wrong identity.
|
|
17329
|
+
Never needed for --rotate or an ordinary same-machine re-apply.
|
|
15952
17330
|
--use-scoped-token Legacy fallback.
|
|
15953
17331
|
NOTE: it mints a token WITHOUT the provisioning
|
|
15954
17332
|
capability, so it cannot create a blueprint's
|
|
@@ -15958,10 +17336,21 @@ Options:
|
|
|
15958
17336
|
|
|
15959
17337
|
No --blueprint/--blank in a TTY \u2192 interactive menu.`;
|
|
15960
17338
|
async function resolveBlueprint(args, prompt, isTty) {
|
|
15961
|
-
if (args.blankSlate)
|
|
17339
|
+
if (args.blankSlate) {
|
|
17340
|
+
if (args.set.length > 0 || args.values) {
|
|
17341
|
+
throw new Error(`--set/--values apply only to file blueprints; --blank has no inputs to set.`);
|
|
17342
|
+
}
|
|
17343
|
+
return BLANK_SLATE;
|
|
17344
|
+
}
|
|
15962
17345
|
if (args.blueprint) {
|
|
15963
17346
|
if (looksLikePath(args.blueprint)) {
|
|
15964
|
-
|
|
17347
|
+
const supplied = await buildInputSupply(args.set, args.values);
|
|
17348
|
+
return await loadBlueprintFile(args.blueprint, supplied);
|
|
17349
|
+
}
|
|
17350
|
+
if (args.set.length > 0 || args.values) {
|
|
17351
|
+
throw new Error(
|
|
17352
|
+
`--set/--values apply only to file blueprints; '${args.blueprint}' is a bundled blueprint (no inputs to set).`
|
|
17353
|
+
);
|
|
15965
17354
|
}
|
|
15966
17355
|
const b = getBlueprint(args.blueprint);
|
|
15967
17356
|
if (!b) {
|
|
@@ -15974,6 +17363,11 @@ async function resolveBlueprint(args, prompt, isTty) {
|
|
|
15974
17363
|
if (!isTty) {
|
|
15975
17364
|
throw new Error("No --blueprint or --blank specified and not a TTY (cannot show the menu).");
|
|
15976
17365
|
}
|
|
17366
|
+
if (args.set.length > 0 || args.values) {
|
|
17367
|
+
throw new Error(
|
|
17368
|
+
`--set/--values need an explicit --blueprint <file>; the interactive menu only offers bundled blueprints and blank slate, neither of which has inputs to set.`
|
|
17369
|
+
);
|
|
17370
|
+
}
|
|
15977
17371
|
const options = [
|
|
15978
17372
|
{ label: "Blank slate (credential only)", blueprint: BLANK_SLATE },
|
|
15979
17373
|
...BUNDLED_BLUEPRINTS.map((b) => ({ label: `${b.name} \u2014 ${b.description}`, blueprint: b }))
|
|
@@ -16020,6 +17414,22 @@ function buildRemintForContext(opts) {
|
|
|
16020
17414
|
});
|
|
16021
17415
|
};
|
|
16022
17416
|
}
|
|
17417
|
+
function buildRemintBootstrapForContext(opts) {
|
|
17418
|
+
const mintBridge = opts.mintBridge ?? mintBridgeToken;
|
|
17419
|
+
return async (contextId) => {
|
|
17420
|
+
const ctxToken = await mintBridge({
|
|
17421
|
+
cognitoToken: opts.cognitoToken,
|
|
17422
|
+
apiBaseUrl: opts.apiBaseUrl,
|
|
17423
|
+
contextId,
|
|
17424
|
+
tenant: opts.tenant,
|
|
17425
|
+
useScopedToken: opts.useScopedToken
|
|
17426
|
+
});
|
|
17427
|
+
return new VectrosClient({
|
|
17428
|
+
token: ctxToken.token,
|
|
17429
|
+
environment: opts.apiBaseUrl
|
|
17430
|
+
});
|
|
17431
|
+
};
|
|
17432
|
+
}
|
|
16023
17433
|
async function runBootstrapCli(argv, deps = {}) {
|
|
16024
17434
|
const log = deps.log ?? createLogger2();
|
|
16025
17435
|
let args;
|
|
@@ -16053,6 +17463,15 @@ ${BOOTSTRAP_HELP}
|
|
|
16053
17463
|
\u2716 Scope gate refused blueprint '${blueprint.name}':
|
|
16054
17464
|
`);
|
|
16055
17465
|
for (const r of err.rejected) process.stderr.write(` \u2022 ${r.action}: ${r.reason}
|
|
17466
|
+
`);
|
|
17467
|
+
process.stderr.write(" Minting nothing.\n");
|
|
17468
|
+
return 2;
|
|
17469
|
+
}
|
|
17470
|
+
if (err instanceof CapabilityGateError) {
|
|
17471
|
+
process.stderr.write(`
|
|
17472
|
+
\u2716 Capability gate refused blueprint '${blueprint.name}':
|
|
17473
|
+
`);
|
|
17474
|
+
for (const r of err.rejected) process.stderr.write(` \u2022 ${r.action}: ${r.reason}
|
|
16056
17475
|
`);
|
|
16057
17476
|
process.stderr.write(" Minting nothing.\n");
|
|
16058
17477
|
return 2;
|
|
@@ -16064,6 +17483,11 @@ ${BOOTSTRAP_HELP}
|
|
|
16064
17483
|
Blueprint: ${blueprint.name}
|
|
16065
17484
|
\u2714 Scope gate: [${gate.accepted.join(", ")}] \u2014 all data-plane \u2713
|
|
16066
17485
|
`);
|
|
17486
|
+
if (blueprint.accessProfile.capabilities?.length) {
|
|
17487
|
+
const capGate = evaluateCapabilityGate(blueprint.accessProfile.capabilities);
|
|
17488
|
+
process.stdout.write(` \u2714 Capability gate: [${capGate.accepted.join(", ")}] \u2014 granted \u2713
|
|
17489
|
+
`);
|
|
17490
|
+
}
|
|
16067
17491
|
let noSeed = args.noSeed;
|
|
16068
17492
|
let seedDeclinedInteractively = false;
|
|
16069
17493
|
if (shouldOfferSeedOptOut(args, blueprint, isTty)) {
|
|
@@ -16137,13 +17561,16 @@ ${renderPlan(blueprint, planSteps)}
|
|
|
16137
17561
|
keyringOpenError = err;
|
|
16138
17562
|
}
|
|
16139
17563
|
let storedFreshInKeyring = false;
|
|
17564
|
+
const lastResolvedSecret = /* @__PURE__ */ new Map();
|
|
16140
17565
|
const readExistingKey = async (contextId) => {
|
|
16141
17566
|
if (!keyring) return void 0;
|
|
17567
|
+
const alias = keyringAliasFor(contextId, args.tenant);
|
|
16142
17568
|
try {
|
|
16143
|
-
const existing = await keyring.get(
|
|
17569
|
+
const existing = await keyring.get(alias);
|
|
16144
17570
|
if (!existing?.keyId) return void 0;
|
|
16145
|
-
const rawKey = await keyring.resolveSecret(
|
|
16146
|
-
|
|
17571
|
+
const rawKey = await keyring.resolveSecret(alias);
|
|
17572
|
+
lastResolvedSecret.set(alias, rawKey);
|
|
17573
|
+
return rawKey ? { keyId: existing.keyId, rawKey, principalId: existing.principalId } : void 0;
|
|
16147
17574
|
} catch {
|
|
16148
17575
|
return void 0;
|
|
16149
17576
|
}
|
|
@@ -16159,17 +17586,36 @@ ${renderPlan(blueprint, planSteps)}
|
|
|
16159
17586
|
);
|
|
16160
17587
|
return;
|
|
16161
17588
|
}
|
|
17589
|
+
const alias = keyringAliasFor(contextId, args.tenant);
|
|
16162
17590
|
try {
|
|
16163
|
-
warnReplacingAlias(await keyring.get(
|
|
17591
|
+
warnReplacingAlias(await keyring.get(alias), alias, record.revokedKeyId);
|
|
17592
|
+
const willActivate = await determineActivateDecision(keyring, {
|
|
17593
|
+
tenant: args.tenant,
|
|
17594
|
+
explicitActivate: args.activate,
|
|
17595
|
+
hint: lastResolvedSecret.has(alias) ? { alias, secret: lastResolvedSecret.get(alias) } : void 0
|
|
17596
|
+
});
|
|
16164
17597
|
await storeMintedKey(
|
|
16165
17598
|
keyring,
|
|
16166
|
-
{
|
|
17599
|
+
{
|
|
17600
|
+
alias,
|
|
17601
|
+
context: contextId,
|
|
17602
|
+
principalId: record.principalId,
|
|
17603
|
+
keyId: record.keyId,
|
|
17604
|
+
// Derived from the REQUEST's actual baseUrl, not echoed from args.env — the two
|
|
17605
|
+
// diverge whenever VECTROS_API_BASE_URL/--base-url alone selected the environment.
|
|
17606
|
+
// `keyring list`/`keyring doctor` display this field; a stale echo would show
|
|
17607
|
+
// "env: production" next to a key this run actually minted against staging.
|
|
17608
|
+
env: deriveEnvironment(baseUrl) ?? args.env
|
|
17609
|
+
},
|
|
16167
17610
|
record.rawKey,
|
|
16168
|
-
{ activate:
|
|
17611
|
+
{ activate: willActivate, revokedKeyId: record.revokedKeyId }
|
|
16169
17612
|
);
|
|
16170
17613
|
if (record.revokedKeyId) {
|
|
16171
|
-
await warnOtherAliasesNaming(keyring, record.revokedKeyId,
|
|
17614
|
+
await warnOtherAliasesNaming(keyring, record.revokedKeyId, alias);
|
|
16172
17615
|
}
|
|
17616
|
+
process.stdout.write(
|
|
17617
|
+
tenantActivateMessage({ alias, tenant: args.tenant, willActivate, explicitlyDeclined: args.activate === false })
|
|
17618
|
+
);
|
|
16173
17619
|
storedFreshInKeyring = true;
|
|
16174
17620
|
} catch (err) {
|
|
16175
17621
|
process.stderr.write(
|
|
@@ -16190,12 +17636,20 @@ ${renderPlan(blueprint, planSteps)}
|
|
|
16190
17636
|
skipSeed: noSeed,
|
|
16191
17637
|
readExistingKey,
|
|
16192
17638
|
persistMintedKey,
|
|
17639
|
+
remintBootstrapForContext: buildRemintBootstrapForContext({
|
|
17640
|
+
cognitoToken,
|
|
17641
|
+
apiBaseUrl: baseUrl,
|
|
17642
|
+
tenant: args.tenant,
|
|
17643
|
+
useScopedToken: args.useScopedToken,
|
|
17644
|
+
mintBridge
|
|
17645
|
+
}),
|
|
16193
17646
|
remintForContext: buildRemintForContext({
|
|
16194
17647
|
cognitoToken,
|
|
16195
17648
|
apiBaseUrl: baseUrl,
|
|
16196
17649
|
tenant: args.tenant,
|
|
16197
17650
|
mintContext
|
|
16198
17651
|
}),
|
|
17652
|
+
confirmExistingPrincipal: buildConfirmExistingPrincipal(args, promptCtx.prompt, isTty),
|
|
16199
17653
|
log
|
|
16200
17654
|
});
|
|
16201
17655
|
process.stdout.write(
|
|
@@ -16213,7 +17667,7 @@ ${renderPlan(blueprint, planSteps)}
|
|
|
16213
17667
|
);
|
|
16214
17668
|
entry = buildVectrosServerEntry({ apiKey: result.rawKey, apiBaseUrl: baseUrl });
|
|
16215
17669
|
} else if (storedFreshInKeyring || result.keyAction === "reused") {
|
|
16216
|
-
entry = buildVectrosServerEntry({ alias: blueprint.contextId, apiBaseUrl: baseUrl });
|
|
17670
|
+
entry = buildVectrosServerEntry({ alias: keyringAliasFor(blueprint.contextId, args.tenant), apiBaseUrl: baseUrl });
|
|
16217
17671
|
} else {
|
|
16218
17672
|
entry = buildVectrosServerEntry({ apiKey: result.rawKey, apiBaseUrl: baseUrl });
|
|
16219
17673
|
}
|
|
@@ -16273,6 +17727,18 @@ ${codeAddHint}`);
|
|
|
16273
17727
|
`);
|
|
16274
17728
|
return 3;
|
|
16275
17729
|
}
|
|
17730
|
+
if (err instanceof ExistingPrincipalNotConfirmedError) {
|
|
17731
|
+
process.stderr.write(
|
|
17732
|
+
`
|
|
17733
|
+
\u2716 ${err.message}
|
|
17734
|
+
|
|
17735
|
+
If this is expected (a re-apply, or a deliberate second machine for an integration you already own),
|
|
17736
|
+
re-run with --confirm-existing-principal, or confirm interactively.
|
|
17737
|
+
If you don't recognize this identity, investigate the blueprint's servicePrincipal.externalId before re-running.
|
|
17738
|
+
`
|
|
17739
|
+
);
|
|
17740
|
+
return 3;
|
|
17741
|
+
}
|
|
16276
17742
|
if (err instanceof BlueprintIdentityError || err instanceof UnresolvedIdentitiesError) {
|
|
16277
17743
|
process.stderr.write(`
|
|
16278
17744
|
\u2716 ${err.message}
|
|
@@ -16296,7 +17762,7 @@ ${formatCliError(err, { action: "Bootstrap failed" })}`);
|
|
|
16296
17762
|
import { writeFile as writeFile2, access } from "fs/promises";
|
|
16297
17763
|
|
|
16298
17764
|
// src/scope-grammar.ts
|
|
16299
|
-
var
|
|
17765
|
+
var FORBIDDEN_SCOPE_NAMESPACES2 = /* @__PURE__ */ new Set([
|
|
16300
17766
|
"user",
|
|
16301
17767
|
"record",
|
|
16302
17768
|
"document",
|
|
@@ -16308,23 +17774,23 @@ var FORBIDDEN_SCOPE_NAMESPACES = /* @__PURE__ */ new Set([
|
|
|
16308
17774
|
"versions",
|
|
16309
17775
|
"lookup"
|
|
16310
17776
|
]);
|
|
16311
|
-
var
|
|
17777
|
+
var SCOPE_NAMESPACE_RE2 = /^[a-z][a-z0-9_-]{1,31}$/;
|
|
16312
17778
|
var SCOPE_NAMESPACE_MIN_LENGTH = 2;
|
|
16313
17779
|
var SCOPE_NAMESPACE_MAX_LENGTH = 32;
|
|
16314
|
-
var
|
|
17780
|
+
var SCOPE_VALUE_RE2 = /^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$/;
|
|
16315
17781
|
var SCOPE_VALUE_MIN_LENGTH = 1;
|
|
16316
17782
|
var SCOPE_VALUE_MAX_LENGTH = 128;
|
|
16317
17783
|
function valueGrammarError(value) {
|
|
16318
|
-
if (!
|
|
17784
|
+
if (!SCOPE_VALUE_RE2.test(value)) {
|
|
16319
17785
|
return `'${value}' is not a valid scope value \u2014 ${SCOPE_VALUE_MIN_LENGTH}-${SCOPE_VALUE_MAX_LENGTH} characters, starting with a letter or digit and continuing with letters, digits, '_' or '-'. A value may not contain a colon.`;
|
|
16320
17786
|
}
|
|
16321
17787
|
return null;
|
|
16322
17788
|
}
|
|
16323
17789
|
function namespaceGrammarError(ns) {
|
|
16324
|
-
if (
|
|
17790
|
+
if (FORBIDDEN_SCOPE_NAMESPACES2.has(ns)) {
|
|
16325
17791
|
return `'${ns}' is a reserved namespace and cannot be used as an ownership scope`;
|
|
16326
17792
|
}
|
|
16327
|
-
if (!
|
|
17793
|
+
if (!SCOPE_NAMESPACE_RE2.test(ns)) {
|
|
16328
17794
|
return `'${ns}' is not a valid namespace \u2014 lowercase, ${SCOPE_NAMESPACE_MIN_LENGTH}-${SCOPE_NAMESPACE_MAX_LENGTH} characters, starting with a letter and continuing with letters, digits, '_' or '-'`;
|
|
16329
17795
|
}
|
|
16330
17796
|
return null;
|
|
@@ -16493,6 +17959,10 @@ function lintBlueprint(bp) {
|
|
|
16493
17959
|
}
|
|
16494
17960
|
}
|
|
16495
17961
|
}
|
|
17962
|
+
{
|
|
17963
|
+
const reached = ownerScopedReachWithoutDataScope(bp.accessProfile.allowedActions, ds);
|
|
17964
|
+
if (reached.length > 0) findings.push(unconstrainedReachFinding("accessProfile", reached));
|
|
17965
|
+
}
|
|
16496
17966
|
for (const roleId of Object.keys(bp.roles ?? {})) {
|
|
16497
17967
|
if (!ROLE_ID_RE.test(roleId)) {
|
|
16498
17968
|
findings.push({
|
|
@@ -16505,6 +17975,10 @@ function lintBlueprint(bp) {
|
|
|
16505
17975
|
for (const [roleId, clauses] of Object.entries(bp.roles ?? {})) {
|
|
16506
17976
|
clauses.forEach((clause, i) => {
|
|
16507
17977
|
const cds = clause.dataScope;
|
|
17978
|
+
const reached = ownerScopedReachWithoutDataScope(clause.allowedActions, cds);
|
|
17979
|
+
if (reached.length > 0) {
|
|
17980
|
+
findings.push(unconstrainedReachFinding(`role '${roleId}' clause ${i}`, reached));
|
|
17981
|
+
}
|
|
16508
17982
|
if (!cds) return;
|
|
16509
17983
|
for (const [key, values] of Object.entries(cds)) {
|
|
16510
17984
|
for (const value of values) {
|
|
@@ -16521,6 +17995,17 @@ function lintBlueprint(bp) {
|
|
|
16521
17995
|
}
|
|
16522
17996
|
});
|
|
16523
17997
|
}
|
|
17998
|
+
for (const issuer of bp.issuers ?? []) {
|
|
17999
|
+
for (const policy of issuer.selfSignupPolicies ?? []) {
|
|
18000
|
+
if (!Object.prototype.hasOwnProperty.call(bp.roles ?? {}, policy.roleId)) {
|
|
18001
|
+
findings.push({
|
|
18002
|
+
severity: "warning",
|
|
18003
|
+
rule: "self-signup-role-unknown",
|
|
18004
|
+
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.`
|
|
18005
|
+
});
|
|
18006
|
+
}
|
|
18007
|
+
}
|
|
18008
|
+
}
|
|
16524
18009
|
return findings;
|
|
16525
18010
|
}
|
|
16526
18011
|
var ROLE_ID_RE = /^[a-z][a-z0-9-]{2,30}$/;
|
|
@@ -16546,6 +18031,25 @@ function placeholderFinding(where, key, value) {
|
|
|
16546
18031
|
message: `${where}: dataScope.${key} value '${value}' is not a placeholder the platform recognizes \u2014 it will be rejected at apply. Valid: \${{ any }}, \${{ self.userId }}, \${{ self.scope.<namespace> }}, \${{ under.self.userId }}, \${{ under.self.scope.<namespace> }}. Spelling is exact and lowercase; use a plain value for a literal owner id.`
|
|
16547
18032
|
};
|
|
16548
18033
|
}
|
|
18034
|
+
var OWNER_SCOPED_RESOURCES = /* @__PURE__ */ new Set(["records", "documents", "entities"]);
|
|
18035
|
+
var UNCONSTRAINED_REACH_OPS = /* @__PURE__ */ new Set(["r", "u", "d"]);
|
|
18036
|
+
function ownerScopedReachWithoutDataScope(allowedActions, dataScope) {
|
|
18037
|
+
const reached = /* @__PURE__ */ new Set();
|
|
18038
|
+
for (const action of allowedActions) {
|
|
18039
|
+
const [resource, ops] = action.split(":", 2);
|
|
18040
|
+
if (!resource || !ops || !OWNER_SCOPED_RESOURCES.has(resource)) continue;
|
|
18041
|
+
if (ops === "*" || [...ops].some((op) => UNCONSTRAINED_REACH_OPS.has(op))) reached.add(resource);
|
|
18042
|
+
}
|
|
18043
|
+
if (dataScope !== void 0) return [];
|
|
18044
|
+
return [...reached].sort();
|
|
18045
|
+
}
|
|
18046
|
+
function unconstrainedReachFinding(where, resources) {
|
|
18047
|
+
return {
|
|
18048
|
+
severity: "warning",
|
|
18049
|
+
rule: "datascope-absent-unconstrained-reach",
|
|
18050
|
+
message: `${where}: no dataScope at all, but allowedActions grants r/u/d on ${resources.join("/")} \u2014 this reaches EVERY row of EVERY owner on ${resources.length > 1 ? "those resources" : "that resource"}, not just "your own org" or any other value you may have intended. If that's deliberate (a genuinely shared/tenant-wide resource), you can ignore this; otherwise add a dataScope entry.`
|
|
18051
|
+
};
|
|
18052
|
+
}
|
|
16549
18053
|
function lintPasses(findings) {
|
|
16550
18054
|
return !findings.some((f) => f.severity === "error");
|
|
16551
18055
|
}
|
|
@@ -16584,8 +18088,12 @@ schemas:
|
|
|
16584
18088
|
lookupFields: [title] # extra O(1) exact-match lookup indexes (max 10)
|
|
16585
18089
|
|
|
16586
18090
|
accessProfile:
|
|
16587
|
-
# SCOPE GATE: data-plane only (records/schemas/search/documents/folders/inference)
|
|
16588
|
-
#
|
|
18091
|
+
# SCOPE GATE: data-plane only (records/schemas/search/documents/folders/inference/entities),
|
|
18092
|
+
# PLUS profiles:r/u/d (bounded by the platform to redistributing access you already have,
|
|
18093
|
+
# never granting more) and profiles:c:self (create a profile for yourself only \u2014 the
|
|
18094
|
+
# platform resolves "self" from the caller, not from anything you declare here). A bare
|
|
18095
|
+
# profiles:c, or profiles:c:usr_<id> naming another principal, is HARD-REJECTED \u2014 by
|
|
18096
|
+
# design, no override. Same for keys/users/admin/namespaces/*.
|
|
16589
18097
|
# Ops: c|r|u|d, combinable (records:cru). Prefer the narrowest set (least privilege).
|
|
16590
18098
|
allowedActions: [records:r, records:c, records:u, search:r, schemas:r]
|
|
16591
18099
|
# dataScope binds the key to specific owners. Include null to ALSO see
|
|
@@ -16593,6 +18101,17 @@ accessProfile:
|
|
|
16593
18101
|
# owners' records, not shared/seed data. Keys are userId (the principal) and
|
|
16594
18102
|
# namespaced scopes (scope:org, scope:client, ...):
|
|
16595
18103
|
# dataScope: { "scope:org": [<org-uuid>, null] }
|
|
18104
|
+
# Left empty ({}) below \u2014 the explicit marker for "deliberately tenant-wide,
|
|
18105
|
+
# every record visible" ('blueprint validate' warns on a MISSING dataScope
|
|
18106
|
+
# key, but not on this one). This is a SCAFFOLD PLACEHOLDER, not a
|
|
18107
|
+
# recommendation: it also grants records:c/u tenant-wide, so before this
|
|
18108
|
+
# profile touches real data, replace it with a real scope (the example
|
|
18109
|
+
# above) unless every owner genuinely sharing every record is what you want.
|
|
18110
|
+
dataScope: {}
|
|
18111
|
+
# capabilities is a SEPARATE gated dimension from allowedActions \u2014 named platform
|
|
18112
|
+
# effects a resource:ops verb can't express. Only 'member-lifecycle' is admitted
|
|
18113
|
+
# today; every other name is HARD-REJECTED, same posture as the scope gate above.
|
|
18114
|
+
# capabilities: [member-lifecycle]
|
|
16596
18115
|
|
|
16597
18116
|
servicePrincipal:
|
|
16598
18117
|
externalId: ${name}-sp # stable; the principal the profile + key bind to
|
|
@@ -16712,15 +18231,19 @@ async function resolveAndValidate(arg, verb, supplied) {
|
|
|
16712
18231
|
return null;
|
|
16713
18232
|
}
|
|
16714
18233
|
try {
|
|
16715
|
-
|
|
16716
|
-
for (const clauses of Object.values(blueprint.roles ?? {})) {
|
|
16717
|
-
for (const clause of clauses) assertScopeGate(clause.allowedActions);
|
|
16718
|
-
}
|
|
18234
|
+
assertBlueprintGates(blueprint);
|
|
16719
18235
|
} catch (err) {
|
|
16720
18236
|
if (err instanceof ScopeGateError) {
|
|
16721
18237
|
process.stderr.write(` \u2716 Scope gate rejected '${blueprint.name}':
|
|
16722
18238
|
`);
|
|
16723
18239
|
for (const r of err.rejected) process.stderr.write(` \u2022 ${r.action}: ${r.reason}
|
|
18240
|
+
`);
|
|
18241
|
+
return null;
|
|
18242
|
+
}
|
|
18243
|
+
if (err instanceof CapabilityGateError) {
|
|
18244
|
+
process.stderr.write(` \u2716 Capability gate rejected '${blueprint.name}':
|
|
18245
|
+
`);
|
|
18246
|
+
for (const r of err.rejected) process.stderr.write(` \u2022 ${r.action}: ${r.reason}
|
|
16724
18247
|
`);
|
|
16725
18248
|
return null;
|
|
16726
18249
|
}
|
|
@@ -16752,9 +18275,10 @@ async function runValidate(args) {
|
|
|
16752
18275
|
const blueprint = await resolveAndValidate(args.file, "validate", supplied);
|
|
16753
18276
|
if (!blueprint) return 2;
|
|
16754
18277
|
const gate = evaluateScopeGate(blueprint.accessProfile.allowedActions);
|
|
18278
|
+
const capGate = evaluateCapabilityGate(blueprint.accessProfile.capabilities);
|
|
16755
18279
|
const warnCount = lintBlueprint(blueprint).length;
|
|
16756
18280
|
process.stdout.write(
|
|
16757
|
-
` \u2714 '${blueprint.name}' is valid \u2014 structural \u2713, scope gate \u2713 [${gate.accepted.join(", ")}], lint \u2713` + (warnCount ? ` (${warnCount} warning${warnCount === 1 ? "" : "s"})` : "") + "\n"
|
|
18281
|
+
` \u2714 '${blueprint.name}' is valid \u2014 structural \u2713, scope gate \u2713 [${gate.accepted.join(", ")}]` + (capGate.accepted.length ? `, capability gate \u2713 [${capGate.accepted.join(", ")}]` : "") + `, lint \u2713` + (warnCount ? ` (${warnCount} warning${warnCount === 1 ? "" : "s"})` : "") + "\n"
|
|
16758
18282
|
);
|
|
16759
18283
|
return 0;
|
|
16760
18284
|
}
|
|
@@ -17085,7 +18609,14 @@ async function resolveKeyDestination(opts, deps) {
|
|
|
17085
18609
|
`);
|
|
17086
18610
|
return 2;
|
|
17087
18611
|
}
|
|
17088
|
-
|
|
18612
|
+
let alias;
|
|
18613
|
+
try {
|
|
18614
|
+
alias = keyringAliasFor(opts.alias ?? opts.context, opts.tenant ?? "live");
|
|
18615
|
+
} catch (err) {
|
|
18616
|
+
process.stderr.write(` \u2716 ${formatError(err)}
|
|
18617
|
+
`);
|
|
18618
|
+
return 2;
|
|
18619
|
+
}
|
|
17089
18620
|
if (opts.activate === false && await keyring.activeAlias() === alias) {
|
|
17090
18621
|
process.stderr.write(
|
|
17091
18622
|
` \u2716 --no-activate cannot be used on '${alias}': it is your ACTIVE identity, and storing
|
|
@@ -17106,6 +18637,8 @@ async function storeAndReport(dest, key, auth, opts) {
|
|
|
17106
18637
|
return 1;
|
|
17107
18638
|
}
|
|
17108
18639
|
const principalId = opts.principalId;
|
|
18640
|
+
const tenant = keyTenant(key.rawKey) ?? "test";
|
|
18641
|
+
const willActivate = await determineActivateDecision(dest.keyring, { tenant, explicitActivate: opts.activate });
|
|
17109
18642
|
let entry;
|
|
17110
18643
|
try {
|
|
17111
18644
|
warnReplacingAlias(await dest.keyring.get(dest.alias), dest.alias, opts.revokedKeyId);
|
|
@@ -17120,7 +18653,7 @@ async function storeAndReport(dest, key, auth, opts) {
|
|
|
17120
18653
|
env: auth.env
|
|
17121
18654
|
},
|
|
17122
18655
|
key.rawKey,
|
|
17123
|
-
{ activate:
|
|
18656
|
+
{ activate: willActivate, revokedKeyId: opts.revokedKeyId }
|
|
17124
18657
|
));
|
|
17125
18658
|
} catch (err) {
|
|
17126
18659
|
process.stderr.write(
|
|
@@ -17132,7 +18665,6 @@ async function storeAndReport(dest, key, auth, opts) {
|
|
|
17132
18665
|
);
|
|
17133
18666
|
return 1;
|
|
17134
18667
|
}
|
|
17135
|
-
const active = opts.activate !== false;
|
|
17136
18668
|
process.stdout.write(
|
|
17137
18669
|
`
|
|
17138
18670
|
\u2714 Stored the new key in your ${describeEntryStoreNoun(entry)} \u2014 it was not printed.
|
|
@@ -17142,9 +18674,19 @@ async function storeAndReport(dest, key, auth, opts) {
|
|
|
17142
18674
|
["principal", entry.principalId || void 0],
|
|
17143
18675
|
["key id", entry.keyId],
|
|
17144
18676
|
["env", entry.env],
|
|
17145
|
-
["active",
|
|
17146
|
-
]) + "\n\n" +
|
|
17147
|
-
|
|
18677
|
+
["active", willActivate ? "yes" : "no"]
|
|
18678
|
+
]) + "\n\n" + // The plain "now active" note fires only on the activated path. When it
|
|
18679
|
+
// did NOT activate, tenantActivateMessage below is the ONLY explanation —
|
|
18680
|
+
// it already says to run `switch`, AND (unlike a bare "Not active") says
|
|
18681
|
+
// WHY: an explicit --no-activate vs. this call's tenant-safety default.
|
|
18682
|
+
// Saying both here would print two different "run switch" instructions
|
|
18683
|
+
// for one outcome.
|
|
18684
|
+
(willActivate ? " Anything that resolves a key through the CLI \u2014 the MCP server included \u2014 now\n reads this one. No VECTROS_API_KEY needed.\n" : "") + tenantActivateMessage({
|
|
18685
|
+
alias: entry.alias,
|
|
18686
|
+
tenant,
|
|
18687
|
+
willActivate,
|
|
18688
|
+
explicitlyDeclined: opts.activate === false
|
|
18689
|
+
})
|
|
17148
18690
|
);
|
|
17149
18691
|
return 0;
|
|
17150
18692
|
}
|
|
@@ -17365,9 +18907,25 @@ async function runKeyRotate(opts, deps = {}) {
|
|
|
17365
18907
|
return 1;
|
|
17366
18908
|
}
|
|
17367
18909
|
}
|
|
17368
|
-
const
|
|
17369
|
-
|
|
17370
|
-
)
|
|
18910
|
+
const activeKeys = await drainPages(() => client.auth.listScopedKeys());
|
|
18911
|
+
const existing = activeKeys.find((k) => matchesTuple(k, opts.context, userId, keyName));
|
|
18912
|
+
if (!existing && keyName === DEFAULT_KEY_NAME && !opts.forceNew) {
|
|
18913
|
+
const others = activeKeys.filter(
|
|
18914
|
+
(k) => k.contextId === opts.context && k.userId === userId && k.status === "active"
|
|
18915
|
+
);
|
|
18916
|
+
if (others.length > 0) {
|
|
18917
|
+
const list = others.map((k) => ` \u2022 ${k.keyId} (name: '${k.keyName}')`).join("\n");
|
|
18918
|
+
process.stderr.write(
|
|
18919
|
+
` \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):
|
|
18920
|
+
${list}
|
|
18921
|
+
If one of these is it, re-run with --name '<its name>'.
|
|
18922
|
+
If you really want a new key alongside them, re-run with --force-new.
|
|
18923
|
+
Nothing was revoked or minted.
|
|
18924
|
+
`
|
|
18925
|
+
);
|
|
18926
|
+
return 1;
|
|
18927
|
+
}
|
|
18928
|
+
}
|
|
17371
18929
|
if (existing?.keyId) {
|
|
17372
18930
|
await client.auth.revokeScopedKey({ keyId: existing.keyId });
|
|
17373
18931
|
process.stdout.write(` \u2714 Revoked prior key ${existing.keyId}
|
|
@@ -17800,7 +19358,7 @@ function isRecognizedOwnershipKey(key) {
|
|
|
17800
19358
|
if (key === "userId" || key === "partnerUserId") return true;
|
|
17801
19359
|
if (!key.startsWith("scope:")) return false;
|
|
17802
19360
|
const ns = key.slice("scope:".length);
|
|
17803
|
-
return !
|
|
19361
|
+
return !FORBIDDEN_SCOPE_NAMESPACES2.has(ns) && SCOPE_NAMESPACE_RE2.test(ns);
|
|
17804
19362
|
}
|
|
17805
19363
|
var DIM_WILDCARD = "*";
|
|
17806
19364
|
var ANY_TOKEN_RE = /^\$\{\{\s*any\s*\}\}$/;
|
|
@@ -18547,7 +20105,24 @@ async function captureSnapshot(client, blueprint) {
|
|
|
18547
20105
|
}
|
|
18548
20106
|
return { existingSchemaTypes, existingProfilePrincipals, existingRoleIds, failed };
|
|
18549
20107
|
}
|
|
18550
|
-
async function
|
|
20108
|
+
async function captureIssuerSnapshot(bootstrapClient) {
|
|
20109
|
+
const existingIssuerIds = /* @__PURE__ */ new Set();
|
|
20110
|
+
let failed = false;
|
|
20111
|
+
try {
|
|
20112
|
+
let startFrom;
|
|
20113
|
+
for (; ; ) {
|
|
20114
|
+
const page = await bootstrapClient.auth.listIssuers({ limit: SNAPSHOT_PAGE_SIZE, startFrom });
|
|
20115
|
+
for (const i of page.data ?? []) if (i.issuerId) existingIssuerIds.add(i.issuerId);
|
|
20116
|
+
const next = page.nextCursor;
|
|
20117
|
+
if (!next) break;
|
|
20118
|
+
startFrom = next;
|
|
20119
|
+
}
|
|
20120
|
+
} catch {
|
|
20121
|
+
failed = true;
|
|
20122
|
+
}
|
|
20123
|
+
return { existingIssuerIds, failed };
|
|
20124
|
+
}
|
|
20125
|
+
async function runAssertions(client, bootstrapClient, deps, blueprint, tenantId, result) {
|
|
18551
20126
|
const assertions = [];
|
|
18552
20127
|
try {
|
|
18553
20128
|
const ping = await deps.pingWithKey(result.rawKey);
|
|
@@ -18632,9 +20207,35 @@ async function runAssertions(client, deps, blueprint, tenantId, result) {
|
|
|
18632
20207
|
}
|
|
18633
20208
|
}
|
|
18634
20209
|
}
|
|
20210
|
+
for (const { issuerId } of result.issuerActions) {
|
|
20211
|
+
try {
|
|
20212
|
+
const issuer = await bootstrapClient.auth.getIssuer({ issuerId });
|
|
20213
|
+
const ok = issuer.issuerId === issuerId;
|
|
20214
|
+
assertions.push({
|
|
20215
|
+
name: `issuer:${issuerId}`,
|
|
20216
|
+
ok,
|
|
20217
|
+
detail: ok ? `issuer '${issuerId}' registered + readable` : `getIssuer(${issuerId}) returned issuerId '${issuer.issuerId ?? "(none)"}'`
|
|
20218
|
+
});
|
|
20219
|
+
} catch (e) {
|
|
20220
|
+
assertions.push({ name: `issuer:${issuerId}`, ok: false, detail: `getIssuer(${issuerId}) failed: ${errMsg(e)}` });
|
|
20221
|
+
}
|
|
20222
|
+
}
|
|
20223
|
+
for (const { namespace } of result.namespaceActions) {
|
|
20224
|
+
try {
|
|
20225
|
+
const ns = await bootstrapClient.identity.getNamespace({ namespace, contextId: blueprint.contextId });
|
|
20226
|
+
const ok = ns.namespace === namespace && ns.contextId === blueprint.contextId;
|
|
20227
|
+
assertions.push({
|
|
20228
|
+
name: `namespace:${namespace}`,
|
|
20229
|
+
ok,
|
|
20230
|
+
detail: ok ? `namespace '${namespace}' registered + readable in context '${blueprint.contextId}'` : `getNamespace(${namespace}) returned namespace '${ns.namespace ?? "(none)"}' in context '${ns.contextId ?? "(tenant-wide)"}', expected '${blueprint.contextId}'`
|
|
20231
|
+
});
|
|
20232
|
+
} catch (e) {
|
|
20233
|
+
assertions.push({ name: `namespace:${namespace}`, ok: false, detail: `getNamespace(${namespace}) failed: ${errMsg(e)}` });
|
|
20234
|
+
}
|
|
20235
|
+
}
|
|
18635
20236
|
return assertions;
|
|
18636
20237
|
}
|
|
18637
|
-
async function runTeardown(client, blueprint, machineId, snapshot, result) {
|
|
20238
|
+
async function runTeardown(client, bootstrapClient, blueprint, machineId, snapshot, result) {
|
|
18638
20239
|
const steps = [];
|
|
18639
20240
|
const keyName = result?.keyName ?? keyNameFor(blueprint.name, machineId);
|
|
18640
20241
|
try {
|
|
@@ -18728,10 +20329,34 @@ async function runTeardown(client, blueprint, machineId, snapshot, result) {
|
|
|
18728
20329
|
for (const [typeName] of toDelete) {
|
|
18729
20330
|
steps.push({ step: `delete schema ${typeName}`, ok: false, error: lastError.get(typeName) ?? "could not delete" });
|
|
18730
20331
|
}
|
|
20332
|
+
for (const { issuerId } of result.issuerActions) {
|
|
20333
|
+
if (snapshot.failed) {
|
|
20334
|
+
steps.push({ step: `delete issuer ${issuerId}`, ok: true, skipped: true, error: "skipped \u2014 pre-apply snapshot failed; cannot prove ownership" });
|
|
20335
|
+
continue;
|
|
20336
|
+
}
|
|
20337
|
+
if (snapshot.existingIssuerIds.has(issuerId)) {
|
|
20338
|
+
steps.push({ step: `delete issuer ${issuerId}`, ok: true, skipped: true, error: "skipped \u2014 pre-existed this run" });
|
|
20339
|
+
continue;
|
|
20340
|
+
}
|
|
20341
|
+
try {
|
|
20342
|
+
await bootstrapClient.auth.deleteIssuer({ issuerId });
|
|
20343
|
+
steps.push({ step: `delete issuer ${issuerId}`, ok: true });
|
|
20344
|
+
} catch (e) {
|
|
20345
|
+
steps.push({ step: `delete issuer ${issuerId}`, ok: false, error: errMsg(e) });
|
|
20346
|
+
}
|
|
20347
|
+
}
|
|
20348
|
+
for (const { namespace } of result.namespaceActions) {
|
|
20349
|
+
steps.push({
|
|
20350
|
+
step: `delete namespace ${namespace}`,
|
|
20351
|
+
ok: true,
|
|
20352
|
+
skipped: true,
|
|
20353
|
+
error: "namespace deletion requires a root API key (sk_*) \u2014 blueprint-test never holds one; the registration is left in place"
|
|
20354
|
+
});
|
|
20355
|
+
}
|
|
18731
20356
|
return steps;
|
|
18732
20357
|
}
|
|
18733
20358
|
async function runBlueprintHarness(client, input, deps) {
|
|
18734
|
-
const { tenantId, machineId, keep = false, log, remintForContext } = input;
|
|
20359
|
+
const { tenantId, machineId, keep = false, log, remintForContext, remintBootstrapForContext } = input;
|
|
18735
20360
|
const readExistingKey = async () => void 0;
|
|
18736
20361
|
const persistMintedKey = async () => {
|
|
18737
20362
|
};
|
|
@@ -18741,12 +20366,15 @@ async function runBlueprintHarness(client, input, deps) {
|
|
|
18741
20366
|
log
|
|
18742
20367
|
);
|
|
18743
20368
|
const remint = remintForContext ?? (async () => client);
|
|
18744
|
-
|
|
20369
|
+
const remintBootstrap = remintBootstrapForContext ?? (async () => client);
|
|
20370
|
+
const hasIssuers = (blueprint.issuers ?? []).length > 0;
|
|
20371
|
+
const hasNamespaces = (blueprint.namespaces ?? []).length > 0;
|
|
20372
|
+
let contextSnapshot;
|
|
18745
20373
|
if (remintForContext) {
|
|
18746
20374
|
try {
|
|
18747
|
-
|
|
20375
|
+
contextSnapshot = await captureSnapshot(await remintForContext(blueprint.contextId), blueprint);
|
|
18748
20376
|
} catch (e) {
|
|
18749
|
-
|
|
20377
|
+
contextSnapshot = {
|
|
18750
20378
|
existingSchemaTypes: /* @__PURE__ */ new Set(),
|
|
18751
20379
|
existingProfilePrincipals: /* @__PURE__ */ new Set(),
|
|
18752
20380
|
existingRoleIds: /* @__PURE__ */ new Set(),
|
|
@@ -18754,15 +20382,48 @@ async function runBlueprintHarness(client, input, deps) {
|
|
|
18754
20382
|
};
|
|
18755
20383
|
}
|
|
18756
20384
|
} else {
|
|
18757
|
-
|
|
20385
|
+
contextSnapshot = await captureSnapshot(client, blueprint);
|
|
20386
|
+
}
|
|
20387
|
+
let issuerSnapshot;
|
|
20388
|
+
if (!hasIssuers) {
|
|
20389
|
+
issuerSnapshot = { existingIssuerIds: /* @__PURE__ */ new Set(), failed: false };
|
|
20390
|
+
} else if (remintBootstrapForContext) {
|
|
20391
|
+
try {
|
|
20392
|
+
issuerSnapshot = await captureIssuerSnapshot(await remintBootstrapForContext(blueprint.contextId));
|
|
20393
|
+
} catch (e) {
|
|
20394
|
+
issuerSnapshot = { existingIssuerIds: /* @__PURE__ */ new Set(), failed: !isNotFound(e) };
|
|
20395
|
+
}
|
|
20396
|
+
} else {
|
|
20397
|
+
issuerSnapshot = await captureIssuerSnapshot(client);
|
|
18758
20398
|
}
|
|
20399
|
+
const snapshot = {
|
|
20400
|
+
...contextSnapshot,
|
|
20401
|
+
existingIssuerIds: issuerSnapshot.existingIssuerIds,
|
|
20402
|
+
failed: contextSnapshot.failed || issuerSnapshot.failed
|
|
20403
|
+
};
|
|
18759
20404
|
let result;
|
|
18760
20405
|
let assertions = [];
|
|
18761
20406
|
let applyError;
|
|
18762
20407
|
try {
|
|
18763
|
-
result = await runLoader(client, {
|
|
20408
|
+
result = await runLoader(client, {
|
|
20409
|
+
blueprint,
|
|
20410
|
+
tenantId,
|
|
20411
|
+
machineId,
|
|
20412
|
+
readExistingKey,
|
|
20413
|
+
persistMintedKey,
|
|
20414
|
+
log,
|
|
20415
|
+
remintForContext,
|
|
20416
|
+
remintBootstrapForContext
|
|
20417
|
+
});
|
|
18764
20418
|
log.debug({ component: "harness", contextId: result.contextId, keyName: result.keyName }, "applied");
|
|
18765
|
-
assertions = await runAssertions(
|
|
20419
|
+
assertions = await runAssertions(
|
|
20420
|
+
await remint(blueprint.contextId),
|
|
20421
|
+
hasIssuers || hasNamespaces ? await remintBootstrap(blueprint.contextId) : client,
|
|
20422
|
+
deps,
|
|
20423
|
+
blueprint,
|
|
20424
|
+
tenantId,
|
|
20425
|
+
result
|
|
20426
|
+
);
|
|
18766
20427
|
} catch (e) {
|
|
18767
20428
|
applyError = errMsg(e);
|
|
18768
20429
|
log.warn({ component: "harness", err: applyError }, "apply phase failed");
|
|
@@ -18774,7 +20435,14 @@ async function runBlueprintHarness(client, input, deps) {
|
|
|
18774
20435
|
tdClient = await remint(blueprint.contextId);
|
|
18775
20436
|
} catch {
|
|
18776
20437
|
}
|
|
18777
|
-
|
|
20438
|
+
let tdBootstrapClient = client;
|
|
20439
|
+
if (hasIssuers) {
|
|
20440
|
+
try {
|
|
20441
|
+
tdBootstrapClient = await remintBootstrap(blueprint.contextId);
|
|
20442
|
+
} catch {
|
|
20443
|
+
}
|
|
20444
|
+
}
|
|
20445
|
+
teardown = await runTeardown(tdClient, tdBootstrapClient, blueprint, machineId, snapshot, result);
|
|
18778
20446
|
}
|
|
18779
20447
|
const passed = applyError === void 0 && assertions.length > 0 && assertions.every((a) => a.ok);
|
|
18780
20448
|
return {
|
|
@@ -18848,6 +20516,13 @@ async function runBlueprintTest(fileOrName, opts, deps = {}) {
|
|
|
18848
20516
|
process.stderr.write(` \u2716 Scope gate refused blueprint:
|
|
18849
20517
|
`);
|
|
18850
20518
|
for (const r of err.rejected) process.stderr.write(` \u2022 ${r.action}: ${r.reason}
|
|
20519
|
+
`);
|
|
20520
|
+
return 2;
|
|
20521
|
+
}
|
|
20522
|
+
if (err instanceof CapabilityGateError) {
|
|
20523
|
+
process.stderr.write(` \u2716 Capability gate refused blueprint:
|
|
20524
|
+
`);
|
|
20525
|
+
for (const r of err.rejected) process.stderr.write(` \u2022 ${r.action}: ${r.reason}
|
|
18851
20526
|
`);
|
|
18852
20527
|
return 2;
|
|
18853
20528
|
}
|
|
@@ -18883,9 +20558,22 @@ async function runBlueprintTest(fileOrName, opts, deps = {}) {
|
|
|
18883
20558
|
environment: auth.baseUrl
|
|
18884
20559
|
});
|
|
18885
20560
|
};
|
|
20561
|
+
const remintBootstrapForContext = async (contextId) => {
|
|
20562
|
+
const ctxToken = await mintBridgeToken({
|
|
20563
|
+
cognitoToken: auth.cognitoToken,
|
|
20564
|
+
apiBaseUrl: auth.baseUrl,
|
|
20565
|
+
contextId,
|
|
20566
|
+
tenant: auth.tenant,
|
|
20567
|
+
useScopedToken: opts.useScopedToken
|
|
20568
|
+
});
|
|
20569
|
+
return new VectrosClient({
|
|
20570
|
+
token: ctxToken.token,
|
|
20571
|
+
environment: auth.baseUrl
|
|
20572
|
+
});
|
|
20573
|
+
};
|
|
18886
20574
|
const report = await runBlueprintHarness(
|
|
18887
20575
|
client,
|
|
18888
|
-
{ blueprint, tenantId, machineId, keep: opts.keep, log, remintForContext },
|
|
20576
|
+
{ blueprint, tenantId, machineId, keep: opts.keep, log, remintForContext, remintBootstrapForContext },
|
|
18889
20577
|
{ pingWithKey }
|
|
18890
20578
|
);
|
|
18891
20579
|
if (opts.json) {
|
|
@@ -18966,7 +20654,15 @@ async function runJoin(opts, deps = {}) {
|
|
|
18966
20654
|
return 2;
|
|
18967
20655
|
}
|
|
18968
20656
|
const context = opts.contextArg;
|
|
18969
|
-
const
|
|
20657
|
+
const tenant = opts.tenant ?? "live";
|
|
20658
|
+
let alias;
|
|
20659
|
+
try {
|
|
20660
|
+
alias = keyringAliasFor(opts.alias ?? context, tenant);
|
|
20661
|
+
} catch (err) {
|
|
20662
|
+
process.stderr.write(` \u2716 ${formatError(err)}
|
|
20663
|
+
`);
|
|
20664
|
+
return 2;
|
|
20665
|
+
}
|
|
18970
20666
|
let keyring;
|
|
18971
20667
|
try {
|
|
18972
20668
|
keyring = deps.keyring ?? await Keyring.open();
|
|
@@ -18996,8 +20692,23 @@ async function runJoin(opts, deps = {}) {
|
|
|
18996
20692
|
const heldSecret = existingEntry ? await keyring.resolveSecret(alias) : void 0;
|
|
18997
20693
|
const canReuse = existingEntry && heldSecret && existingEntry.context === context && existingEntry.principalId === principalId && existingEntry.env === auth.env;
|
|
18998
20694
|
if (canReuse && existingEntry) {
|
|
18999
|
-
await keyring
|
|
19000
|
-
|
|
20695
|
+
const willActivate2 = await determineActivateDecision(keyring, {
|
|
20696
|
+
tenant,
|
|
20697
|
+
explicitActivate: opts.activate,
|
|
20698
|
+
hint: { alias, secret: heldSecret }
|
|
20699
|
+
});
|
|
20700
|
+
if (willActivate2) await keyring.setActive(alias);
|
|
20701
|
+
emitJoined({
|
|
20702
|
+
alias,
|
|
20703
|
+
context,
|
|
20704
|
+
principalId,
|
|
20705
|
+
role: opts.role,
|
|
20706
|
+
keyId: existingEntry.keyId,
|
|
20707
|
+
reused: true,
|
|
20708
|
+
tenant,
|
|
20709
|
+
willActivate: willActivate2,
|
|
20710
|
+
explicitlyDeclined: opts.activate === false
|
|
20711
|
+
});
|
|
19001
20712
|
return 0;
|
|
19002
20713
|
}
|
|
19003
20714
|
warnReplacingAlias(existingEntry, alias);
|
|
@@ -19009,13 +20720,24 @@ async function runJoin(opts, deps = {}) {
|
|
|
19009
20720
|
);
|
|
19010
20721
|
}
|
|
19011
20722
|
if (revokedKeyId) await warnOtherAliasesNaming(keyring, revokedKeyId, alias);
|
|
20723
|
+
const willActivate = await determineActivateDecision(keyring, { tenant, explicitActivate: opts.activate });
|
|
19012
20724
|
const { entry } = await storeMintedKey(
|
|
19013
20725
|
keyring,
|
|
19014
20726
|
{ alias, context, principalId, role: opts.role, keyId: key.keyId, env: auth.env },
|
|
19015
20727
|
key.rawKey,
|
|
19016
|
-
{ revokedKeyId }
|
|
20728
|
+
{ activate: willActivate, revokedKeyId }
|
|
19017
20729
|
);
|
|
19018
|
-
emitJoined(
|
|
20730
|
+
emitJoined({
|
|
20731
|
+
alias,
|
|
20732
|
+
context,
|
|
20733
|
+
principalId,
|
|
20734
|
+
role: opts.role,
|
|
20735
|
+
keyId: entry.keyId,
|
|
20736
|
+
reused: false,
|
|
20737
|
+
tenant,
|
|
20738
|
+
willActivate,
|
|
20739
|
+
explicitlyDeclined: opts.activate === false
|
|
20740
|
+
});
|
|
19019
20741
|
return 0;
|
|
19020
20742
|
} catch (err) {
|
|
19021
20743
|
process.stderr.write(formatCliError(err, { action: "join failed" }));
|
|
@@ -19023,27 +20745,34 @@ async function runJoin(opts, deps = {}) {
|
|
|
19023
20745
|
}
|
|
19024
20746
|
});
|
|
19025
20747
|
}
|
|
19026
|
-
function emitJoined(
|
|
19027
|
-
|
|
19028
|
-
|
|
19029
|
-
|
|
19030
|
-
|
|
19031
|
-
|
|
19032
|
-
|
|
19033
|
-
|
|
19034
|
-
|
|
19035
|
-
|
|
20748
|
+
function emitJoined(opts) {
|
|
20749
|
+
const header = `
|
|
20750
|
+
\u2714 Joined '${opts.context}' as ${opts.principalId}` + (opts.reused ? " (existing key reused)" : "") + "\n" + formatKeyValues([
|
|
20751
|
+
["alias", opts.alias],
|
|
20752
|
+
["context", opts.context],
|
|
20753
|
+
["principal", opts.principalId],
|
|
20754
|
+
["role", opts.role],
|
|
20755
|
+
["key id", opts.keyId]
|
|
20756
|
+
]);
|
|
20757
|
+
const activationNote = opts.willActivate ? `
|
|
19036
20758
|
|
|
19037
|
-
Now active. Use \`vectros switch ${alias}\` to return to it, \`vectros keyring show --format env\` to export the key for the MCP server.
|
|
19038
|
-
`
|
|
20759
|
+
Now active. Use \`vectros switch ${opts.alias}\` to return to it, \`vectros keyring show --format env\` to export the key for the MCP server.
|
|
20760
|
+
` : "";
|
|
20761
|
+
process.stdout.write(
|
|
20762
|
+
header + activationNote + tenantActivateMessage({
|
|
20763
|
+
alias: opts.alias,
|
|
20764
|
+
tenant: opts.tenant,
|
|
20765
|
+
willActivate: opts.willActivate,
|
|
20766
|
+
explicitlyDeclined: opts.explicitlyDeclined
|
|
20767
|
+
})
|
|
19039
20768
|
);
|
|
19040
20769
|
}
|
|
19041
20770
|
|
|
19042
20771
|
// src/build-info.ts
|
|
19043
20772
|
var BUILD_INFO = {
|
|
19044
|
-
cli: true ? "0.
|
|
19045
|
-
sdk: true ? "0.
|
|
19046
|
-
blueprints: true ? "0.
|
|
20773
|
+
cli: true ? "0.16.0" : "dev",
|
|
20774
|
+
sdk: true ? "0.40.0" : "dev",
|
|
20775
|
+
blueprints: true ? "0.13.0" : "dev"
|
|
19047
20776
|
};
|
|
19048
20777
|
function formatBuildInfo() {
|
|
19049
20778
|
return `vectros ${BUILD_INFO.cli} (sdk ${BUILD_INFO.sdk}, blueprints ${BUILD_INFO.blueprints})`;
|
|
@@ -19067,6 +20796,14 @@ function addCommonAuthOptions(cmd) {
|
|
|
19067
20796
|
"emit machine-readable JSON instead of the human-readable view"
|
|
19068
20797
|
);
|
|
19069
20798
|
}
|
|
20799
|
+
function parseTenantFlag(raw) {
|
|
20800
|
+
if (raw !== void 0 && raw !== "test" && raw !== "live") {
|
|
20801
|
+
process.stderr.write(` \u2716 --tenant must be 'test' or 'live' (got '${raw}')
|
|
20802
|
+
`);
|
|
20803
|
+
process.exit(2);
|
|
20804
|
+
}
|
|
20805
|
+
return raw;
|
|
20806
|
+
}
|
|
19070
20807
|
function toCommonOptions(flags) {
|
|
19071
20808
|
return {
|
|
19072
20809
|
env: flags.env,
|
|
@@ -19120,7 +20857,10 @@ function buildProgram(deps = {}) {
|
|
|
19120
20857
|
return program;
|
|
19121
20858
|
}
|
|
19122
20859
|
function registerJoinCommand(program, deps) {
|
|
19123
|
-
const join3 = program.command("join").description("Enroll a principal in a context (grant + key) and keep the key in the keyring.").argument("<context>", "the app context to join").option("--role <roleId>", "reference a Role to grant (XOR --actions)").option("--actions <csv>", "inline single-clause actions to grant, comma-separated (XOR --role)").option("--as <principal>", "who to enroll: 'me' (default) or an explicit 'usr_<id>'", "me").option("--scope <ns:value>", "identity override: a scope the member acts under (repeatable, e.g. org:<id>)", collect, []).option("--alias <name>", "keyring entry name (defaults to the context id)").option("--tenant <tenant>", "target tenant: 'test' or 'live' (default live)")
|
|
20860
|
+
const join3 = program.command("join").description("Enroll a principal in a context (grant + key) and keep the key in the keyring.").argument("<context>", "the app context to join").option("--role <roleId>", "reference a Role to grant (XOR --actions)").option("--actions <csv>", "inline single-clause actions to grant, comma-separated (XOR --role)").option("--as <principal>", "who to enroll: 'me' (default) or an explicit 'usr_<id>'", "me").option("--scope <ns:value>", "identity override: a scope the member acts under (repeatable, e.g. org:<id>)", collect, []).option("--alias <name>", "keyring entry name (defaults to the context id)").option("--tenant <tenant>", "target tenant: 'test' or 'live' (default live)").option(
|
|
20861
|
+
"--activate",
|
|
20862
|
+
"force the freshly-stored key to become your active identity, even over a currently-active LIVE credential (the one case --tenant test does NOT do automatically)"
|
|
20863
|
+
).option("--no-activate", "store the key without activating it, regardless of tenant");
|
|
19124
20864
|
addAuthOptions(join3).action(
|
|
19125
20865
|
async (contextArg, flags) => {
|
|
19126
20866
|
if (flags.tenant !== void 0 && flags.tenant !== "test" && flags.tenant !== "live") {
|
|
@@ -19138,7 +20878,8 @@ function registerJoinCommand(program, deps) {
|
|
|
19138
20878
|
as: flags.as,
|
|
19139
20879
|
scope: flags.scope,
|
|
19140
20880
|
alias: flags.alias,
|
|
19141
|
-
tenant: flags.tenant
|
|
20881
|
+
tenant: flags.tenant,
|
|
20882
|
+
activate: flags.activate
|
|
19142
20883
|
},
|
|
19143
20884
|
deps
|
|
19144
20885
|
)
|
|
@@ -19385,9 +21126,10 @@ function toKeyDestination(flags) {
|
|
|
19385
21126
|
function registerKeyCommands(program, deps) {
|
|
19386
21127
|
const key = program.command("key").description("Manage scoped API keys (ssk_*) \u2014 issue, list, revoke, rotate.");
|
|
19387
21128
|
const issue = addKeyDestinationOptions(
|
|
19388
|
-
key.command("issue").description("Mint an ssk_* for a principal that already has a profile in the context.").requiredOption("--principal <p>", "principal (usr_<id> or <id>) with an AccessProfile in the context").requiredOption("--context <c>", "app context the key operates in").option("--name <name>", "key name (idempotency tuple component)", DEFAULT_KEY_NAME).option("--label <label>", "optional principal label (surfaced in /v1/ping)")
|
|
21129
|
+
key.command("issue").description("Mint an ssk_* for a principal that already has a profile in the context.").requiredOption("--principal <p>", "principal (usr_<id> or <id>) with an AccessProfile in the context").requiredOption("--context <c>", "app context the key operates in").option("--name <name>", "key name (idempotency tuple component)", DEFAULT_KEY_NAME).option("--label <label>", "optional principal label (surfaced in /v1/ping)").option("--tenant <tenant>", "tenant the key lives in: 'test' or 'live' (default live)")
|
|
19389
21130
|
);
|
|
19390
21131
|
addAuthOptions(issue).action(async (flags) => {
|
|
21132
|
+
const tenant = parseTenantFlag(flags.tenant);
|
|
19391
21133
|
process.exit(
|
|
19392
21134
|
await runKeyIssue({
|
|
19393
21135
|
...toCommonOptions(flags),
|
|
@@ -19395,6 +21137,7 @@ function registerKeyCommands(program, deps) {
|
|
|
19395
21137
|
context: flags.context,
|
|
19396
21138
|
name: flags.name,
|
|
19397
21139
|
label: flags.label,
|
|
21140
|
+
tenant,
|
|
19398
21141
|
...toKeyDestination(flags)
|
|
19399
21142
|
}, deps)
|
|
19400
21143
|
);
|
|
@@ -19402,40 +21145,58 @@ function registerKeyCommands(program, deps) {
|
|
|
19402
21145
|
const list = key.command("list").description("List active scoped keys in one context (default, or --context <id>); --all-contexts for every context.").option("--principal <p>", "only keys for this principal (usr_<id> or <id>)").option("--context <c>", "only keys in this context (default: the default context)").option(
|
|
19403
21146
|
"--all-contexts",
|
|
19404
21147
|
"list every key across ALL your contexts (owner-only) \u2014 the tenant-wide view for the key limit"
|
|
19405
|
-
);
|
|
21148
|
+
).option("--tenant <tenant>", "tenant to list: 'test' or 'live' (default live; not combinable with --all-contexts)");
|
|
19406
21149
|
addCommonAuthOptions(list).action(
|
|
19407
21150
|
async (flags) => {
|
|
21151
|
+
if (flags.allContexts && flags.tenant !== void 0) {
|
|
21152
|
+
process.stderr.write(" \u2716 --tenant is not combinable with --all-contexts (which already spans both tenants).\n");
|
|
21153
|
+
process.exit(2);
|
|
21154
|
+
}
|
|
21155
|
+
const tenant = parseTenantFlag(flags.tenant);
|
|
19408
21156
|
process.exit(
|
|
19409
21157
|
await runKeyList(
|
|
19410
21158
|
{
|
|
19411
21159
|
...toCommonOptions(flags),
|
|
19412
21160
|
principal: flags.principal,
|
|
19413
21161
|
context: flags.context,
|
|
19414
|
-
allContexts: Boolean(flags.allContexts)
|
|
21162
|
+
allContexts: Boolean(flags.allContexts),
|
|
21163
|
+
tenant
|
|
19415
21164
|
},
|
|
19416
21165
|
deps
|
|
19417
21166
|
)
|
|
19418
21167
|
);
|
|
19419
21168
|
}
|
|
19420
21169
|
);
|
|
19421
|
-
const get = key.command("get").description("Show one key\u2019s metadata (no secret).").argument("<keyId>").option("--context <c>", "context the key is bound to (default: the default context)");
|
|
19422
|
-
addCommonAuthOptions(get).action(
|
|
19423
|
-
|
|
19424
|
-
|
|
19425
|
-
|
|
19426
|
-
|
|
19427
|
-
|
|
19428
|
-
|
|
21170
|
+
const get = key.command("get").description("Show one key\u2019s metadata (no secret).").argument("<keyId>").option("--context <c>", "context the key is bound to (default: the default context)").option("--tenant <tenant>", "tenant the key lives in: 'test' or 'live' (default live)");
|
|
21171
|
+
addCommonAuthOptions(get).action(
|
|
21172
|
+
async (keyId, flags) => {
|
|
21173
|
+
const tenant = parseTenantFlag(flags.tenant);
|
|
21174
|
+
process.exit(await runKeyGet(keyId, { ...toCommonOptions(flags), context: flags.context, tenant }, deps));
|
|
21175
|
+
}
|
|
21176
|
+
);
|
|
21177
|
+
const revoke = key.command("revoke").description("Revoke a key (soft-delete; ~5 min to take effect).").argument("<keyId>").option("--context <c>", "context the key is bound to (default: the default context)").option("--tenant <tenant>", "tenant the key lives in: 'test' or 'live' (default live)");
|
|
21178
|
+
addAuthOptions(revoke).action(
|
|
21179
|
+
async (keyId, flags) => {
|
|
21180
|
+
const tenant = parseTenantFlag(flags.tenant);
|
|
21181
|
+
process.exit(await runKeyRevoke(keyId, { ...toCommonOptions(flags), context: flags.context, tenant }, deps));
|
|
21182
|
+
}
|
|
21183
|
+
);
|
|
19429
21184
|
const rotate = addKeyDestinationOptions(
|
|
19430
|
-
key.command("rotate").description("Revoke the matching active key and mint a fresh secret.").requiredOption("--principal <p>", "principal (usr_<id> or <id>)").requiredOption("--context <c>", "app context").option("--name <name>", "key name to rotate", DEFAULT_KEY_NAME)
|
|
21185
|
+
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(
|
|
21186
|
+
"--force-new",
|
|
21187
|
+
"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)"
|
|
21188
|
+
).option("--tenant <tenant>", "tenant the key lives in: 'test' or 'live' (default live)")
|
|
19431
21189
|
);
|
|
19432
21190
|
addAuthOptions(rotate).action(async (flags) => {
|
|
21191
|
+
const tenant = parseTenantFlag(flags.tenant);
|
|
19433
21192
|
process.exit(
|
|
19434
21193
|
await runKeyRotate({
|
|
19435
21194
|
...toCommonOptions(flags),
|
|
19436
21195
|
principal: flags.principal,
|
|
19437
21196
|
context: flags.context,
|
|
19438
21197
|
name: flags.name,
|
|
21198
|
+
forceNew: flags.forceNew,
|
|
21199
|
+
tenant,
|
|
19439
21200
|
...toKeyDestination(flags)
|
|
19440
21201
|
}, deps)
|
|
19441
21202
|
);
|