@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.js
CHANGED
|
@@ -1675,6 +1675,7 @@ __export(api_exports, {
|
|
|
1675
1675
|
TooManyRequestsError: () => TooManyRequestsError,
|
|
1676
1676
|
TruncationWarningEvent: () => TruncationWarningEvent,
|
|
1677
1677
|
UnauthorizedError: () => UnauthorizedError,
|
|
1678
|
+
UserExistsResponse: () => UserExistsResponse,
|
|
1678
1679
|
UserRequest: () => UserRequest,
|
|
1679
1680
|
UserResponse: () => UserResponse,
|
|
1680
1681
|
auth: () => auth_exports,
|
|
@@ -2351,6 +2352,14 @@ var TruncationWarningEvent;
|
|
|
2351
2352
|
ContextWindowBudget: "context_window_budget"
|
|
2352
2353
|
};
|
|
2353
2354
|
})(TruncationWarningEvent || (TruncationWarningEvent = {}));
|
|
2355
|
+
var UserExistsResponse;
|
|
2356
|
+
((UserExistsResponse2) => {
|
|
2357
|
+
UserExistsResponse2.Status = {
|
|
2358
|
+
Active: "ACTIVE",
|
|
2359
|
+
Suspended: "SUSPENDED",
|
|
2360
|
+
Pending: "PENDING"
|
|
2361
|
+
};
|
|
2362
|
+
})(UserExistsResponse || (UserExistsResponse = {}));
|
|
2354
2363
|
var UserRequest;
|
|
2355
2364
|
((UserRequest2) => {
|
|
2356
2365
|
UserRequest2.Status = {
|
|
@@ -4094,7 +4103,7 @@ var AuthClient = class {
|
|
|
4094
4103
|
return handleNonStatusCodeError(_response.error, _response.rawResponse, "GET", "/v1/admin/keys/scoped");
|
|
4095
4104
|
}
|
|
4096
4105
|
/**
|
|
4097
|
-
* Creates a scoped API key (an `ssk_*` secret) that inherits its permissions from an existing access profile in your account. The call is idempotent on the combination of tenant, context, user, and key name: re-issuing the same request returns the existing key WITHOUT re-disclosing its raw secret. The raw key is returned ONLY in this response — store it securely, as it cannot be retrieved again. Requires the `keys:c` scope.
|
|
4106
|
+
* Creates a scoped API key (an `ssk_*` secret) that inherits its permissions from an existing access profile in your account. The call is idempotent on the combination of tenant, context, user, and key name: re-issuing the same request returns the existing key WITHOUT re-disclosing its raw secret. The raw key is returned ONLY in this response — store it securely, as it cannot be retrieved again. Requires the `keys:c` scope. If you use a scoped credential, `keys:c` alone is not sufficient: because the minted key is durably bound to the profile you name, the profile's effective scopes may not exceed your own, and you may only mint against a profile whose `identityOverrides` values your own identity holds. A root API key (`sk_`) is exempt from both bounds.
|
|
4098
4107
|
*
|
|
4099
4108
|
* @param {Vectros.CreateScopedKeyRequest} request
|
|
4100
4109
|
* @param {AuthClient.RequestOptions} requestOptions - Request-specific configuration.
|
|
@@ -4426,7 +4435,7 @@ var AuthClient = class {
|
|
|
4426
4435
|
);
|
|
4427
4436
|
}
|
|
4428
4437
|
/**
|
|
4429
|
-
* 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.
|
|
4438
|
+
* 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.
|
|
4430
4439
|
*
|
|
4431
4440
|
* @param {Vectros.CreateAccessProfileRequest} request
|
|
4432
4441
|
* @param {AuthClient.RequestOptions} requestOptions - Request-specific configuration.
|
|
@@ -4844,11 +4853,12 @@ var AuthClient = class {
|
|
|
4844
4853
|
);
|
|
4845
4854
|
}
|
|
4846
4855
|
/**
|
|
4847
|
-
* Updates an access profile. This is a partial update: any field you omit (or send as null) keeps its existing value. A profile must reference either inline `scopes` or a `roleId`, never both — so setting `scopes` clears any `roleId`, and setting `roleId` clears any inline `scopes`. The `contextId` and `principalId` are immutable. Status changes (for example active to suspended) take effect within about five minutes. If you use a scoped credential, the profile's effective scopes may not exceed your own, and its `identityOverrides` are bounded twice: you may only set a value your own identity holds, and you may only change or clear a value the profile already holds if that value is yours as well. Repointing or clearing another principal's established identity therefore returns 403. A root API key (`sk_`) is exempt. Requires the `profiles:u` scope.
|
|
4856
|
+
* Updates an access profile. This is a partial update: any field you omit (or send as null) keeps its existing value. A profile must reference either inline `scopes` or a `roleId`, never both — so setting `scopes` clears any `roleId`, and setting `roleId` clears any inline `scopes`. The `contextId` and `principalId` are immutable. Status changes (for example active to suspended) take effect within about five minutes. If you use a scoped credential, the profile's effective scopes may not exceed your own, and its `identityOverrides` are bounded twice: you may only set a value your own identity holds, and you may only change or clear a value the profile already holds if that value is yours as well. Repointing or clearing another principal's established identity therefore returns 403. A root API key (`sk_`) is exempt. If you set `roleId`, it must reference a role that already exists in this context. Requires the `profiles:u` scope.
|
|
4848
4857
|
*
|
|
4849
4858
|
* @param {Vectros.UpdateAccessProfileRequest} request
|
|
4850
4859
|
* @param {AuthClient.RequestOptions} requestOptions - Request-specific configuration.
|
|
4851
4860
|
*
|
|
4861
|
+
* @throws {@link Vectros.BadRequestError}
|
|
4852
4862
|
* @throws {@link Vectros.ForbiddenError}
|
|
4853
4863
|
* @throws {@link Vectros.NotFoundError}
|
|
4854
4864
|
* @throws {@link Vectros.TooManyRequestsError}
|
|
@@ -4895,6 +4905,8 @@ var AuthClient = class {
|
|
|
4895
4905
|
}
|
|
4896
4906
|
if (_response.error.reason === "status-code") {
|
|
4897
4907
|
switch (_response.error.statusCode) {
|
|
4908
|
+
case 400:
|
|
4909
|
+
throw new BadRequestError(_response.error.body, _response.rawResponse);
|
|
4898
4910
|
case 403:
|
|
4899
4911
|
throw new ForbiddenError(_response.error.body, _response.rawResponse);
|
|
4900
4912
|
case 404:
|
|
@@ -5524,7 +5536,7 @@ var AuthClient = class {
|
|
|
5524
5536
|
);
|
|
5525
5537
|
}
|
|
5526
5538
|
/**
|
|
5527
|
-
* 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.
|
|
5539
|
+
* 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.
|
|
5528
5540
|
*
|
|
5529
5541
|
* @param {Vectros.GetUsageRequest} request
|
|
5530
5542
|
* @param {AuthClient.RequestOptions} requestOptions - Request-specific configuration.
|
|
@@ -5586,6 +5598,257 @@ var AuthClient = class {
|
|
|
5586
5598
|
}
|
|
5587
5599
|
return handleNonStatusCodeError(_response.error, _response.rawResponse, "GET", "/v1/usage");
|
|
5588
5600
|
}
|
|
5601
|
+
/**
|
|
5602
|
+
* Retrieves a single registered issuer by issuerId. Requires a root API key or the bootstrap's provisioning capability. 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.
|
|
5603
|
+
*
|
|
5604
|
+
* @param {Vectros.GetIssuerRequest} request
|
|
5605
|
+
* @param {AuthClient.RequestOptions} requestOptions - Request-specific configuration.
|
|
5606
|
+
*
|
|
5607
|
+
* @throws {@link Vectros.ForbiddenError}
|
|
5608
|
+
* @throws {@link Vectros.NotFoundError}
|
|
5609
|
+
*
|
|
5610
|
+
* @example
|
|
5611
|
+
* await client.auth.getIssuer({
|
|
5612
|
+
* issuerId: "auth0-prod"
|
|
5613
|
+
* })
|
|
5614
|
+
*/
|
|
5615
|
+
getIssuer(request, requestOptions) {
|
|
5616
|
+
return HttpResponsePromise.fromPromise(this.__getIssuer(request, requestOptions));
|
|
5617
|
+
}
|
|
5618
|
+
async __getIssuer(request, requestOptions) {
|
|
5619
|
+
const { issuerId } = request;
|
|
5620
|
+
const _authRequest = await this._options.authProvider.getAuthRequest();
|
|
5621
|
+
const _headers = mergeHeaders(
|
|
5622
|
+
_authRequest.headers,
|
|
5623
|
+
this._options?.headers,
|
|
5624
|
+
requestOptions?.headers
|
|
5625
|
+
);
|
|
5626
|
+
const _response = await fetcher({
|
|
5627
|
+
url: url_exports.join(
|
|
5628
|
+
await Supplier.get(this._options.baseUrl) ?? await Supplier.get(this._options.environment),
|
|
5629
|
+
`v1/auth/issuers/${url_exports.encodePathParam(issuerId)}`
|
|
5630
|
+
),
|
|
5631
|
+
method: "GET",
|
|
5632
|
+
headers: _headers,
|
|
5633
|
+
queryString: url_exports.queryBuilder().mergeAdditional(requestOptions?.queryParams).build(),
|
|
5634
|
+
timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1e3,
|
|
5635
|
+
maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries,
|
|
5636
|
+
abortSignal: requestOptions?.abortSignal,
|
|
5637
|
+
fetchFn: this._options?.fetch,
|
|
5638
|
+
logging: this._options.logging
|
|
5639
|
+
});
|
|
5640
|
+
if (_response.ok) {
|
|
5641
|
+
return { data: _response.body, rawResponse: _response.rawResponse };
|
|
5642
|
+
}
|
|
5643
|
+
if (_response.error.reason === "status-code") {
|
|
5644
|
+
switch (_response.error.statusCode) {
|
|
5645
|
+
case 403:
|
|
5646
|
+
throw new ForbiddenError(_response.error.body, _response.rawResponse);
|
|
5647
|
+
case 404:
|
|
5648
|
+
throw new NotFoundError(_response.error.body, _response.rawResponse);
|
|
5649
|
+
default:
|
|
5650
|
+
throw new VectrosError({
|
|
5651
|
+
statusCode: _response.error.statusCode,
|
|
5652
|
+
body: _response.error.body,
|
|
5653
|
+
rawResponse: _response.rawResponse
|
|
5654
|
+
});
|
|
5655
|
+
}
|
|
5656
|
+
}
|
|
5657
|
+
return handleNonStatusCodeError(_response.error, _response.rawResponse, "GET", "/v1/auth/issuers/{issuerId}");
|
|
5658
|
+
}
|
|
5659
|
+
/**
|
|
5660
|
+
* Deregisters a trusted third-party IdP issuer. Requires a root API key or the bootstrap's provisioning capability. 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.
|
|
5661
|
+
*
|
|
5662
|
+
* @param {Vectros.DeleteIssuerRequest} request
|
|
5663
|
+
* @param {AuthClient.RequestOptions} requestOptions - Request-specific configuration.
|
|
5664
|
+
*
|
|
5665
|
+
* @throws {@link Vectros.ForbiddenError}
|
|
5666
|
+
* @throws {@link Vectros.NotFoundError}
|
|
5667
|
+
* @throws {@link Vectros.ConflictError}
|
|
5668
|
+
* @throws {@link Vectros.TooManyRequestsError}
|
|
5669
|
+
*
|
|
5670
|
+
* @example
|
|
5671
|
+
* await client.auth.deleteIssuer({
|
|
5672
|
+
* issuerId: "auth0-prod"
|
|
5673
|
+
* })
|
|
5674
|
+
*/
|
|
5675
|
+
deleteIssuer(request, requestOptions) {
|
|
5676
|
+
return HttpResponsePromise.fromPromise(this.__deleteIssuer(request, requestOptions));
|
|
5677
|
+
}
|
|
5678
|
+
async __deleteIssuer(request, requestOptions) {
|
|
5679
|
+
const { issuerId } = request;
|
|
5680
|
+
const _authRequest = await this._options.authProvider.getAuthRequest();
|
|
5681
|
+
const _headers = mergeHeaders(
|
|
5682
|
+
_authRequest.headers,
|
|
5683
|
+
this._options?.headers,
|
|
5684
|
+
requestOptions?.headers
|
|
5685
|
+
);
|
|
5686
|
+
const _response = await fetcher({
|
|
5687
|
+
url: url_exports.join(
|
|
5688
|
+
await Supplier.get(this._options.baseUrl) ?? await Supplier.get(this._options.environment),
|
|
5689
|
+
`v1/auth/issuers/${url_exports.encodePathParam(issuerId)}`
|
|
5690
|
+
),
|
|
5691
|
+
method: "DELETE",
|
|
5692
|
+
headers: _headers,
|
|
5693
|
+
queryString: url_exports.queryBuilder().mergeAdditional(requestOptions?.queryParams).build(),
|
|
5694
|
+
timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1e3,
|
|
5695
|
+
maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries,
|
|
5696
|
+
abortSignal: requestOptions?.abortSignal,
|
|
5697
|
+
fetchFn: this._options?.fetch,
|
|
5698
|
+
logging: this._options.logging
|
|
5699
|
+
});
|
|
5700
|
+
if (_response.ok) {
|
|
5701
|
+
return { data: void 0, rawResponse: _response.rawResponse };
|
|
5702
|
+
}
|
|
5703
|
+
if (_response.error.reason === "status-code") {
|
|
5704
|
+
switch (_response.error.statusCode) {
|
|
5705
|
+
case 403:
|
|
5706
|
+
throw new ForbiddenError(_response.error.body, _response.rawResponse);
|
|
5707
|
+
case 404:
|
|
5708
|
+
throw new NotFoundError(_response.error.body, _response.rawResponse);
|
|
5709
|
+
case 409:
|
|
5710
|
+
throw new ConflictError(_response.error.body, _response.rawResponse);
|
|
5711
|
+
case 429:
|
|
5712
|
+
throw new TooManyRequestsError(_response.error.body, _response.rawResponse);
|
|
5713
|
+
default:
|
|
5714
|
+
throw new VectrosError({
|
|
5715
|
+
statusCode: _response.error.statusCode,
|
|
5716
|
+
body: _response.error.body,
|
|
5717
|
+
rawResponse: _response.rawResponse
|
|
5718
|
+
});
|
|
5719
|
+
}
|
|
5720
|
+
}
|
|
5721
|
+
return handleNonStatusCodeError(
|
|
5722
|
+
_response.error,
|
|
5723
|
+
_response.rawResponse,
|
|
5724
|
+
"DELETE",
|
|
5725
|
+
"/v1/auth/issuers/{issuerId}"
|
|
5726
|
+
);
|
|
5727
|
+
}
|
|
5728
|
+
/**
|
|
5729
|
+
* 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.
|
|
5730
|
+
*
|
|
5731
|
+
* @param {Vectros.ListIssuersRequest} request
|
|
5732
|
+
* @param {AuthClient.RequestOptions} requestOptions - Request-specific configuration.
|
|
5733
|
+
*
|
|
5734
|
+
* @throws {@link Vectros.ForbiddenError}
|
|
5735
|
+
*
|
|
5736
|
+
* @example
|
|
5737
|
+
* await client.auth.listIssuers()
|
|
5738
|
+
*/
|
|
5739
|
+
listIssuers(request = {}, requestOptions) {
|
|
5740
|
+
return HttpResponsePromise.fromPromise(this.__listIssuers(request, requestOptions));
|
|
5741
|
+
}
|
|
5742
|
+
async __listIssuers(request = {}, requestOptions) {
|
|
5743
|
+
const { startFrom, limit } = request;
|
|
5744
|
+
const _queryParams = {
|
|
5745
|
+
startFrom,
|
|
5746
|
+
limit
|
|
5747
|
+
};
|
|
5748
|
+
const _authRequest = await this._options.authProvider.getAuthRequest();
|
|
5749
|
+
const _headers = mergeHeaders(
|
|
5750
|
+
_authRequest.headers,
|
|
5751
|
+
this._options?.headers,
|
|
5752
|
+
requestOptions?.headers
|
|
5753
|
+
);
|
|
5754
|
+
const _response = await fetcher({
|
|
5755
|
+
url: url_exports.join(
|
|
5756
|
+
await Supplier.get(this._options.baseUrl) ?? await Supplier.get(this._options.environment),
|
|
5757
|
+
"v1/auth/issuers"
|
|
5758
|
+
),
|
|
5759
|
+
method: "GET",
|
|
5760
|
+
headers: _headers,
|
|
5761
|
+
queryString: url_exports.queryBuilder().addMany(_queryParams).mergeAdditional(requestOptions?.queryParams).build(),
|
|
5762
|
+
timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1e3,
|
|
5763
|
+
maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries,
|
|
5764
|
+
abortSignal: requestOptions?.abortSignal,
|
|
5765
|
+
fetchFn: this._options?.fetch,
|
|
5766
|
+
logging: this._options.logging
|
|
5767
|
+
});
|
|
5768
|
+
if (_response.ok) {
|
|
5769
|
+
return { data: _response.body, rawResponse: _response.rawResponse };
|
|
5770
|
+
}
|
|
5771
|
+
if (_response.error.reason === "status-code") {
|
|
5772
|
+
switch (_response.error.statusCode) {
|
|
5773
|
+
case 403:
|
|
5774
|
+
throw new ForbiddenError(_response.error.body, _response.rawResponse);
|
|
5775
|
+
default:
|
|
5776
|
+
throw new VectrosError({
|
|
5777
|
+
statusCode: _response.error.statusCode,
|
|
5778
|
+
body: _response.error.body,
|
|
5779
|
+
rawResponse: _response.rawResponse
|
|
5780
|
+
});
|
|
5781
|
+
}
|
|
5782
|
+
}
|
|
5783
|
+
return handleNonStatusCodeError(_response.error, _response.rawResponse, "GET", "/v1/auth/issuers");
|
|
5784
|
+
}
|
|
5785
|
+
/**
|
|
5786
|
+
* 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`.
|
|
5787
|
+
*
|
|
5788
|
+
* @param {Vectros.IssuerRequest} request
|
|
5789
|
+
* @param {AuthClient.RequestOptions} requestOptions - Request-specific configuration.
|
|
5790
|
+
*
|
|
5791
|
+
* @throws {@link Vectros.BadRequestError}
|
|
5792
|
+
* @throws {@link Vectros.ForbiddenError}
|
|
5793
|
+
* @throws {@link Vectros.TooManyRequestsError}
|
|
5794
|
+
*
|
|
5795
|
+
* @example
|
|
5796
|
+
* await client.auth.registerIssuer({
|
|
5797
|
+
* issuerId: "auth0-prod",
|
|
5798
|
+
* issuer: "https://your-tenant.us.auth0.com/",
|
|
5799
|
+
* jwksUri: "https://your-tenant.us.auth0.com/.well-known/jwks.json",
|
|
5800
|
+
* audience: "https://api.your-app.example.com",
|
|
5801
|
+
* contextId: "casework"
|
|
5802
|
+
* })
|
|
5803
|
+
*/
|
|
5804
|
+
registerIssuer(request, requestOptions) {
|
|
5805
|
+
return HttpResponsePromise.fromPromise(this.__registerIssuer(request, requestOptions));
|
|
5806
|
+
}
|
|
5807
|
+
async __registerIssuer(request, requestOptions) {
|
|
5808
|
+
const _authRequest = await this._options.authProvider.getAuthRequest();
|
|
5809
|
+
const _headers = mergeHeaders(
|
|
5810
|
+
_authRequest.headers,
|
|
5811
|
+
this._options?.headers,
|
|
5812
|
+
requestOptions?.headers
|
|
5813
|
+
);
|
|
5814
|
+
const _response = await fetcher({
|
|
5815
|
+
url: url_exports.join(
|
|
5816
|
+
await Supplier.get(this._options.baseUrl) ?? await Supplier.get(this._options.environment),
|
|
5817
|
+
"v1/auth/issuers"
|
|
5818
|
+
),
|
|
5819
|
+
method: "POST",
|
|
5820
|
+
headers: _headers,
|
|
5821
|
+
contentType: "application/json",
|
|
5822
|
+
queryString: url_exports.queryBuilder().mergeAdditional(requestOptions?.queryParams).build(),
|
|
5823
|
+
requestType: "json",
|
|
5824
|
+
body: request,
|
|
5825
|
+
timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1e3,
|
|
5826
|
+
maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries,
|
|
5827
|
+
abortSignal: requestOptions?.abortSignal,
|
|
5828
|
+
fetchFn: this._options?.fetch,
|
|
5829
|
+
logging: this._options.logging
|
|
5830
|
+
});
|
|
5831
|
+
if (_response.ok) {
|
|
5832
|
+
return { data: _response.body, rawResponse: _response.rawResponse };
|
|
5833
|
+
}
|
|
5834
|
+
if (_response.error.reason === "status-code") {
|
|
5835
|
+
switch (_response.error.statusCode) {
|
|
5836
|
+
case 400:
|
|
5837
|
+
throw new BadRequestError(_response.error.body, _response.rawResponse);
|
|
5838
|
+
case 403:
|
|
5839
|
+
throw new ForbiddenError(_response.error.body, _response.rawResponse);
|
|
5840
|
+
case 429:
|
|
5841
|
+
throw new TooManyRequestsError(_response.error.body, _response.rawResponse);
|
|
5842
|
+
default:
|
|
5843
|
+
throw new VectrosError({
|
|
5844
|
+
statusCode: _response.error.statusCode,
|
|
5845
|
+
body: _response.error.body,
|
|
5846
|
+
rawResponse: _response.rawResponse
|
|
5847
|
+
});
|
|
5848
|
+
}
|
|
5849
|
+
}
|
|
5850
|
+
return handleNonStatusCodeError(_response.error, _response.rawResponse, "POST", "/v1/auth/issuers");
|
|
5851
|
+
}
|
|
5589
5852
|
/**
|
|
5590
5853
|
* Returns the identity bound to your credential — your account, principal type, key id, and scope details — so you can confirm who you are authenticated as and that the credential is valid. MCP clients use this to render "signed in as ..." in a chat UI without a separate identity endpoint.
|
|
5591
5854
|
*
|
|
@@ -5631,7 +5894,7 @@ var AuthClient = class {
|
|
|
5631
5894
|
return handleNonStatusCodeError(_response.error, _response.rawResponse, "GET", "/v1/ping");
|
|
5632
5895
|
}
|
|
5633
5896
|
/**
|
|
5634
|
-
* Returns the access profiles for the given principal across
|
|
5897
|
+
* 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.
|
|
5635
5898
|
*
|
|
5636
5899
|
* @param {Vectros.ListProfilesForPrincipalRequest} request
|
|
5637
5900
|
* @param {AuthClient.RequestOptions} requestOptions - Request-specific configuration.
|
|
@@ -5909,40 +6172,34 @@ var AuthClient = class {
|
|
|
5909
6172
|
}
|
|
5910
6173
|
return handleNonStatusCodeError(_response.error, _response.rawResponse, "POST", "/v1/users/invite/resend");
|
|
5911
6174
|
}
|
|
5912
|
-
};
|
|
5913
|
-
var ComplianceClient = class {
|
|
5914
|
-
constructor(options) {
|
|
5915
|
-
this._options = normalizeClientOptionsWithAuth(options);
|
|
5916
|
-
}
|
|
5917
6175
|
/**
|
|
5918
|
-
*
|
|
6176
|
+
* 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.
|
|
5919
6177
|
*
|
|
5920
|
-
* @param {Vectros.
|
|
5921
|
-
* @param {
|
|
6178
|
+
* @param {Vectros.TokenExchangeRequest} request
|
|
6179
|
+
* @param {AuthClient.RequestOptions} requestOptions - Request-specific configuration.
|
|
5922
6180
|
*
|
|
6181
|
+
* @throws {@link Vectros.BadRequestError}
|
|
6182
|
+
* @throws {@link Vectros.UnauthorizedError}
|
|
5923
6183
|
* @throws {@link Vectros.ForbiddenError}
|
|
6184
|
+
* @throws {@link Vectros.NotFoundError}
|
|
5924
6185
|
* @throws {@link Vectros.TooManyRequestsError}
|
|
5925
|
-
* @throws {@link Vectros.NotImplementedError}
|
|
5926
6186
|
*
|
|
5927
6187
|
* @example
|
|
5928
|
-
* await client.
|
|
5929
|
-
*
|
|
6188
|
+
* await client.auth.exchangeToken({
|
|
6189
|
+
* grant_type: "urn:ietf:params:oauth:grant-type:token-exchange",
|
|
6190
|
+
* subject_token: "subject_token",
|
|
6191
|
+
* subject_token_type: "urn:ietf:params:oauth:token-type:jwt"
|
|
5930
6192
|
* })
|
|
5931
6193
|
*/
|
|
5932
|
-
|
|
5933
|
-
return HttpResponsePromise.fromPromise(this.
|
|
6194
|
+
exchangeToken(request, requestOptions) {
|
|
6195
|
+
return HttpResponsePromise.fromPromise(this.__exchangeToken(request, requestOptions));
|
|
5934
6196
|
}
|
|
5935
|
-
async
|
|
5936
|
-
const
|
|
5937
|
-
const _headers = mergeHeaders(
|
|
5938
|
-
_authRequest.headers,
|
|
5939
|
-
this._options?.headers,
|
|
5940
|
-
requestOptions?.headers
|
|
5941
|
-
);
|
|
6197
|
+
async __exchangeToken(request, requestOptions) {
|
|
6198
|
+
const _headers = mergeHeaders(this._options?.headers, requestOptions?.headers);
|
|
5942
6199
|
const _response = await fetcher({
|
|
5943
6200
|
url: url_exports.join(
|
|
5944
6201
|
await Supplier.get(this._options.baseUrl) ?? await Supplier.get(this._options.environment),
|
|
5945
|
-
"v1/
|
|
6202
|
+
"v1/auth/token/exchange"
|
|
5946
6203
|
),
|
|
5947
6204
|
method: "POST",
|
|
5948
6205
|
headers: _headers,
|
|
@@ -5961,12 +6218,16 @@ var ComplianceClient = class {
|
|
|
5961
6218
|
}
|
|
5962
6219
|
if (_response.error.reason === "status-code") {
|
|
5963
6220
|
switch (_response.error.statusCode) {
|
|
6221
|
+
case 400:
|
|
6222
|
+
throw new BadRequestError(_response.error.body, _response.rawResponse);
|
|
6223
|
+
case 401:
|
|
6224
|
+
throw new UnauthorizedError(_response.error.body, _response.rawResponse);
|
|
5964
6225
|
case 403:
|
|
5965
6226
|
throw new ForbiddenError(_response.error.body, _response.rawResponse);
|
|
6227
|
+
case 404:
|
|
6228
|
+
throw new NotFoundError(_response.error.body, _response.rawResponse);
|
|
5966
6229
|
case 429:
|
|
5967
6230
|
throw new TooManyRequestsError(_response.error.body, _response.rawResponse);
|
|
5968
|
-
case 501:
|
|
5969
|
-
throw new NotImplementedError(_response.error.body, _response.rawResponse);
|
|
5970
6231
|
default:
|
|
5971
6232
|
throw new VectrosError({
|
|
5972
6233
|
statusCode: _response.error.statusCode,
|
|
@@ -5975,10 +6236,78 @@ var ComplianceClient = class {
|
|
|
5975
6236
|
});
|
|
5976
6237
|
}
|
|
5977
6238
|
}
|
|
5978
|
-
return handleNonStatusCodeError(_response.error, _response.rawResponse, "POST", "/v1/
|
|
6239
|
+
return handleNonStatusCodeError(_response.error, _response.rawResponse, "POST", "/v1/auth/token/exchange");
|
|
6240
|
+
}
|
|
6241
|
+
};
|
|
6242
|
+
var ComplianceClient = class {
|
|
6243
|
+
constructor(options) {
|
|
6244
|
+
this._options = normalizeClientOptionsWithAuth(options);
|
|
5979
6245
|
}
|
|
5980
6246
|
/**
|
|
5981
|
-
*
|
|
6247
|
+
* 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.
|
|
6248
|
+
*
|
|
6249
|
+
* @param {Vectros.ErasureRequest} request
|
|
6250
|
+
* @param {ComplianceClient.RequestOptions} requestOptions - Request-specific configuration.
|
|
6251
|
+
*
|
|
6252
|
+
* @throws {@link Vectros.ForbiddenError}
|
|
6253
|
+
* @throws {@link Vectros.TooManyRequestsError}
|
|
6254
|
+
* @throws {@link Vectros.NotImplementedError}
|
|
6255
|
+
*
|
|
6256
|
+
* @example
|
|
6257
|
+
* await client.compliance.createErasureRequest({
|
|
6258
|
+
* subjectType: "user"
|
|
6259
|
+
* })
|
|
6260
|
+
*/
|
|
6261
|
+
createErasureRequest(request, requestOptions) {
|
|
6262
|
+
return HttpResponsePromise.fromPromise(this.__createErasureRequest(request, requestOptions));
|
|
6263
|
+
}
|
|
6264
|
+
async __createErasureRequest(request, requestOptions) {
|
|
6265
|
+
const _authRequest = await this._options.authProvider.getAuthRequest();
|
|
6266
|
+
const _headers = mergeHeaders(
|
|
6267
|
+
_authRequest.headers,
|
|
6268
|
+
this._options?.headers,
|
|
6269
|
+
requestOptions?.headers
|
|
6270
|
+
);
|
|
6271
|
+
const _response = await fetcher({
|
|
6272
|
+
url: url_exports.join(
|
|
6273
|
+
await Supplier.get(this._options.baseUrl) ?? await Supplier.get(this._options.environment),
|
|
6274
|
+
"v1/erasure-requests"
|
|
6275
|
+
),
|
|
6276
|
+
method: "POST",
|
|
6277
|
+
headers: _headers,
|
|
6278
|
+
contentType: "application/json",
|
|
6279
|
+
queryString: url_exports.queryBuilder().mergeAdditional(requestOptions?.queryParams).build(),
|
|
6280
|
+
requestType: "json",
|
|
6281
|
+
body: request,
|
|
6282
|
+
timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1e3,
|
|
6283
|
+
maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries,
|
|
6284
|
+
abortSignal: requestOptions?.abortSignal,
|
|
6285
|
+
fetchFn: this._options?.fetch,
|
|
6286
|
+
logging: this._options.logging
|
|
6287
|
+
});
|
|
6288
|
+
if (_response.ok) {
|
|
6289
|
+
return { data: _response.body, rawResponse: _response.rawResponse };
|
|
6290
|
+
}
|
|
6291
|
+
if (_response.error.reason === "status-code") {
|
|
6292
|
+
switch (_response.error.statusCode) {
|
|
6293
|
+
case 403:
|
|
6294
|
+
throw new ForbiddenError(_response.error.body, _response.rawResponse);
|
|
6295
|
+
case 429:
|
|
6296
|
+
throw new TooManyRequestsError(_response.error.body, _response.rawResponse);
|
|
6297
|
+
case 501:
|
|
6298
|
+
throw new NotImplementedError(_response.error.body, _response.rawResponse);
|
|
6299
|
+
default:
|
|
6300
|
+
throw new VectrosError({
|
|
6301
|
+
statusCode: _response.error.statusCode,
|
|
6302
|
+
body: _response.error.body,
|
|
6303
|
+
rawResponse: _response.rawResponse
|
|
6304
|
+
});
|
|
6305
|
+
}
|
|
6306
|
+
}
|
|
6307
|
+
return handleNonStatusCodeError(_response.error, _response.rawResponse, "POST", "/v1/erasure-requests");
|
|
6308
|
+
}
|
|
6309
|
+
/**
|
|
6310
|
+
* 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.
|
|
5982
6311
|
*
|
|
5983
6312
|
* @param {Vectros.GetErasureRequestRequest} request
|
|
5984
6313
|
* @param {ComplianceClient.RequestOptions} requestOptions - Request-specific configuration.
|
|
@@ -7386,6 +7715,7 @@ var IdentityClient = class {
|
|
|
7386
7715
|
* @example
|
|
7387
7716
|
* await client.identity.listEntities({
|
|
7388
7717
|
* namespace: "team",
|
|
7718
|
+
* contextId: "myapp",
|
|
7389
7719
|
* startFrom: "b3BhcXVlLWN1cnNvci1mcm9tLXRoZS1wcmV2aW91cy1wYWdl"
|
|
7390
7720
|
* })
|
|
7391
7721
|
*/
|
|
@@ -7395,6 +7725,7 @@ var IdentityClient = class {
|
|
|
7395
7725
|
async __listEntities(request, requestOptions) {
|
|
7396
7726
|
const {
|
|
7397
7727
|
namespace,
|
|
7728
|
+
contextId,
|
|
7398
7729
|
userId,
|
|
7399
7730
|
externalId,
|
|
7400
7731
|
scope,
|
|
@@ -7409,6 +7740,7 @@ var IdentityClient = class {
|
|
|
7409
7740
|
limit
|
|
7410
7741
|
} = request;
|
|
7411
7742
|
const _queryParams = {
|
|
7743
|
+
contextId,
|
|
7412
7744
|
userId,
|
|
7413
7745
|
externalId,
|
|
7414
7746
|
scope,
|
|
@@ -7467,6 +7799,7 @@ var IdentityClient = class {
|
|
|
7467
7799
|
* @example
|
|
7468
7800
|
* await client.identity.createEntity({
|
|
7469
7801
|
* namespace: "team",
|
|
7802
|
+
* contextId: "myapp",
|
|
7470
7803
|
* body: {
|
|
7471
7804
|
* externalId: "team_eng_platform"
|
|
7472
7805
|
* }
|
|
@@ -7476,9 +7809,10 @@ var IdentityClient = class {
|
|
|
7476
7809
|
return HttpResponsePromise.fromPromise(this.__createEntity(request, requestOptions));
|
|
7477
7810
|
}
|
|
7478
7811
|
async __createEntity(request, requestOptions) {
|
|
7479
|
-
const { namespace, upsert, body: _body } = request;
|
|
7812
|
+
const { namespace, upsert, contextId, body: _body } = request;
|
|
7480
7813
|
const _queryParams = {
|
|
7481
|
-
upsert
|
|
7814
|
+
upsert,
|
|
7815
|
+
contextId
|
|
7482
7816
|
};
|
|
7483
7817
|
const _authRequest = await this._options.authProvider.getAuthRequest();
|
|
7484
7818
|
const _headers = mergeHeaders(
|
|
@@ -7535,14 +7869,18 @@ var IdentityClient = class {
|
|
|
7535
7869
|
* @example
|
|
7536
7870
|
* await client.identity.getEntity({
|
|
7537
7871
|
* namespace: "team",
|
|
7538
|
-
* id: "6ba7b810-9dad-11d1-80b4-00c04fd430c8"
|
|
7872
|
+
* id: "6ba7b810-9dad-11d1-80b4-00c04fd430c8",
|
|
7873
|
+
* contextId: "myapp"
|
|
7539
7874
|
* })
|
|
7540
7875
|
*/
|
|
7541
7876
|
getEntity(request, requestOptions) {
|
|
7542
7877
|
return HttpResponsePromise.fromPromise(this.__getEntity(request, requestOptions));
|
|
7543
7878
|
}
|
|
7544
7879
|
async __getEntity(request, requestOptions) {
|
|
7545
|
-
const { namespace, id } = request;
|
|
7880
|
+
const { namespace, id, contextId } = request;
|
|
7881
|
+
const _queryParams = {
|
|
7882
|
+
contextId
|
|
7883
|
+
};
|
|
7546
7884
|
const _authRequest = await this._options.authProvider.getAuthRequest();
|
|
7547
7885
|
const _headers = mergeHeaders(
|
|
7548
7886
|
_authRequest.headers,
|
|
@@ -7556,7 +7894,7 @@ var IdentityClient = class {
|
|
|
7556
7894
|
),
|
|
7557
7895
|
method: "GET",
|
|
7558
7896
|
headers: _headers,
|
|
7559
|
-
queryString: url_exports.queryBuilder().mergeAdditional(requestOptions?.queryParams).build(),
|
|
7897
|
+
queryString: url_exports.queryBuilder().addMany(_queryParams).mergeAdditional(requestOptions?.queryParams).build(),
|
|
7560
7898
|
timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1e3,
|
|
7561
7899
|
maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries,
|
|
7562
7900
|
abortSignal: requestOptions?.abortSignal,
|
|
@@ -7593,6 +7931,7 @@ var IdentityClient = class {
|
|
|
7593
7931
|
* await client.identity.updateEntity({
|
|
7594
7932
|
* namespace: "team",
|
|
7595
7933
|
* id: "id",
|
|
7934
|
+
* contextId: "myapp",
|
|
7596
7935
|
* body: {
|
|
7597
7936
|
* externalId: "team_eng_platform"
|
|
7598
7937
|
* }
|
|
@@ -7602,7 +7941,10 @@ var IdentityClient = class {
|
|
|
7602
7941
|
return HttpResponsePromise.fromPromise(this.__updateEntity(request, requestOptions));
|
|
7603
7942
|
}
|
|
7604
7943
|
async __updateEntity(request, requestOptions) {
|
|
7605
|
-
const { namespace, id, body: _body } = request;
|
|
7944
|
+
const { namespace, id, contextId, body: _body } = request;
|
|
7945
|
+
const _queryParams = {
|
|
7946
|
+
contextId
|
|
7947
|
+
};
|
|
7606
7948
|
const _authRequest = await this._options.authProvider.getAuthRequest();
|
|
7607
7949
|
const _headers = mergeHeaders(
|
|
7608
7950
|
_authRequest.headers,
|
|
@@ -7617,7 +7959,7 @@ var IdentityClient = class {
|
|
|
7617
7959
|
method: "PUT",
|
|
7618
7960
|
headers: _headers,
|
|
7619
7961
|
contentType: "application/json",
|
|
7620
|
-
queryString: url_exports.queryBuilder().mergeAdditional(requestOptions?.queryParams).build(),
|
|
7962
|
+
queryString: url_exports.queryBuilder().addMany(_queryParams).mergeAdditional(requestOptions?.queryParams).build(),
|
|
7621
7963
|
requestType: "json",
|
|
7622
7964
|
body: _body,
|
|
7623
7965
|
timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1e3,
|
|
@@ -7657,14 +7999,18 @@ var IdentityClient = class {
|
|
|
7657
7999
|
* @example
|
|
7658
8000
|
* await client.identity.deleteEntity({
|
|
7659
8001
|
* namespace: "team",
|
|
7660
|
-
* id: "id"
|
|
8002
|
+
* id: "id",
|
|
8003
|
+
* contextId: "myapp"
|
|
7661
8004
|
* })
|
|
7662
8005
|
*/
|
|
7663
8006
|
deleteEntity(request, requestOptions) {
|
|
7664
8007
|
return HttpResponsePromise.fromPromise(this.__deleteEntity(request, requestOptions));
|
|
7665
8008
|
}
|
|
7666
8009
|
async __deleteEntity(request, requestOptions) {
|
|
7667
|
-
const { namespace, id } = request;
|
|
8010
|
+
const { namespace, id, contextId } = request;
|
|
8011
|
+
const _queryParams = {
|
|
8012
|
+
contextId
|
|
8013
|
+
};
|
|
7668
8014
|
const _authRequest = await this._options.authProvider.getAuthRequest();
|
|
7669
8015
|
const _headers = mergeHeaders(
|
|
7670
8016
|
_authRequest.headers,
|
|
@@ -7678,7 +8024,7 @@ var IdentityClient = class {
|
|
|
7678
8024
|
),
|
|
7679
8025
|
method: "DELETE",
|
|
7680
8026
|
headers: _headers,
|
|
7681
|
-
queryString: url_exports.queryBuilder().mergeAdditional(requestOptions?.queryParams).build(),
|
|
8027
|
+
queryString: url_exports.queryBuilder().addMany(_queryParams).mergeAdditional(requestOptions?.queryParams).build(),
|
|
7682
8028
|
timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1e3,
|
|
7683
8029
|
maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries,
|
|
7684
8030
|
abortSignal: requestOptions?.abortSignal,
|
|
@@ -7721,6 +8067,7 @@ var IdentityClient = class {
|
|
|
7721
8067
|
* @example
|
|
7722
8068
|
* await client.identity.lookupEntities({
|
|
7723
8069
|
* namespace: "team",
|
|
8070
|
+
* contextId: "myapp",
|
|
7724
8071
|
* body: {
|
|
7725
8072
|
* type: "person_v1",
|
|
7726
8073
|
* field: "ssn"
|
|
@@ -7731,7 +8078,10 @@ var IdentityClient = class {
|
|
|
7731
8078
|
return HttpResponsePromise.fromPromise(this.__lookupEntities(request, requestOptions));
|
|
7732
8079
|
}
|
|
7733
8080
|
async __lookupEntities(request, requestOptions) {
|
|
7734
|
-
const { namespace, body: _body } = request;
|
|
8081
|
+
const { namespace, contextId, body: _body } = request;
|
|
8082
|
+
const _queryParams = {
|
|
8083
|
+
contextId
|
|
8084
|
+
};
|
|
7735
8085
|
const _authRequest = await this._options.authProvider.getAuthRequest();
|
|
7736
8086
|
const _headers = mergeHeaders(
|
|
7737
8087
|
_authRequest.headers,
|
|
@@ -7746,7 +8096,7 @@ var IdentityClient = class {
|
|
|
7746
8096
|
method: "POST",
|
|
7747
8097
|
headers: _headers,
|
|
7748
8098
|
contentType: "application/json",
|
|
7749
|
-
queryString: url_exports.queryBuilder().mergeAdditional(requestOptions?.queryParams).build(),
|
|
8099
|
+
queryString: url_exports.queryBuilder().addMany(_queryParams).mergeAdditional(requestOptions?.queryParams).build(),
|
|
7750
8100
|
requestType: "json",
|
|
7751
8101
|
body: _body,
|
|
7752
8102
|
timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1e3,
|
|
@@ -7790,15 +8140,17 @@ var IdentityClient = class {
|
|
|
7790
8140
|
* @example
|
|
7791
8141
|
* await client.identity.getEntityVersions({
|
|
7792
8142
|
* namespace: "team",
|
|
7793
|
-
* id: "id"
|
|
8143
|
+
* id: "id",
|
|
8144
|
+
* contextId: "myapp"
|
|
7794
8145
|
* })
|
|
7795
8146
|
*/
|
|
7796
8147
|
getEntityVersions(request, requestOptions) {
|
|
7797
8148
|
return HttpResponsePromise.fromPromise(this.__getEntityVersions(request, requestOptions));
|
|
7798
8149
|
}
|
|
7799
8150
|
async __getEntityVersions(request, requestOptions) {
|
|
7800
|
-
const { namespace, id, startFrom } = request;
|
|
8151
|
+
const { namespace, id, contextId, startFrom } = request;
|
|
7801
8152
|
const _queryParams = {
|
|
8153
|
+
contextId,
|
|
7802
8154
|
startFrom
|
|
7803
8155
|
};
|
|
7804
8156
|
const _authRequest = await this._options.authProvider.getAuthRequest();
|
|
@@ -7860,7 +8212,10 @@ var IdentityClient = class {
|
|
|
7860
8212
|
return HttpResponsePromise.fromPromise(this.__getNamespace(request, requestOptions));
|
|
7861
8213
|
}
|
|
7862
8214
|
async __getNamespace(request, requestOptions) {
|
|
7863
|
-
const { namespace } = request;
|
|
8215
|
+
const { namespace, contextId } = request;
|
|
8216
|
+
const _queryParams = {
|
|
8217
|
+
contextId
|
|
8218
|
+
};
|
|
7864
8219
|
const _authRequest = await this._options.authProvider.getAuthRequest();
|
|
7865
8220
|
const _headers = mergeHeaders(
|
|
7866
8221
|
_authRequest.headers,
|
|
@@ -7874,7 +8229,7 @@ var IdentityClient = class {
|
|
|
7874
8229
|
),
|
|
7875
8230
|
method: "GET",
|
|
7876
8231
|
headers: _headers,
|
|
7877
|
-
queryString: url_exports.queryBuilder().mergeAdditional(requestOptions?.queryParams).build(),
|
|
8232
|
+
queryString: url_exports.queryBuilder().addMany(_queryParams).mergeAdditional(requestOptions?.queryParams).build(),
|
|
7878
8233
|
timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1e3,
|
|
7879
8234
|
maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries,
|
|
7880
8235
|
abortSignal: requestOptions?.abortSignal,
|
|
@@ -7899,7 +8254,7 @@ var IdentityClient = class {
|
|
|
7899
8254
|
return handleNonStatusCodeError(_response.error, _response.rawResponse, "GET", "/v1/namespaces/{namespace}");
|
|
7900
8255
|
}
|
|
7901
8256
|
/**
|
|
7902
|
-
* Updates the mutable fields (`entityBacked`, `defaultSchemaId`, `specificityRank`) of a registered namespace. The namespace name
|
|
8257
|
+
* 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.
|
|
7903
8258
|
*
|
|
7904
8259
|
* @param {Vectros.UpdateNamespaceRequest} request
|
|
7905
8260
|
* @param {IdentityClient.RequestOptions} requestOptions - Request-specific configuration.
|
|
@@ -7921,7 +8276,10 @@ var IdentityClient = class {
|
|
|
7921
8276
|
return HttpResponsePromise.fromPromise(this.__updateNamespace(request, requestOptions));
|
|
7922
8277
|
}
|
|
7923
8278
|
async __updateNamespace(request, requestOptions) {
|
|
7924
|
-
const { namespace, body: _body } = request;
|
|
8279
|
+
const { namespace, contextId, body: _body } = request;
|
|
8280
|
+
const _queryParams = {
|
|
8281
|
+
contextId
|
|
8282
|
+
};
|
|
7925
8283
|
const _authRequest = await this._options.authProvider.getAuthRequest();
|
|
7926
8284
|
const _headers = mergeHeaders(
|
|
7927
8285
|
_authRequest.headers,
|
|
@@ -7936,7 +8294,7 @@ var IdentityClient = class {
|
|
|
7936
8294
|
method: "PUT",
|
|
7937
8295
|
headers: _headers,
|
|
7938
8296
|
contentType: "application/json",
|
|
7939
|
-
queryString: url_exports.queryBuilder().mergeAdditional(requestOptions?.queryParams).build(),
|
|
8297
|
+
queryString: url_exports.queryBuilder().addMany(_queryParams).mergeAdditional(requestOptions?.queryParams).build(),
|
|
7940
8298
|
requestType: "json",
|
|
7941
8299
|
body: _body,
|
|
7942
8300
|
timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1e3,
|
|
@@ -7967,7 +8325,7 @@ var IdentityClient = class {
|
|
|
7967
8325
|
return handleNonStatusCodeError(_response.error, _response.rawResponse, "PUT", "/v1/namespaces/{namespace}");
|
|
7968
8326
|
}
|
|
7969
8327
|
/**
|
|
7970
|
-
* Deletes a registered scope namespace. Requires a root API key.
|
|
8328
|
+
* 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.
|
|
7971
8329
|
*
|
|
7972
8330
|
* @param {Vectros.DeleteNamespaceRequest} request
|
|
7973
8331
|
* @param {IdentityClient.RequestOptions} requestOptions - Request-specific configuration.
|
|
@@ -7985,7 +8343,10 @@ var IdentityClient = class {
|
|
|
7985
8343
|
return HttpResponsePromise.fromPromise(this.__deleteNamespace(request, requestOptions));
|
|
7986
8344
|
}
|
|
7987
8345
|
async __deleteNamespace(request, requestOptions) {
|
|
7988
|
-
const { namespace } = request;
|
|
8346
|
+
const { namespace, contextId } = request;
|
|
8347
|
+
const _queryParams = {
|
|
8348
|
+
contextId
|
|
8349
|
+
};
|
|
7989
8350
|
const _authRequest = await this._options.authProvider.getAuthRequest();
|
|
7990
8351
|
const _headers = mergeHeaders(
|
|
7991
8352
|
_authRequest.headers,
|
|
@@ -7999,7 +8360,7 @@ var IdentityClient = class {
|
|
|
7999
8360
|
),
|
|
8000
8361
|
method: "DELETE",
|
|
8001
8362
|
headers: _headers,
|
|
8002
|
-
queryString: url_exports.queryBuilder().mergeAdditional(requestOptions?.queryParams).build(),
|
|
8363
|
+
queryString: url_exports.queryBuilder().addMany(_queryParams).mergeAdditional(requestOptions?.queryParams).build(),
|
|
8003
8364
|
timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1e3,
|
|
8004
8365
|
maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries,
|
|
8005
8366
|
abortSignal: requestOptions?.abortSignal,
|
|
@@ -8028,7 +8389,7 @@ var IdentityClient = class {
|
|
|
8028
8389
|
return handleNonStatusCodeError(_response.error, _response.rawResponse, "DELETE", "/v1/namespaces/{namespace}");
|
|
8029
8390
|
}
|
|
8030
8391
|
/**
|
|
8031
|
-
* Returns the scope namespaces
|
|
8392
|
+
* 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.
|
|
8032
8393
|
*
|
|
8033
8394
|
* @param {Vectros.ListNamespacesRequest} request
|
|
8034
8395
|
* @param {IdentityClient.RequestOptions} requestOptions - Request-specific configuration.
|
|
@@ -8040,10 +8401,11 @@ var IdentityClient = class {
|
|
|
8040
8401
|
return HttpResponsePromise.fromPromise(this.__listNamespaces(request, requestOptions));
|
|
8041
8402
|
}
|
|
8042
8403
|
async __listNamespaces(request = {}, requestOptions) {
|
|
8043
|
-
const { startFrom, limit } = request;
|
|
8404
|
+
const { startFrom, limit, contextId } = request;
|
|
8044
8405
|
const _queryParams = {
|
|
8045
8406
|
startFrom,
|
|
8046
|
-
limit
|
|
8407
|
+
limit,
|
|
8408
|
+
contextId
|
|
8047
8409
|
};
|
|
8048
8410
|
const _authRequest = await this._options.authProvider.getAuthRequest();
|
|
8049
8411
|
const _headers = mergeHeaders(
|
|
@@ -8078,9 +8440,9 @@ var IdentityClient = class {
|
|
|
8078
8440
|
return handleNonStatusCodeError(_response.error, _response.rawResponse, "GET", "/v1/namespaces");
|
|
8079
8441
|
}
|
|
8080
8442
|
/**
|
|
8081
|
-
* 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
|
|
8443
|
+
* 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.
|
|
8082
8444
|
*
|
|
8083
|
-
* @param {Vectros.
|
|
8445
|
+
* @param {Vectros.RegisterNamespaceRequest} request
|
|
8084
8446
|
* @param {IdentityClient.RequestOptions} requestOptions - Request-specific configuration.
|
|
8085
8447
|
*
|
|
8086
8448
|
* @throws {@link Vectros.BadRequestError}
|
|
@@ -8089,14 +8451,20 @@ var IdentityClient = class {
|
|
|
8089
8451
|
*
|
|
8090
8452
|
* @example
|
|
8091
8453
|
* await client.identity.registerNamespace({
|
|
8092
|
-
*
|
|
8093
|
-
*
|
|
8454
|
+
* body: {
|
|
8455
|
+
* namespace: "team",
|
|
8456
|
+
* specificityRank: 1500
|
|
8457
|
+
* }
|
|
8094
8458
|
* })
|
|
8095
8459
|
*/
|
|
8096
8460
|
registerNamespace(request, requestOptions) {
|
|
8097
8461
|
return HttpResponsePromise.fromPromise(this.__registerNamespace(request, requestOptions));
|
|
8098
8462
|
}
|
|
8099
8463
|
async __registerNamespace(request, requestOptions) {
|
|
8464
|
+
const { contextId, body: _body } = request;
|
|
8465
|
+
const _queryParams = {
|
|
8466
|
+
contextId
|
|
8467
|
+
};
|
|
8100
8468
|
const _authRequest = await this._options.authProvider.getAuthRequest();
|
|
8101
8469
|
const _headers = mergeHeaders(
|
|
8102
8470
|
_authRequest.headers,
|
|
@@ -8111,9 +8479,9 @@ var IdentityClient = class {
|
|
|
8111
8479
|
method: "POST",
|
|
8112
8480
|
headers: _headers,
|
|
8113
8481
|
contentType: "application/json",
|
|
8114
|
-
queryString: url_exports.queryBuilder().mergeAdditional(requestOptions?.queryParams).build(),
|
|
8482
|
+
queryString: url_exports.queryBuilder().addMany(_queryParams).mergeAdditional(requestOptions?.queryParams).build(),
|
|
8115
8483
|
requestType: "json",
|
|
8116
|
-
body:
|
|
8484
|
+
body: _body,
|
|
8117
8485
|
timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1e3,
|
|
8118
8486
|
maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries,
|
|
8119
8487
|
abortSignal: requestOptions?.abortSignal,
|
|
@@ -8398,12 +8766,13 @@ var IdentityClient = class {
|
|
|
8398
8766
|
return handleNonStatusCodeError(_response.error, _response.rawResponse, "PUT", "/v1/users/{id}");
|
|
8399
8767
|
}
|
|
8400
8768
|
/**
|
|
8401
|
-
* 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.
|
|
8769
|
+
* 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.
|
|
8402
8770
|
*
|
|
8403
8771
|
* @param {Vectros.DeleteUserRequest} request
|
|
8404
8772
|
* @param {IdentityClient.RequestOptions} requestOptions - Request-specific configuration.
|
|
8405
8773
|
*
|
|
8406
8774
|
* @throws {@link Vectros.NotFoundError}
|
|
8775
|
+
* @throws {@link Vectros.ConflictError}
|
|
8407
8776
|
* @throws {@link Vectros.TooManyRequestsError}
|
|
8408
8777
|
*
|
|
8409
8778
|
* @example
|
|
@@ -8443,6 +8812,8 @@ var IdentityClient = class {
|
|
|
8443
8812
|
switch (_response.error.statusCode) {
|
|
8444
8813
|
case 404:
|
|
8445
8814
|
throw new NotFoundError(_response.error.body, _response.rawResponse);
|
|
8815
|
+
case 409:
|
|
8816
|
+
throw new ConflictError(_response.error.body, _response.rawResponse);
|
|
8446
8817
|
case 429:
|
|
8447
8818
|
throw new TooManyRequestsError(_response.error.body, _response.rawResponse);
|
|
8448
8819
|
default:
|
|
@@ -8516,6 +8887,69 @@ var IdentityClient = class {
|
|
|
8516
8887
|
}
|
|
8517
8888
|
return handleNonStatusCodeError(_response.error, _response.rawResponse, "POST", "/v1/users/lookup");
|
|
8518
8889
|
}
|
|
8890
|
+
/**
|
|
8891
|
+
* 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.
|
|
8892
|
+
*
|
|
8893
|
+
* @param {Vectros.UserExistsByEmailRequest} request
|
|
8894
|
+
* @param {IdentityClient.RequestOptions} requestOptions - Request-specific configuration.
|
|
8895
|
+
*
|
|
8896
|
+
* @throws {@link Vectros.BadRequestError}
|
|
8897
|
+
* @throws {@link Vectros.ForbiddenError}
|
|
8898
|
+
*
|
|
8899
|
+
* @example
|
|
8900
|
+
* await client.identity.userExistsByEmail({
|
|
8901
|
+
* email: "user@example.com",
|
|
8902
|
+
* contextId: "myapp"
|
|
8903
|
+
* })
|
|
8904
|
+
*/
|
|
8905
|
+
userExistsByEmail(request = {}, requestOptions) {
|
|
8906
|
+
return HttpResponsePromise.fromPromise(this.__userExistsByEmail(request, requestOptions));
|
|
8907
|
+
}
|
|
8908
|
+
async __userExistsByEmail(request = {}, requestOptions) {
|
|
8909
|
+
const { email, contextId } = request;
|
|
8910
|
+
const _queryParams = {
|
|
8911
|
+
email,
|
|
8912
|
+
contextId
|
|
8913
|
+
};
|
|
8914
|
+
const _authRequest = await this._options.authProvider.getAuthRequest();
|
|
8915
|
+
const _headers = mergeHeaders(
|
|
8916
|
+
_authRequest.headers,
|
|
8917
|
+
this._options?.headers,
|
|
8918
|
+
requestOptions?.headers
|
|
8919
|
+
);
|
|
8920
|
+
const _response = await fetcher({
|
|
8921
|
+
url: url_exports.join(
|
|
8922
|
+
await Supplier.get(this._options.baseUrl) ?? await Supplier.get(this._options.environment),
|
|
8923
|
+
"v1/users/exists-by-email"
|
|
8924
|
+
),
|
|
8925
|
+
method: "GET",
|
|
8926
|
+
headers: _headers,
|
|
8927
|
+
queryString: url_exports.queryBuilder().addMany(_queryParams).mergeAdditional(requestOptions?.queryParams).build(),
|
|
8928
|
+
timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1e3,
|
|
8929
|
+
maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries,
|
|
8930
|
+
abortSignal: requestOptions?.abortSignal,
|
|
8931
|
+
fetchFn: this._options?.fetch,
|
|
8932
|
+
logging: this._options.logging
|
|
8933
|
+
});
|
|
8934
|
+
if (_response.ok) {
|
|
8935
|
+
return { data: _response.body, rawResponse: _response.rawResponse };
|
|
8936
|
+
}
|
|
8937
|
+
if (_response.error.reason === "status-code") {
|
|
8938
|
+
switch (_response.error.statusCode) {
|
|
8939
|
+
case 400:
|
|
8940
|
+
throw new BadRequestError(_response.error.body, _response.rawResponse);
|
|
8941
|
+
case 403:
|
|
8942
|
+
throw new ForbiddenError(_response.error.body, _response.rawResponse);
|
|
8943
|
+
default:
|
|
8944
|
+
throw new VectrosError({
|
|
8945
|
+
statusCode: _response.error.statusCode,
|
|
8946
|
+
body: _response.error.body,
|
|
8947
|
+
rawResponse: _response.rawResponse
|
|
8948
|
+
});
|
|
8949
|
+
}
|
|
8950
|
+
}
|
|
8951
|
+
return handleNonStatusCodeError(_response.error, _response.rawResponse, "GET", "/v1/users/exists-by-email");
|
|
8952
|
+
}
|
|
8519
8953
|
/**
|
|
8520
8954
|
* Returns the audit trail of changes to a user, most recent first. Identity history is always recorded and always available. Sensitive field values are redacted in every historical version. Returns a page in the `{data, nextCursor}` envelope. Requires the `users:r` scope.
|
|
8521
8955
|
*
|
|
@@ -10233,10 +10667,11 @@ var logging;
|
|
|
10233
10667
|
logging2.ConsoleLogger = ConsoleLogger;
|
|
10234
10668
|
})(logging || (logging = {}));
|
|
10235
10669
|
|
|
10236
|
-
//
|
|
10670
|
+
// ../blueprints/dist/index.mjs
|
|
10237
10671
|
var import_zod = require("zod");
|
|
10238
10672
|
var import_zod2 = require("zod");
|
|
10239
10673
|
var CONTEXT_ID_RE = /^[a-z][a-z0-9-]{2,30}$/;
|
|
10674
|
+
var MAX_SCOPE_DIMENSIONS = 2;
|
|
10240
10675
|
var ValidationRulesSchema = import_zod.z.object({
|
|
10241
10676
|
required: import_zod.z.boolean().optional(),
|
|
10242
10677
|
minLength: import_zod.z.number().int().optional(),
|
|
@@ -10431,11 +10866,14 @@ var BlueprintSchemaSchema = import_zod.z.object({
|
|
|
10431
10866
|
// Schema-level ownership defaults, matching `SchemaRequest`: the principal
|
|
10432
10867
|
// `userId` plus `scopes` — namespaced parent edges as `<namespace>:<value>`
|
|
10433
10868
|
// (`org:...`, `client:...`, or a namespace you registered), at most two
|
|
10434
|
-
// namespaces. The flat `orgId`/`clientId` fields fold into `scopes`.
|
|
10869
|
+
// namespaces. The flat `orgId`/`clientId` fields fold into `scopes`. Each
|
|
10870
|
+
// entry's namespace + value are validated below
|
|
10871
|
+
// (`lintNamespacedScopeArrays`) — this `.max(MAX_SCOPE_DIMENSIONS)` is
|
|
10872
|
+
// only the shape-level cap, not the grammar check.
|
|
10435
10873
|
// With a scoped token these must be consistent with the profile's dataScope
|
|
10436
10874
|
// (a cross-consistency lint is deferred to the lint slice).
|
|
10437
10875
|
userId: import_zod.z.string().min(1).optional(),
|
|
10438
|
-
scopes: import_zod.z.array(import_zod.z.string().min(1)).max(
|
|
10876
|
+
scopes: import_zod.z.array(import_zod.z.string().min(1)).max(MAX_SCOPE_DIMENSIONS).optional(),
|
|
10439
10877
|
// The id of an existing schema this one is a CUSTOMIZATION of, when a schema
|
|
10440
10878
|
// named `typeName` already exists in this context — required in that case,
|
|
10441
10879
|
// and must be omitted when this create is the first schema under that name
|
|
@@ -10458,6 +10896,35 @@ var BlueprintSchemaSchema = import_zod.z.object({
|
|
|
10458
10896
|
});
|
|
10459
10897
|
}
|
|
10460
10898
|
});
|
|
10899
|
+
var CAPABILITY_NAME_RE = /^[a-z][a-z0-9-]*$/;
|
|
10900
|
+
var GrantedCapabilitiesSchema = import_zod.z.array(import_zod.z.string().min(1)).optional().superRefine((caps, ctx) => {
|
|
10901
|
+
if (!caps) return;
|
|
10902
|
+
const seen = /* @__PURE__ */ new Set();
|
|
10903
|
+
caps.forEach((name, i) => {
|
|
10904
|
+
if (name === "") return;
|
|
10905
|
+
if (name === "*") {
|
|
10906
|
+
ctx.addIssue({
|
|
10907
|
+
code: import_zod.z.ZodIssueCode.custom,
|
|
10908
|
+
path: [i],
|
|
10909
|
+
message: "'*' is not a capability \u2014 granted_capabilities has no wildcard, and a '*' in allowedActions confers zero capabilities. Name each capability explicitly."
|
|
10910
|
+
});
|
|
10911
|
+
} else if (!CAPABILITY_NAME_RE.test(name)) {
|
|
10912
|
+
ctx.addIssue({
|
|
10913
|
+
code: import_zod.z.ZodIssueCode.custom,
|
|
10914
|
+
path: [i],
|
|
10915
|
+
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)`
|
|
10916
|
+
});
|
|
10917
|
+
}
|
|
10918
|
+
if (seen.has(name)) {
|
|
10919
|
+
ctx.addIssue({
|
|
10920
|
+
code: import_zod.z.ZodIssueCode.custom,
|
|
10921
|
+
path: [i],
|
|
10922
|
+
message: `'${name}' is declared more than once`
|
|
10923
|
+
});
|
|
10924
|
+
}
|
|
10925
|
+
seen.add(name);
|
|
10926
|
+
});
|
|
10927
|
+
});
|
|
10461
10928
|
var BlueprintAccessProfileSchema = import_zod.z.object({
|
|
10462
10929
|
// Validated structurally here; the SCOPE GATE (in @vectros-ai/cli) is
|
|
10463
10930
|
// what enforces the data-plane-only security boundary.
|
|
@@ -10473,7 +10940,9 @@ var BlueprintAccessProfileSchema = import_zod.z.object({
|
|
|
10473
10940
|
// ...) — the flat `orgId`/`clientId` keys are gone. e.g.
|
|
10474
10941
|
// `{ "scope:org": ["org_x", null] }` = "org_x's records AND tenant-shared
|
|
10475
10942
|
// records". Omitting null restricts to the listed owners ONLY (the key will
|
|
10476
|
-
// NOT see tenant-level/seed records).
|
|
10943
|
+
// NOT see tenant-level/seed records). Every KEY is validated below
|
|
10944
|
+
// (`lintDataScopeKeys`) — `userId` or a grammar-valid `scope:<ns>`, no
|
|
10945
|
+
// other shape.
|
|
10477
10946
|
dataScope: import_zod.z.record(import_zod.z.array(import_zod.z.union([import_zod.z.string().min(1), import_zod.z.null()]))).optional(),
|
|
10478
10947
|
// Optional identity overrides for the service principal's profile — scope
|
|
10479
10948
|
// values its key STAMPS onto everything it writes (a credential can only
|
|
@@ -10482,11 +10951,36 @@ var BlueprintAccessProfileSchema = import_zod.z.object({
|
|
|
10482
10951
|
// `orgId`/`clientId` keys are gone); values may be `${{ identities.* }}`
|
|
10483
10952
|
// tokens (substituted at apply time). e.g. `{ "scope:org": '${{ identities.team }}' }`
|
|
10484
10953
|
// makes every record the service key writes org/team-owned.
|
|
10485
|
-
|
|
10954
|
+
//
|
|
10955
|
+
// Every KEY, the ≤2-dimension cap, and every LITERAL value are validated
|
|
10956
|
+
// below (`lintIdentityOverrides`, placeholder-aware — see that function's
|
|
10957
|
+
// docstring); this `.min(1)` is only the shape-level "non-blank" guard on
|
|
10958
|
+
// the value, not the grammar check.
|
|
10959
|
+
identityOverrides: import_zod.z.record(import_zod.z.string().min(1)).optional(),
|
|
10960
|
+
// Optional capabilities granted to the blueprint's own service-principal
|
|
10961
|
+
// profile. DECISION: allowed here, not role-clause-only. On the platform,
|
|
10962
|
+
// an access profile's scopes are themselves a list of clauses — the exact
|
|
10963
|
+
// same wire shape a role clause carries — and the loader already builds
|
|
10964
|
+
// this profile as ONE such clause. Refusing `capabilities` here while
|
|
10965
|
+
// allowing it on a role clause would be an asymmetry this FORMAT package
|
|
10966
|
+
// has no basis to invent: nothing on the platform side distinguishes the
|
|
10967
|
+
// two, and whether granting a capability to a blueprint's own service key
|
|
10968
|
+
// is sound POLICY is exactly the question the CLI's scope gate exists to
|
|
10969
|
+
// answer (a separate package, out of scope for this format layer) — the
|
|
10970
|
+
// same posture this package already takes with `allowedActions` above.
|
|
10971
|
+
//
|
|
10972
|
+
// Worth naming for whoever writes that gate: this profile is a long-lived,
|
|
10973
|
+
// non-human-attributable machine credential (typically stored in CI/CD
|
|
10974
|
+
// secrets), not an ephemeral human session — a materially different
|
|
10975
|
+
// blast-radius question from granting the same capability to a role that
|
|
10976
|
+
// a person is bound to. The gate should weigh the two separately rather
|
|
10977
|
+
// than reuse one policy for both.
|
|
10978
|
+
capabilities: GrantedCapabilitiesSchema
|
|
10486
10979
|
}).strict();
|
|
10487
10980
|
var BlueprintRoleClauseSchema = import_zod.z.object({
|
|
10488
10981
|
allowedActions: import_zod.z.array(import_zod.z.string().min(1)).min(1),
|
|
10489
|
-
dataScope: import_zod.z.record(import_zod.z.array(import_zod.z.union([import_zod.z.string().min(1), import_zod.z.null()]))).optional()
|
|
10982
|
+
dataScope: import_zod.z.record(import_zod.z.array(import_zod.z.union([import_zod.z.string().min(1), import_zod.z.null()]))).optional(),
|
|
10983
|
+
capabilities: GrantedCapabilitiesSchema
|
|
10490
10984
|
}).strict();
|
|
10491
10985
|
var BlueprintRolesSchema = import_zod.z.record(import_zod.z.array(BlueprintRoleClauseSchema).min(1));
|
|
10492
10986
|
var IDENTITY_NAMESPACE_RE = /^[a-z][a-z0-9_-]{1,31}$/;
|
|
@@ -10511,57 +11005,260 @@ var IdentityDeclSchema = import_zod.z.object({
|
|
|
10511
11005
|
displayName: import_zod.z.string().min(1).optional(),
|
|
10512
11006
|
metadata: import_zod.z.record(import_zod.z.unknown()).optional()
|
|
10513
11007
|
}).strict();
|
|
10514
|
-
var
|
|
11008
|
+
var IDENTITY_NAME_RE = /^[A-Za-z_]\w*$/;
|
|
11009
|
+
var IdentitiesDeclSchema = import_zod.z.record(
|
|
11010
|
+
import_zod.z.string().regex(IDENTITY_NAME_RE, {
|
|
11011
|
+
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"
|
|
11012
|
+
}),
|
|
11013
|
+
IdentityDeclSchema
|
|
11014
|
+
);
|
|
10515
11015
|
var BlueprintServicePrincipalSchema = import_zod.z.object({
|
|
10516
11016
|
externalId: import_zod.z.string().min(1),
|
|
10517
11017
|
displayName: import_zod.z.string().min(1)
|
|
10518
11018
|
}).strict();
|
|
10519
|
-
var
|
|
10520
|
-
|
|
10521
|
-
|
|
11019
|
+
var ISSUER_ID_RE = CONTEXT_ID_RE;
|
|
11020
|
+
var BlueprintIssuerSchema = import_zod.z.object({
|
|
11021
|
+
/** Short slug identifying this issuer within your account. Immutable once registered. */
|
|
11022
|
+
issuerId: import_zod.z.string().regex(ISSUER_ID_RE, {
|
|
11023
|
+
message: "issuerId must be 3-31 chars, start with a lowercase letter, then lowercase letters/digits/dashes (e.g. 'auth0-prod')"
|
|
11024
|
+
}),
|
|
11025
|
+
/** The IdP's `iss` claim value, exactly as it appears in tokens it issues. */
|
|
11026
|
+
issuer: import_zod.z.string().min(1),
|
|
11027
|
+
/** The IdP's remote JWKS endpoint, used to verify presented tokens' signatures. */
|
|
11028
|
+
jwksUri: import_zod.z.string().min(1),
|
|
10522
11029
|
/**
|
|
10523
|
-
*
|
|
10524
|
-
*
|
|
10525
|
-
*
|
|
11030
|
+
* The `aud` claim value a presented subject_token must carry. Must be globally
|
|
11031
|
+
* unique in combination with `issuer` — use a distinct audience per
|
|
11032
|
+
* environment/context sharing one IdP account (most OIDC providers support
|
|
11033
|
+
* this as an ordinary per-API/application default).
|
|
10526
11034
|
*/
|
|
10527
|
-
|
|
11035
|
+
audience: import_zod.z.string().min(1),
|
|
10528
11036
|
/**
|
|
10529
|
-
*
|
|
10530
|
-
*
|
|
10531
|
-
*
|
|
10532
|
-
*
|
|
11037
|
+
* Which app context an exchanged token targets. **Must equal the blueprint's own
|
|
11038
|
+
* `contextId`** — an issuer is a trust anchor, so a blueprint may only attach one to the
|
|
11039
|
+
* context it actually provisions. One IdP account serving several contexts needs one entry
|
|
11040
|
+
* per context, each in that context's own blueprint.
|
|
10533
11041
|
*/
|
|
10534
|
-
|
|
10535
|
-
|
|
10536
|
-
|
|
10537
|
-
|
|
10538
|
-
|
|
10539
|
-
/** The
|
|
10540
|
-
|
|
11042
|
+
contextId: import_zod.z.string().regex(CONTEXT_ID_RE, {
|
|
11043
|
+
message: "contextId must be 3-31 chars, start with a lowercase letter, then lowercase letters/digits/dashes (e.g. 'casework')"
|
|
11044
|
+
}),
|
|
11045
|
+
/** The claim carrying the subject identifier. Platform defaults to 'sub' when omitted. */
|
|
11046
|
+
subClaim: import_zod.z.string().min(1).optional(),
|
|
11047
|
+
/** The claim carrying the subject's email (first-login invite matching). Platform defaults to 'email' when omitted. */
|
|
11048
|
+
emailClaim: import_zod.z.string().min(1).optional(),
|
|
11049
|
+
/**
|
|
11050
|
+
* Opt-in self-service signup: a list of {signupType, roleId} pairs. When a first-time exchange
|
|
11051
|
+
* caller presents no invite token but names a signupType matching one of these (or omits it and
|
|
11052
|
+
* exactly one entry exists), a brand-new user is created and bound to that entry's role, no
|
|
11053
|
+
* invite required. Every entry must be something you're willing to grant to ANY caller who can
|
|
11054
|
+
* present a token from this issuer — a role carrying elevated (provisioning or wildcard) scope
|
|
11055
|
+
* is rejected. Omit entirely to leave self-signup disabled (the default).
|
|
11056
|
+
*/
|
|
11057
|
+
selfSignupPolicies: import_zod.z.array(
|
|
11058
|
+
import_zod.z.object({
|
|
11059
|
+
signupType: import_zod.z.string().regex(ISSUER_ID_RE, {
|
|
11060
|
+
message: "signupType must be 3-31 chars, start with a lowercase letter, then lowercase letters/digits/dashes (e.g. 'practitioner')"
|
|
11061
|
+
}),
|
|
11062
|
+
roleId: import_zod.z.string().regex(ISSUER_ID_RE, {
|
|
11063
|
+
message: "roleId must be 3-31 chars, start with a lowercase letter, then lowercase letters/digits/dashes (e.g. 'practitioner-member')"
|
|
11064
|
+
})
|
|
11065
|
+
}).strict()
|
|
11066
|
+
).optional()
|
|
10541
11067
|
}).strict();
|
|
10542
|
-
var
|
|
10543
|
-
|
|
10544
|
-
|
|
10545
|
-
/**
|
|
10546
|
-
|
|
10547
|
-
|
|
10548
|
-
|
|
10549
|
-
|
|
10550
|
-
|
|
11068
|
+
var BlueprintNamespaceSchema = import_zod.z.object({
|
|
11069
|
+
/** The namespace to register — same grammar as a `scope:<namespace>` key. */
|
|
11070
|
+
namespace: import_zod.z.string().min(1),
|
|
11071
|
+
/**
|
|
11072
|
+
* When `true`, every value in this namespace must resolve to an existing
|
|
11073
|
+
* identity entity (create via `entities:c` at the in-context phase, or a
|
|
11074
|
+
* declared `identities:` entry). When `false`/omitted, values are
|
|
11075
|
+
* free-form strings validated by grammar only.
|
|
11076
|
+
*/
|
|
11077
|
+
entityBacked: import_zod.z.boolean().optional(),
|
|
11078
|
+
/**
|
|
11079
|
+
* This namespace's position in your account's specificity order (used to
|
|
11080
|
+
* break a tie when a caller holds two scope dimensions at once). Required
|
|
11081
|
+
* by the platform on registration; MUST be unique across every namespace
|
|
11082
|
+
* in the account, including `org` (1000) and `client` (2000) — pick a
|
|
11083
|
+
* value outside that pair, and outside any other namespace this same
|
|
11084
|
+
* account might register (its own, or another blueprint's).
|
|
11085
|
+
*/
|
|
11086
|
+
specificityRank: import_zod.z.number().int().min(0).max(1e6),
|
|
11087
|
+
/**
|
|
11088
|
+
* Optional membership backing — declares which record type + field hold
|
|
11089
|
+
* grants of THIS namespace's values. Supply together with
|
|
11090
|
+
* `membershipTargetField`, or omit both. Declaring this grants nobody
|
|
11091
|
+
* anything on its own: a role opts in explicitly with
|
|
11092
|
+
* `${{ member.scope.<namespace> }}` in its `dataScope`.
|
|
11093
|
+
*/
|
|
11094
|
+
membershipRecordType: import_zod.z.string().min(1).optional(),
|
|
11095
|
+
/** The field on `membershipRecordType` naming the user a grant is FOR. Required alongside `membershipRecordType`. */
|
|
11096
|
+
membershipTargetField: import_zod.z.string().min(1).optional(),
|
|
11097
|
+
/**
|
|
11098
|
+
* The field on `membershipRecordType` naming a grant's LEVEL, so the same
|
|
11099
|
+
* user can hold different levels in different values of this namespace.
|
|
11100
|
+
* Supply together with `membershipLevels`, or omit both for plain
|
|
11101
|
+
* in-or-out membership.
|
|
11102
|
+
*/
|
|
11103
|
+
membershipLevelField: import_zod.z.string().min(1).optional(),
|
|
11104
|
+
/** The complete set of level labels this namespace allows. Required alongside `membershipLevelField`. */
|
|
11105
|
+
membershipLevels: import_zod.z.array(import_zod.z.string().min(1)).optional()
|
|
10551
11106
|
}).strict();
|
|
10552
|
-
var
|
|
10553
|
-
|
|
10554
|
-
|
|
10555
|
-
|
|
10556
|
-
|
|
10557
|
-
|
|
10558
|
-
const
|
|
10559
|
-
|
|
10560
|
-
|
|
11107
|
+
var MEMBERSHIP_FIELD_NAME_RE = /^[A-Za-z0-9_-]{1,64}$/;
|
|
11108
|
+
function lintNamespaces(value, ctx) {
|
|
11109
|
+
if (!value || typeof value !== "object" || !("namespaces" in value)) return;
|
|
11110
|
+
const namespaces = value.namespaces;
|
|
11111
|
+
if (!Array.isArray(namespaces)) return;
|
|
11112
|
+
const rawSchemas = value.schemas;
|
|
11113
|
+
const declaredSchemaTypes = new Set(
|
|
11114
|
+
(Array.isArray(rawSchemas) ? rawSchemas : []).map((s) => s && typeof s === "object" && "typeName" in s ? s.typeName : void 0).filter((t) => typeof t === "string")
|
|
11115
|
+
);
|
|
11116
|
+
const seenNames = /* @__PURE__ */ new Set();
|
|
11117
|
+
const seenRanks = /* @__PURE__ */ new Map();
|
|
11118
|
+
namespaces.forEach((entry, i) => {
|
|
11119
|
+
if (!entry || typeof entry !== "object") return;
|
|
11120
|
+
const ns = entry;
|
|
11121
|
+
const path4 = ["namespaces", i];
|
|
11122
|
+
if (typeof ns.namespace === "string") {
|
|
11123
|
+
const err = scopeNamespaceGrammarError(ns.namespace);
|
|
11124
|
+
if (err) ctx.addIssue({ code: import_zod.z.ZodIssueCode.custom, path: [...path4, "namespace"], message: err });
|
|
11125
|
+
if (seenNames.has(ns.namespace)) {
|
|
11126
|
+
ctx.addIssue({
|
|
11127
|
+
code: import_zod.z.ZodIssueCode.custom,
|
|
11128
|
+
path: [...path4, "namespace"],
|
|
11129
|
+
message: `'${ns.namespace}' is declared more than once in namespaces[]`
|
|
11130
|
+
});
|
|
11131
|
+
}
|
|
11132
|
+
seenNames.add(ns.namespace);
|
|
11133
|
+
}
|
|
11134
|
+
if (typeof ns.specificityRank === "number") {
|
|
11135
|
+
const prior = seenRanks.get(ns.specificityRank);
|
|
11136
|
+
if (prior !== void 0) {
|
|
11137
|
+
ctx.addIssue({
|
|
11138
|
+
code: import_zod.z.ZodIssueCode.custom,
|
|
11139
|
+
path: [...path4, "specificityRank"],
|
|
11140
|
+
message: `specificityRank ${ns.specificityRank} is already used by namespaces[${prior}] \u2014 every namespace's rank must be unique`
|
|
11141
|
+
});
|
|
11142
|
+
}
|
|
11143
|
+
seenRanks.set(ns.specificityRank, i);
|
|
11144
|
+
}
|
|
11145
|
+
const hasRecordType = ns.membershipRecordType !== void 0;
|
|
11146
|
+
const hasTargetField = ns.membershipTargetField !== void 0;
|
|
11147
|
+
if (hasRecordType !== hasTargetField) {
|
|
11148
|
+
ctx.addIssue({
|
|
11149
|
+
code: import_zod.z.ZodIssueCode.custom,
|
|
11150
|
+
path: [...path4, "membershipRecordType"],
|
|
11151
|
+
message: "membershipRecordType and membershipTargetField must be declared together, or neither"
|
|
11152
|
+
});
|
|
11153
|
+
}
|
|
11154
|
+
if (typeof ns.membershipRecordType === "string" && !declaredSchemaTypes.has(ns.membershipRecordType)) {
|
|
11155
|
+
ctx.addIssue({
|
|
11156
|
+
code: import_zod.z.ZodIssueCode.custom,
|
|
11157
|
+
path: [...path4, "membershipRecordType"],
|
|
11158
|
+
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.`
|
|
11159
|
+
});
|
|
11160
|
+
}
|
|
11161
|
+
for (const [field, value2] of [
|
|
11162
|
+
["membershipRecordType", ns.membershipRecordType],
|
|
11163
|
+
["membershipTargetField", ns.membershipTargetField],
|
|
11164
|
+
["membershipLevelField", ns.membershipLevelField]
|
|
11165
|
+
]) {
|
|
11166
|
+
if (typeof value2 === "string" && !MEMBERSHIP_FIELD_NAME_RE.test(value2)) {
|
|
11167
|
+
ctx.addIssue({
|
|
11168
|
+
code: import_zod.z.ZodIssueCode.custom,
|
|
11169
|
+
path: [...path4, field],
|
|
11170
|
+
message: `${field} '${value2}' is invalid \u2014 must be 1-64 characters of letters, digits, underscores, or hyphens`
|
|
11171
|
+
});
|
|
11172
|
+
}
|
|
11173
|
+
}
|
|
11174
|
+
const hasLevelField = ns.membershipLevelField !== void 0;
|
|
11175
|
+
const hasLevels = Array.isArray(ns.membershipLevels) && ns.membershipLevels.length > 0;
|
|
11176
|
+
if (hasLevelField !== hasLevels) {
|
|
11177
|
+
ctx.addIssue({
|
|
11178
|
+
code: import_zod.z.ZodIssueCode.custom,
|
|
11179
|
+
path: [...path4, "membershipLevelField"],
|
|
11180
|
+
message: "membershipLevelField and a non-empty membershipLevels must be declared together, or neither"
|
|
11181
|
+
});
|
|
11182
|
+
}
|
|
11183
|
+
if ((hasLevelField || hasLevels) && !hasRecordType) {
|
|
11184
|
+
ctx.addIssue({
|
|
11185
|
+
code: import_zod.z.ZodIssueCode.custom,
|
|
11186
|
+
path: [...path4, "membershipLevelField"],
|
|
11187
|
+
message: "membershipLevelField/membershipLevels require membershipRecordType + membershipTargetField too"
|
|
11188
|
+
});
|
|
11189
|
+
}
|
|
11190
|
+
if (Array.isArray(ns.membershipLevels)) {
|
|
11191
|
+
const seenLevels = /* @__PURE__ */ new Set();
|
|
11192
|
+
for (const level of ns.membershipLevels) {
|
|
11193
|
+
if (typeof level !== "string") continue;
|
|
11194
|
+
const err = scopeNamespaceGrammarError(level);
|
|
11195
|
+
if (err) {
|
|
11196
|
+
ctx.addIssue({
|
|
11197
|
+
code: import_zod.z.ZodIssueCode.custom,
|
|
11198
|
+
path: [...path4, "membershipLevels"],
|
|
11199
|
+
message: `level '${level}': ${err}`
|
|
11200
|
+
});
|
|
11201
|
+
} else if (seenLevels.has(level)) {
|
|
11202
|
+
ctx.addIssue({
|
|
11203
|
+
code: import_zod.z.ZodIssueCode.custom,
|
|
11204
|
+
path: [...path4, "membershipLevels"],
|
|
11205
|
+
message: `membershipLevels contains '${level}' twice`
|
|
11206
|
+
});
|
|
11207
|
+
}
|
|
11208
|
+
seenLevels.add(level);
|
|
11209
|
+
}
|
|
11210
|
+
}
|
|
11211
|
+
});
|
|
11212
|
+
}
|
|
11213
|
+
var SeedCommonShape = {
|
|
11214
|
+
/** The schema typeName this seed instantiates (should match a declared schema). */
|
|
11215
|
+
typeName: import_zod.z.string().min(1),
|
|
11216
|
+
/**
|
|
11217
|
+
* Stable, caller-supplied id — the loader's idempotency key AND the value other
|
|
11218
|
+
* seeds resolve a `reference` against (across surfaces: a record seed may
|
|
11219
|
+
* reference a document seed by its externalId, and vice versa).
|
|
11220
|
+
*/
|
|
11221
|
+
externalId: import_zod.z.string().min(1),
|
|
11222
|
+
/**
|
|
11223
|
+
* Optional scope ownership for the seeded item, as `namespace:value` entries
|
|
11224
|
+
* (≤2, e.g. `['org:${{ identities.team }}']`; tokens substitute at apply
|
|
11225
|
+
* time). `[]` = a private, user-owned item. Omitted = the seeding
|
|
11226
|
+
* credential's full identity (the default). Each entry's namespace + value
|
|
11227
|
+
* are validated below (`lintNamespacedScopeArrays`) — this
|
|
11228
|
+
* `.max(MAX_SCOPE_DIMENSIONS)` is only the shape-level cap, not the grammar
|
|
11229
|
+
* check.
|
|
11230
|
+
*/
|
|
11231
|
+
scopes: import_zod.z.array(import_zod.z.string()).max(MAX_SCOPE_DIMENSIONS).optional()
|
|
11232
|
+
};
|
|
11233
|
+
var RecordSeedSchema = import_zod.z.object({
|
|
11234
|
+
surface: import_zod.z.literal("record"),
|
|
11235
|
+
...SeedCommonShape,
|
|
11236
|
+
/** The record payload, validated against the bound schema. */
|
|
11237
|
+
fields: import_zod.z.record(import_zod.z.unknown())
|
|
11238
|
+
}).strict();
|
|
11239
|
+
var DocumentSeedSchema = import_zod.z.object({
|
|
11240
|
+
surface: import_zod.z.literal("document"),
|
|
11241
|
+
...SeedCommonShape,
|
|
11242
|
+
/** Human-readable document title — REQUIRED by the text-ingest path. */
|
|
11243
|
+
title: import_zod.z.string().min(1),
|
|
11244
|
+
/** Raw text content to ingest + index — REQUIRED and non-empty (the platform rejects a blank ingest). */
|
|
11245
|
+
text: import_zod.z.string().min(1),
|
|
11246
|
+
/** Optional structured payload bound to the schema (the document's `fields`). */
|
|
11247
|
+
fields: import_zod.z.record(import_zod.z.unknown()).optional()
|
|
11248
|
+
}).strict();
|
|
11249
|
+
var BlueprintSeedRecordSchema = import_zod.z.discriminatedUnion("surface", [
|
|
11250
|
+
RecordSeedSchema,
|
|
11251
|
+
DocumentSeedSchema
|
|
11252
|
+
]);
|
|
11253
|
+
var SELF_TOKEN_RE = /\$\{\{\s*(?:self|under|member)\.[^}]*?\s*\}\}/;
|
|
11254
|
+
function lintSelfTokenPlacement(value, ctx) {
|
|
11255
|
+
const walk = (node, path4, inRoleDataScope) => {
|
|
11256
|
+
if (typeof node === "string") {
|
|
11257
|
+
if (!inRoleDataScope && SELF_TOKEN_RE.test(node)) {
|
|
10561
11258
|
ctx.addIssue({
|
|
10562
11259
|
code: import_zod.z.ZodIssueCode.custom,
|
|
10563
11260
|
path: path4,
|
|
10564
|
-
message: "'${{ self.* }}' is a runtime
|
|
11261
|
+
message: "'${{ self.* }}' / '${{ member.* }}' is a runtime placeholder \u2014 it is only valid inside a roles[].dataScope value"
|
|
10565
11262
|
});
|
|
10566
11263
|
}
|
|
10567
11264
|
return;
|
|
@@ -10580,12 +11277,20 @@ function lintSelfTokenPlacement(value, ctx) {
|
|
|
10580
11277
|
walk(value, [], false);
|
|
10581
11278
|
}
|
|
10582
11279
|
var IDENTITY_REF_RE = /\$\{\{\s*identities\.([A-Za-z_]\w*)\s*\}\}/g;
|
|
10583
|
-
|
|
11280
|
+
var IDENTITY_REF_LOOSE_RE = /\$\{\{\s*identities\.([^}]*?)\s*\}\}/g;
|
|
11281
|
+
function lintIdentityRefs(value, ctx) {
|
|
10584
11282
|
const declared = new Set(
|
|
10585
11283
|
value && typeof value === "object" && "identities" in value && value.identities ? Object.keys(value.identities) : []
|
|
10586
11284
|
);
|
|
10587
11285
|
const walk = (node, path4) => {
|
|
10588
11286
|
if (typeof node === "string") {
|
|
11287
|
+
for (const m of node.matchAll(IDENTITY_REF_LOOSE_RE)) {
|
|
11288
|
+
const captured = m[1];
|
|
11289
|
+
if (IDENTITY_NAME_RE.test(captured)) continue;
|
|
11290
|
+
const dotIndex = captured.indexOf(".");
|
|
11291
|
+
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)} }}'`;
|
|
11292
|
+
ctx.addIssue({ code: import_zod.z.ZodIssueCode.custom, path: path4, message });
|
|
11293
|
+
}
|
|
10589
11294
|
for (const m of node.matchAll(IDENTITY_REF_RE)) {
|
|
10590
11295
|
if (!declared.has(m[1])) {
|
|
10591
11296
|
ctx.addIssue({
|
|
@@ -10608,6 +11313,134 @@ function lintIdentityRefsDeclared(value, ctx) {
|
|
|
10608
11313
|
};
|
|
10609
11314
|
walk(value, []);
|
|
10610
11315
|
}
|
|
11316
|
+
var SCOPE_VALUE_RE = /^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$/;
|
|
11317
|
+
var SCOPE_NAMESPACE_RE = /^[a-z][a-z0-9_-]{1,31}$/;
|
|
11318
|
+
var FORBIDDEN_SCOPE_NAMESPACES = /* @__PURE__ */ new Set([
|
|
11319
|
+
"user",
|
|
11320
|
+
"record",
|
|
11321
|
+
"document",
|
|
11322
|
+
"entity",
|
|
11323
|
+
"self",
|
|
11324
|
+
"tenant",
|
|
11325
|
+
"context",
|
|
11326
|
+
"scope",
|
|
11327
|
+
"versions",
|
|
11328
|
+
"lookup"
|
|
11329
|
+
]);
|
|
11330
|
+
function scopeNamespaceGrammarError(namespace) {
|
|
11331
|
+
if (namespace === "") return "scope namespace must not be blank";
|
|
11332
|
+
if (FORBIDDEN_SCOPE_NAMESPACES.has(namespace)) {
|
|
11333
|
+
return `'${namespace}' is a reserved namespace and cannot be used as an ownership scope`;
|
|
11334
|
+
}
|
|
11335
|
+
if (!SCOPE_NAMESPACE_RE.test(namespace)) {
|
|
11336
|
+
return `'${namespace}' is not a valid namespace \u2014 2-32 chars, lowercase letter first, then lowercase letters, digits, '_' or '-'`;
|
|
11337
|
+
}
|
|
11338
|
+
return null;
|
|
11339
|
+
}
|
|
11340
|
+
var IDENTITY_PLACEHOLDER_WHOLE_RE = new RegExp(`^${IDENTITY_REF_LOOSE_RE.source}$`);
|
|
11341
|
+
function lintIdentityOverrides(value, ctx) {
|
|
11342
|
+
const overrides = value.accessProfile?.identityOverrides;
|
|
11343
|
+
if (!overrides) return;
|
|
11344
|
+
const entries = Object.entries(overrides);
|
|
11345
|
+
if (entries.length > MAX_SCOPE_DIMENSIONS) {
|
|
11346
|
+
ctx.addIssue({
|
|
11347
|
+
code: import_zod.z.ZodIssueCode.custom,
|
|
11348
|
+
path: ["accessProfile", "identityOverrides"],
|
|
11349
|
+
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}.`
|
|
11350
|
+
});
|
|
11351
|
+
}
|
|
11352
|
+
for (const [key, v] of entries) {
|
|
11353
|
+
const ns = key.startsWith("scope:") ? key.slice("scope:".length) : null;
|
|
11354
|
+
const nsError = ns === null ? null : scopeNamespaceGrammarError(ns);
|
|
11355
|
+
if (ns === null || nsError !== null) {
|
|
11356
|
+
ctx.addIssue({
|
|
11357
|
+
code: import_zod.z.ZodIssueCode.custom,
|
|
11358
|
+
path: ["accessProfile", "identityOverrides", key],
|
|
11359
|
+
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}`
|
|
11360
|
+
});
|
|
11361
|
+
}
|
|
11362
|
+
if (typeof v !== "string") continue;
|
|
11363
|
+
if (IDENTITY_PLACEHOLDER_WHOLE_RE.test(v)) continue;
|
|
11364
|
+
if (!SCOPE_VALUE_RE.test(v)) {
|
|
11365
|
+
ctx.addIssue({
|
|
11366
|
+
code: import_zod.z.ZodIssueCode.custom,
|
|
11367
|
+
path: ["accessProfile", "identityOverrides", key],
|
|
11368
|
+
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).`
|
|
11369
|
+
});
|
|
11370
|
+
}
|
|
11371
|
+
}
|
|
11372
|
+
}
|
|
11373
|
+
function lintScopeEntries(entries, path4, ctx) {
|
|
11374
|
+
if (!entries) return;
|
|
11375
|
+
const seenValueByNamespace = /* @__PURE__ */ new Map();
|
|
11376
|
+
for (let i = 0; i < entries.length; i++) {
|
|
11377
|
+
const raw = entries[i];
|
|
11378
|
+
if (typeof raw !== "string") continue;
|
|
11379
|
+
const sep = raw.indexOf(":");
|
|
11380
|
+
if (sep <= 0 || sep === raw.length - 1) {
|
|
11381
|
+
ctx.addIssue({
|
|
11382
|
+
code: import_zod.z.ZodIssueCode.custom,
|
|
11383
|
+
path: [...path4, i],
|
|
11384
|
+
message: `'${raw}' must be '<namespace>:<value>' (e.g. 'org:6ba7...' or 'team:${"${{ identities.team }}"}')`
|
|
11385
|
+
});
|
|
11386
|
+
continue;
|
|
11387
|
+
}
|
|
11388
|
+
const namespace = raw.slice(0, sep);
|
|
11389
|
+
const val = raw.slice(sep + 1);
|
|
11390
|
+
const nsError = scopeNamespaceGrammarError(namespace);
|
|
11391
|
+
if (nsError !== null) {
|
|
11392
|
+
ctx.addIssue({ code: import_zod.z.ZodIssueCode.custom, path: [...path4, i], message: `'${raw}' ${nsError}` });
|
|
11393
|
+
} else {
|
|
11394
|
+
const priorValue = seenValueByNamespace.get(namespace);
|
|
11395
|
+
if (priorValue !== void 0 && priorValue !== val) {
|
|
11396
|
+
ctx.addIssue({
|
|
11397
|
+
code: import_zod.z.ZodIssueCode.custom,
|
|
11398
|
+
path: [...path4, i],
|
|
11399
|
+
message: `'${namespace}' appears more than once with different values ('${priorValue}' and '${val}') \u2014 an item carries at most one value per namespace`
|
|
11400
|
+
});
|
|
11401
|
+
}
|
|
11402
|
+
seenValueByNamespace.set(namespace, val);
|
|
11403
|
+
}
|
|
11404
|
+
if (IDENTITY_PLACEHOLDER_WHOLE_RE.test(val)) continue;
|
|
11405
|
+
if (!SCOPE_VALUE_RE.test(val)) {
|
|
11406
|
+
ctx.addIssue({
|
|
11407
|
+
code: import_zod.z.ZodIssueCode.custom,
|
|
11408
|
+
path: [...path4, i],
|
|
11409
|
+
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).`
|
|
11410
|
+
});
|
|
11411
|
+
}
|
|
11412
|
+
}
|
|
11413
|
+
}
|
|
11414
|
+
function lintNamespacedScopeArrays(value, ctx) {
|
|
11415
|
+
const bp = value;
|
|
11416
|
+
(bp.schemas ?? []).forEach((s, i) => lintScopeEntries(s.scopes, ["schemas", i, "scopes"], ctx));
|
|
11417
|
+
(bp.seed ?? []).forEach((s, i) => lintScopeEntries(s.scopes, ["seed", i, "scopes"], ctx));
|
|
11418
|
+
}
|
|
11419
|
+
function lintDataScopeKey(key, path4, ctx) {
|
|
11420
|
+
if (key === "userId") return;
|
|
11421
|
+
const ns = key.startsWith("scope:") ? key.slice("scope:".length) : null;
|
|
11422
|
+
const nsError = ns === null ? null : scopeNamespaceGrammarError(ns);
|
|
11423
|
+
if (ns === null || nsError !== null) {
|
|
11424
|
+
ctx.addIssue({
|
|
11425
|
+
code: import_zod.z.ZodIssueCode.custom,
|
|
11426
|
+
path: path4,
|
|
11427
|
+
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}`
|
|
11428
|
+
});
|
|
11429
|
+
}
|
|
11430
|
+
}
|
|
11431
|
+
function lintDataScopeKeys(value, ctx) {
|
|
11432
|
+
const bp = value;
|
|
11433
|
+
for (const key of Object.keys(bp.accessProfile?.dataScope ?? {})) {
|
|
11434
|
+
lintDataScopeKey(key, ["accessProfile", "dataScope", key], ctx);
|
|
11435
|
+
}
|
|
11436
|
+
for (const [roleId, clauses] of Object.entries(bp.roles ?? {})) {
|
|
11437
|
+
clauses.forEach((clause, i) => {
|
|
11438
|
+
for (const key of Object.keys(clause.dataScope ?? {})) {
|
|
11439
|
+
lintDataScopeKey(key, ["roles", roleId, i, "dataScope", key], ctx);
|
|
11440
|
+
}
|
|
11441
|
+
});
|
|
11442
|
+
}
|
|
11443
|
+
}
|
|
10611
11444
|
function lintSeedSurfaces(value, ctx) {
|
|
10612
11445
|
const bp = value;
|
|
10613
11446
|
if (!Array.isArray(bp.seed)) return;
|
|
@@ -10625,6 +11458,41 @@ function lintSeedSurfaces(value, ctx) {
|
|
|
10625
11458
|
}
|
|
10626
11459
|
});
|
|
10627
11460
|
}
|
|
11461
|
+
function lintIdentitySurfaces(value, ctx) {
|
|
11462
|
+
const bp = value;
|
|
11463
|
+
if (!Array.isArray(bp.schemas)) return;
|
|
11464
|
+
bp.schemas.forEach((schema, i) => {
|
|
11465
|
+
if (!schema.allowedSurfaces?.includes("user")) return;
|
|
11466
|
+
ctx.addIssue({
|
|
11467
|
+
code: import_zod.z.ZodIssueCode.custom,
|
|
11468
|
+
path: ["schemas", i, "allowedSurfaces"],
|
|
11469
|
+
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).`
|
|
11470
|
+
});
|
|
11471
|
+
});
|
|
11472
|
+
}
|
|
11473
|
+
function lintIssuerContext(value, ctx) {
|
|
11474
|
+
const bp = value;
|
|
11475
|
+
if (!Array.isArray(bp.issuers)) return;
|
|
11476
|
+
bp.issuers.forEach((issuer, i) => {
|
|
11477
|
+
if (issuer.contextId === void 0 || issuer.contextId === bp.contextId) return;
|
|
11478
|
+
ctx.addIssue({
|
|
11479
|
+
code: import_zod.z.ZodIssueCode.custom,
|
|
11480
|
+
path: ["issuers", i, "contextId"],
|
|
11481
|
+
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}'.`
|
|
11482
|
+
});
|
|
11483
|
+
});
|
|
11484
|
+
}
|
|
11485
|
+
var BLUEPRINT_FIELD_PHASES = Object.freeze({
|
|
11486
|
+
servicePrincipal: "bootstrap",
|
|
11487
|
+
issuers: "bootstrap",
|
|
11488
|
+
// Registration requires the platform's provisioning:c capability, which
|
|
11489
|
+
// only the bootstrap credential carries — same reasoning as issuers.
|
|
11490
|
+
namespaces: "bootstrap",
|
|
11491
|
+
schemas: "in-context",
|
|
11492
|
+
accessProfile: "in-context",
|
|
11493
|
+
roles: "in-context",
|
|
11494
|
+
seed: "in-context"
|
|
11495
|
+
});
|
|
10628
11496
|
var BlueprintSchema = import_zod.z.object({
|
|
10629
11497
|
/** Stable blueprint id (the `--blueprint <name>` selector + idempotency key). */
|
|
10630
11498
|
name: import_zod.z.string().min(1),
|
|
@@ -10643,11 +11511,33 @@ var BlueprintSchema = import_zod.z.object({
|
|
|
10643
11511
|
/** Optional multi-clause roles, bound to principals via `access grant --role`. */
|
|
10644
11512
|
roles: BlueprintRolesSchema.optional(),
|
|
10645
11513
|
/** Optional principals ensured-exist at apply; referenced via ${{ identities.* }}. */
|
|
10646
|
-
identities: IdentitiesDeclSchema.optional()
|
|
11514
|
+
identities: IdentitiesDeclSchema.optional(),
|
|
11515
|
+
/**
|
|
11516
|
+
* Optional trusted third-party IdP issuers to register (BYO-IdP token
|
|
11517
|
+
* exchange). Applied in the BOOTSTRAP-token phase, alongside app-context/
|
|
11518
|
+
* service-principal creation — NOT the in-context load (see
|
|
11519
|
+
* {@link BlueprintIssuerSchema}).
|
|
11520
|
+
*/
|
|
11521
|
+
issuers: import_zod.z.array(BlueprintIssuerSchema).optional(),
|
|
11522
|
+
/**
|
|
11523
|
+
* Optional namespaces to register before anything else needs them — see
|
|
11524
|
+
* {@link BlueprintNamespaceSchema}. Applied in the BOOTSTRAP-token phase,
|
|
11525
|
+
* alongside app-context/service-principal creation and issuers, BEFORE
|
|
11526
|
+
* schemas/seed (an `entityBacked` namespace's entities, and an
|
|
11527
|
+
* `allowedSurfaces: ['entity']` schema, both need the registration to
|
|
11528
|
+
* already exist). `org`/`client` need no entry — they exist already.
|
|
11529
|
+
*/
|
|
11530
|
+
namespaces: import_zod.z.array(BlueprintNamespaceSchema).optional()
|
|
10647
11531
|
}).strict().superRefine((bp, ctx) => {
|
|
10648
11532
|
lintSelfTokenPlacement(bp, ctx);
|
|
10649
|
-
|
|
11533
|
+
lintIdentityRefs(bp, ctx);
|
|
11534
|
+
lintIdentityOverrides(bp, ctx);
|
|
11535
|
+
lintNamespacedScopeArrays(bp, ctx);
|
|
11536
|
+
lintDataScopeKeys(bp, ctx);
|
|
10650
11537
|
lintSeedSurfaces(bp, ctx);
|
|
11538
|
+
lintIdentitySurfaces(bp, ctx);
|
|
11539
|
+
lintIssuerContext(bp, ctx);
|
|
11540
|
+
lintNamespaces(bp, ctx);
|
|
10651
11541
|
});
|
|
10652
11542
|
function formatIssuePath(path4) {
|
|
10653
11543
|
let out = "";
|
|
@@ -10802,7 +11692,11 @@ function deriveSuffix(contextId) {
|
|
|
10802
11692
|
var WHOLE_TOKEN_RE = /^\$\{\{\s*([A-Za-z_]\w*)\.([A-Za-z_]\w*)\s*\}\}$/;
|
|
10803
11693
|
var TOKEN_SCAN_RE = /\$\$\{\{|\$\{\{([\s\S]*?)\}\}|\$\{\{/g;
|
|
10804
11694
|
var INNER_REF_RE = /^([A-Za-z_]\w*)\.([A-Za-z_]\w*)$/;
|
|
10805
|
-
var DEFERRED_NAMESPACES = /* @__PURE__ */ new Set(["self", "identities"]);
|
|
11695
|
+
var DEFERRED_NAMESPACES = /* @__PURE__ */ new Set(["self", "identities", "under", "member"]);
|
|
11696
|
+
var DEFERRED_NS_ALTERNATION = [...DEFERRED_NAMESPACES].join("|");
|
|
11697
|
+
var DEFERRED_INNER_RE = new RegExp(
|
|
11698
|
+
`^(?:${DEFERRED_NS_ALTERNATION})(?:\\.[A-Za-z_]\\w*)+(?::[A-Za-z_]\\w*)?$`
|
|
11699
|
+
);
|
|
10806
11700
|
function resolveRef(ns, name, ctx, path4) {
|
|
10807
11701
|
if (ns === "inputs") {
|
|
10808
11702
|
if (!(name in ctx.declared)) {
|
|
@@ -10860,14 +11754,17 @@ function interpolate(str, ctx, path4) {
|
|
|
10860
11754
|
});
|
|
10861
11755
|
} else {
|
|
10862
11756
|
const inner = (m[1] ?? "").trim();
|
|
11757
|
+
if (DEFERRED_INNER_RE.test(inner)) {
|
|
11758
|
+
out += m[0];
|
|
11759
|
+
last = TOKEN_SCAN_RE.lastIndex;
|
|
11760
|
+
continue;
|
|
11761
|
+
}
|
|
10863
11762
|
const ref = inner.match(INNER_REF_RE);
|
|
10864
11763
|
if (!ref) {
|
|
10865
11764
|
ctx.issues.push({
|
|
10866
11765
|
path: path4,
|
|
10867
11766
|
message: `malformed reference '\${{ ${inner} }}' \u2014 expected '\${{ namespace.name }}' (escape a literal with '$\${{')`
|
|
10868
11767
|
});
|
|
10869
|
-
} else if (DEFERRED_NAMESPACES.has(ref[1])) {
|
|
10870
|
-
out += m[0];
|
|
10871
11768
|
} else {
|
|
10872
11769
|
const v = resolveRef(ref[1], ref[2], ctx, path4);
|
|
10873
11770
|
if (v !== void 0) out += String(v);
|
|
@@ -11049,324 +11946,82 @@ var taskManagement = {
|
|
|
11049
11946
|
// Equality, NOT range: the values are an ordinal vocabulary but they sort
|
|
11050
11947
|
// LEXICALLY (high < low < medium < urgent), not by severity — a range
|
|
11051
11948
|
// index here would be permanently wrong. Filter by exact priority instead.
|
|
11052
|
-
renderHints: { label: "Priority", widget: "select", order: 4, section: "Tracking" }
|
|
11053
|
-
},
|
|
11054
|
-
{
|
|
11055
|
-
fieldId: "assignee",
|
|
11056
|
-
fieldType: "string",
|
|
11057
|
-
filterable: true,
|
|
11058
|
-
description: "userId or display name.",
|
|
11059
|
-
renderHints: { label: "Assignee", widget: "text", order: 5, section: "Tracking" }
|
|
11060
|
-
},
|
|
11061
|
-
{
|
|
11062
|
-
fieldId: "project",
|
|
11063
|
-
fieldType: "string",
|
|
11064
|
-
filterable: true,
|
|
11065
|
-
description: "Grouping key for cross-session continuity.",
|
|
11066
|
-
renderHints: { label: "Project", widget: "text", order: 6, section: "Tracking" }
|
|
11067
|
-
},
|
|
11068
|
-
{
|
|
11069
|
-
fieldId: "dueDate",
|
|
11070
|
-
fieldType: "date",
|
|
11071
|
-
description: "ISO-8601. Range-queryable (see lookupFields) \u2014 list tasks due in a window.",
|
|
11072
|
-
renderHints: { label: "Due date", widget: "date", order: 7, section: "Tracking" }
|
|
11073
|
-
},
|
|
11074
|
-
{
|
|
11075
|
-
fieldId: "tags",
|
|
11076
|
-
fieldType: "array",
|
|
11077
|
-
filterable: true,
|
|
11078
|
-
renderHints: { label: "Tags", order: 8, section: "Tracking" }
|
|
11079
|
-
}
|
|
11080
|
-
// externalId is the record's FIRST-CLASS identifier (the dedup/lookup key + the
|
|
11081
|
-
// value a `reference` resolves against) — not a payload field; the loader sends
|
|
11082
|
-
// it top-level on the RecordRequest.
|
|
11083
|
-
],
|
|
11084
|
-
// Two lookup shapes, chosen deliberately because the equality-vs-range choice
|
|
11085
|
-
// per field is MIGRATION-LOCKED once the schema is live (you cannot flip a
|
|
11086
|
-
// field slot↔range later, even by removing and re-adding it):
|
|
11087
|
-
// • `project` — EQUALITY (a grouping key, not ordered): enumerate one
|
|
11088
|
-
// project's tasks directly, no search. Each equality lookup uses 1 of the
|
|
11089
|
-
// schema's 7 fast index slots.
|
|
11090
|
-
// • `dueDate` — RANGE: ordered `from`/`to`/`prefix` queries ("tasks due this
|
|
11091
|
-
// week", "due in 2026-06"). Range lookups use a relationship row, not a
|
|
11092
|
-
// slot, and are billed at the range rate. ISO-8601 sorts chronologically,
|
|
11093
|
-
// so the order is correct.
|
|
11094
|
-
// `externalId` has a built-in first-class finder and must NOT be redeclared here.
|
|
11095
|
-
lookupFields: ["project", { fieldName: "dueDate", rangeEnabled: true }]
|
|
11096
|
-
}
|
|
11097
|
-
],
|
|
11098
|
-
// Least-privilege profile — MUST pass the CLI scope gate.
|
|
11099
|
-
// r/c/u + search + schema discovery. NOT records:d (least privilege).
|
|
11100
|
-
accessProfile: {
|
|
11101
|
-
allowedActions: ["records:r", "records:c", "records:u", "search:r", "schemas:r"]
|
|
11102
|
-
// dataScope omitted → tenant-level shared tracker. Add a `scope:org`
|
|
11103
|
-
// entry for per-org isolation.
|
|
11104
|
-
},
|
|
11105
|
-
servicePrincipal: {
|
|
11106
|
-
externalId: "task-management",
|
|
11107
|
-
displayName: "Task Management"
|
|
11108
|
-
},
|
|
11109
|
-
seed: [
|
|
11110
|
-
{
|
|
11111
|
-
surface: "record",
|
|
11112
|
-
typeName: "task",
|
|
11113
|
-
externalId: "seed-welcome",
|
|
11114
|
-
fields: {
|
|
11115
|
-
title: "Welcome to your Vectros task tracker",
|
|
11116
|
-
description: "Created by the bootstrap loader. Ask your agent to add tasks.",
|
|
11117
|
-
status: "todo",
|
|
11118
|
-
priority: "low",
|
|
11119
|
-
project: "getting-started",
|
|
11120
|
-
dueDate: "2026-06-30"
|
|
11121
|
-
}
|
|
11122
|
-
}
|
|
11123
|
-
]
|
|
11124
|
-
};
|
|
11125
|
-
var task_management_default = taskManagement;
|
|
11126
|
-
var codingAgentMemory = {
|
|
11127
|
-
name: "coding-agent-memory",
|
|
11128
|
-
version: "1.0.0",
|
|
11129
|
-
description: "Persistent project memory for a coding agent \u2014 decisions, conventions, and gotchas that survive across sessions.",
|
|
11130
|
-
contextId: "coding-memory",
|
|
11131
|
-
contextName: "Coding Agent \u2014 Project Memory",
|
|
11132
|
-
schemas: [
|
|
11133
|
-
{
|
|
11134
|
-
// A durable architectural/product decision: what was decided, and WHY.
|
|
11135
|
-
// The rationale is the high-value, RAG-able body — "why did we do X?"
|
|
11136
|
-
// is the question a cold-context agent most needs answered.
|
|
11137
|
-
typeName: "decision",
|
|
11138
|
-
displayName: "Decision",
|
|
11139
|
-
indexMode: "HYBRID",
|
|
11140
|
-
// keyword on titles + semantic on statement/rationale
|
|
11141
|
-
fields: [
|
|
11142
|
-
{
|
|
11143
|
-
fieldId: "title",
|
|
11144
|
-
fieldType: "string",
|
|
11145
|
-
required: true,
|
|
11146
|
-
searchable: true,
|
|
11147
|
-
validation: { minLength: 1, maxLength: 200 },
|
|
11148
|
-
renderHints: { label: "Title", widget: "text", order: 1, displayField: true }
|
|
11149
|
-
},
|
|
11150
|
-
{
|
|
11151
|
-
fieldId: "statement",
|
|
11152
|
-
fieldType: "string",
|
|
11153
|
-
searchable: true,
|
|
11154
|
-
description: "What was decided, in one or two sentences.",
|
|
11155
|
-
renderHints: { label: "Statement", widget: "textarea", order: 2 }
|
|
11156
|
-
},
|
|
11157
|
-
{
|
|
11158
|
-
fieldId: "rationale",
|
|
11159
|
-
fieldType: "string",
|
|
11160
|
-
searchable: true,
|
|
11161
|
-
description: "Why \u2014 the trade-offs and the context. The most-recalled field.",
|
|
11162
|
-
renderHints: { label: "Rationale", widget: "textarea", order: 3 }
|
|
11163
|
-
},
|
|
11164
|
-
{
|
|
11165
|
-
fieldId: "status",
|
|
11166
|
-
fieldType: "enum",
|
|
11167
|
-
filterable: true,
|
|
11168
|
-
enumValues: ["proposed", "active", "superseded"],
|
|
11169
|
-
renderHints: { label: "Status", widget: "select", order: 4 }
|
|
11170
|
-
},
|
|
11171
|
-
{
|
|
11172
|
-
fieldId: "area",
|
|
11173
|
-
fieldType: "string",
|
|
11174
|
-
filterable: true,
|
|
11175
|
-
description: 'Subsystem / module the decision applies to (e.g. "auth", "search").',
|
|
11176
|
-
renderHints: { label: "Area", widget: "text", order: 5 }
|
|
11177
|
-
},
|
|
11178
|
-
{
|
|
11179
|
-
fieldId: "decidedOn",
|
|
11180
|
-
fieldType: "date",
|
|
11181
|
-
description: 'ISO-8601 date. Range-queryable \u2014 "decisions made this quarter".',
|
|
11182
|
-
renderHints: { label: "Decided on", widget: "date", order: 6 }
|
|
11183
|
-
}
|
|
11184
|
-
// externalId is the record's FIRST-CLASS identifier (the dedup/upsert key + the
|
|
11185
|
-
// value a `reference` resolves against) — it is NOT a payload field, so it is not
|
|
11186
|
-
// declared here; the loader sends it top-level on the RecordRequest.
|
|
11187
|
-
],
|
|
11188
|
-
// `externalId` is a first-class identifier with its own finder (exact
|
|
11189
|
-
// get/upsert) — look it up directly, never redeclare it as a schema lookup.
|
|
11190
|
-
// The choice below is MIGRATION-LOCKED once the schema is live:
|
|
11191
|
-
// • `area`/`status` — EQUALITY (categorical): "list active decisions",
|
|
11192
|
-
// "all decisions in the auth area". 2 of the 7 fast index slots.
|
|
11193
|
-
// • `decidedOn` — RANGE: ordered `from`/`to`/`prefix` over the date.
|
|
11194
|
-
lookupFields: ["area", "status", { fieldName: "decidedOn", rangeEnabled: true }]
|
|
11195
|
-
},
|
|
11196
|
-
{
|
|
11197
|
-
// A coding convention the team follows — the agent reads these before it
|
|
11198
|
-
// writes code so it matches the surrounding style instead of inventing one.
|
|
11199
|
-
typeName: "convention",
|
|
11200
|
-
displayName: "Convention",
|
|
11201
|
-
indexMode: "HYBRID",
|
|
11202
|
-
fields: [
|
|
11203
|
-
{
|
|
11204
|
-
fieldId: "name",
|
|
11205
|
-
fieldType: "string",
|
|
11206
|
-
required: true,
|
|
11207
|
-
searchable: true,
|
|
11208
|
-
validation: { minLength: 1, maxLength: 200 },
|
|
11209
|
-
renderHints: { label: "Name", widget: "text", order: 1, displayField: true }
|
|
11210
|
-
},
|
|
11211
|
-
{
|
|
11212
|
-
fieldId: "rule",
|
|
11213
|
-
fieldType: "string",
|
|
11214
|
-
searchable: true,
|
|
11215
|
-
description: "The convention itself, stated as an imperative.",
|
|
11216
|
-
renderHints: { label: "Rule", widget: "textarea", order: 2 }
|
|
11217
|
-
},
|
|
11218
|
-
{
|
|
11219
|
-
fieldId: "area",
|
|
11220
|
-
fieldType: "string",
|
|
11221
|
-
filterable: true,
|
|
11222
|
-
renderHints: { label: "Area", widget: "text", order: 3 }
|
|
11223
|
-
},
|
|
11224
|
-
{
|
|
11225
|
-
// Parallels `decision.status` so a convention can be RETIRED rather than
|
|
11226
|
-
// deleted (the blueprint omits records:d — memory is superseded, not lost).
|
|
11227
|
-
fieldId: "status",
|
|
11228
|
-
fieldType: "enum",
|
|
11229
|
-
filterable: true,
|
|
11230
|
-
enumValues: ["active", "retired"],
|
|
11231
|
-
renderHints: { label: "Status", widget: "select", order: 4 }
|
|
11232
|
-
},
|
|
11233
|
-
{
|
|
11234
|
-
// A typed REFERENCE to the decision that established this convention — a
|
|
11235
|
-
// foreign-key link between record types (the knowledge-graph edge). The
|
|
11236
|
-
// platform requires both the target type AND its surface; the value is the
|
|
11237
|
-
// target decision's externalId (the default `targetField`). Provenance:
|
|
11238
|
-
// "why does this convention exist? → open the decision behind it." Not
|
|
11239
|
-
// searchable (a foreign-key id is search noise); declared as an equality
|
|
11240
|
-
// lookup below so you can also enumerate "conventions from decision X".
|
|
11241
|
-
fieldId: "establishedByDecision",
|
|
11242
|
-
fieldType: "reference",
|
|
11243
|
-
targetTypeName: "decision",
|
|
11244
|
-
targetSurface: "record",
|
|
11245
|
-
targetField: "externalId",
|
|
11246
|
-
cardinality: "one",
|
|
11247
|
-
renderHints: { label: "Established by (decision)", order: 5 }
|
|
11248
|
-
},
|
|
11249
|
-
{
|
|
11250
|
-
fieldId: "adoptedOn",
|
|
11251
|
-
fieldType: "date",
|
|
11252
|
-
description: "ISO-8601 \u2014 when this convention was adopted. Range-queryable.",
|
|
11253
|
-
renderHints: { label: "Adopted on", widget: "date", order: 6 }
|
|
11254
|
-
}
|
|
11255
|
-
// externalId: first-class identifier, not a payload field (see `decision`).
|
|
11256
|
-
],
|
|
11257
|
-
// externalId has its own first-class finder for exact get — look it up
|
|
11258
|
-
// directly, not as a schema lookup. Locked equality-vs-range choices:
|
|
11259
|
-
// • `area`/`status` — EQUALITY: "active conventions for auth".
|
|
11260
|
-
// • `establishedByDecision` — EQUALITY on the reference value: enumerate
|
|
11261
|
-
// "conventions established by decision X" (a forward link query; the
|
|
11262
|
-
// platform has no reverse-reference index on this surface).
|
|
11263
|
-
// • `adoptedOn` — RANGE over the adoption date.
|
|
11264
|
-
// 3 equality lookups (of 7 fast slots) + 1 range row.
|
|
11265
|
-
lookupFields: ["area", "status", "establishedByDecision", { fieldName: "adoptedOn", rangeEnabled: true }]
|
|
11266
|
-
},
|
|
11267
|
-
{
|
|
11268
|
-
// A gotcha / sharp edge: a symptom, its cause, and the fix. Saves the
|
|
11269
|
-
// agent (and the human) from re-discovering the same trap.
|
|
11270
|
-
typeName: "gotcha",
|
|
11271
|
-
displayName: "Gotcha",
|
|
11272
|
-
indexMode: "HYBRID",
|
|
11273
|
-
fields: [
|
|
11274
|
-
{
|
|
11275
|
-
fieldId: "symptom",
|
|
11276
|
-
fieldType: "string",
|
|
11277
|
-
required: true,
|
|
11278
|
-
searchable: true,
|
|
11279
|
-
validation: { minLength: 1, maxLength: 500 },
|
|
11280
|
-
renderHints: { label: "Symptom", widget: "textarea", order: 1, displayField: true }
|
|
11281
|
-
},
|
|
11282
|
-
{
|
|
11283
|
-
fieldId: "cause",
|
|
11284
|
-
fieldType: "string",
|
|
11285
|
-
searchable: true,
|
|
11286
|
-
renderHints: { label: "Cause", widget: "textarea", order: 2 }
|
|
11949
|
+
renderHints: { label: "Priority", widget: "select", order: 4, section: "Tracking" }
|
|
11287
11950
|
},
|
|
11288
11951
|
{
|
|
11289
|
-
fieldId: "
|
|
11952
|
+
fieldId: "assignee",
|
|
11290
11953
|
fieldType: "string",
|
|
11291
|
-
|
|
11292
|
-
|
|
11954
|
+
filterable: true,
|
|
11955
|
+
description: "userId or display name.",
|
|
11956
|
+
renderHints: { label: "Assignee", widget: "text", order: 5, section: "Tracking" }
|
|
11293
11957
|
},
|
|
11294
11958
|
{
|
|
11295
|
-
fieldId: "
|
|
11959
|
+
fieldId: "project",
|
|
11296
11960
|
fieldType: "string",
|
|
11297
11961
|
filterable: true,
|
|
11298
|
-
|
|
11962
|
+
description: "Grouping key for cross-session continuity.",
|
|
11963
|
+
renderHints: { label: "Project", widget: "text", order: 6, section: "Tracking" }
|
|
11299
11964
|
},
|
|
11300
11965
|
{
|
|
11301
|
-
|
|
11302
|
-
|
|
11303
|
-
|
|
11304
|
-
|
|
11305
|
-
filterable: true,
|
|
11306
|
-
enumValues: ["active", "retired"],
|
|
11307
|
-
renderHints: { label: "Status", widget: "select", order: 5 }
|
|
11966
|
+
fieldId: "dueDate",
|
|
11967
|
+
fieldType: "date",
|
|
11968
|
+
description: "ISO-8601. Range-queryable (see lookupFields) \u2014 list tasks due in a window.",
|
|
11969
|
+
renderHints: { label: "Due date", widget: "date", order: 7, section: "Tracking" }
|
|
11308
11970
|
},
|
|
11309
11971
|
{
|
|
11310
|
-
fieldId: "
|
|
11311
|
-
fieldType: "
|
|
11312
|
-
|
|
11313
|
-
renderHints: { label: "
|
|
11972
|
+
fieldId: "tags",
|
|
11973
|
+
fieldType: "array",
|
|
11974
|
+
filterable: true,
|
|
11975
|
+
renderHints: { label: "Tags", order: 8, section: "Tracking" }
|
|
11314
11976
|
}
|
|
11315
|
-
// externalId
|
|
11977
|
+
// externalId is the record's FIRST-CLASS identifier (the dedup/lookup key + the
|
|
11978
|
+
// value a `reference` resolves against) — not a payload field; the loader sends
|
|
11979
|
+
// it top-level on the RecordRequest.
|
|
11316
11980
|
],
|
|
11317
|
-
//
|
|
11318
|
-
//
|
|
11319
|
-
//
|
|
11320
|
-
|
|
11981
|
+
// Two lookup shapes, chosen deliberately because the equality-vs-range choice
|
|
11982
|
+
// per field is MIGRATION-LOCKED once the schema is live (you cannot flip a
|
|
11983
|
+
// field slot↔range later, even by removing and re-adding it):
|
|
11984
|
+
// • `project` — EQUALITY (a grouping key, not ordered): enumerate one
|
|
11985
|
+
// project's tasks directly, no search. Each equality lookup uses 1 of the
|
|
11986
|
+
// schema's 7 fast index slots.
|
|
11987
|
+
// • `dueDate` — RANGE: ordered `from`/`to`/`prefix` queries ("tasks due this
|
|
11988
|
+
// week", "due in 2026-06"). Range lookups use a relationship row, not a
|
|
11989
|
+
// slot, and are billed at the range rate. ISO-8601 sorts chronologically,
|
|
11990
|
+
// so the order is correct.
|
|
11991
|
+
// `externalId` has a built-in first-class finder and must NOT be redeclared here.
|
|
11992
|
+
lookupFields: ["project", { fieldName: "dueDate", rangeEnabled: true }]
|
|
11321
11993
|
}
|
|
11322
11994
|
],
|
|
11323
11995
|
// Least-privilege profile — MUST pass the CLI scope gate.
|
|
11324
|
-
// r/c/u + search + schema discovery
|
|
11325
|
-
// recall over the captured rationale). NOT records:d: memory is superseded,
|
|
11326
|
-
// not deleted, so the project's decision history stays auditable.
|
|
11996
|
+
// r/c/u + search + schema discovery. NOT records:d (least privilege).
|
|
11327
11997
|
accessProfile: {
|
|
11328
|
-
allowedActions: ["records:r", "records:c", "records:u", "search:r", "schemas:r",
|
|
11998
|
+
allowedActions: ["records:r", "records:c", "records:u", "search:r", "schemas:r"],
|
|
11999
|
+
// dataScope: {} (present, empty) is the deliberate marker for a
|
|
12000
|
+
// tenant-level shared tracker — every task visible to every holder of
|
|
12001
|
+
// this credential. Add a `scope:org` entry instead for per-org isolation.
|
|
12002
|
+
dataScope: {}
|
|
11329
12003
|
},
|
|
11330
12004
|
servicePrincipal: {
|
|
11331
|
-
externalId: "
|
|
11332
|
-
displayName: "
|
|
12005
|
+
externalId: "task-management",
|
|
12006
|
+
displayName: "Task Management"
|
|
11333
12007
|
},
|
|
11334
12008
|
seed: [
|
|
11335
12009
|
{
|
|
11336
|
-
// Seeded FIRST so the convention below resolves its reference to it — the
|
|
11337
|
-
// loader creates seeds in array order, and a reference target must exist when
|
|
11338
|
-
// the referencing record is written.
|
|
11339
|
-
surface: "record",
|
|
11340
|
-
typeName: "decision",
|
|
11341
|
-
externalId: "seed-use-vectros-for-memory",
|
|
11342
|
-
fields: {
|
|
11343
|
-
title: "Use Vectros as the coding agent\u2019s project memory",
|
|
11344
|
-
statement: "Persist decisions, conventions, and gotchas as Vectros records so they survive across agent sessions.",
|
|
11345
|
-
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.",
|
|
11346
|
-
status: "active",
|
|
11347
|
-
area: "getting-started",
|
|
11348
|
-
decidedOn: "2026-06-14"
|
|
11349
|
-
}
|
|
11350
|
-
},
|
|
11351
|
-
{
|
|
11352
|
-
// Demonstrates a live typed link at bootstrap: this convention references the
|
|
11353
|
-
// decision above by its externalId, so "open the decision behind this rule"
|
|
11354
|
-
// works the moment the blueprint is applied.
|
|
11355
12010
|
surface: "record",
|
|
11356
|
-
typeName: "
|
|
11357
|
-
externalId: "seed-
|
|
12011
|
+
typeName: "task",
|
|
12012
|
+
externalId: "seed-welcome",
|
|
11358
12013
|
fields: {
|
|
11359
|
-
|
|
11360
|
-
|
|
11361
|
-
|
|
11362
|
-
|
|
11363
|
-
|
|
11364
|
-
|
|
12014
|
+
title: "Welcome to your Vectros task tracker",
|
|
12015
|
+
description: "Created by the bootstrap loader. Ask your agent to add tasks.",
|
|
12016
|
+
status: "todo",
|
|
12017
|
+
priority: "low",
|
|
12018
|
+
project: "getting-started",
|
|
12019
|
+
dueDate: "2026-06-30"
|
|
11365
12020
|
}
|
|
11366
12021
|
}
|
|
11367
12022
|
]
|
|
11368
12023
|
};
|
|
11369
|
-
var
|
|
12024
|
+
var task_management_default = taskManagement;
|
|
11370
12025
|
var DATA_PLANE_ACTIONS = [
|
|
11371
12026
|
"records:r",
|
|
11372
12027
|
"records:c",
|
|
@@ -11394,7 +12049,7 @@ var MEMBER_SHARED_READ_ACTIONS = [
|
|
|
11394
12049
|
];
|
|
11395
12050
|
var agenticSdlc = {
|
|
11396
12051
|
name: "agentic-sdlc",
|
|
11397
|
-
version: "1.
|
|
12052
|
+
version: "1.9.0",
|
|
11398
12053
|
description: "A whole-SDLC system of record for an AI development team \u2014 decisions, designs, references, runbooks, post-mortems (as documents) plus controls, conventions, gotchas, and a glossary (as records), cross-linked and recalled by meaning.",
|
|
11399
12054
|
contextId: "agentic-sdlc",
|
|
11400
12055
|
contextName: "Agentic SDLC Knowledge Base",
|
|
@@ -12260,13 +12915,244 @@ var agenticSdlc = {
|
|
|
12260
12915
|
{ fieldName: "updatedOn", rangeEnabled: true },
|
|
12261
12916
|
{ fieldName: "priority", rangeEnabled: true }
|
|
12262
12917
|
]
|
|
12918
|
+
},
|
|
12919
|
+
{
|
|
12920
|
+
// A PROPOSED memory, waiting on a verification step — the staging area in
|
|
12921
|
+
// front of the `memory` type above. An agent that distills its own working
|
|
12922
|
+
// sessions produces claims faster than it can check them, and an unchecked
|
|
12923
|
+
// claim is worse than no claim: it is a plausible sentence that will be
|
|
12924
|
+
// recalled as fact. So a proposal lands here, an agent later verifies it,
|
|
12925
|
+
// and only a verified one is written to `memory`.
|
|
12926
|
+
//
|
|
12927
|
+
// ⚠ indexMode NONE IS THE LOAD-BEARING DECISION, not a tuning choice.
|
|
12928
|
+
// Store-only means these records are never indexed, so a search or a
|
|
12929
|
+
// grounded answer CANNOT return them under any query — the separation
|
|
12930
|
+
// between unverified proposals and trusted knowledge is enforced by the
|
|
12931
|
+
// platform rather than by every caller remembering to filter. Make this
|
|
12932
|
+
// HYBRID and the corpus of unchecked claims starts competing with the
|
|
12933
|
+
// curated KB for retrieval slots, which is the one failure this type
|
|
12934
|
+
// exists to prevent. They stay reachable by id and by the two lookups
|
|
12935
|
+
// below, which is all a disposition workflow needs.
|
|
12936
|
+
//
|
|
12937
|
+
// Note the consequence for the fields: `filterable` narrows SEARCH, and
|
|
12938
|
+
// nothing here is searchable, so no field declares it. Query paths are the
|
|
12939
|
+
// lookups, deliberately — a flag that does nothing would just read as
|
|
12940
|
+
// capability that isn't there.
|
|
12941
|
+
typeName: "candidate",
|
|
12942
|
+
displayName: "Memory candidate",
|
|
12943
|
+
indexMode: "NONE",
|
|
12944
|
+
fields: [
|
|
12945
|
+
{
|
|
12946
|
+
fieldId: "title",
|
|
12947
|
+
fieldType: "string",
|
|
12948
|
+
required: true,
|
|
12949
|
+
validation: { minLength: 1, maxLength: 200 },
|
|
12950
|
+
description: "One line naming the claim \u2014 what a reviewer reads first.",
|
|
12951
|
+
renderHints: { label: "Title", widget: "text", order: 1, displayField: true }
|
|
12952
|
+
},
|
|
12953
|
+
{
|
|
12954
|
+
fieldId: "body",
|
|
12955
|
+
fieldType: "string",
|
|
12956
|
+
description: "The proposed fact in full, as the distiller stated it \u2014 the thing to verify.",
|
|
12957
|
+
renderHints: { label: "Body", widget: "textarea", order: 2 }
|
|
12958
|
+
},
|
|
12959
|
+
{
|
|
12960
|
+
// Mirrors `memory.kind` exactly, so a verified candidate maps 1:1 onto
|
|
12961
|
+
// the memory it becomes with no vocabulary translation in between.
|
|
12962
|
+
fieldId: "kind",
|
|
12963
|
+
fieldType: "enum",
|
|
12964
|
+
enumValues: ["user", "feedback", "project", "reference", "observation"],
|
|
12965
|
+
description: "Which flavour of memory this would become \u2014 the same vocabulary as `memory.kind`.",
|
|
12966
|
+
renderHints: { label: "Kind", widget: "select", order: 3 }
|
|
12967
|
+
},
|
|
12968
|
+
{
|
|
12969
|
+
// The distiller's ROUTING suggestion, and the one field here with no
|
|
12970
|
+
// counterpart on `memory` — because it stops existing the moment the
|
|
12971
|
+
// question it asks is answered. A verified claim either becomes a
|
|
12972
|
+
// memory record or becomes a paragraph in a document; this is the
|
|
12973
|
+
// proposer's guess about which, and the verifier is free to overrule
|
|
12974
|
+
// it (the verdict lands in `disposition`, not here).
|
|
12975
|
+
fieldId: "dest",
|
|
12976
|
+
fieldType: "enum",
|
|
12977
|
+
enumValues: ["memory", "doc"],
|
|
12978
|
+
description: "Where the proposer thinks this belongs if it survives review \u2014 a suggestion, not the verdict.",
|
|
12979
|
+
renderHints: { label: "Suggested destination", widget: "select", order: 4 }
|
|
12980
|
+
},
|
|
12981
|
+
{
|
|
12982
|
+
// `area`, `tags` and `sourceRef` below mirror `memory`'s exactly, for
|
|
12983
|
+
// the same reason `kind` does: a verified candidate is COPIED into a
|
|
12984
|
+
// memory, and any field that exists on only one side has to be
|
|
12985
|
+
// re-derived or dropped at exactly the moment a human is deciding
|
|
12986
|
+
// whether the claim is true.
|
|
12987
|
+
fieldId: "area",
|
|
12988
|
+
fieldType: "string",
|
|
12989
|
+
description: 'Subsystem/topic the claim applies to (e.g. "auth", "search") \u2014 the shared area vocabulary.',
|
|
12990
|
+
renderHints: { label: "Area", widget: "text", order: 5 }
|
|
12991
|
+
},
|
|
12992
|
+
{
|
|
12993
|
+
fieldId: "tags",
|
|
12994
|
+
fieldType: "array",
|
|
12995
|
+
description: "Freeform labels carried over from the proposal.",
|
|
12996
|
+
renderHints: { label: "Tags", order: 6 }
|
|
12997
|
+
},
|
|
12998
|
+
{
|
|
12999
|
+
// Provenance the VERIFIER needs most: checking a claim starts with the
|
|
13000
|
+
// file or issue it is about. A candidate whose sourceRef was dropped
|
|
13001
|
+
// in transit costs the reviewer the search that the proposer had
|
|
13002
|
+
// already done.
|
|
13003
|
+
fieldId: "sourceRef",
|
|
13004
|
+
fieldType: "string",
|
|
13005
|
+
description: "Provenance: the file path / issue / URL the claim is about \u2014 where a verifier starts.",
|
|
13006
|
+
renderHints: { label: "Source ref", widget: "text", order: 7 }
|
|
13007
|
+
},
|
|
13008
|
+
{
|
|
13009
|
+
fieldId: "sessionId",
|
|
13010
|
+
fieldType: "string",
|
|
13011
|
+
description: "The conversation that proposed it \u2014 your runtime's session identifier.",
|
|
13012
|
+
renderHints: { label: "Session", widget: "text", order: 8 }
|
|
13013
|
+
},
|
|
13014
|
+
{
|
|
13015
|
+
// Also the stable ordering key: a display ordinal is derived by sorting
|
|
13016
|
+
// on this, so proposals keep the numbering a reviewer saw. Nothing is
|
|
13017
|
+
// ever hard-deleted here, which is what keeps that ordering stable.
|
|
13018
|
+
fieldId: "proposedAt",
|
|
13019
|
+
fieldType: "date",
|
|
13020
|
+
description: "ISO-8601 \u2014 when it was proposed. Also the stable ordering key for display.",
|
|
13021
|
+
renderHints: { label: "Proposed at", widget: "date", order: 9 }
|
|
13022
|
+
},
|
|
13023
|
+
{
|
|
13024
|
+
// THE VERDICT, and deliberately a plain payload field rather than the
|
|
13025
|
+
// record's own ACTIVE/ARCHIVED status. Settling a candidate is
|
|
13026
|
+
// REVERSIBLE — a wrong dismissal has to be undoable — and a write-side
|
|
13027
|
+
// visibility flip is the wrong tool for a reversible state: it is
|
|
13028
|
+
// asynchronous, it moves the record out from under readers, and it
|
|
13029
|
+
// leaves the index status of a once-indexed row stranded. A filterable
|
|
13030
|
+
// payload field flips synchronously and reads the same for everyone.
|
|
13031
|
+
fieldId: "disposition",
|
|
13032
|
+
fieldType: "enum",
|
|
13033
|
+
enumValues: ["pending", "stored", "documented", "ignored"],
|
|
13034
|
+
description: "The verdict: still pending, written to memory, captured in a doc, or dismissed.",
|
|
13035
|
+
renderHints: { label: "Disposition", widget: "select", order: 10 }
|
|
13036
|
+
},
|
|
13037
|
+
{
|
|
13038
|
+
fieldId: "ref",
|
|
13039
|
+
fieldType: "string",
|
|
13040
|
+
description: "The citation given when settling it \u2014 the record id, document path, or reason.",
|
|
13041
|
+
renderHints: { label: "Ref", widget: "text", order: 11 }
|
|
13042
|
+
},
|
|
13043
|
+
{
|
|
13044
|
+
// What the settling step actually CHECKED, which is not always what the
|
|
13045
|
+
// agent typed. Keeping both makes a bad citation auditable after the
|
|
13046
|
+
// fact instead of indistinguishable from a good one.
|
|
13047
|
+
fieldId: "resolved",
|
|
13048
|
+
fieldType: "string",
|
|
13049
|
+
description: "What was verified when it was settled \u2014 kept alongside `ref`, which is what was claimed.",
|
|
13050
|
+
renderHints: { label: "Resolved", widget: "text", order: 12 }
|
|
13051
|
+
},
|
|
13052
|
+
{
|
|
13053
|
+
fieldId: "origin",
|
|
13054
|
+
fieldType: "enum",
|
|
13055
|
+
enumValues: ["session", "recovered"],
|
|
13056
|
+
description: "Proposed by a live session, or recovered later from one that ended without settling.",
|
|
13057
|
+
renderHints: { label: "Origin", widget: "select", order: 13 }
|
|
13058
|
+
},
|
|
13059
|
+
{
|
|
13060
|
+
fieldId: "reopenedWhy",
|
|
13061
|
+
fieldType: "string",
|
|
13062
|
+
description: "Why a settled candidate was reopened \u2014 the audit trail for undoing a verdict.",
|
|
13063
|
+
renderHints: { label: "Reopened why", widget: "textarea", order: 14 }
|
|
13064
|
+
},
|
|
13065
|
+
{
|
|
13066
|
+
// The forward half of a correction: this candidate restates an earlier
|
|
13067
|
+
// one whose claim was wrong. Written at create, so it costs nothing.
|
|
13068
|
+
fieldId: "revises",
|
|
13069
|
+
fieldType: "reference",
|
|
13070
|
+
targetTypeName: "candidate",
|
|
13071
|
+
targetSurface: "record",
|
|
13072
|
+
targetField: "externalId",
|
|
13073
|
+
cardinality: "one",
|
|
13074
|
+
description: "The candidate this one corrects (by externalId).",
|
|
13075
|
+
renderHints: { label: "Revises", order: 15 }
|
|
13076
|
+
},
|
|
13077
|
+
{
|
|
13078
|
+
// The reverse half, and NOT redundant: "is this still open?" has to be
|
|
13079
|
+
// answerable without asking whether some OTHER record points here, and
|
|
13080
|
+
// there is no reverse-reference read. So the superseded candidate is
|
|
13081
|
+
// marked directly.
|
|
13082
|
+
//
|
|
13083
|
+
// Kept SEPARATE from `disposition` on purpose. Superseded and dismissed
|
|
13084
|
+
// are different facts: a dismissal may be undone, while resurrecting a
|
|
13085
|
+
// claim that a later run already corrected would re-offer the version
|
|
13086
|
+
// known to be wrong. Folding both into one enum makes that distinction
|
|
13087
|
+
// unrepresentable exactly when it matters.
|
|
13088
|
+
fieldId: "supersededBy",
|
|
13089
|
+
fieldType: "reference",
|
|
13090
|
+
targetTypeName: "candidate",
|
|
13091
|
+
targetSurface: "record",
|
|
13092
|
+
targetField: "externalId",
|
|
13093
|
+
cardinality: "one",
|
|
13094
|
+
description: "The candidate that corrected this one (by externalId) \u2014 set on the older record.",
|
|
13095
|
+
renderHints: { label: "Superseded by", order: 16 }
|
|
13096
|
+
}
|
|
13097
|
+
],
|
|
13098
|
+
// FOUR lookups, because this type is asked three genuinely different
|
|
13099
|
+
// questions and one of them is a conjunction:
|
|
13100
|
+
//
|
|
13101
|
+
// `disposition` — "what is waiting ANYWHERE?" The cross-session
|
|
13102
|
+
// review queue. Low cardinality, so its
|
|
13103
|
+
// partitions are large; that is inherent to the
|
|
13104
|
+
// question and the pending set is small.
|
|
13105
|
+
// `sessionId` — "everything belonging to this conversation",
|
|
13106
|
+
// settled candidates included.
|
|
13107
|
+
// (sessionId, disposition) — "what is still waiting IN this conversation".
|
|
13108
|
+
// `proposedAt` (range) — worked by age: "what has been sitting
|
|
13109
|
+
// longest", "what came in this week". A range
|
|
13110
|
+
// read, not a filter over everything ever
|
|
13111
|
+
// proposed.
|
|
13112
|
+
//
|
|
13113
|
+
// The composite earns its slot because that third question is the HOTTEST
|
|
13114
|
+
// read in the system — it is what an agent is shown as it works, so it runs
|
|
13115
|
+
// far more often than the queue-wide scan does. Without it the answer is the
|
|
13116
|
+
// more selective single lookup plus a filter in the caller.
|
|
13117
|
+
//
|
|
13118
|
+
// `sessionId` LEADS, and the order is not cosmetic: the leading field
|
|
13119
|
+
// becomes the partition key. Leading with `disposition` would sort every
|
|
13120
|
+
// candidate ever proposed into four partitions; leading with `sessionId`
|
|
13121
|
+
// gives one small partition per conversation. Order is also
|
|
13122
|
+
// migration-locked, so it cannot be corrected later.
|
|
13123
|
+
//
|
|
13124
|
+
// A composite matches a LEADING RUN of its fields, so `(sessionId,
|
|
13125
|
+
// disposition)` also answers `sessionId` alone. The plain `sessionId`
|
|
13126
|
+
// lookup is kept anyway: under a partial tuple the unspecified field
|
|
13127
|
+
// becomes part of the ordering, so results come back grouped by
|
|
13128
|
+
// `disposition` rather than in one sequence — right for a filtered view,
|
|
13129
|
+
// wrong for "this conversation's candidates, oldest first", which is the
|
|
13130
|
+
// read the review surface and the queue/record agreement check both make.
|
|
13131
|
+
// One declaration out of ten is a cheap price for not depending on that
|
|
13132
|
+
// subtlety in the two places that must not be wrong.
|
|
13133
|
+
//
|
|
13134
|
+
// No `sortBy`: it defaults to the record's creation time, which is already
|
|
13135
|
+
// the primary key the review queue orders by. Naming `proposedAt` instead
|
|
13136
|
+
// would order by the WORKER's clock, and that is stamped when a proposal is
|
|
13137
|
+
// queued rather than when it is stored — the two disagree by exactly the
|
|
13138
|
+
// outage a durable queue exists to survive.
|
|
13139
|
+
lookupFields: [
|
|
13140
|
+
"disposition",
|
|
13141
|
+
"sessionId",
|
|
13142
|
+
{ fieldNames: ["sessionId", "disposition"] },
|
|
13143
|
+
{ fieldName: "proposedAt", rangeEnabled: true }
|
|
13144
|
+
]
|
|
12263
13145
|
}
|
|
12264
13146
|
],
|
|
12265
13147
|
// Least-privilege, data-plane only. The scope of the `ssk_*` key the bootstrap
|
|
12266
13148
|
// mints for THIS blueprint's service principal (the MCP/API runtime). See
|
|
12267
13149
|
// DATA_PLANE_ACTIONS above for the action set + rationale.
|
|
12268
13150
|
accessProfile: {
|
|
12269
|
-
allowedActions: DATA_PLANE_ACTIONS
|
|
13151
|
+
allowedActions: DATA_PLANE_ACTIONS,
|
|
13152
|
+
// dataScope: {} (present, empty) is the deliberate marker for tenant-wide
|
|
13153
|
+
// reach — this service key IS the runtime's own access to the whole KB,
|
|
13154
|
+
// not a multi-owner resource to isolate.
|
|
13155
|
+
dataScope: {}
|
|
12270
13156
|
},
|
|
12271
13157
|
// A reusable `editor` role for the HUMAN owner — DISTINCT from `accessProfile`
|
|
12272
13158
|
// (which scopes only the service-principal key). `bootstrap` provisions this
|
|
@@ -12286,7 +13172,9 @@ var agenticSdlc = {
|
|
|
12286
13172
|
// which includes members' private memory — the trusted-administrator tradeoff,
|
|
12287
13173
|
// called out in the bundled guide. Scope it down if your deployment wants
|
|
12288
13174
|
// curator access without private-memory visibility.
|
|
12289
|
-
|
|
13175
|
+
// dataScope: {} echoes the "deliberately UNSCOPED (whole-context)" call-out
|
|
13176
|
+
// above as an explicit marker, not just prose — see it for the tradeoff.
|
|
13177
|
+
editor: [{ allowedActions: EDITOR_ACTIONS, dataScope: {} }],
|
|
12290
13178
|
// A team member (or their agent) — TWO memory tiers as two UNIONed clauses
|
|
12291
13179
|
// (a record is accessible when ANY clause grants it; within one clause every
|
|
12292
13180
|
// dataScope dimension must match):
|
|
@@ -12306,14 +13194,21 @@ var agenticSdlc = {
|
|
|
12306
13194
|
// `vectros join agentic-sdlc --role member`; verify a binding with
|
|
12307
13195
|
// `vectros access explain --principal me --context agentic-sdlc`.
|
|
12308
13196
|
member: [
|
|
12309
|
-
|
|
13197
|
+
// dataScope: {} echoes clause 1's own "UNSCOPED, but safe" call-out above
|
|
13198
|
+
// as an explicit marker, not just prose.
|
|
13199
|
+
{ allowedActions: MEMBER_SHARED_READ_ACTIONS, dataScope: {} },
|
|
12310
13200
|
{
|
|
12311
13201
|
// `inference:r` is IN this self-scoped clause (not only clause 1) so a
|
|
12312
13202
|
// member's `rag_ask` can ground on its OWN private memory — the read
|
|
12313
13203
|
// grant (`records:r:memory`) and the inference capability travel in the
|
|
12314
13204
|
// SAME clause, so grounding works regardless of cross-clause admission
|
|
12315
13205
|
// semantics. Still self-fenced by dataScope: never another principal's.
|
|
12316
|
-
|
|
13206
|
+
// `candidate` is READ-only here, unlike `memory`'s full CRU. A candidate's
|
|
13207
|
+
// verdict is the output of a verification step, so it is written by the
|
|
13208
|
+
// agent runtime that performed the check — a human editing the field
|
|
13209
|
+
// directly would record a verdict nothing actually verified. Reading them
|
|
13210
|
+
// is the point: the queue, the verdicts and the corrections are browsable.
|
|
13211
|
+
allowedActions: ["records:cru:memory", "records:r:candidate", "search:r", "inference:r"],
|
|
12317
13212
|
dataScope: { userId: ["${{ self.userId }}"] }
|
|
12318
13213
|
}
|
|
12319
13214
|
]
|
|
@@ -12416,7 +13311,11 @@ var secondBrain = {
|
|
|
12416
13311
|
// (so the agent can `rag_ask` a grounded, cited question over your notes). No
|
|
12417
13312
|
// records:d: notes are archived (status → 'archived'), never deleted.
|
|
12418
13313
|
accessProfile: {
|
|
12419
|
-
allowedActions: ["records:r", "records:c", "records:u", "search:r", "schemas:r", "inference:r"]
|
|
13314
|
+
allowedActions: ["records:r", "records:c", "records:u", "search:r", "schemas:r", "inference:r"],
|
|
13315
|
+
// dataScope: {} (present, empty) is the deliberate marker for tenant-wide
|
|
13316
|
+
// reach — this is a single personal-knowledge-base credential with one
|
|
13317
|
+
// holder, not a multi-owner shared resource to isolate.
|
|
13318
|
+
dataScope: {}
|
|
12420
13319
|
},
|
|
12421
13320
|
servicePrincipal: {
|
|
12422
13321
|
externalId: "second-brain",
|
|
@@ -12548,7 +13447,11 @@ var clinicalIntake = {
|
|
|
12548
13447
|
// reveal qualifier (the demo key literally cannot un-redact the PHI fields),
|
|
12549
13448
|
// and NO inference:r (we never point a RAG capability at a PHI corpus here).
|
|
12550
13449
|
accessProfile: {
|
|
12551
|
-
allowedActions: ["records:r", "records:c", "records:u", "search:r", "schemas:r"]
|
|
13450
|
+
allowedActions: ["records:r", "records:c", "records:u", "search:r", "schemas:r"],
|
|
13451
|
+
// dataScope: {} (present, empty) is the deliberate marker for tenant-wide
|
|
13452
|
+
// reach, unchanged from this demo's original intent — a real clinical
|
|
13453
|
+
// deployment should scope this (e.g. `scope:org`) before handling real PHI.
|
|
13454
|
+
dataScope: {}
|
|
12552
13455
|
},
|
|
12553
13456
|
servicePrincipal: {
|
|
12554
13457
|
externalId: "clinical-intake",
|
|
@@ -12577,7 +13480,6 @@ var clinicalIntake = {
|
|
|
12577
13480
|
var clinical_intake_default = clinicalIntake;
|
|
12578
13481
|
var BUNDLED_BLUEPRINTS = [
|
|
12579
13482
|
task_management_default,
|
|
12580
|
-
coding_agent_memory_default,
|
|
12581
13483
|
agentic_sdlc_default,
|
|
12582
13484
|
second_brain_default,
|
|
12583
13485
|
clinical_intake_default
|
|
@@ -12876,18 +13778,17 @@ async function mintBridgeToken(opts) {
|
|
|
12876
13778
|
ttlSeconds = DEFAULT_BRIDGE_TTL_SECONDS,
|
|
12877
13779
|
tenant = "live",
|
|
12878
13780
|
useScopedToken = false,
|
|
13781
|
+
contextId,
|
|
12879
13782
|
fetchImpl = fetch
|
|
12880
13783
|
} = opts;
|
|
12881
13784
|
const base = apiBaseUrl.replace(/\/$/, "");
|
|
12882
13785
|
const path4 = bridgeTokenPath(useScopedToken);
|
|
12883
|
-
|
|
12884
|
-
|
|
12885
|
-
|
|
12886
|
-
|
|
12887
|
-
|
|
12888
|
-
|
|
12889
|
-
(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)` : ""
|
|
12890
|
-
);
|
|
13786
|
+
let url = `${base}${path4}?ttl=${encodeURIComponent(String(ttlSeconds))}&tenant=${encodeURIComponent(tenant)}`;
|
|
13787
|
+
if (contextId) url += `&context=${encodeURIComponent(contextId)}`;
|
|
13788
|
+
return fetchStToken(url, cognitoToken, fetchImpl, path4, (status) => {
|
|
13789
|
+
if (status !== 404 || useScopedToken || contextId) return "";
|
|
13790
|
+
return ` (the dedicated ${CLI_BOOTSTRAP_TOKEN_PATH} endpoint may not be deployed yet \u2014 retry with --use-scoped-token for the fallback path)`;
|
|
13791
|
+
});
|
|
12891
13792
|
}
|
|
12892
13793
|
async function mintContextToken(opts) {
|
|
12893
13794
|
const {
|
|
@@ -13159,7 +14060,17 @@ async function resolveAuthContext(opts) {
|
|
|
13159
14060
|
bearer: bridge.token,
|
|
13160
14061
|
cognitoToken,
|
|
13161
14062
|
baseUrl,
|
|
13162
|
-
|
|
14063
|
+
/**
|
|
14064
|
+
* Derived from the REQUEST'S actual `baseUrl`, not echoed from `opts.env` — the two
|
|
14065
|
+
* diverge whenever `VECTROS_API_BASE_URL`/`--base-url` alone selects the environment
|
|
14066
|
+
* (the common shape when targeting staging without also passing `--env staging`,
|
|
14067
|
+
* since `envToBaseUrl` lets an explicit override win over `opts.env`'s default). Every
|
|
14068
|
+
* display surface that shows "on {env}" (blueprint-test's banner, `whoami`, etc.) reads
|
|
14069
|
+
* this field, so it must reflect where the request actually went, not which hidden flag
|
|
14070
|
+
* defaulted to what. `deriveEnvironment` falls through to `opts.env` only for a base URL
|
|
14071
|
+
* it doesn't recognize (a non-standard override, e.g. a future custom/self-hosted host).
|
|
14072
|
+
*/
|
|
14073
|
+
env: deriveEnvironment(baseUrl) ?? opts.env,
|
|
13163
14074
|
identity,
|
|
13164
14075
|
tenantId: typeof identity.tenantId === "string" ? identity.tenantId : void 0,
|
|
13165
14076
|
tenant
|
|
@@ -13243,7 +14154,8 @@ var DATA_PLANE_ALLOWLIST = [
|
|
|
13243
14154
|
"search",
|
|
13244
14155
|
"documents",
|
|
13245
14156
|
"folders",
|
|
13246
|
-
"inference"
|
|
14157
|
+
"inference",
|
|
14158
|
+
"entities"
|
|
13247
14159
|
];
|
|
13248
14160
|
var KNOWN_CONTROL_PLANE_RESOURCES = [
|
|
13249
14161
|
"keys",
|
|
@@ -13252,14 +14164,36 @@ var KNOWN_CONTROL_PLANE_RESOURCES = [
|
|
|
13252
14164
|
"users",
|
|
13253
14165
|
"billing",
|
|
13254
14166
|
"admin",
|
|
13255
|
-
//
|
|
13256
|
-
//
|
|
13257
|
-
//
|
|
13258
|
-
//
|
|
13259
|
-
//
|
|
13260
|
-
|
|
14167
|
+
// The namespace REGISTRY (as opposed to `entities`, which creates/reads a
|
|
14168
|
+
// ROW within an already-registered namespace — see the data-plane-allowlist
|
|
14169
|
+
// note above). Registering a new namespace TYPE is a tenant-config action,
|
|
14170
|
+
// root-key-gated regardless of any scope a token carries — genuinely
|
|
14171
|
+
// control-plane, unlike per-namespace entity CRUD. (Cosmetic: any
|
|
14172
|
+
// non-data-plane resource hard-rejects whether or not it is listed here;
|
|
14173
|
+
// this list only sharpens the error message.)
|
|
14174
|
+
//
|
|
14175
|
+
// ⚠️ This bars `namespaces` as a SCOPE — no partner-authored role can ever
|
|
14176
|
+
// be granted it. It does NOT bar a blueprint's own top-level `namespaces[]`
|
|
14177
|
+
// FORMAT field (@vectros-ai/blueprints), which deliberately DOES write this
|
|
14178
|
+
// same registry — the same "DISCRETIONARY vs UNCONDITIONAL" distinction
|
|
14179
|
+
// {@link PARTIALLY_ADMITTED_OPS} draws for `entities`, applied here: the
|
|
14180
|
+
// write rides `provisioning:c` (non-grantable — no partner role can ever
|
|
14181
|
+
// hold it, the same bearer `keys`/`app-contexts` writes already require)
|
|
14182
|
+
// and the platform confines it to the blueprint's OWN context
|
|
14183
|
+
// unconditionally, never tenant-wide, so the reach is bounded to what the
|
|
14184
|
+
// blueprint already provisions, never the pack's say-so alone. See
|
|
14185
|
+
// `@vectros-ai/blueprints`' `namespaces[]` doc for the full design.
|
|
13261
14186
|
"namespaces"
|
|
13262
14187
|
];
|
|
14188
|
+
var PARTIALLY_ADMITTED_OPS = /* @__PURE__ */ new Map([
|
|
14189
|
+
["profiles", /* @__PURE__ */ new Set(["r", "u", "d"])]
|
|
14190
|
+
]);
|
|
14191
|
+
var SELF_QUALIFIED_OPS = /* @__PURE__ */ new Map([
|
|
14192
|
+
["profiles", /* @__PURE__ */ new Set(["c"])]
|
|
14193
|
+
]);
|
|
14194
|
+
var UNQUALIFIABLE_WITH_ANY_TAIL = /* @__PURE__ */ new Map([
|
|
14195
|
+
["profiles", /* @__PURE__ */ new Set(["r"])]
|
|
14196
|
+
]);
|
|
13263
14197
|
var VALID_OPS = /* @__PURE__ */ new Set(["c", "r", "u", "d"]);
|
|
13264
14198
|
var DATA_PLANE_SET = new Set(DATA_PLANE_ALLOWLIST);
|
|
13265
14199
|
var CONTROL_PLANE_SET = new Set(KNOWN_CONTROL_PLANE_RESOURCES);
|
|
@@ -13276,6 +14210,59 @@ ${summary}`
|
|
|
13276
14210
|
this.rejected = rejected;
|
|
13277
14211
|
}
|
|
13278
14212
|
};
|
|
14213
|
+
var GRANTED_CAPABILITIES_ALLOWLIST = ["member-lifecycle"];
|
|
14214
|
+
var GRANTED_CAPABILITIES_SET = new Set(GRANTED_CAPABILITIES_ALLOWLIST);
|
|
14215
|
+
var CAPABILITIES_HINT = GRANTED_CAPABILITIES_ALLOWLIST.join("/");
|
|
14216
|
+
var CapabilityGateError = class extends Error {
|
|
14217
|
+
rejected;
|
|
14218
|
+
constructor(rejected) {
|
|
14219
|
+
const summary = rejected.map((r) => ` \u2022 ${r.action}: ${r.reason}`).join("\n");
|
|
14220
|
+
super(
|
|
14221
|
+
`Capability gate rejected ${rejected.length} requested capability(ies); the bootstrap loader grants (${CAPABILITIES_HINT}) only and mints nothing on reject:
|
|
14222
|
+
${summary}`
|
|
14223
|
+
);
|
|
14224
|
+
this.name = "CapabilityGateError";
|
|
14225
|
+
this.rejected = rejected;
|
|
14226
|
+
}
|
|
14227
|
+
};
|
|
14228
|
+
function evaluateCapabilityToken(name) {
|
|
14229
|
+
const token = name.trim();
|
|
14230
|
+
if (token.length === 0) {
|
|
14231
|
+
return { action: name, reason: "empty capability name" };
|
|
14232
|
+
}
|
|
14233
|
+
if (!GRANTED_CAPABILITIES_SET.has(token)) {
|
|
14234
|
+
return {
|
|
14235
|
+
action: name,
|
|
14236
|
+
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`
|
|
14237
|
+
};
|
|
14238
|
+
}
|
|
14239
|
+
return null;
|
|
14240
|
+
}
|
|
14241
|
+
function evaluateCapabilityGate(capabilities) {
|
|
14242
|
+
const accepted = [];
|
|
14243
|
+
const rejected = [];
|
|
14244
|
+
for (const name of capabilities ?? []) {
|
|
14245
|
+
const rejection = evaluateCapabilityToken(name);
|
|
14246
|
+
if (rejection) {
|
|
14247
|
+
rejected.push(rejection);
|
|
14248
|
+
} else {
|
|
14249
|
+
accepted.push(name);
|
|
14250
|
+
}
|
|
14251
|
+
}
|
|
14252
|
+
return { ok: rejected.length === 0, accepted, rejected };
|
|
14253
|
+
}
|
|
14254
|
+
function assertCapabilityGate(capabilities) {
|
|
14255
|
+
const result = evaluateCapabilityGate(capabilities);
|
|
14256
|
+
if (!result.ok) {
|
|
14257
|
+
throw new CapabilityGateError(result.rejected);
|
|
14258
|
+
}
|
|
14259
|
+
}
|
|
14260
|
+
function firstInvalidOp(ops) {
|
|
14261
|
+
for (const ch of ops) {
|
|
14262
|
+
if (!VALID_OPS.has(ch)) return ch;
|
|
14263
|
+
}
|
|
14264
|
+
return void 0;
|
|
14265
|
+
}
|
|
13279
14266
|
function evaluateScopeToken(action) {
|
|
13280
14267
|
const token = action.trim();
|
|
13281
14268
|
if (token.length === 0) {
|
|
@@ -13297,6 +14284,48 @@ function evaluateScopeToken(action) {
|
|
|
13297
14284
|
};
|
|
13298
14285
|
}
|
|
13299
14286
|
if (!DATA_PLANE_SET.has(resource)) {
|
|
14287
|
+
const admittedOps = PARTIALLY_ADMITTED_OPS.get(resource);
|
|
14288
|
+
const selfOnlyOps = SELF_QUALIFIED_OPS.get(resource);
|
|
14289
|
+
if (admittedOps || selfOnlyOps) {
|
|
14290
|
+
const admittedHint = [.../* @__PURE__ */ new Set([...admittedOps ?? [], ...selfOnlyOps ?? []])].sort().join("/");
|
|
14291
|
+
if (ops === "*") {
|
|
14292
|
+
return {
|
|
14293
|
+
action,
|
|
14294
|
+
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`
|
|
14295
|
+
};
|
|
14296
|
+
}
|
|
14297
|
+
const invalidOp2 = firstInvalidOp(ops);
|
|
14298
|
+
if (invalidOp2) {
|
|
14299
|
+
return {
|
|
14300
|
+
action,
|
|
14301
|
+
reason: `invalid operation character '${invalidOp2}' (valid operations are c, r, u, d, or '*')`
|
|
14302
|
+
};
|
|
14303
|
+
}
|
|
14304
|
+
const qualifier = parts.length > 2 ? parts.slice(2).join(":") : void 0;
|
|
14305
|
+
const isSelfQualified = qualifier === "self";
|
|
14306
|
+
const unqualifiableOps = UNQUALIFIABLE_WITH_ANY_TAIL.get(resource);
|
|
14307
|
+
for (const ch of ops) {
|
|
14308
|
+
if (qualifier !== void 0 && unqualifiableOps?.has(ch)) {
|
|
14309
|
+
return {
|
|
14310
|
+
action,
|
|
14311
|
+
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).`
|
|
14312
|
+
};
|
|
14313
|
+
}
|
|
14314
|
+
if (admittedOps?.has(ch)) continue;
|
|
14315
|
+
if (selfOnlyOps?.has(ch)) {
|
|
14316
|
+
if (isSelfQualified) continue;
|
|
14317
|
+
return {
|
|
14318
|
+
action,
|
|
14319
|
+
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`
|
|
14320
|
+
};
|
|
14321
|
+
}
|
|
14322
|
+
return {
|
|
14323
|
+
action,
|
|
14324
|
+
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`
|
|
14325
|
+
};
|
|
14326
|
+
}
|
|
14327
|
+
return null;
|
|
14328
|
+
}
|
|
13300
14329
|
if (CONTROL_PLANE_SET.has(resource)) {
|
|
13301
14330
|
return {
|
|
13302
14331
|
action,
|
|
@@ -13311,13 +14340,12 @@ function evaluateScopeToken(action) {
|
|
|
13311
14340
|
if (ops === "*") {
|
|
13312
14341
|
return null;
|
|
13313
14342
|
}
|
|
13314
|
-
|
|
13315
|
-
|
|
13316
|
-
|
|
13317
|
-
|
|
13318
|
-
|
|
13319
|
-
|
|
13320
|
-
}
|
|
14343
|
+
const invalidOp = firstInvalidOp(ops);
|
|
14344
|
+
if (invalidOp) {
|
|
14345
|
+
return {
|
|
14346
|
+
action,
|
|
14347
|
+
reason: `invalid operation character '${invalidOp}' (valid operations are c, r, u, d, or '*')`
|
|
14348
|
+
};
|
|
13321
14349
|
}
|
|
13322
14350
|
return null;
|
|
13323
14351
|
}
|
|
@@ -13340,6 +14368,16 @@ function assertScopeGate(allowedActions) {
|
|
|
13340
14368
|
throw new ScopeGateError(result.rejected);
|
|
13341
14369
|
}
|
|
13342
14370
|
}
|
|
14371
|
+
function assertBlueprintGates(blueprint) {
|
|
14372
|
+
assertScopeGate(blueprint.accessProfile.allowedActions);
|
|
14373
|
+
assertCapabilityGate(blueprint.accessProfile.capabilities);
|
|
14374
|
+
for (const clauses of Object.values(blueprint.roles ?? {})) {
|
|
14375
|
+
for (const clause of clauses) {
|
|
14376
|
+
assertScopeGate(clause.allowedActions);
|
|
14377
|
+
assertCapabilityGate(clause.capabilities);
|
|
14378
|
+
}
|
|
14379
|
+
}
|
|
14380
|
+
}
|
|
13343
14381
|
|
|
13344
14382
|
// src/machine-id.ts
|
|
13345
14383
|
var import_node_fs = require("fs");
|
|
@@ -13444,6 +14482,68 @@ async function resolveApplyTimeIdentities(blueprint, client, log) {
|
|
|
13444
14482
|
function actionOf(created) {
|
|
13445
14483
|
return created ? "created" : "reconciled";
|
|
13446
14484
|
}
|
|
14485
|
+
async function registerNamespaceIdempotently(client, contextIdForRequest, decl) {
|
|
14486
|
+
const body = {
|
|
14487
|
+
namespace: decl.namespace,
|
|
14488
|
+
entityBacked: decl.entityBacked,
|
|
14489
|
+
specificityRank: decl.specificityRank,
|
|
14490
|
+
membershipRecordType: decl.membershipRecordType,
|
|
14491
|
+
membershipTargetField: decl.membershipTargetField,
|
|
14492
|
+
membershipLevelField: decl.membershipLevelField,
|
|
14493
|
+
membershipLevels: decl.membershipLevels
|
|
14494
|
+
};
|
|
14495
|
+
try {
|
|
14496
|
+
await client.registerNamespace({ contextId: contextIdForRequest, body });
|
|
14497
|
+
return "created";
|
|
14498
|
+
} catch (err) {
|
|
14499
|
+
const info = extractServerError(err);
|
|
14500
|
+
if (info.statusCode !== 400) throw err;
|
|
14501
|
+
let existing;
|
|
14502
|
+
try {
|
|
14503
|
+
existing = await client.getNamespace({ namespace: decl.namespace, contextId: contextIdForRequest });
|
|
14504
|
+
} catch {
|
|
14505
|
+
throw err;
|
|
14506
|
+
}
|
|
14507
|
+
if (existing.contextId !== contextIdForRequest) throw err;
|
|
14508
|
+
const mismatch = describeNamespaceMismatch(decl, existing);
|
|
14509
|
+
if (mismatch) {
|
|
14510
|
+
throw new Error(
|
|
14511
|
+
`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.`
|
|
14512
|
+
);
|
|
14513
|
+
}
|
|
14514
|
+
return "reconciled";
|
|
14515
|
+
}
|
|
14516
|
+
}
|
|
14517
|
+
function describeNamespaceMismatch(decl, existing) {
|
|
14518
|
+
const mismatches = [];
|
|
14519
|
+
const declaredEntityBacked = decl.entityBacked ?? false;
|
|
14520
|
+
const existingEntityBacked = existing.entityBacked ?? false;
|
|
14521
|
+
if (declaredEntityBacked !== existingEntityBacked) {
|
|
14522
|
+
mismatches.push(
|
|
14523
|
+
`entityBacked: declared ${JSON.stringify(declaredEntityBacked)}, registered ${JSON.stringify(existingEntityBacked)}`
|
|
14524
|
+
);
|
|
14525
|
+
}
|
|
14526
|
+
const fields = [
|
|
14527
|
+
"specificityRank",
|
|
14528
|
+
"membershipRecordType",
|
|
14529
|
+
"membershipTargetField",
|
|
14530
|
+
"membershipLevelField"
|
|
14531
|
+
];
|
|
14532
|
+
for (const field of fields) {
|
|
14533
|
+
const declared = decl[field];
|
|
14534
|
+
const existingValue = existing[field];
|
|
14535
|
+
if (declared === void 0 && (existingValue === void 0 || existingValue === null)) continue;
|
|
14536
|
+
if (declared === existingValue) continue;
|
|
14537
|
+
mismatches.push(`${field}: declared ${JSON.stringify(declared)}, registered ${JSON.stringify(existingValue)}`);
|
|
14538
|
+
}
|
|
14539
|
+
const declaredLevels = decl.membershipLevels;
|
|
14540
|
+
const existingLevels = existing.membershipLevels;
|
|
14541
|
+
const levelsDiffer = declaredLevels === void 0 ? existingLevels != null && existingLevels.length > 0 : JSON.stringify([...declaredLevels].sort()) !== JSON.stringify([...existingLevels ?? []].sort());
|
|
14542
|
+
if (levelsDiffer) {
|
|
14543
|
+
mismatches.push(`membershipLevels: declared ${JSON.stringify(declaredLevels)}, registered ${JSON.stringify(existingLevels)}`);
|
|
14544
|
+
}
|
|
14545
|
+
return mismatches.length > 0 ? mismatches.join("; ") : null;
|
|
14546
|
+
}
|
|
13447
14547
|
var LostKeyError = class extends Error {
|
|
13448
14548
|
constructor(keyName) {
|
|
13449
14549
|
super(
|
|
@@ -13454,6 +14554,20 @@ var LostKeyError = class extends Error {
|
|
|
13454
14554
|
}
|
|
13455
14555
|
keyName;
|
|
13456
14556
|
};
|
|
14557
|
+
var ExistingPrincipalNotConfirmedError = class extends Error {
|
|
14558
|
+
constructor(contextId, userId, externalId) {
|
|
14559
|
+
super(
|
|
14560
|
+
`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.`
|
|
14561
|
+
);
|
|
14562
|
+
this.contextId = contextId;
|
|
14563
|
+
this.userId = userId;
|
|
14564
|
+
this.externalId = externalId;
|
|
14565
|
+
this.name = "ExistingPrincipalNotConfirmedError";
|
|
14566
|
+
}
|
|
14567
|
+
contextId;
|
|
14568
|
+
userId;
|
|
14569
|
+
externalId;
|
|
14570
|
+
};
|
|
13457
14571
|
function mapEnumValues(enumValues) {
|
|
13458
14572
|
if (!enumValues) return void 0;
|
|
13459
14573
|
return enumValues.map((value) => ({ value }));
|
|
@@ -13528,7 +14642,8 @@ function toCapabilities(capabilities) {
|
|
|
13528
14642
|
function toRoleScopeClauses(clauses) {
|
|
13529
14643
|
return clauses.map((c) => ({
|
|
13530
14644
|
allowed_actions: c.allowedActions,
|
|
13531
|
-
data_scope: c.dataScope
|
|
14645
|
+
data_scope: c.dataScope,
|
|
14646
|
+
granted_capabilities: c.capabilities
|
|
13532
14647
|
}));
|
|
13533
14648
|
}
|
|
13534
14649
|
var UnresolvedIdentitiesError = class extends Error {
|
|
@@ -13548,11 +14663,18 @@ function assertNoUnresolvedIdentities(blueprint) {
|
|
|
13548
14663
|
}
|
|
13549
14664
|
}
|
|
13550
14665
|
async function runLoader(client, input) {
|
|
13551
|
-
const {
|
|
13552
|
-
|
|
13553
|
-
|
|
13554
|
-
|
|
13555
|
-
|
|
14666
|
+
const {
|
|
14667
|
+
blueprint,
|
|
14668
|
+
tenantId,
|
|
14669
|
+
machineId,
|
|
14670
|
+
rotate = false,
|
|
14671
|
+
skipSeed = false,
|
|
14672
|
+
readExistingKey,
|
|
14673
|
+
persistMintedKey,
|
|
14674
|
+
confirmExistingPrincipal,
|
|
14675
|
+
log
|
|
14676
|
+
} = input;
|
|
14677
|
+
assertBlueprintGates(blueprint);
|
|
13556
14678
|
assertNoUnresolvedIdentities(blueprint);
|
|
13557
14679
|
const ctxResp = await client.auth.createAppContext({ body: { contextId: blueprint.contextId, name: contextNameOf(blueprint) } });
|
|
13558
14680
|
const contextAction = actionOf(ctxResp.created);
|
|
@@ -13570,7 +14692,65 @@ async function runLoader(client, input) {
|
|
|
13570
14692
|
if (!userId) {
|
|
13571
14693
|
throw new Error("Service-principal create returned no user id; cannot bind AccessProfile/key.");
|
|
13572
14694
|
}
|
|
13573
|
-
|
|
14695
|
+
const userAction = actionOf(user.created);
|
|
14696
|
+
log.debug(
|
|
14697
|
+
{ component: "loader", externalId: blueprint.servicePrincipal.externalId, userId, userAction },
|
|
14698
|
+
"service principal ensured"
|
|
14699
|
+
);
|
|
14700
|
+
const localKeyForContext = await readExistingKey(blueprint.contextId);
|
|
14701
|
+
const storedPrincipalId = localKeyForContext?.principalId;
|
|
14702
|
+
const localKeyNamesThisPrincipal = storedPrincipalId === `usr_${userId}` || storedPrincipalId === userId;
|
|
14703
|
+
if (userAction === "reconciled" && !localKeyNamesThisPrincipal) {
|
|
14704
|
+
const proceed = confirmExistingPrincipal ? await confirmExistingPrincipal({
|
|
14705
|
+
contextId: blueprint.contextId,
|
|
14706
|
+
userId,
|
|
14707
|
+
externalId: blueprint.servicePrincipal.externalId
|
|
14708
|
+
}) : true;
|
|
14709
|
+
if (!proceed) {
|
|
14710
|
+
throw new ExistingPrincipalNotConfirmedError(blueprint.contextId, userId, blueprint.servicePrincipal.externalId);
|
|
14711
|
+
}
|
|
14712
|
+
log.debug(
|
|
14713
|
+
{ component: "loader", contextId: blueprint.contextId, userId },
|
|
14714
|
+
"existing-principal collision confirmed (or unwired) \u2014 proceeding with any further write to this principal"
|
|
14715
|
+
);
|
|
14716
|
+
}
|
|
14717
|
+
const needsContextPinnedBootstrap = (blueprint.issuers ?? []).length > 0 || (blueprint.namespaces ?? []).length > 0;
|
|
14718
|
+
const ctxBootstrapClient = needsContextPinnedBootstrap ? input.remintBootstrapForContext ? await input.remintBootstrapForContext(blueprint.contextId) : client : void 0;
|
|
14719
|
+
if (ctxBootstrapClient && input.remintBootstrapForContext) {
|
|
14720
|
+
log.debug({ component: "loader", contextId: blueprint.contextId }, "re-minted context-pinned bootstrap client");
|
|
14721
|
+
}
|
|
14722
|
+
const namespaceActions = [];
|
|
14723
|
+
for (const ns of blueprint.namespaces ?? []) {
|
|
14724
|
+
const action = await registerNamespaceIdempotently(ctxBootstrapClient.identity, blueprint.contextId, ns);
|
|
14725
|
+
namespaceActions.push({ namespace: ns.namespace, action });
|
|
14726
|
+
log.debug({ component: "loader", namespace: ns.namespace, contextId: blueprint.contextId, action }, "namespace ensured");
|
|
14727
|
+
}
|
|
14728
|
+
const issuerActions = [];
|
|
14729
|
+
for (const issuer of blueprint.issuers ?? []) {
|
|
14730
|
+
const issuerResp = await ctxBootstrapClient.auth.registerIssuer({
|
|
14731
|
+
issuerId: issuer.issuerId,
|
|
14732
|
+
issuer: issuer.issuer,
|
|
14733
|
+
jwksUri: issuer.jwksUri,
|
|
14734
|
+
audience: issuer.audience,
|
|
14735
|
+
contextId: issuer.contextId,
|
|
14736
|
+
subClaim: issuer.subClaim,
|
|
14737
|
+
emailClaim: issuer.emailClaim,
|
|
14738
|
+
selfSignupPolicies: issuer.selfSignupPolicies?.map((p) => ({
|
|
14739
|
+
signup_type: p.signupType,
|
|
14740
|
+
role_id: p.roleId
|
|
14741
|
+
}))
|
|
14742
|
+
});
|
|
14743
|
+
const action = actionOf(issuerResp.created);
|
|
14744
|
+
issuerActions.push({ issuerId: issuer.issuerId, action });
|
|
14745
|
+
if (action === "reconciled") {
|
|
14746
|
+
log.warn(
|
|
14747
|
+
{ component: "loader", issuerId: issuer.issuerId },
|
|
14748
|
+
"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"
|
|
14749
|
+
);
|
|
14750
|
+
} else {
|
|
14751
|
+
log.debug({ component: "loader", issuerId: issuer.issuerId, action }, "issuer registered");
|
|
14752
|
+
}
|
|
14753
|
+
}
|
|
13574
14754
|
const ctxClient = input.remintForContext ? await input.remintForContext(blueprint.contextId) : client;
|
|
13575
14755
|
log.debug({ component: "loader", contextId: blueprint.contextId }, "re-minted per-context client");
|
|
13576
14756
|
const schemaIds = {};
|
|
@@ -13617,7 +14797,10 @@ async function runLoader(client, input) {
|
|
|
13617
14797
|
// literal JSON null, which is exactly what the platform's scope
|
|
13618
14798
|
// matcher tests via `allowedValues.contains(null)`. The cast bridges
|
|
13619
14799
|
// to the SDK's loosely-typed Record<string, Record<string,unknown>>.
|
|
13620
|
-
data_scope: blueprint.accessProfile.dataScope
|
|
14800
|
+
data_scope: blueprint.accessProfile.dataScope,
|
|
14801
|
+
// Already gate-validated (assertBlueprintGates, above this function's
|
|
14802
|
+
// call site) before this body is ever built.
|
|
14803
|
+
granted_capabilities: blueprint.accessProfile.capabilities
|
|
13621
14804
|
}
|
|
13622
14805
|
],
|
|
13623
14806
|
// Optional identity overrides — scope values the service key STAMPS onto
|
|
@@ -13675,7 +14858,7 @@ async function runLoader(client, input) {
|
|
|
13675
14858
|
keyId = keyResp.keyId ?? "";
|
|
13676
14859
|
rawKey = keyResp.rawKey;
|
|
13677
14860
|
if (keyId) {
|
|
13678
|
-
await persistMintedKey(blueprint.contextId, { keyId, rawKey, keyName, principalId: userId
|
|
14861
|
+
await persistMintedKey(blueprint.contextId, { keyId, rawKey, keyName, principalId: `usr_${userId}`, revokedKeyId });
|
|
13679
14862
|
}
|
|
13680
14863
|
if (keyAction !== "rotated") keyAction = "created";
|
|
13681
14864
|
} else {
|
|
@@ -13749,9 +14932,12 @@ async function runLoader(client, input) {
|
|
|
13749
14932
|
keyName,
|
|
13750
14933
|
keyAction,
|
|
13751
14934
|
contextAction,
|
|
14935
|
+
userAction,
|
|
13752
14936
|
profileAction,
|
|
13753
14937
|
schemaActions,
|
|
13754
14938
|
roleActions,
|
|
14939
|
+
issuerActions,
|
|
14940
|
+
namespaceActions,
|
|
13755
14941
|
rawKey,
|
|
13756
14942
|
keyId,
|
|
13757
14943
|
revokedKeyId,
|
|
@@ -13770,6 +14956,22 @@ function buildPlan(blueprint, opts = {}) {
|
|
|
13770
14956
|
kind: "principal",
|
|
13771
14957
|
summary: `principal '${blueprint.servicePrincipal.externalId}' (SERVICE)`
|
|
13772
14958
|
});
|
|
14959
|
+
for (const ns of blueprint.namespaces ?? []) {
|
|
14960
|
+
const ownerNote = `owned by '${blueprint.contextId}'`;
|
|
14961
|
+
const entityNote = ns.entityBacked ? ", entity-backed" : "";
|
|
14962
|
+
const membershipNote = ns.membershipRecordType ? `, membership: ${ns.membershipRecordType}.${ns.membershipTargetField}` + (ns.membershipLevelField ? ` (levels: ${ns.membershipLevels?.join("/")})` : "") : "";
|
|
14963
|
+
steps.push({
|
|
14964
|
+
kind: "namespace",
|
|
14965
|
+
summary: `namespace '${ns.namespace}' (${ownerNote}${entityNote}${membershipNote}) [${BLUEPRINT_FIELD_PHASES.namespaces}-token phase]`
|
|
14966
|
+
});
|
|
14967
|
+
}
|
|
14968
|
+
for (const iss of blueprint.issuers ?? []) {
|
|
14969
|
+
const selfSignup = iss.selfSignupPolicies?.length ? `, self-signup: ${iss.selfSignupPolicies.length} polic${iss.selfSignupPolicies.length === 1 ? "y" : "ies"}` : "";
|
|
14970
|
+
steps.push({
|
|
14971
|
+
kind: "issuer",
|
|
14972
|
+
summary: `issuer '${iss.issuerId}' (${iss.issuer} \u2192 context '${iss.contextId}'${selfSignup}) [${BLUEPRINT_FIELD_PHASES.issuers}-token phase]`
|
|
14973
|
+
});
|
|
14974
|
+
}
|
|
13773
14975
|
for (const s of blueprint.schemas) {
|
|
13774
14976
|
const lookups = toLookupDefs(s.lookupFields)?.map((l) => l.fieldNames ? `(${l.fieldNames.join("+")})` : l.fieldName) ?? [];
|
|
13775
14977
|
const lookupNote = lookups.length ? ` lookup: [${lookups.join(", ")}]` : "";
|
|
@@ -13785,15 +14987,18 @@ function buildPlan(blueprint, opts = {}) {
|
|
|
13785
14987
|
}
|
|
13786
14988
|
const actions = blueprint.accessProfile.allowedActions;
|
|
13787
14989
|
const scopeNote = blueprint.accessProfile.dataScope && Object.keys(blueprint.accessProfile.dataScope).length ? ` dataScope: ${Object.keys(blueprint.accessProfile.dataScope).join(", ")}` : "";
|
|
14990
|
+
const capabilityNote = blueprint.accessProfile.capabilities?.length ? ` capabilities: [${blueprint.accessProfile.capabilities.join(", ")}]` : "";
|
|
13788
14991
|
steps.push({
|
|
13789
14992
|
kind: "profile",
|
|
13790
|
-
summary: `profile scopes [${actions.join(", ")}] (data-plane \u2713)${scopeNote}`
|
|
14993
|
+
summary: `profile scopes [${actions.join(", ")}] (data-plane \u2713)${scopeNote}${capabilityNote}`
|
|
13791
14994
|
});
|
|
13792
14995
|
for (const [roleId, clauses] of Object.entries(blueprint.roles ?? {})) {
|
|
13793
14996
|
const actionSet = [...new Set(clauses.flatMap((c) => c.allowedActions))];
|
|
14997
|
+
const capSet = [...new Set(clauses.flatMap((c) => c.capabilities ?? []))];
|
|
14998
|
+
const roleCapabilityNote = capSet.length ? `, capabilities [${capSet.join(", ")}]` : "";
|
|
13794
14999
|
steps.push({
|
|
13795
15000
|
kind: "role",
|
|
13796
|
-
summary: `role '${roleId}' (${clauses.length} clause${clauses.length === 1 ? "" : "s"}, actions [${actionSet.join(", ")}])`
|
|
15001
|
+
summary: `role '${roleId}' (${clauses.length} clause${clauses.length === 1 ? "" : "s"}, actions [${actionSet.join(", ")}]${roleCapabilityNote})`
|
|
13797
15002
|
});
|
|
13798
15003
|
}
|
|
13799
15004
|
steps.push({ kind: "key", summary: `key ${blueprint.name}-<machine> (mint 1 scoped ssk_*)` });
|
|
@@ -13810,8 +15015,24 @@ function buildPlan(blueprint, opts = {}) {
|
|
|
13810
15015
|
}
|
|
13811
15016
|
function renderPlan(blueprint, steps) {
|
|
13812
15017
|
const header = ` Plan for blueprint '${blueprint.name}' (v${blueprint.version}) \u2192 context '${blueprint.contextId}':`;
|
|
13813
|
-
const
|
|
13814
|
-
|
|
15018
|
+
const legendLines = [
|
|
15019
|
+
` Apply is a non-destructive reconcile \u2014 each item is created if absent, else`,
|
|
15020
|
+
` updated in place to match. Nothing is dropped or recreated.`
|
|
15021
|
+
];
|
|
15022
|
+
if (steps.some((s) => s.kind === "issuer")) {
|
|
15023
|
+
legendLines.push(
|
|
15024
|
+
` Exception: issuers cannot be updated once registered \u2014 an existing`,
|
|
15025
|
+
` issuer is left unchanged, not reconciled to match.`
|
|
15026
|
+
);
|
|
15027
|
+
}
|
|
15028
|
+
if (steps.some((s) => s.kind === "namespace")) {
|
|
15029
|
+
legendLines.push(
|
|
15030
|
+
` Exception: namespace registration is not idempotent \u2014 a re-apply matching`,
|
|
15031
|
+
` an existing registration converges silently, but one that disagrees FAILS`,
|
|
15032
|
+
` the apply rather than reconciling it.`
|
|
15033
|
+
);
|
|
15034
|
+
}
|
|
15035
|
+
const legend = legendLines.join("\n");
|
|
13815
15036
|
const lines = steps.map((s) => ` + ${s.summary}`);
|
|
13816
15037
|
return [header, legend, "", ...lines].join("\n");
|
|
13817
15038
|
}
|
|
@@ -14059,6 +15280,15 @@ var OsKeychainSecretStore = class {
|
|
|
14059
15280
|
* against the real Windows store on every MR — the dev-machine probe is a point
|
|
14060
15281
|
* measurement, not a regression gate. (Tracked internally; this file ships to
|
|
14061
15282
|
* public npm, so no tracker number here.)
|
|
15283
|
+
*
|
|
15284
|
+
* ⚠️ THIS METHOD'S FAIL-CLOSED GUARANTEE IS AN UPSTREAM BEHAVIOURAL CONTRACT, NOT
|
|
15285
|
+
* SOMETHING THIS CODE ENFORCES. `@napi-rs/keyring` is pinned to an EXACT version
|
|
15286
|
+
* in `package.json` for exactly this reason: if a future release returned `[]`
|
|
15287
|
+
* from `findCredentials()` on a locked store instead of throwing, the `catch`
|
|
15288
|
+
* block above would never run — `!.some(...)` on an empty array reads as "gone",
|
|
15289
|
+
* `deleteSecret` returns `true`, and `remove`/`prune` drop the index row of a
|
|
15290
|
+
* still-live credential. A version bump of this dependency is a security review
|
|
15291
|
+
* of the paragraph above against the new release, not a routine dependency bump.
|
|
14062
15292
|
*/
|
|
14063
15293
|
async deleteSecret(ref) {
|
|
14064
15294
|
try {
|
|
@@ -14992,6 +16222,26 @@ The key itself WAS created server-side and is live \u2014 its id is ${entry.keyI
|
|
|
14992
16222
|
const entry = await this.getActive();
|
|
14993
16223
|
return entry ? this.readSecret(entry) : void 0;
|
|
14994
16224
|
}
|
|
16225
|
+
/**
|
|
16226
|
+
* The ACTIVE entry's tenant (test/live), or undefined if there is no active
|
|
16227
|
+
* entry or its tenant cannot be determined — WITHOUT migrating its secret.
|
|
16228
|
+
*
|
|
16229
|
+
* Deliberately NOT built on {@link resolveActiveSecret}/{@link readSecret}:
|
|
16230
|
+
* both migrate the secret to this machine's preferred store as a side effect
|
|
16231
|
+
* of reading it, and a caller that only wants to CLASSIFY the active identity
|
|
16232
|
+
* (e.g. "is it safe to activate a different one over it?") must not silently
|
|
16233
|
+
* relocate — and take the cross-process index lock for — a credential it has
|
|
16234
|
+
* no other reason to touch. Same non-migrating shape {@link assess} already
|
|
16235
|
+
* uses per entry (`store.getSecret` directly, never `readSecret`).
|
|
16236
|
+
*/
|
|
16237
|
+
async activeTenant() {
|
|
16238
|
+
const entry = await this.getActive();
|
|
16239
|
+
if (!entry) return void 0;
|
|
16240
|
+
const store = this.storeFor(entry);
|
|
16241
|
+
if (!store) return void 0;
|
|
16242
|
+
const secret = await store.getSecret(entry.secretRef);
|
|
16243
|
+
return secret !== void 0 ? keyTenant(secret) : void 0;
|
|
16244
|
+
}
|
|
14995
16245
|
/**
|
|
14996
16246
|
* Read one entry's secret, moving it to this machine's preferred store if it is
|
|
14997
16247
|
* somewhere else.
|
|
@@ -15118,26 +16368,31 @@ var readline = __toESM(require("readline/promises"));
|
|
|
15118
16368
|
async function runKeyringList(opts, deps = {}) {
|
|
15119
16369
|
try {
|
|
15120
16370
|
const keyring = deps.keyring ?? await Keyring.open();
|
|
15121
|
-
const entries = await keyring.list();
|
|
15122
|
-
const active = await keyring.activeAlias();
|
|
15123
16371
|
if (opts.json) {
|
|
16372
|
+
const entries = await keyring.list();
|
|
16373
|
+
const active = await keyring.activeAlias();
|
|
15124
16374
|
process.stdout.write(`${formatJson({ active, entries })}
|
|
15125
16375
|
`);
|
|
15126
16376
|
return 0;
|
|
15127
16377
|
}
|
|
15128
|
-
|
|
16378
|
+
const { health } = await assessEntries(keyring);
|
|
16379
|
+
if (health.length === 0) {
|
|
15129
16380
|
process.stdout.write("\n (no keyring entries yet \u2014 run `vectros join <context>` to add one)\n");
|
|
15130
16381
|
return 0;
|
|
15131
16382
|
}
|
|
15132
16383
|
process.stdout.write(
|
|
15133
|
-
"\n" + formatTable(
|
|
15134
|
-
{ header: "", get: (
|
|
15135
|
-
{ header: "alias", get: (
|
|
15136
|
-
{ header: "context", get: (
|
|
15137
|
-
{ header: "principal", get: (
|
|
15138
|
-
{ header: "role", get: (
|
|
15139
|
-
|
|
15140
|
-
|
|
16384
|
+
"\n" + formatTable(health, [
|
|
16385
|
+
{ header: "", get: (h) => h.active ? "*" : "" },
|
|
16386
|
+
{ header: "alias", get: (h) => h.entry.alias },
|
|
16387
|
+
{ header: "context", get: (h) => h.entry.context },
|
|
16388
|
+
{ header: "principal", get: (h) => h.entry.principalId },
|
|
16389
|
+
{ header: "role", get: (h) => h.entry.role },
|
|
16390
|
+
// `tenant` (test/live) replaces the old `env` (staging/production) column
|
|
16391
|
+
// here: env is an internal-team concern an external developer's CLI never
|
|
16392
|
+
// sees, while tenant is the distinction every user routinely holds two
|
|
16393
|
+
// credentials side by side over.
|
|
16394
|
+
{ header: "tenant", get: (h) => tenantCell(h, { upper: true }) },
|
|
16395
|
+
{ header: "key id", get: (h) => h.entry.keyId }
|
|
15141
16396
|
]) + "\n"
|
|
15142
16397
|
);
|
|
15143
16398
|
return 0;
|
|
@@ -15319,6 +16574,10 @@ async function assessEntries(keyring) {
|
|
|
15319
16574
|
}))
|
|
15320
16575
|
};
|
|
15321
16576
|
}
|
|
16577
|
+
function tenantCell(h, opts) {
|
|
16578
|
+
if (h.status !== "ok") return void 0;
|
|
16579
|
+
return h.tenant ?? (opts.upper ? "UNKNOWN" : "unknown");
|
|
16580
|
+
}
|
|
15322
16581
|
function describeEntry(h) {
|
|
15323
16582
|
return `${h.entry.alias} (${h.entry.context}, ${h.entry.keyId})`;
|
|
15324
16583
|
}
|
|
@@ -15463,9 +16722,7 @@ async function runKeyringDoctor(opts, deps = {}) {
|
|
|
15463
16722
|
// `unknown` can escalate; one that sees a confident wrong answer cannot.
|
|
15464
16723
|
store: storeKindOf(h.entry) ?? "unknown",
|
|
15465
16724
|
secret: h.status,
|
|
15466
|
-
|
|
15467
|
-
// live key" from "we could not read the shape of this one".
|
|
15468
|
-
tenant: h.status === "ok" ? h.tenant ?? "unknown" : void 0
|
|
16725
|
+
tenant: tenantCell(h, { upper: false })
|
|
15469
16726
|
})),
|
|
15470
16727
|
liveKeys: liveKeys.map((h) => h.entry.alias),
|
|
15471
16728
|
findings
|
|
@@ -15480,7 +16737,7 @@ async function runKeyringDoctor(opts, deps = {}) {
|
|
|
15480
16737
|
{ header: "", get: (h) => h.active ? "*" : "" },
|
|
15481
16738
|
{ header: "alias", get: (h) => h.entry.alias },
|
|
15482
16739
|
{ header: "context", get: (h) => h.entry.context },
|
|
15483
|
-
{ header: "tenant", get: (h) => h
|
|
16740
|
+
{ header: "tenant", get: (h) => tenantCell(h, { upper: true }) },
|
|
15484
16741
|
{ header: "key id", get: (h) => h.entry.keyId },
|
|
15485
16742
|
{ header: "secret", get: (h) => h.status === "ok" ? "ok" : h.status.toUpperCase() }
|
|
15486
16743
|
]) + "\n"
|
|
@@ -15666,6 +16923,55 @@ function revokeWarning(targets) {
|
|
|
15666
16923
|
}
|
|
15667
16924
|
|
|
15668
16925
|
// src/minted-key-store.ts
|
|
16926
|
+
function keyringAliasFor(alias, tenant) {
|
|
16927
|
+
if (tenant === "live" && alias.endsWith(".test")) {
|
|
16928
|
+
throw new Error(
|
|
16929
|
+
`'${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.`
|
|
16930
|
+
);
|
|
16931
|
+
}
|
|
16932
|
+
return tenant === "test" ? `${alias}.test` : alias;
|
|
16933
|
+
}
|
|
16934
|
+
function resolveActivateDecision(opts) {
|
|
16935
|
+
if (opts.explicitActivate !== void 0) return opts.explicitActivate;
|
|
16936
|
+
if (opts.tenant === "live") return true;
|
|
16937
|
+
return opts.currentActiveTenant !== "live";
|
|
16938
|
+
}
|
|
16939
|
+
async function determineActivateDecision(keyring, opts) {
|
|
16940
|
+
if (opts.explicitActivate !== void 0 || opts.tenant === "live") {
|
|
16941
|
+
return resolveActivateDecision(opts);
|
|
16942
|
+
}
|
|
16943
|
+
let currentActiveTenant;
|
|
16944
|
+
try {
|
|
16945
|
+
if (opts.hint && opts.hint.alias === await keyring.activeAlias()) {
|
|
16946
|
+
currentActiveTenant = keyTenant(opts.hint.secret);
|
|
16947
|
+
} else {
|
|
16948
|
+
currentActiveTenant = await keyring.activeTenant();
|
|
16949
|
+
}
|
|
16950
|
+
} catch {
|
|
16951
|
+
}
|
|
16952
|
+
return resolveActivateDecision({ ...opts, currentActiveTenant });
|
|
16953
|
+
}
|
|
16954
|
+
function tenantActivateMessage(opts) {
|
|
16955
|
+
if (opts.willActivate) {
|
|
16956
|
+
if (opts.tenant !== "test") return "";
|
|
16957
|
+
return `
|
|
16958
|
+
\u26A0 '${opts.alias}' is now your ACTIVE identity \u2014 a TEST tenant credential. Anything
|
|
16959
|
+
resolving a key with no explicit --alias (including the MCP server) follows it.
|
|
16960
|
+
`;
|
|
16961
|
+
}
|
|
16962
|
+
if (opts.explicitlyDeclined) {
|
|
16963
|
+
return `
|
|
16964
|
+
\u26A0 Stored '${opts.alias}' (${opts.tenant} tenant) without activating it \u2014 you passed --no-activate.
|
|
16965
|
+
Run \`vectros switch ${opts.alias}\` when you're ready to use it.
|
|
16966
|
+
`;
|
|
16967
|
+
}
|
|
16968
|
+
return `
|
|
16969
|
+
\u26A0 Stored '${opts.alias}' (test tenant) WITHOUT activating it \u2014 your currently-active
|
|
16970
|
+
identity is a LIVE credential, and switching it out from under you silently is
|
|
16971
|
+
exactly the mistake this check exists to prevent. Run \`vectros switch ${opts.alias}\`
|
|
16972
|
+
to use the test key, or re-run with --activate to do so automatically.
|
|
16973
|
+
`;
|
|
16974
|
+
}
|
|
15669
16975
|
function nowIso() {
|
|
15670
16976
|
return (/* @__PURE__ */ new Date()).toISOString();
|
|
15671
16977
|
}
|
|
@@ -15762,10 +17068,7 @@ async function storeMintedKey(keyring, binding, secret, opts = {}) {
|
|
|
15762
17068
|
// src/cli-bootstrap.ts
|
|
15763
17069
|
function validateBlueprint(input) {
|
|
15764
17070
|
const blueprint = parseBlueprint(input);
|
|
15765
|
-
|
|
15766
|
-
for (const clauses of Object.values(blueprint.roles ?? {})) {
|
|
15767
|
-
for (const clause of clauses) assertScopeGate(clause.allowedActions);
|
|
15768
|
-
}
|
|
17071
|
+
assertBlueprintGates(blueprint);
|
|
15769
17072
|
return blueprint;
|
|
15770
17073
|
}
|
|
15771
17074
|
var BLANK_SLATE = {
|
|
@@ -15782,10 +17085,12 @@ var BLANK_SLATE = {
|
|
|
15782
17085
|
};
|
|
15783
17086
|
function parseBootstrapArgs(argv) {
|
|
15784
17087
|
const args = {
|
|
17088
|
+
set: [],
|
|
15785
17089
|
env: "production",
|
|
15786
17090
|
yes: false,
|
|
15787
17091
|
noMerge: false,
|
|
15788
17092
|
rotate: false,
|
|
17093
|
+
confirmExistingPrincipal: false,
|
|
15789
17094
|
useScopedToken: false,
|
|
15790
17095
|
blankSlate: false,
|
|
15791
17096
|
noSeed: false,
|
|
@@ -15799,6 +17104,12 @@ function parseBootstrapArgs(argv) {
|
|
|
15799
17104
|
case "--blueprint":
|
|
15800
17105
|
args.blueprint = argv[++i];
|
|
15801
17106
|
break;
|
|
17107
|
+
case "--set":
|
|
17108
|
+
args.set.push(argv[++i]);
|
|
17109
|
+
break;
|
|
17110
|
+
case "--values":
|
|
17111
|
+
args.values = argv[++i];
|
|
17112
|
+
break;
|
|
15802
17113
|
case "--env": {
|
|
15803
17114
|
const v = argv[++i];
|
|
15804
17115
|
if (v !== "staging" && v !== "production") {
|
|
@@ -15831,6 +17142,9 @@ function parseBootstrapArgs(argv) {
|
|
|
15831
17142
|
case "--rotate":
|
|
15832
17143
|
args.rotate = true;
|
|
15833
17144
|
break;
|
|
17145
|
+
case "--confirm-existing-principal":
|
|
17146
|
+
args.confirmExistingPrincipal = true;
|
|
17147
|
+
break;
|
|
15834
17148
|
case "--use-scoped-token":
|
|
15835
17149
|
args.useScopedToken = true;
|
|
15836
17150
|
break;
|
|
@@ -15852,6 +17166,12 @@ function parseBootstrapArgs(argv) {
|
|
|
15852
17166
|
case "--test":
|
|
15853
17167
|
args.tenant = "test";
|
|
15854
17168
|
break;
|
|
17169
|
+
case "--activate":
|
|
17170
|
+
args.activate = true;
|
|
17171
|
+
break;
|
|
17172
|
+
case "--no-activate":
|
|
17173
|
+
args.activate = false;
|
|
17174
|
+
break;
|
|
15855
17175
|
case "--help":
|
|
15856
17176
|
case "-h":
|
|
15857
17177
|
args.help = true;
|
|
@@ -15869,6 +17189,26 @@ function seedDeclined(answer) {
|
|
|
15869
17189
|
const a = answer.trim().toLowerCase();
|
|
15870
17190
|
return a === "n" || a === "no";
|
|
15871
17191
|
}
|
|
17192
|
+
function existingPrincipalConfirmed(answer) {
|
|
17193
|
+
const a = answer.trim().toLowerCase();
|
|
17194
|
+
return a === "y" || a === "yes";
|
|
17195
|
+
}
|
|
17196
|
+
function buildConfirmExistingPrincipal(args, prompt, isTty) {
|
|
17197
|
+
return async (details) => {
|
|
17198
|
+
if (!args.yes && isTty) {
|
|
17199
|
+
const ans = await prompt(
|
|
17200
|
+
`
|
|
17201
|
+
\u26A0 '${details.externalId}' already exists in your tenant (context '${details.contextId}') \u2014 this key will bind to that EXISTING identity, not a new one.
|
|
17202
|
+
Expected if you're re-applying a blueprint from a new machine, or setting up a second
|
|
17203
|
+
credential for an integration you already own. If you don't recognize this identity,
|
|
17204
|
+
do NOT proceed \u2014 this may be a name collision with something you don't control.
|
|
17205
|
+
Proceed and bind to the existing identity? [y/N]: `
|
|
17206
|
+
);
|
|
17207
|
+
return existingPrincipalConfirmed(ans);
|
|
17208
|
+
}
|
|
17209
|
+
return args.confirmExistingPrincipal;
|
|
17210
|
+
};
|
|
17211
|
+
}
|
|
15872
17212
|
function seedSummaryLine(opts) {
|
|
15873
17213
|
if (opts.noSeed) {
|
|
15874
17214
|
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";
|
|
@@ -15896,22 +17236,45 @@ function reconcileTally(items, noun) {
|
|
|
15896
17236
|
if (reconciled) seg.push(`${reconciled} reconciled`);
|
|
15897
17237
|
return `${items.length} ${noun}${items.length === 1 ? "" : "s"} (${seg.join(", ")})`;
|
|
15898
17238
|
}
|
|
17239
|
+
function issuerTally(items) {
|
|
17240
|
+
if (items.length === 0) return null;
|
|
17241
|
+
const created = items.filter((i) => i.action === "created").length;
|
|
17242
|
+
const unchanged = items.length - created;
|
|
17243
|
+
const seg = [];
|
|
17244
|
+
if (created) seg.push(`${created} created`);
|
|
17245
|
+
if (unchanged) seg.push(`${unchanged} already registered \u2014 unchanged (issuers cannot be updated once registered)`);
|
|
17246
|
+
return `${items.length} issuer${items.length === 1 ? "" : "s"} (${seg.join(", ")})`;
|
|
17247
|
+
}
|
|
17248
|
+
function namespaceTally(items) {
|
|
17249
|
+
if (items.length === 0) return null;
|
|
17250
|
+
const created = items.filter((i) => i.action === "created").length;
|
|
17251
|
+
const matched = items.length - created;
|
|
17252
|
+
const seg = [];
|
|
17253
|
+
if (created) seg.push(`${created} created`);
|
|
17254
|
+
if (matched) seg.push(`${matched} already registered \u2014 confirmed matching`);
|
|
17255
|
+
return `${items.length} namespace${items.length === 1 ? "" : "s"} (${seg.join(", ")})`;
|
|
17256
|
+
}
|
|
15899
17257
|
function applyOutcomeSummary(result) {
|
|
15900
17258
|
const parts = [
|
|
15901
17259
|
reconcileTally(result.schemaActions, "schema"),
|
|
15902
17260
|
reconcileTally(result.roleActions, "role"),
|
|
17261
|
+
issuerTally(result.issuerActions),
|
|
17262
|
+
namespaceTally(result.namespaceActions),
|
|
15903
17263
|
`profile ${result.profileAction}`
|
|
15904
17264
|
].filter((p) => p !== null);
|
|
15905
17265
|
const detail = parts.length ? ` \u2014 ${parts.join("; ")}` : "";
|
|
17266
|
+
const principalNote = result.userAction === "reconciled" ? ` \u26A0 service principal already existed in this tenant before this run (not newly created)
|
|
17267
|
+
` : "";
|
|
15906
17268
|
return ` \u2714 context '${result.contextId}' ${result.contextAction}${detail}
|
|
15907
|
-
`;
|
|
17269
|
+
${principalNote}`;
|
|
15908
17270
|
}
|
|
15909
17271
|
var BOOTSTRAP_HELP = `vectros bootstrap \u2014 provision a least-privilege MCP credential.
|
|
15910
17272
|
|
|
15911
17273
|
Usage:
|
|
15912
17274
|
vectros bootstrap [--blueprint <name> | --blank] [--no-seed]
|
|
17275
|
+
[--set name=value ...] [--values <file>]
|
|
15913
17276
|
[--tenant test|live | --test] [--client desktop|code]
|
|
15914
|
-
[--yes] [--no-merge] [--rotate]
|
|
17277
|
+
[--yes] [--no-merge] [--rotate] [--confirm-existing-principal]
|
|
15915
17278
|
[--token <t> | env VECTROS_BOOTSTRAP_TOKEN]
|
|
15916
17279
|
[--base-url <url>] [--use-scoped-token]
|
|
15917
17280
|
|
|
@@ -15919,6 +17282,10 @@ Options:
|
|
|
15919
17282
|
--blueprint <name|file> A bundled blueprint (${BLUEPRINT_NAMES.join(", ")}),
|
|
15920
17283
|
OR a path to your own blueprint YAML/JSON (./my.yaml) \u2014 the "copy &
|
|
15921
17284
|
own" path. File blueprints get the identical scope gate (no override).
|
|
17285
|
+
--set name=value Set an install-time input value (repeatable) for a file blueprint's
|
|
17286
|
+
'inputs:' block. Beats --values + the declared default. (Same flag as
|
|
17287
|
+
'vectros blueprint validate/plan'.)
|
|
17288
|
+
--values <file> A YAML/JSON map of input values. (File blueprints only.)
|
|
15922
17289
|
--blank Credential only (read-only data-plane profile, no schemas).
|
|
15923
17290
|
--no-seed Provision the blueprint's schemas, profile and key but SKIP its
|
|
15924
17291
|
sample seed records \u2014 the right choice for a production context
|
|
@@ -15930,6 +17297,10 @@ Options:
|
|
|
15930
17297
|
The tenant rides the key (ssk_test_* vs ssk_live_*), so
|
|
15931
17298
|
the MCP/client config is otherwise identical.
|
|
15932
17299
|
--test Shorthand for --tenant test.
|
|
17300
|
+
--activate Force the freshly-stored key to become your active identity, even
|
|
17301
|
+
if that would switch away from a currently-active LIVE credential
|
|
17302
|
+
(the one case --tenant test does NOT do this automatically).
|
|
17303
|
+
--no-activate Store the key without activating it, regardless of tenant.
|
|
15933
17304
|
--base-url <url> Override the API base URL (beats VECTROS_API_BASE_URL).
|
|
15934
17305
|
--client <desktop|code> MCP host to configure (default: desktop). 'desktop' merges
|
|
15935
17306
|
claude_desktop_config.json (also works for Cursor/Cline);
|
|
@@ -15944,6 +17315,13 @@ Options:
|
|
|
15944
17315
|
print the config snippet instead of merging it into your client's
|
|
15945
17316
|
config file. Not a dry run; see 'blueprint plan' for that.
|
|
15946
17317
|
--rotate Revoke + re-mint THIS machine's key.
|
|
17318
|
+
--confirm-existing-principal
|
|
17319
|
+
Required (with --yes, or with no TTY) to mint a NEW key bound to a
|
|
17320
|
+
service principal that already existed in your tenant before this run
|
|
17321
|
+
\u2014 e.g. a deliberate second machine for an integration you already own.
|
|
17322
|
+
Refused by default so an unexpected collision halts a CI run loudly
|
|
17323
|
+
instead of silently minting a credential bound to the wrong identity.
|
|
17324
|
+
Never needed for --rotate or an ordinary same-machine re-apply.
|
|
15947
17325
|
--use-scoped-token Legacy fallback.
|
|
15948
17326
|
NOTE: it mints a token WITHOUT the provisioning
|
|
15949
17327
|
capability, so it cannot create a blueprint's
|
|
@@ -15953,10 +17331,21 @@ Options:
|
|
|
15953
17331
|
|
|
15954
17332
|
No --blueprint/--blank in a TTY \u2192 interactive menu.`;
|
|
15955
17333
|
async function resolveBlueprint(args, prompt, isTty) {
|
|
15956
|
-
if (args.blankSlate)
|
|
17334
|
+
if (args.blankSlate) {
|
|
17335
|
+
if (args.set.length > 0 || args.values) {
|
|
17336
|
+
throw new Error(`--set/--values apply only to file blueprints; --blank has no inputs to set.`);
|
|
17337
|
+
}
|
|
17338
|
+
return BLANK_SLATE;
|
|
17339
|
+
}
|
|
15957
17340
|
if (args.blueprint) {
|
|
15958
17341
|
if (looksLikePath(args.blueprint)) {
|
|
15959
|
-
|
|
17342
|
+
const supplied = await buildInputSupply(args.set, args.values);
|
|
17343
|
+
return await loadBlueprintFile(args.blueprint, supplied);
|
|
17344
|
+
}
|
|
17345
|
+
if (args.set.length > 0 || args.values) {
|
|
17346
|
+
throw new Error(
|
|
17347
|
+
`--set/--values apply only to file blueprints; '${args.blueprint}' is a bundled blueprint (no inputs to set).`
|
|
17348
|
+
);
|
|
15960
17349
|
}
|
|
15961
17350
|
const b = getBlueprint(args.blueprint);
|
|
15962
17351
|
if (!b) {
|
|
@@ -15969,6 +17358,11 @@ async function resolveBlueprint(args, prompt, isTty) {
|
|
|
15969
17358
|
if (!isTty) {
|
|
15970
17359
|
throw new Error("No --blueprint or --blank specified and not a TTY (cannot show the menu).");
|
|
15971
17360
|
}
|
|
17361
|
+
if (args.set.length > 0 || args.values) {
|
|
17362
|
+
throw new Error(
|
|
17363
|
+
`--set/--values need an explicit --blueprint <file>; the interactive menu only offers bundled blueprints and blank slate, neither of which has inputs to set.`
|
|
17364
|
+
);
|
|
17365
|
+
}
|
|
15972
17366
|
const options = [
|
|
15973
17367
|
{ label: "Blank slate (credential only)", blueprint: BLANK_SLATE },
|
|
15974
17368
|
...BUNDLED_BLUEPRINTS.map((b) => ({ label: `${b.name} \u2014 ${b.description}`, blueprint: b }))
|
|
@@ -16015,6 +17409,22 @@ function buildRemintForContext(opts) {
|
|
|
16015
17409
|
});
|
|
16016
17410
|
};
|
|
16017
17411
|
}
|
|
17412
|
+
function buildRemintBootstrapForContext(opts) {
|
|
17413
|
+
const mintBridge = opts.mintBridge ?? mintBridgeToken;
|
|
17414
|
+
return async (contextId) => {
|
|
17415
|
+
const ctxToken = await mintBridge({
|
|
17416
|
+
cognitoToken: opts.cognitoToken,
|
|
17417
|
+
apiBaseUrl: opts.apiBaseUrl,
|
|
17418
|
+
contextId,
|
|
17419
|
+
tenant: opts.tenant,
|
|
17420
|
+
useScopedToken: opts.useScopedToken
|
|
17421
|
+
});
|
|
17422
|
+
return new VectrosClient({
|
|
17423
|
+
token: ctxToken.token,
|
|
17424
|
+
environment: opts.apiBaseUrl
|
|
17425
|
+
});
|
|
17426
|
+
};
|
|
17427
|
+
}
|
|
16018
17428
|
async function runBootstrapCli(argv, deps = {}) {
|
|
16019
17429
|
const log = deps.log ?? createLogger2();
|
|
16020
17430
|
let args;
|
|
@@ -16048,6 +17458,15 @@ ${BOOTSTRAP_HELP}
|
|
|
16048
17458
|
\u2716 Scope gate refused blueprint '${blueprint.name}':
|
|
16049
17459
|
`);
|
|
16050
17460
|
for (const r of err.rejected) process.stderr.write(` \u2022 ${r.action}: ${r.reason}
|
|
17461
|
+
`);
|
|
17462
|
+
process.stderr.write(" Minting nothing.\n");
|
|
17463
|
+
return 2;
|
|
17464
|
+
}
|
|
17465
|
+
if (err instanceof CapabilityGateError) {
|
|
17466
|
+
process.stderr.write(`
|
|
17467
|
+
\u2716 Capability gate refused blueprint '${blueprint.name}':
|
|
17468
|
+
`);
|
|
17469
|
+
for (const r of err.rejected) process.stderr.write(` \u2022 ${r.action}: ${r.reason}
|
|
16051
17470
|
`);
|
|
16052
17471
|
process.stderr.write(" Minting nothing.\n");
|
|
16053
17472
|
return 2;
|
|
@@ -16059,6 +17478,11 @@ ${BOOTSTRAP_HELP}
|
|
|
16059
17478
|
Blueprint: ${blueprint.name}
|
|
16060
17479
|
\u2714 Scope gate: [${gate.accepted.join(", ")}] \u2014 all data-plane \u2713
|
|
16061
17480
|
`);
|
|
17481
|
+
if (blueprint.accessProfile.capabilities?.length) {
|
|
17482
|
+
const capGate = evaluateCapabilityGate(blueprint.accessProfile.capabilities);
|
|
17483
|
+
process.stdout.write(` \u2714 Capability gate: [${capGate.accepted.join(", ")}] \u2014 granted \u2713
|
|
17484
|
+
`);
|
|
17485
|
+
}
|
|
16062
17486
|
let noSeed = args.noSeed;
|
|
16063
17487
|
let seedDeclinedInteractively = false;
|
|
16064
17488
|
if (shouldOfferSeedOptOut(args, blueprint, isTty)) {
|
|
@@ -16132,13 +17556,16 @@ ${renderPlan(blueprint, planSteps)}
|
|
|
16132
17556
|
keyringOpenError = err;
|
|
16133
17557
|
}
|
|
16134
17558
|
let storedFreshInKeyring = false;
|
|
17559
|
+
const lastResolvedSecret = /* @__PURE__ */ new Map();
|
|
16135
17560
|
const readExistingKey = async (contextId) => {
|
|
16136
17561
|
if (!keyring) return void 0;
|
|
17562
|
+
const alias = keyringAliasFor(contextId, args.tenant);
|
|
16137
17563
|
try {
|
|
16138
|
-
const existing = await keyring.get(
|
|
17564
|
+
const existing = await keyring.get(alias);
|
|
16139
17565
|
if (!existing?.keyId) return void 0;
|
|
16140
|
-
const rawKey = await keyring.resolveSecret(
|
|
16141
|
-
|
|
17566
|
+
const rawKey = await keyring.resolveSecret(alias);
|
|
17567
|
+
lastResolvedSecret.set(alias, rawKey);
|
|
17568
|
+
return rawKey ? { keyId: existing.keyId, rawKey, principalId: existing.principalId } : void 0;
|
|
16142
17569
|
} catch {
|
|
16143
17570
|
return void 0;
|
|
16144
17571
|
}
|
|
@@ -16154,17 +17581,36 @@ ${renderPlan(blueprint, planSteps)}
|
|
|
16154
17581
|
);
|
|
16155
17582
|
return;
|
|
16156
17583
|
}
|
|
17584
|
+
const alias = keyringAliasFor(contextId, args.tenant);
|
|
16157
17585
|
try {
|
|
16158
|
-
warnReplacingAlias(await keyring.get(
|
|
17586
|
+
warnReplacingAlias(await keyring.get(alias), alias, record.revokedKeyId);
|
|
17587
|
+
const willActivate = await determineActivateDecision(keyring, {
|
|
17588
|
+
tenant: args.tenant,
|
|
17589
|
+
explicitActivate: args.activate,
|
|
17590
|
+
hint: lastResolvedSecret.has(alias) ? { alias, secret: lastResolvedSecret.get(alias) } : void 0
|
|
17591
|
+
});
|
|
16159
17592
|
await storeMintedKey(
|
|
16160
17593
|
keyring,
|
|
16161
|
-
{
|
|
17594
|
+
{
|
|
17595
|
+
alias,
|
|
17596
|
+
context: contextId,
|
|
17597
|
+
principalId: record.principalId,
|
|
17598
|
+
keyId: record.keyId,
|
|
17599
|
+
// Derived from the REQUEST's actual baseUrl, not echoed from args.env — the two
|
|
17600
|
+
// diverge whenever VECTROS_API_BASE_URL/--base-url alone selected the environment.
|
|
17601
|
+
// `keyring list`/`keyring doctor` display this field; a stale echo would show
|
|
17602
|
+
// "env: production" next to a key this run actually minted against staging.
|
|
17603
|
+
env: deriveEnvironment(baseUrl) ?? args.env
|
|
17604
|
+
},
|
|
16162
17605
|
record.rawKey,
|
|
16163
|
-
{ activate:
|
|
17606
|
+
{ activate: willActivate, revokedKeyId: record.revokedKeyId }
|
|
16164
17607
|
);
|
|
16165
17608
|
if (record.revokedKeyId) {
|
|
16166
|
-
await warnOtherAliasesNaming(keyring, record.revokedKeyId,
|
|
17609
|
+
await warnOtherAliasesNaming(keyring, record.revokedKeyId, alias);
|
|
16167
17610
|
}
|
|
17611
|
+
process.stdout.write(
|
|
17612
|
+
tenantActivateMessage({ alias, tenant: args.tenant, willActivate, explicitlyDeclined: args.activate === false })
|
|
17613
|
+
);
|
|
16168
17614
|
storedFreshInKeyring = true;
|
|
16169
17615
|
} catch (err) {
|
|
16170
17616
|
process.stderr.write(
|
|
@@ -16185,12 +17631,20 @@ ${renderPlan(blueprint, planSteps)}
|
|
|
16185
17631
|
skipSeed: noSeed,
|
|
16186
17632
|
readExistingKey,
|
|
16187
17633
|
persistMintedKey,
|
|
17634
|
+
remintBootstrapForContext: buildRemintBootstrapForContext({
|
|
17635
|
+
cognitoToken,
|
|
17636
|
+
apiBaseUrl: baseUrl,
|
|
17637
|
+
tenant: args.tenant,
|
|
17638
|
+
useScopedToken: args.useScopedToken,
|
|
17639
|
+
mintBridge
|
|
17640
|
+
}),
|
|
16188
17641
|
remintForContext: buildRemintForContext({
|
|
16189
17642
|
cognitoToken,
|
|
16190
17643
|
apiBaseUrl: baseUrl,
|
|
16191
17644
|
tenant: args.tenant,
|
|
16192
17645
|
mintContext
|
|
16193
17646
|
}),
|
|
17647
|
+
confirmExistingPrincipal: buildConfirmExistingPrincipal(args, promptCtx.prompt, isTty),
|
|
16194
17648
|
log
|
|
16195
17649
|
});
|
|
16196
17650
|
process.stdout.write(
|
|
@@ -16208,7 +17662,7 @@ ${renderPlan(blueprint, planSteps)}
|
|
|
16208
17662
|
);
|
|
16209
17663
|
entry = buildVectrosServerEntry({ apiKey: result.rawKey, apiBaseUrl: baseUrl });
|
|
16210
17664
|
} else if (storedFreshInKeyring || result.keyAction === "reused") {
|
|
16211
|
-
entry = buildVectrosServerEntry({ alias: blueprint.contextId, apiBaseUrl: baseUrl });
|
|
17665
|
+
entry = buildVectrosServerEntry({ alias: keyringAliasFor(blueprint.contextId, args.tenant), apiBaseUrl: baseUrl });
|
|
16212
17666
|
} else {
|
|
16213
17667
|
entry = buildVectrosServerEntry({ apiKey: result.rawKey, apiBaseUrl: baseUrl });
|
|
16214
17668
|
}
|
|
@@ -16268,6 +17722,18 @@ ${codeAddHint}`);
|
|
|
16268
17722
|
`);
|
|
16269
17723
|
return 3;
|
|
16270
17724
|
}
|
|
17725
|
+
if (err instanceof ExistingPrincipalNotConfirmedError) {
|
|
17726
|
+
process.stderr.write(
|
|
17727
|
+
`
|
|
17728
|
+
\u2716 ${err.message}
|
|
17729
|
+
|
|
17730
|
+
If this is expected (a re-apply, or a deliberate second machine for an integration you already own),
|
|
17731
|
+
re-run with --confirm-existing-principal, or confirm interactively.
|
|
17732
|
+
If you don't recognize this identity, investigate the blueprint's servicePrincipal.externalId before re-running.
|
|
17733
|
+
`
|
|
17734
|
+
);
|
|
17735
|
+
return 3;
|
|
17736
|
+
}
|
|
16271
17737
|
if (err instanceof BlueprintIdentityError || err instanceof UnresolvedIdentitiesError) {
|
|
16272
17738
|
process.stderr.write(`
|
|
16273
17739
|
\u2716 ${err.message}
|
|
@@ -16291,7 +17757,7 @@ ${formatCliError(err, { action: "Bootstrap failed" })}`);
|
|
|
16291
17757
|
var import_promises3 = require("fs/promises");
|
|
16292
17758
|
|
|
16293
17759
|
// src/scope-grammar.ts
|
|
16294
|
-
var
|
|
17760
|
+
var FORBIDDEN_SCOPE_NAMESPACES2 = /* @__PURE__ */ new Set([
|
|
16295
17761
|
"user",
|
|
16296
17762
|
"record",
|
|
16297
17763
|
"document",
|
|
@@ -16303,23 +17769,23 @@ var FORBIDDEN_SCOPE_NAMESPACES = /* @__PURE__ */ new Set([
|
|
|
16303
17769
|
"versions",
|
|
16304
17770
|
"lookup"
|
|
16305
17771
|
]);
|
|
16306
|
-
var
|
|
17772
|
+
var SCOPE_NAMESPACE_RE2 = /^[a-z][a-z0-9_-]{1,31}$/;
|
|
16307
17773
|
var SCOPE_NAMESPACE_MIN_LENGTH = 2;
|
|
16308
17774
|
var SCOPE_NAMESPACE_MAX_LENGTH = 32;
|
|
16309
|
-
var
|
|
17775
|
+
var SCOPE_VALUE_RE2 = /^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$/;
|
|
16310
17776
|
var SCOPE_VALUE_MIN_LENGTH = 1;
|
|
16311
17777
|
var SCOPE_VALUE_MAX_LENGTH = 128;
|
|
16312
17778
|
function valueGrammarError(value) {
|
|
16313
|
-
if (!
|
|
17779
|
+
if (!SCOPE_VALUE_RE2.test(value)) {
|
|
16314
17780
|
return `'${value}' is not a valid scope value \u2014 ${SCOPE_VALUE_MIN_LENGTH}-${SCOPE_VALUE_MAX_LENGTH} characters, starting with a letter or digit and continuing with letters, digits, '_' or '-'. A value may not contain a colon.`;
|
|
16315
17781
|
}
|
|
16316
17782
|
return null;
|
|
16317
17783
|
}
|
|
16318
17784
|
function namespaceGrammarError(ns) {
|
|
16319
|
-
if (
|
|
17785
|
+
if (FORBIDDEN_SCOPE_NAMESPACES2.has(ns)) {
|
|
16320
17786
|
return `'${ns}' is a reserved namespace and cannot be used as an ownership scope`;
|
|
16321
17787
|
}
|
|
16322
|
-
if (!
|
|
17788
|
+
if (!SCOPE_NAMESPACE_RE2.test(ns)) {
|
|
16323
17789
|
return `'${ns}' is not a valid namespace \u2014 lowercase, ${SCOPE_NAMESPACE_MIN_LENGTH}-${SCOPE_NAMESPACE_MAX_LENGTH} characters, starting with a letter and continuing with letters, digits, '_' or '-'`;
|
|
16324
17790
|
}
|
|
16325
17791
|
return null;
|
|
@@ -16488,6 +17954,10 @@ function lintBlueprint(bp) {
|
|
|
16488
17954
|
}
|
|
16489
17955
|
}
|
|
16490
17956
|
}
|
|
17957
|
+
{
|
|
17958
|
+
const reached = ownerScopedReachWithoutDataScope(bp.accessProfile.allowedActions, ds);
|
|
17959
|
+
if (reached.length > 0) findings.push(unconstrainedReachFinding("accessProfile", reached));
|
|
17960
|
+
}
|
|
16491
17961
|
for (const roleId of Object.keys(bp.roles ?? {})) {
|
|
16492
17962
|
if (!ROLE_ID_RE.test(roleId)) {
|
|
16493
17963
|
findings.push({
|
|
@@ -16500,6 +17970,10 @@ function lintBlueprint(bp) {
|
|
|
16500
17970
|
for (const [roleId, clauses] of Object.entries(bp.roles ?? {})) {
|
|
16501
17971
|
clauses.forEach((clause, i) => {
|
|
16502
17972
|
const cds = clause.dataScope;
|
|
17973
|
+
const reached = ownerScopedReachWithoutDataScope(clause.allowedActions, cds);
|
|
17974
|
+
if (reached.length > 0) {
|
|
17975
|
+
findings.push(unconstrainedReachFinding(`role '${roleId}' clause ${i}`, reached));
|
|
17976
|
+
}
|
|
16503
17977
|
if (!cds) return;
|
|
16504
17978
|
for (const [key, values] of Object.entries(cds)) {
|
|
16505
17979
|
for (const value of values) {
|
|
@@ -16516,6 +17990,17 @@ function lintBlueprint(bp) {
|
|
|
16516
17990
|
}
|
|
16517
17991
|
});
|
|
16518
17992
|
}
|
|
17993
|
+
for (const issuer of bp.issuers ?? []) {
|
|
17994
|
+
for (const policy of issuer.selfSignupPolicies ?? []) {
|
|
17995
|
+
if (!Object.prototype.hasOwnProperty.call(bp.roles ?? {}, policy.roleId)) {
|
|
17996
|
+
findings.push({
|
|
17997
|
+
severity: "warning",
|
|
17998
|
+
rule: "self-signup-role-unknown",
|
|
17999
|
+
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.`
|
|
18000
|
+
});
|
|
18001
|
+
}
|
|
18002
|
+
}
|
|
18003
|
+
}
|
|
16519
18004
|
return findings;
|
|
16520
18005
|
}
|
|
16521
18006
|
var ROLE_ID_RE = /^[a-z][a-z0-9-]{2,30}$/;
|
|
@@ -16541,6 +18026,25 @@ function placeholderFinding(where, key, value) {
|
|
|
16541
18026
|
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.`
|
|
16542
18027
|
};
|
|
16543
18028
|
}
|
|
18029
|
+
var OWNER_SCOPED_RESOURCES = /* @__PURE__ */ new Set(["records", "documents", "entities"]);
|
|
18030
|
+
var UNCONSTRAINED_REACH_OPS = /* @__PURE__ */ new Set(["r", "u", "d"]);
|
|
18031
|
+
function ownerScopedReachWithoutDataScope(allowedActions, dataScope) {
|
|
18032
|
+
const reached = /* @__PURE__ */ new Set();
|
|
18033
|
+
for (const action of allowedActions) {
|
|
18034
|
+
const [resource, ops] = action.split(":", 2);
|
|
18035
|
+
if (!resource || !ops || !OWNER_SCOPED_RESOURCES.has(resource)) continue;
|
|
18036
|
+
if (ops === "*" || [...ops].some((op) => UNCONSTRAINED_REACH_OPS.has(op))) reached.add(resource);
|
|
18037
|
+
}
|
|
18038
|
+
if (dataScope !== void 0) return [];
|
|
18039
|
+
return [...reached].sort();
|
|
18040
|
+
}
|
|
18041
|
+
function unconstrainedReachFinding(where, resources) {
|
|
18042
|
+
return {
|
|
18043
|
+
severity: "warning",
|
|
18044
|
+
rule: "datascope-absent-unconstrained-reach",
|
|
18045
|
+
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.`
|
|
18046
|
+
};
|
|
18047
|
+
}
|
|
16544
18048
|
function lintPasses(findings) {
|
|
16545
18049
|
return !findings.some((f) => f.severity === "error");
|
|
16546
18050
|
}
|
|
@@ -16579,8 +18083,12 @@ schemas:
|
|
|
16579
18083
|
lookupFields: [title] # extra O(1) exact-match lookup indexes (max 10)
|
|
16580
18084
|
|
|
16581
18085
|
accessProfile:
|
|
16582
|
-
# SCOPE GATE: data-plane only (records/schemas/search/documents/folders/inference)
|
|
16583
|
-
#
|
|
18086
|
+
# SCOPE GATE: data-plane only (records/schemas/search/documents/folders/inference/entities),
|
|
18087
|
+
# PLUS profiles:r/u/d (bounded by the platform to redistributing access you already have,
|
|
18088
|
+
# never granting more) and profiles:c:self (create a profile for yourself only \u2014 the
|
|
18089
|
+
# platform resolves "self" from the caller, not from anything you declare here). A bare
|
|
18090
|
+
# profiles:c, or profiles:c:usr_<id> naming another principal, is HARD-REJECTED \u2014 by
|
|
18091
|
+
# design, no override. Same for keys/users/admin/namespaces/*.
|
|
16584
18092
|
# Ops: c|r|u|d, combinable (records:cru). Prefer the narrowest set (least privilege).
|
|
16585
18093
|
allowedActions: [records:r, records:c, records:u, search:r, schemas:r]
|
|
16586
18094
|
# dataScope binds the key to specific owners. Include null to ALSO see
|
|
@@ -16588,6 +18096,17 @@ accessProfile:
|
|
|
16588
18096
|
# owners' records, not shared/seed data. Keys are userId (the principal) and
|
|
16589
18097
|
# namespaced scopes (scope:org, scope:client, ...):
|
|
16590
18098
|
# dataScope: { "scope:org": [<org-uuid>, null] }
|
|
18099
|
+
# Left empty ({}) below \u2014 the explicit marker for "deliberately tenant-wide,
|
|
18100
|
+
# every record visible" ('blueprint validate' warns on a MISSING dataScope
|
|
18101
|
+
# key, but not on this one). This is a SCAFFOLD PLACEHOLDER, not a
|
|
18102
|
+
# recommendation: it also grants records:c/u tenant-wide, so before this
|
|
18103
|
+
# profile touches real data, replace it with a real scope (the example
|
|
18104
|
+
# above) unless every owner genuinely sharing every record is what you want.
|
|
18105
|
+
dataScope: {}
|
|
18106
|
+
# capabilities is a SEPARATE gated dimension from allowedActions \u2014 named platform
|
|
18107
|
+
# effects a resource:ops verb can't express. Only 'member-lifecycle' is admitted
|
|
18108
|
+
# today; every other name is HARD-REJECTED, same posture as the scope gate above.
|
|
18109
|
+
# capabilities: [member-lifecycle]
|
|
16591
18110
|
|
|
16592
18111
|
servicePrincipal:
|
|
16593
18112
|
externalId: ${name}-sp # stable; the principal the profile + key bind to
|
|
@@ -16707,15 +18226,19 @@ async function resolveAndValidate(arg, verb, supplied) {
|
|
|
16707
18226
|
return null;
|
|
16708
18227
|
}
|
|
16709
18228
|
try {
|
|
16710
|
-
|
|
16711
|
-
for (const clauses of Object.values(blueprint.roles ?? {})) {
|
|
16712
|
-
for (const clause of clauses) assertScopeGate(clause.allowedActions);
|
|
16713
|
-
}
|
|
18229
|
+
assertBlueprintGates(blueprint);
|
|
16714
18230
|
} catch (err) {
|
|
16715
18231
|
if (err instanceof ScopeGateError) {
|
|
16716
18232
|
process.stderr.write(` \u2716 Scope gate rejected '${blueprint.name}':
|
|
16717
18233
|
`);
|
|
16718
18234
|
for (const r of err.rejected) process.stderr.write(` \u2022 ${r.action}: ${r.reason}
|
|
18235
|
+
`);
|
|
18236
|
+
return null;
|
|
18237
|
+
}
|
|
18238
|
+
if (err instanceof CapabilityGateError) {
|
|
18239
|
+
process.stderr.write(` \u2716 Capability gate rejected '${blueprint.name}':
|
|
18240
|
+
`);
|
|
18241
|
+
for (const r of err.rejected) process.stderr.write(` \u2022 ${r.action}: ${r.reason}
|
|
16719
18242
|
`);
|
|
16720
18243
|
return null;
|
|
16721
18244
|
}
|
|
@@ -16747,9 +18270,10 @@ async function runValidate(args) {
|
|
|
16747
18270
|
const blueprint = await resolveAndValidate(args.file, "validate", supplied);
|
|
16748
18271
|
if (!blueprint) return 2;
|
|
16749
18272
|
const gate = evaluateScopeGate(blueprint.accessProfile.allowedActions);
|
|
18273
|
+
const capGate = evaluateCapabilityGate(blueprint.accessProfile.capabilities);
|
|
16750
18274
|
const warnCount = lintBlueprint(blueprint).length;
|
|
16751
18275
|
process.stdout.write(
|
|
16752
|
-
` \u2714 '${blueprint.name}' is valid \u2014 structural \u2713, scope gate \u2713 [${gate.accepted.join(", ")}], lint \u2713` + (warnCount ? ` (${warnCount} warning${warnCount === 1 ? "" : "s"})` : "") + "\n"
|
|
18276
|
+
` \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"
|
|
16753
18277
|
);
|
|
16754
18278
|
return 0;
|
|
16755
18279
|
}
|
|
@@ -17080,7 +18604,14 @@ async function resolveKeyDestination(opts, deps) {
|
|
|
17080
18604
|
`);
|
|
17081
18605
|
return 2;
|
|
17082
18606
|
}
|
|
17083
|
-
|
|
18607
|
+
let alias;
|
|
18608
|
+
try {
|
|
18609
|
+
alias = keyringAliasFor(opts.alias ?? opts.context, opts.tenant ?? "live");
|
|
18610
|
+
} catch (err) {
|
|
18611
|
+
process.stderr.write(` \u2716 ${formatError(err)}
|
|
18612
|
+
`);
|
|
18613
|
+
return 2;
|
|
18614
|
+
}
|
|
17084
18615
|
if (opts.activate === false && await keyring.activeAlias() === alias) {
|
|
17085
18616
|
process.stderr.write(
|
|
17086
18617
|
` \u2716 --no-activate cannot be used on '${alias}': it is your ACTIVE identity, and storing
|
|
@@ -17101,6 +18632,8 @@ async function storeAndReport(dest, key, auth, opts) {
|
|
|
17101
18632
|
return 1;
|
|
17102
18633
|
}
|
|
17103
18634
|
const principalId = opts.principalId;
|
|
18635
|
+
const tenant = keyTenant(key.rawKey) ?? "test";
|
|
18636
|
+
const willActivate = await determineActivateDecision(dest.keyring, { tenant, explicitActivate: opts.activate });
|
|
17104
18637
|
let entry;
|
|
17105
18638
|
try {
|
|
17106
18639
|
warnReplacingAlias(await dest.keyring.get(dest.alias), dest.alias, opts.revokedKeyId);
|
|
@@ -17115,7 +18648,7 @@ async function storeAndReport(dest, key, auth, opts) {
|
|
|
17115
18648
|
env: auth.env
|
|
17116
18649
|
},
|
|
17117
18650
|
key.rawKey,
|
|
17118
|
-
{ activate:
|
|
18651
|
+
{ activate: willActivate, revokedKeyId: opts.revokedKeyId }
|
|
17119
18652
|
));
|
|
17120
18653
|
} catch (err) {
|
|
17121
18654
|
process.stderr.write(
|
|
@@ -17127,7 +18660,6 @@ async function storeAndReport(dest, key, auth, opts) {
|
|
|
17127
18660
|
);
|
|
17128
18661
|
return 1;
|
|
17129
18662
|
}
|
|
17130
|
-
const active = opts.activate !== false;
|
|
17131
18663
|
process.stdout.write(
|
|
17132
18664
|
`
|
|
17133
18665
|
\u2714 Stored the new key in your ${describeEntryStoreNoun(entry)} \u2014 it was not printed.
|
|
@@ -17137,9 +18669,19 @@ async function storeAndReport(dest, key, auth, opts) {
|
|
|
17137
18669
|
["principal", entry.principalId || void 0],
|
|
17138
18670
|
["key id", entry.keyId],
|
|
17139
18671
|
["env", entry.env],
|
|
17140
|
-
["active",
|
|
17141
|
-
]) + "\n\n" +
|
|
17142
|
-
|
|
18672
|
+
["active", willActivate ? "yes" : "no"]
|
|
18673
|
+
]) + "\n\n" + // The plain "now active" note fires only on the activated path. When it
|
|
18674
|
+
// did NOT activate, tenantActivateMessage below is the ONLY explanation —
|
|
18675
|
+
// it already says to run `switch`, AND (unlike a bare "Not active") says
|
|
18676
|
+
// WHY: an explicit --no-activate vs. this call's tenant-safety default.
|
|
18677
|
+
// Saying both here would print two different "run switch" instructions
|
|
18678
|
+
// for one outcome.
|
|
18679
|
+
(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({
|
|
18680
|
+
alias: entry.alias,
|
|
18681
|
+
tenant,
|
|
18682
|
+
willActivate,
|
|
18683
|
+
explicitlyDeclined: opts.activate === false
|
|
18684
|
+
})
|
|
17143
18685
|
);
|
|
17144
18686
|
return 0;
|
|
17145
18687
|
}
|
|
@@ -17360,9 +18902,25 @@ async function runKeyRotate(opts, deps = {}) {
|
|
|
17360
18902
|
return 1;
|
|
17361
18903
|
}
|
|
17362
18904
|
}
|
|
17363
|
-
const
|
|
17364
|
-
|
|
17365
|
-
)
|
|
18905
|
+
const activeKeys = await drainPages(() => client.auth.listScopedKeys());
|
|
18906
|
+
const existing = activeKeys.find((k) => matchesTuple(k, opts.context, userId, keyName));
|
|
18907
|
+
if (!existing && keyName === DEFAULT_KEY_NAME && !opts.forceNew) {
|
|
18908
|
+
const others = activeKeys.filter(
|
|
18909
|
+
(k) => k.contextId === opts.context && k.userId === userId && k.status === "active"
|
|
18910
|
+
);
|
|
18911
|
+
if (others.length > 0) {
|
|
18912
|
+
const list = others.map((k) => ` \u2022 ${k.keyId} (name: '${k.keyName}')`).join("\n");
|
|
18913
|
+
process.stderr.write(
|
|
18914
|
+
` \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):
|
|
18915
|
+
${list}
|
|
18916
|
+
If one of these is it, re-run with --name '<its name>'.
|
|
18917
|
+
If you really want a new key alongside them, re-run with --force-new.
|
|
18918
|
+
Nothing was revoked or minted.
|
|
18919
|
+
`
|
|
18920
|
+
);
|
|
18921
|
+
return 1;
|
|
18922
|
+
}
|
|
18923
|
+
}
|
|
17366
18924
|
if (existing?.keyId) {
|
|
17367
18925
|
await client.auth.revokeScopedKey({ keyId: existing.keyId });
|
|
17368
18926
|
process.stdout.write(` \u2714 Revoked prior key ${existing.keyId}
|
|
@@ -17795,7 +19353,7 @@ function isRecognizedOwnershipKey(key) {
|
|
|
17795
19353
|
if (key === "userId" || key === "partnerUserId") return true;
|
|
17796
19354
|
if (!key.startsWith("scope:")) return false;
|
|
17797
19355
|
const ns = key.slice("scope:".length);
|
|
17798
|
-
return !
|
|
19356
|
+
return !FORBIDDEN_SCOPE_NAMESPACES2.has(ns) && SCOPE_NAMESPACE_RE2.test(ns);
|
|
17799
19357
|
}
|
|
17800
19358
|
var DIM_WILDCARD = "*";
|
|
17801
19359
|
var ANY_TOKEN_RE = /^\$\{\{\s*any\s*\}\}$/;
|
|
@@ -18542,7 +20100,24 @@ async function captureSnapshot(client, blueprint) {
|
|
|
18542
20100
|
}
|
|
18543
20101
|
return { existingSchemaTypes, existingProfilePrincipals, existingRoleIds, failed };
|
|
18544
20102
|
}
|
|
18545
|
-
async function
|
|
20103
|
+
async function captureIssuerSnapshot(bootstrapClient) {
|
|
20104
|
+
const existingIssuerIds = /* @__PURE__ */ new Set();
|
|
20105
|
+
let failed = false;
|
|
20106
|
+
try {
|
|
20107
|
+
let startFrom;
|
|
20108
|
+
for (; ; ) {
|
|
20109
|
+
const page = await bootstrapClient.auth.listIssuers({ limit: SNAPSHOT_PAGE_SIZE, startFrom });
|
|
20110
|
+
for (const i of page.data ?? []) if (i.issuerId) existingIssuerIds.add(i.issuerId);
|
|
20111
|
+
const next = page.nextCursor;
|
|
20112
|
+
if (!next) break;
|
|
20113
|
+
startFrom = next;
|
|
20114
|
+
}
|
|
20115
|
+
} catch {
|
|
20116
|
+
failed = true;
|
|
20117
|
+
}
|
|
20118
|
+
return { existingIssuerIds, failed };
|
|
20119
|
+
}
|
|
20120
|
+
async function runAssertions(client, bootstrapClient, deps, blueprint, tenantId, result) {
|
|
18546
20121
|
const assertions = [];
|
|
18547
20122
|
try {
|
|
18548
20123
|
const ping = await deps.pingWithKey(result.rawKey);
|
|
@@ -18627,9 +20202,35 @@ async function runAssertions(client, deps, blueprint, tenantId, result) {
|
|
|
18627
20202
|
}
|
|
18628
20203
|
}
|
|
18629
20204
|
}
|
|
20205
|
+
for (const { issuerId } of result.issuerActions) {
|
|
20206
|
+
try {
|
|
20207
|
+
const issuer = await bootstrapClient.auth.getIssuer({ issuerId });
|
|
20208
|
+
const ok = issuer.issuerId === issuerId;
|
|
20209
|
+
assertions.push({
|
|
20210
|
+
name: `issuer:${issuerId}`,
|
|
20211
|
+
ok,
|
|
20212
|
+
detail: ok ? `issuer '${issuerId}' registered + readable` : `getIssuer(${issuerId}) returned issuerId '${issuer.issuerId ?? "(none)"}'`
|
|
20213
|
+
});
|
|
20214
|
+
} catch (e) {
|
|
20215
|
+
assertions.push({ name: `issuer:${issuerId}`, ok: false, detail: `getIssuer(${issuerId}) failed: ${errMsg(e)}` });
|
|
20216
|
+
}
|
|
20217
|
+
}
|
|
20218
|
+
for (const { namespace } of result.namespaceActions) {
|
|
20219
|
+
try {
|
|
20220
|
+
const ns = await bootstrapClient.identity.getNamespace({ namespace, contextId: blueprint.contextId });
|
|
20221
|
+
const ok = ns.namespace === namespace && ns.contextId === blueprint.contextId;
|
|
20222
|
+
assertions.push({
|
|
20223
|
+
name: `namespace:${namespace}`,
|
|
20224
|
+
ok,
|
|
20225
|
+
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}'`
|
|
20226
|
+
});
|
|
20227
|
+
} catch (e) {
|
|
20228
|
+
assertions.push({ name: `namespace:${namespace}`, ok: false, detail: `getNamespace(${namespace}) failed: ${errMsg(e)}` });
|
|
20229
|
+
}
|
|
20230
|
+
}
|
|
18630
20231
|
return assertions;
|
|
18631
20232
|
}
|
|
18632
|
-
async function runTeardown(client, blueprint, machineId, snapshot, result) {
|
|
20233
|
+
async function runTeardown(client, bootstrapClient, blueprint, machineId, snapshot, result) {
|
|
18633
20234
|
const steps = [];
|
|
18634
20235
|
const keyName = result?.keyName ?? keyNameFor(blueprint.name, machineId);
|
|
18635
20236
|
try {
|
|
@@ -18723,10 +20324,34 @@ async function runTeardown(client, blueprint, machineId, snapshot, result) {
|
|
|
18723
20324
|
for (const [typeName] of toDelete) {
|
|
18724
20325
|
steps.push({ step: `delete schema ${typeName}`, ok: false, error: lastError.get(typeName) ?? "could not delete" });
|
|
18725
20326
|
}
|
|
20327
|
+
for (const { issuerId } of result.issuerActions) {
|
|
20328
|
+
if (snapshot.failed) {
|
|
20329
|
+
steps.push({ step: `delete issuer ${issuerId}`, ok: true, skipped: true, error: "skipped \u2014 pre-apply snapshot failed; cannot prove ownership" });
|
|
20330
|
+
continue;
|
|
20331
|
+
}
|
|
20332
|
+
if (snapshot.existingIssuerIds.has(issuerId)) {
|
|
20333
|
+
steps.push({ step: `delete issuer ${issuerId}`, ok: true, skipped: true, error: "skipped \u2014 pre-existed this run" });
|
|
20334
|
+
continue;
|
|
20335
|
+
}
|
|
20336
|
+
try {
|
|
20337
|
+
await bootstrapClient.auth.deleteIssuer({ issuerId });
|
|
20338
|
+
steps.push({ step: `delete issuer ${issuerId}`, ok: true });
|
|
20339
|
+
} catch (e) {
|
|
20340
|
+
steps.push({ step: `delete issuer ${issuerId}`, ok: false, error: errMsg(e) });
|
|
20341
|
+
}
|
|
20342
|
+
}
|
|
20343
|
+
for (const { namespace } of result.namespaceActions) {
|
|
20344
|
+
steps.push({
|
|
20345
|
+
step: `delete namespace ${namespace}`,
|
|
20346
|
+
ok: true,
|
|
20347
|
+
skipped: true,
|
|
20348
|
+
error: "namespace deletion requires a root API key (sk_*) \u2014 blueprint-test never holds one; the registration is left in place"
|
|
20349
|
+
});
|
|
20350
|
+
}
|
|
18726
20351
|
return steps;
|
|
18727
20352
|
}
|
|
18728
20353
|
async function runBlueprintHarness(client, input, deps) {
|
|
18729
|
-
const { tenantId, machineId, keep = false, log, remintForContext } = input;
|
|
20354
|
+
const { tenantId, machineId, keep = false, log, remintForContext, remintBootstrapForContext } = input;
|
|
18730
20355
|
const readExistingKey = async () => void 0;
|
|
18731
20356
|
const persistMintedKey = async () => {
|
|
18732
20357
|
};
|
|
@@ -18736,12 +20361,15 @@ async function runBlueprintHarness(client, input, deps) {
|
|
|
18736
20361
|
log
|
|
18737
20362
|
);
|
|
18738
20363
|
const remint = remintForContext ?? (async () => client);
|
|
18739
|
-
|
|
20364
|
+
const remintBootstrap = remintBootstrapForContext ?? (async () => client);
|
|
20365
|
+
const hasIssuers = (blueprint.issuers ?? []).length > 0;
|
|
20366
|
+
const hasNamespaces = (blueprint.namespaces ?? []).length > 0;
|
|
20367
|
+
let contextSnapshot;
|
|
18740
20368
|
if (remintForContext) {
|
|
18741
20369
|
try {
|
|
18742
|
-
|
|
20370
|
+
contextSnapshot = await captureSnapshot(await remintForContext(blueprint.contextId), blueprint);
|
|
18743
20371
|
} catch (e) {
|
|
18744
|
-
|
|
20372
|
+
contextSnapshot = {
|
|
18745
20373
|
existingSchemaTypes: /* @__PURE__ */ new Set(),
|
|
18746
20374
|
existingProfilePrincipals: /* @__PURE__ */ new Set(),
|
|
18747
20375
|
existingRoleIds: /* @__PURE__ */ new Set(),
|
|
@@ -18749,15 +20377,48 @@ async function runBlueprintHarness(client, input, deps) {
|
|
|
18749
20377
|
};
|
|
18750
20378
|
}
|
|
18751
20379
|
} else {
|
|
18752
|
-
|
|
20380
|
+
contextSnapshot = await captureSnapshot(client, blueprint);
|
|
20381
|
+
}
|
|
20382
|
+
let issuerSnapshot;
|
|
20383
|
+
if (!hasIssuers) {
|
|
20384
|
+
issuerSnapshot = { existingIssuerIds: /* @__PURE__ */ new Set(), failed: false };
|
|
20385
|
+
} else if (remintBootstrapForContext) {
|
|
20386
|
+
try {
|
|
20387
|
+
issuerSnapshot = await captureIssuerSnapshot(await remintBootstrapForContext(blueprint.contextId));
|
|
20388
|
+
} catch (e) {
|
|
20389
|
+
issuerSnapshot = { existingIssuerIds: /* @__PURE__ */ new Set(), failed: !isNotFound(e) };
|
|
20390
|
+
}
|
|
20391
|
+
} else {
|
|
20392
|
+
issuerSnapshot = await captureIssuerSnapshot(client);
|
|
18753
20393
|
}
|
|
20394
|
+
const snapshot = {
|
|
20395
|
+
...contextSnapshot,
|
|
20396
|
+
existingIssuerIds: issuerSnapshot.existingIssuerIds,
|
|
20397
|
+
failed: contextSnapshot.failed || issuerSnapshot.failed
|
|
20398
|
+
};
|
|
18754
20399
|
let result;
|
|
18755
20400
|
let assertions = [];
|
|
18756
20401
|
let applyError;
|
|
18757
20402
|
try {
|
|
18758
|
-
result = await runLoader(client, {
|
|
20403
|
+
result = await runLoader(client, {
|
|
20404
|
+
blueprint,
|
|
20405
|
+
tenantId,
|
|
20406
|
+
machineId,
|
|
20407
|
+
readExistingKey,
|
|
20408
|
+
persistMintedKey,
|
|
20409
|
+
log,
|
|
20410
|
+
remintForContext,
|
|
20411
|
+
remintBootstrapForContext
|
|
20412
|
+
});
|
|
18759
20413
|
log.debug({ component: "harness", contextId: result.contextId, keyName: result.keyName }, "applied");
|
|
18760
|
-
assertions = await runAssertions(
|
|
20414
|
+
assertions = await runAssertions(
|
|
20415
|
+
await remint(blueprint.contextId),
|
|
20416
|
+
hasIssuers || hasNamespaces ? await remintBootstrap(blueprint.contextId) : client,
|
|
20417
|
+
deps,
|
|
20418
|
+
blueprint,
|
|
20419
|
+
tenantId,
|
|
20420
|
+
result
|
|
20421
|
+
);
|
|
18761
20422
|
} catch (e) {
|
|
18762
20423
|
applyError = errMsg(e);
|
|
18763
20424
|
log.warn({ component: "harness", err: applyError }, "apply phase failed");
|
|
@@ -18769,7 +20430,14 @@ async function runBlueprintHarness(client, input, deps) {
|
|
|
18769
20430
|
tdClient = await remint(blueprint.contextId);
|
|
18770
20431
|
} catch {
|
|
18771
20432
|
}
|
|
18772
|
-
|
|
20433
|
+
let tdBootstrapClient = client;
|
|
20434
|
+
if (hasIssuers) {
|
|
20435
|
+
try {
|
|
20436
|
+
tdBootstrapClient = await remintBootstrap(blueprint.contextId);
|
|
20437
|
+
} catch {
|
|
20438
|
+
}
|
|
20439
|
+
}
|
|
20440
|
+
teardown = await runTeardown(tdClient, tdBootstrapClient, blueprint, machineId, snapshot, result);
|
|
18773
20441
|
}
|
|
18774
20442
|
const passed = applyError === void 0 && assertions.length > 0 && assertions.every((a) => a.ok);
|
|
18775
20443
|
return {
|
|
@@ -18843,6 +20511,13 @@ async function runBlueprintTest(fileOrName, opts, deps = {}) {
|
|
|
18843
20511
|
process.stderr.write(` \u2716 Scope gate refused blueprint:
|
|
18844
20512
|
`);
|
|
18845
20513
|
for (const r of err.rejected) process.stderr.write(` \u2022 ${r.action}: ${r.reason}
|
|
20514
|
+
`);
|
|
20515
|
+
return 2;
|
|
20516
|
+
}
|
|
20517
|
+
if (err instanceof CapabilityGateError) {
|
|
20518
|
+
process.stderr.write(` \u2716 Capability gate refused blueprint:
|
|
20519
|
+
`);
|
|
20520
|
+
for (const r of err.rejected) process.stderr.write(` \u2022 ${r.action}: ${r.reason}
|
|
18846
20521
|
`);
|
|
18847
20522
|
return 2;
|
|
18848
20523
|
}
|
|
@@ -18878,9 +20553,22 @@ async function runBlueprintTest(fileOrName, opts, deps = {}) {
|
|
|
18878
20553
|
environment: auth.baseUrl
|
|
18879
20554
|
});
|
|
18880
20555
|
};
|
|
20556
|
+
const remintBootstrapForContext = async (contextId) => {
|
|
20557
|
+
const ctxToken = await mintBridgeToken({
|
|
20558
|
+
cognitoToken: auth.cognitoToken,
|
|
20559
|
+
apiBaseUrl: auth.baseUrl,
|
|
20560
|
+
contextId,
|
|
20561
|
+
tenant: auth.tenant,
|
|
20562
|
+
useScopedToken: opts.useScopedToken
|
|
20563
|
+
});
|
|
20564
|
+
return new VectrosClient({
|
|
20565
|
+
token: ctxToken.token,
|
|
20566
|
+
environment: auth.baseUrl
|
|
20567
|
+
});
|
|
20568
|
+
};
|
|
18881
20569
|
const report = await runBlueprintHarness(
|
|
18882
20570
|
client,
|
|
18883
|
-
{ blueprint, tenantId, machineId, keep: opts.keep, log, remintForContext },
|
|
20571
|
+
{ blueprint, tenantId, machineId, keep: opts.keep, log, remintForContext, remintBootstrapForContext },
|
|
18884
20572
|
{ pingWithKey }
|
|
18885
20573
|
);
|
|
18886
20574
|
if (opts.json) {
|
|
@@ -18961,7 +20649,15 @@ async function runJoin(opts, deps = {}) {
|
|
|
18961
20649
|
return 2;
|
|
18962
20650
|
}
|
|
18963
20651
|
const context = opts.contextArg;
|
|
18964
|
-
const
|
|
20652
|
+
const tenant = opts.tenant ?? "live";
|
|
20653
|
+
let alias;
|
|
20654
|
+
try {
|
|
20655
|
+
alias = keyringAliasFor(opts.alias ?? context, tenant);
|
|
20656
|
+
} catch (err) {
|
|
20657
|
+
process.stderr.write(` \u2716 ${formatError(err)}
|
|
20658
|
+
`);
|
|
20659
|
+
return 2;
|
|
20660
|
+
}
|
|
18965
20661
|
let keyring;
|
|
18966
20662
|
try {
|
|
18967
20663
|
keyring = deps.keyring ?? await Keyring.open();
|
|
@@ -18991,8 +20687,23 @@ async function runJoin(opts, deps = {}) {
|
|
|
18991
20687
|
const heldSecret = existingEntry ? await keyring.resolveSecret(alias) : void 0;
|
|
18992
20688
|
const canReuse = existingEntry && heldSecret && existingEntry.context === context && existingEntry.principalId === principalId && existingEntry.env === auth.env;
|
|
18993
20689
|
if (canReuse && existingEntry) {
|
|
18994
|
-
await keyring
|
|
18995
|
-
|
|
20690
|
+
const willActivate2 = await determineActivateDecision(keyring, {
|
|
20691
|
+
tenant,
|
|
20692
|
+
explicitActivate: opts.activate,
|
|
20693
|
+
hint: { alias, secret: heldSecret }
|
|
20694
|
+
});
|
|
20695
|
+
if (willActivate2) await keyring.setActive(alias);
|
|
20696
|
+
emitJoined({
|
|
20697
|
+
alias,
|
|
20698
|
+
context,
|
|
20699
|
+
principalId,
|
|
20700
|
+
role: opts.role,
|
|
20701
|
+
keyId: existingEntry.keyId,
|
|
20702
|
+
reused: true,
|
|
20703
|
+
tenant,
|
|
20704
|
+
willActivate: willActivate2,
|
|
20705
|
+
explicitlyDeclined: opts.activate === false
|
|
20706
|
+
});
|
|
18996
20707
|
return 0;
|
|
18997
20708
|
}
|
|
18998
20709
|
warnReplacingAlias(existingEntry, alias);
|
|
@@ -19004,13 +20715,24 @@ async function runJoin(opts, deps = {}) {
|
|
|
19004
20715
|
);
|
|
19005
20716
|
}
|
|
19006
20717
|
if (revokedKeyId) await warnOtherAliasesNaming(keyring, revokedKeyId, alias);
|
|
20718
|
+
const willActivate = await determineActivateDecision(keyring, { tenant, explicitActivate: opts.activate });
|
|
19007
20719
|
const { entry } = await storeMintedKey(
|
|
19008
20720
|
keyring,
|
|
19009
20721
|
{ alias, context, principalId, role: opts.role, keyId: key.keyId, env: auth.env },
|
|
19010
20722
|
key.rawKey,
|
|
19011
|
-
{ revokedKeyId }
|
|
20723
|
+
{ activate: willActivate, revokedKeyId }
|
|
19012
20724
|
);
|
|
19013
|
-
emitJoined(
|
|
20725
|
+
emitJoined({
|
|
20726
|
+
alias,
|
|
20727
|
+
context,
|
|
20728
|
+
principalId,
|
|
20729
|
+
role: opts.role,
|
|
20730
|
+
keyId: entry.keyId,
|
|
20731
|
+
reused: false,
|
|
20732
|
+
tenant,
|
|
20733
|
+
willActivate,
|
|
20734
|
+
explicitlyDeclined: opts.activate === false
|
|
20735
|
+
});
|
|
19014
20736
|
return 0;
|
|
19015
20737
|
} catch (err) {
|
|
19016
20738
|
process.stderr.write(formatCliError(err, { action: "join failed" }));
|
|
@@ -19018,27 +20740,34 @@ async function runJoin(opts, deps = {}) {
|
|
|
19018
20740
|
}
|
|
19019
20741
|
});
|
|
19020
20742
|
}
|
|
19021
|
-
function emitJoined(
|
|
19022
|
-
|
|
19023
|
-
|
|
19024
|
-
|
|
19025
|
-
|
|
19026
|
-
|
|
19027
|
-
|
|
19028
|
-
|
|
19029
|
-
|
|
19030
|
-
|
|
20743
|
+
function emitJoined(opts) {
|
|
20744
|
+
const header = `
|
|
20745
|
+
\u2714 Joined '${opts.context}' as ${opts.principalId}` + (opts.reused ? " (existing key reused)" : "") + "\n" + formatKeyValues([
|
|
20746
|
+
["alias", opts.alias],
|
|
20747
|
+
["context", opts.context],
|
|
20748
|
+
["principal", opts.principalId],
|
|
20749
|
+
["role", opts.role],
|
|
20750
|
+
["key id", opts.keyId]
|
|
20751
|
+
]);
|
|
20752
|
+
const activationNote = opts.willActivate ? `
|
|
19031
20753
|
|
|
19032
|
-
Now active. Use \`vectros switch ${alias}\` to return to it, \`vectros keyring show --format env\` to export the key for the MCP server.
|
|
19033
|
-
`
|
|
20754
|
+
Now active. Use \`vectros switch ${opts.alias}\` to return to it, \`vectros keyring show --format env\` to export the key for the MCP server.
|
|
20755
|
+
` : "";
|
|
20756
|
+
process.stdout.write(
|
|
20757
|
+
header + activationNote + tenantActivateMessage({
|
|
20758
|
+
alias: opts.alias,
|
|
20759
|
+
tenant: opts.tenant,
|
|
20760
|
+
willActivate: opts.willActivate,
|
|
20761
|
+
explicitlyDeclined: opts.explicitlyDeclined
|
|
20762
|
+
})
|
|
19034
20763
|
);
|
|
19035
20764
|
}
|
|
19036
20765
|
|
|
19037
20766
|
// src/build-info.ts
|
|
19038
20767
|
var BUILD_INFO = {
|
|
19039
|
-
cli: true ? "0.
|
|
19040
|
-
sdk: true ? "0.
|
|
19041
|
-
blueprints: true ? "0.
|
|
20768
|
+
cli: true ? "0.16.0" : "dev",
|
|
20769
|
+
sdk: true ? "0.40.0" : "dev",
|
|
20770
|
+
blueprints: true ? "0.13.0" : "dev"
|
|
19042
20771
|
};
|
|
19043
20772
|
function formatBuildInfo() {
|
|
19044
20773
|
return `vectros ${BUILD_INFO.cli} (sdk ${BUILD_INFO.sdk}, blueprints ${BUILD_INFO.blueprints})`;
|
|
@@ -19062,6 +20791,14 @@ function addCommonAuthOptions(cmd) {
|
|
|
19062
20791
|
"emit machine-readable JSON instead of the human-readable view"
|
|
19063
20792
|
);
|
|
19064
20793
|
}
|
|
20794
|
+
function parseTenantFlag(raw) {
|
|
20795
|
+
if (raw !== void 0 && raw !== "test" && raw !== "live") {
|
|
20796
|
+
process.stderr.write(` \u2716 --tenant must be 'test' or 'live' (got '${raw}')
|
|
20797
|
+
`);
|
|
20798
|
+
process.exit(2);
|
|
20799
|
+
}
|
|
20800
|
+
return raw;
|
|
20801
|
+
}
|
|
19065
20802
|
function toCommonOptions(flags) {
|
|
19066
20803
|
return {
|
|
19067
20804
|
env: flags.env,
|
|
@@ -19115,7 +20852,10 @@ function buildProgram(deps = {}) {
|
|
|
19115
20852
|
return program;
|
|
19116
20853
|
}
|
|
19117
20854
|
function registerJoinCommand(program, deps) {
|
|
19118
|
-
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)")
|
|
20855
|
+
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(
|
|
20856
|
+
"--activate",
|
|
20857
|
+
"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)"
|
|
20858
|
+
).option("--no-activate", "store the key without activating it, regardless of tenant");
|
|
19119
20859
|
addAuthOptions(join3).action(
|
|
19120
20860
|
async (contextArg, flags) => {
|
|
19121
20861
|
if (flags.tenant !== void 0 && flags.tenant !== "test" && flags.tenant !== "live") {
|
|
@@ -19133,7 +20873,8 @@ function registerJoinCommand(program, deps) {
|
|
|
19133
20873
|
as: flags.as,
|
|
19134
20874
|
scope: flags.scope,
|
|
19135
20875
|
alias: flags.alias,
|
|
19136
|
-
tenant: flags.tenant
|
|
20876
|
+
tenant: flags.tenant,
|
|
20877
|
+
activate: flags.activate
|
|
19137
20878
|
},
|
|
19138
20879
|
deps
|
|
19139
20880
|
)
|
|
@@ -19380,9 +21121,10 @@ function toKeyDestination(flags) {
|
|
|
19380
21121
|
function registerKeyCommands(program, deps) {
|
|
19381
21122
|
const key = program.command("key").description("Manage scoped API keys (ssk_*) \u2014 issue, list, revoke, rotate.");
|
|
19382
21123
|
const issue = addKeyDestinationOptions(
|
|
19383
|
-
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)")
|
|
21124
|
+
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)")
|
|
19384
21125
|
);
|
|
19385
21126
|
addAuthOptions(issue).action(async (flags) => {
|
|
21127
|
+
const tenant = parseTenantFlag(flags.tenant);
|
|
19386
21128
|
process.exit(
|
|
19387
21129
|
await runKeyIssue({
|
|
19388
21130
|
...toCommonOptions(flags),
|
|
@@ -19390,6 +21132,7 @@ function registerKeyCommands(program, deps) {
|
|
|
19390
21132
|
context: flags.context,
|
|
19391
21133
|
name: flags.name,
|
|
19392
21134
|
label: flags.label,
|
|
21135
|
+
tenant,
|
|
19393
21136
|
...toKeyDestination(flags)
|
|
19394
21137
|
}, deps)
|
|
19395
21138
|
);
|
|
@@ -19397,40 +21140,58 @@ function registerKeyCommands(program, deps) {
|
|
|
19397
21140
|
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(
|
|
19398
21141
|
"--all-contexts",
|
|
19399
21142
|
"list every key across ALL your contexts (owner-only) \u2014 the tenant-wide view for the key limit"
|
|
19400
|
-
);
|
|
21143
|
+
).option("--tenant <tenant>", "tenant to list: 'test' or 'live' (default live; not combinable with --all-contexts)");
|
|
19401
21144
|
addCommonAuthOptions(list).action(
|
|
19402
21145
|
async (flags) => {
|
|
21146
|
+
if (flags.allContexts && flags.tenant !== void 0) {
|
|
21147
|
+
process.stderr.write(" \u2716 --tenant is not combinable with --all-contexts (which already spans both tenants).\n");
|
|
21148
|
+
process.exit(2);
|
|
21149
|
+
}
|
|
21150
|
+
const tenant = parseTenantFlag(flags.tenant);
|
|
19403
21151
|
process.exit(
|
|
19404
21152
|
await runKeyList(
|
|
19405
21153
|
{
|
|
19406
21154
|
...toCommonOptions(flags),
|
|
19407
21155
|
principal: flags.principal,
|
|
19408
21156
|
context: flags.context,
|
|
19409
|
-
allContexts: Boolean(flags.allContexts)
|
|
21157
|
+
allContexts: Boolean(flags.allContexts),
|
|
21158
|
+
tenant
|
|
19410
21159
|
},
|
|
19411
21160
|
deps
|
|
19412
21161
|
)
|
|
19413
21162
|
);
|
|
19414
21163
|
}
|
|
19415
21164
|
);
|
|
19416
|
-
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)");
|
|
19417
|
-
addCommonAuthOptions(get).action(
|
|
19418
|
-
|
|
19419
|
-
|
|
19420
|
-
|
|
19421
|
-
|
|
19422
|
-
|
|
19423
|
-
|
|
21165
|
+
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)");
|
|
21166
|
+
addCommonAuthOptions(get).action(
|
|
21167
|
+
async (keyId, flags) => {
|
|
21168
|
+
const tenant = parseTenantFlag(flags.tenant);
|
|
21169
|
+
process.exit(await runKeyGet(keyId, { ...toCommonOptions(flags), context: flags.context, tenant }, deps));
|
|
21170
|
+
}
|
|
21171
|
+
);
|
|
21172
|
+
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)");
|
|
21173
|
+
addAuthOptions(revoke).action(
|
|
21174
|
+
async (keyId, flags) => {
|
|
21175
|
+
const tenant = parseTenantFlag(flags.tenant);
|
|
21176
|
+
process.exit(await runKeyRevoke(keyId, { ...toCommonOptions(flags), context: flags.context, tenant }, deps));
|
|
21177
|
+
}
|
|
21178
|
+
);
|
|
19424
21179
|
const rotate = addKeyDestinationOptions(
|
|
19425
|
-
key.command("rotate").description("Revoke the matching active key and mint a fresh secret.").requiredOption("--principal <p>", "principal (usr_<id> or <id>)").requiredOption("--context <c>", "app context").option("--name <name>", "key name to rotate", DEFAULT_KEY_NAME)
|
|
21180
|
+
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(
|
|
21181
|
+
"--force-new",
|
|
21182
|
+
"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)"
|
|
21183
|
+
).option("--tenant <tenant>", "tenant the key lives in: 'test' or 'live' (default live)")
|
|
19426
21184
|
);
|
|
19427
21185
|
addAuthOptions(rotate).action(async (flags) => {
|
|
21186
|
+
const tenant = parseTenantFlag(flags.tenant);
|
|
19428
21187
|
process.exit(
|
|
19429
21188
|
await runKeyRotate({
|
|
19430
21189
|
...toCommonOptions(flags),
|
|
19431
21190
|
principal: flags.principal,
|
|
19432
21191
|
context: flags.context,
|
|
19433
21192
|
name: flags.name,
|
|
21193
|
+
forceNew: flags.forceNew,
|
|
21194
|
+
tenant,
|
|
19434
21195
|
...toKeyDestination(flags)
|
|
19435
21196
|
}, deps)
|
|
19436
21197
|
);
|