@vectros-ai/cli 0.15.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 +134 -8
- package/CHANGELOG.md +148 -0
- package/README.md +194 -2
- package/dist/cli.js +1120 -406
- package/dist/cli.js.map +1 -1
- package/dist/cli.mjs +1120 -406
- package/dist/cli.mjs.map +1 -1
- package/package.json +4 -4
package/dist/cli.js
CHANGED
|
@@ -4435,7 +4435,7 @@ var AuthClient = class {
|
|
|
4435
4435
|
);
|
|
4436
4436
|
}
|
|
4437
4437
|
/**
|
|
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). 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.
|
|
4439
4439
|
*
|
|
4440
4440
|
* @param {Vectros.CreateAccessProfileRequest} request
|
|
4441
4441
|
* @param {AuthClient.RequestOptions} requestOptions - Request-specific configuration.
|
|
@@ -5536,7 +5536,7 @@ var AuthClient = class {
|
|
|
5536
5536
|
);
|
|
5537
5537
|
}
|
|
5538
5538
|
/**
|
|
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.
|
|
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.
|
|
5540
5540
|
*
|
|
5541
5541
|
* @param {Vectros.GetUsageRequest} request
|
|
5542
5542
|
* @param {AuthClient.RequestOptions} requestOptions - Request-specific configuration.
|
|
@@ -5599,7 +5599,7 @@ var AuthClient = class {
|
|
|
5599
5599
|
return handleNonStatusCodeError(_response.error, _response.rawResponse, "GET", "/v1/usage");
|
|
5600
5600
|
}
|
|
5601
5601
|
/**
|
|
5602
|
-
* Retrieves a single registered issuer by issuerId. Requires a root API key or the bootstrap's provisioning capability.
|
|
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
5603
|
*
|
|
5604
5604
|
* @param {Vectros.GetIssuerRequest} request
|
|
5605
5605
|
* @param {AuthClient.RequestOptions} requestOptions - Request-specific configuration.
|
|
@@ -5657,13 +5657,14 @@ var AuthClient = class {
|
|
|
5657
5657
|
return handleNonStatusCodeError(_response.error, _response.rawResponse, "GET", "/v1/auth/issuers/{issuerId}");
|
|
5658
5658
|
}
|
|
5659
5659
|
/**
|
|
5660
|
-
* Deregisters a trusted third-party IdP issuer. Requires a root API key or the bootstrap's provisioning capability.
|
|
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
5661
|
*
|
|
5662
5662
|
* @param {Vectros.DeleteIssuerRequest} request
|
|
5663
5663
|
* @param {AuthClient.RequestOptions} requestOptions - Request-specific configuration.
|
|
5664
5664
|
*
|
|
5665
5665
|
* @throws {@link Vectros.ForbiddenError}
|
|
5666
5666
|
* @throws {@link Vectros.NotFoundError}
|
|
5667
|
+
* @throws {@link Vectros.ConflictError}
|
|
5667
5668
|
* @throws {@link Vectros.TooManyRequestsError}
|
|
5668
5669
|
*
|
|
5669
5670
|
* @example
|
|
@@ -5705,6 +5706,8 @@ var AuthClient = class {
|
|
|
5705
5706
|
throw new ForbiddenError(_response.error.body, _response.rawResponse);
|
|
5706
5707
|
case 404:
|
|
5707
5708
|
throw new NotFoundError(_response.error.body, _response.rawResponse);
|
|
5709
|
+
case 409:
|
|
5710
|
+
throw new ConflictError(_response.error.body, _response.rawResponse);
|
|
5708
5711
|
case 429:
|
|
5709
5712
|
throw new TooManyRequestsError(_response.error.body, _response.rawResponse);
|
|
5710
5713
|
default:
|
|
@@ -5723,7 +5726,7 @@ var AuthClient = class {
|
|
|
5723
5726
|
);
|
|
5724
5727
|
}
|
|
5725
5728
|
/**
|
|
5726
|
-
* Returns the issuers registered in your tenant. Requires a root API key or the bootstrap's provisioning capability. Returns a `{data, nextCursor}` envelope.
|
|
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.
|
|
5727
5730
|
*
|
|
5728
5731
|
* @param {Vectros.ListIssuersRequest} request
|
|
5729
5732
|
* @param {AuthClient.RequestOptions} requestOptions - Request-specific configuration.
|
|
@@ -5780,7 +5783,7 @@ var AuthClient = class {
|
|
|
5780
5783
|
return handleNonStatusCodeError(_response.error, _response.rawResponse, "GET", "/v1/auth/issuers");
|
|
5781
5784
|
}
|
|
5782
5785
|
/**
|
|
5783
|
-
* Registers a trusted third-party IdP issuer that BYO-IdP token exchange (`POST /v1/auth/token/exchange`) may accept a `subject_token` from. Requires a root API key or the CLI bootstrap's provisioning capability — never an ordinary partner-grantable scope. Idempotent by `issuerId` within your tenant; the `(issuer, audience)` pair must not already be registered by a different issuerId/tenant.
|
|
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`.
|
|
5784
5787
|
*
|
|
5785
5788
|
* @param {Vectros.IssuerRequest} request
|
|
5786
5789
|
* @param {AuthClient.RequestOptions} requestOptions - Request-specific configuration.
|
|
@@ -5891,7 +5894,7 @@ var AuthClient = class {
|
|
|
5891
5894
|
return handleNonStatusCodeError(_response.error, _response.rawResponse, "GET", "/v1/ping");
|
|
5892
5895
|
}
|
|
5893
5896
|
/**
|
|
5894
|
-
* 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.
|
|
5895
5898
|
*
|
|
5896
5899
|
* @param {Vectros.ListProfilesForPrincipalRequest} request
|
|
5897
5900
|
* @param {AuthClient.RequestOptions} requestOptions - Request-specific configuration.
|
|
@@ -6170,7 +6173,7 @@ var AuthClient = class {
|
|
|
6170
6173
|
return handleNonStatusCodeError(_response.error, _response.rawResponse, "POST", "/v1/users/invite/resend");
|
|
6171
6174
|
}
|
|
6172
6175
|
/**
|
|
6173
|
-
* RFC 8693 OAuth 2.0 Token Exchange. Trades a JWT issued by a third-party identity provider you've registered (`POST /v1/auth/issuers`) for a Vectros `st_*` scoped bearer token — no Vectros credential required to call this endpoint. The exchanged token's scope is resolved entirely server-side from the matched user's access profile; this endpoint accepts no caller-supplied scope, resource, or audience parameter (RFC 8693 §2.1's `resource`/`audience`/`scope` are not used in v1 — the registered `(issuer, audience)` pair alone pins the target tenant and app context). On a first-time login (no existing Vectros identity for this subject), two opt-in binding paths exist: `invite_token` (a `PENDING` sub-user invitation), and — if the registration declares one or more self-signup policies — `signup_type` (a brand-new user is created and bound to the policy's configured role). If `invite_token` is present at all, it is the ONLY path tried — a failed invite never falls through to self-signup. Neither field is required for a subject with an existing identity. Uses the OAuth-standard error envelope (`{"error":..., "error_description":...}`, RFC 6749 §5.2), NOT this API's usual `{"message":...}` shape — its client is generic OAuth tooling, not the Vectros SDK.
|
|
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.
|
|
6174
6177
|
*
|
|
6175
6178
|
* @param {Vectros.TokenExchangeRequest} request
|
|
6176
6179
|
* @param {AuthClient.RequestOptions} requestOptions - Request-specific configuration.
|
|
@@ -7712,6 +7715,7 @@ var IdentityClient = class {
|
|
|
7712
7715
|
* @example
|
|
7713
7716
|
* await client.identity.listEntities({
|
|
7714
7717
|
* namespace: "team",
|
|
7718
|
+
* contextId: "myapp",
|
|
7715
7719
|
* startFrom: "b3BhcXVlLWN1cnNvci1mcm9tLXRoZS1wcmV2aW91cy1wYWdl"
|
|
7716
7720
|
* })
|
|
7717
7721
|
*/
|
|
@@ -7721,6 +7725,7 @@ var IdentityClient = class {
|
|
|
7721
7725
|
async __listEntities(request, requestOptions) {
|
|
7722
7726
|
const {
|
|
7723
7727
|
namespace,
|
|
7728
|
+
contextId,
|
|
7724
7729
|
userId,
|
|
7725
7730
|
externalId,
|
|
7726
7731
|
scope,
|
|
@@ -7735,6 +7740,7 @@ var IdentityClient = class {
|
|
|
7735
7740
|
limit
|
|
7736
7741
|
} = request;
|
|
7737
7742
|
const _queryParams = {
|
|
7743
|
+
contextId,
|
|
7738
7744
|
userId,
|
|
7739
7745
|
externalId,
|
|
7740
7746
|
scope,
|
|
@@ -7793,6 +7799,7 @@ var IdentityClient = class {
|
|
|
7793
7799
|
* @example
|
|
7794
7800
|
* await client.identity.createEntity({
|
|
7795
7801
|
* namespace: "team",
|
|
7802
|
+
* contextId: "myapp",
|
|
7796
7803
|
* body: {
|
|
7797
7804
|
* externalId: "team_eng_platform"
|
|
7798
7805
|
* }
|
|
@@ -7802,9 +7809,10 @@ var IdentityClient = class {
|
|
|
7802
7809
|
return HttpResponsePromise.fromPromise(this.__createEntity(request, requestOptions));
|
|
7803
7810
|
}
|
|
7804
7811
|
async __createEntity(request, requestOptions) {
|
|
7805
|
-
const { namespace, upsert, body: _body } = request;
|
|
7812
|
+
const { namespace, upsert, contextId, body: _body } = request;
|
|
7806
7813
|
const _queryParams = {
|
|
7807
|
-
upsert
|
|
7814
|
+
upsert,
|
|
7815
|
+
contextId
|
|
7808
7816
|
};
|
|
7809
7817
|
const _authRequest = await this._options.authProvider.getAuthRequest();
|
|
7810
7818
|
const _headers = mergeHeaders(
|
|
@@ -7861,14 +7869,18 @@ var IdentityClient = class {
|
|
|
7861
7869
|
* @example
|
|
7862
7870
|
* await client.identity.getEntity({
|
|
7863
7871
|
* namespace: "team",
|
|
7864
|
-
* id: "6ba7b810-9dad-11d1-80b4-00c04fd430c8"
|
|
7872
|
+
* id: "6ba7b810-9dad-11d1-80b4-00c04fd430c8",
|
|
7873
|
+
* contextId: "myapp"
|
|
7865
7874
|
* })
|
|
7866
7875
|
*/
|
|
7867
7876
|
getEntity(request, requestOptions) {
|
|
7868
7877
|
return HttpResponsePromise.fromPromise(this.__getEntity(request, requestOptions));
|
|
7869
7878
|
}
|
|
7870
7879
|
async __getEntity(request, requestOptions) {
|
|
7871
|
-
const { namespace, id } = request;
|
|
7880
|
+
const { namespace, id, contextId } = request;
|
|
7881
|
+
const _queryParams = {
|
|
7882
|
+
contextId
|
|
7883
|
+
};
|
|
7872
7884
|
const _authRequest = await this._options.authProvider.getAuthRequest();
|
|
7873
7885
|
const _headers = mergeHeaders(
|
|
7874
7886
|
_authRequest.headers,
|
|
@@ -7882,7 +7894,7 @@ var IdentityClient = class {
|
|
|
7882
7894
|
),
|
|
7883
7895
|
method: "GET",
|
|
7884
7896
|
headers: _headers,
|
|
7885
|
-
queryString: url_exports.queryBuilder().mergeAdditional(requestOptions?.queryParams).build(),
|
|
7897
|
+
queryString: url_exports.queryBuilder().addMany(_queryParams).mergeAdditional(requestOptions?.queryParams).build(),
|
|
7886
7898
|
timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1e3,
|
|
7887
7899
|
maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries,
|
|
7888
7900
|
abortSignal: requestOptions?.abortSignal,
|
|
@@ -7919,6 +7931,7 @@ var IdentityClient = class {
|
|
|
7919
7931
|
* await client.identity.updateEntity({
|
|
7920
7932
|
* namespace: "team",
|
|
7921
7933
|
* id: "id",
|
|
7934
|
+
* contextId: "myapp",
|
|
7922
7935
|
* body: {
|
|
7923
7936
|
* externalId: "team_eng_platform"
|
|
7924
7937
|
* }
|
|
@@ -7928,7 +7941,10 @@ var IdentityClient = class {
|
|
|
7928
7941
|
return HttpResponsePromise.fromPromise(this.__updateEntity(request, requestOptions));
|
|
7929
7942
|
}
|
|
7930
7943
|
async __updateEntity(request, requestOptions) {
|
|
7931
|
-
const { namespace, id, body: _body } = request;
|
|
7944
|
+
const { namespace, id, contextId, body: _body } = request;
|
|
7945
|
+
const _queryParams = {
|
|
7946
|
+
contextId
|
|
7947
|
+
};
|
|
7932
7948
|
const _authRequest = await this._options.authProvider.getAuthRequest();
|
|
7933
7949
|
const _headers = mergeHeaders(
|
|
7934
7950
|
_authRequest.headers,
|
|
@@ -7943,7 +7959,7 @@ var IdentityClient = class {
|
|
|
7943
7959
|
method: "PUT",
|
|
7944
7960
|
headers: _headers,
|
|
7945
7961
|
contentType: "application/json",
|
|
7946
|
-
queryString: url_exports.queryBuilder().mergeAdditional(requestOptions?.queryParams).build(),
|
|
7962
|
+
queryString: url_exports.queryBuilder().addMany(_queryParams).mergeAdditional(requestOptions?.queryParams).build(),
|
|
7947
7963
|
requestType: "json",
|
|
7948
7964
|
body: _body,
|
|
7949
7965
|
timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1e3,
|
|
@@ -7983,14 +7999,18 @@ var IdentityClient = class {
|
|
|
7983
7999
|
* @example
|
|
7984
8000
|
* await client.identity.deleteEntity({
|
|
7985
8001
|
* namespace: "team",
|
|
7986
|
-
* id: "id"
|
|
8002
|
+
* id: "id",
|
|
8003
|
+
* contextId: "myapp"
|
|
7987
8004
|
* })
|
|
7988
8005
|
*/
|
|
7989
8006
|
deleteEntity(request, requestOptions) {
|
|
7990
8007
|
return HttpResponsePromise.fromPromise(this.__deleteEntity(request, requestOptions));
|
|
7991
8008
|
}
|
|
7992
8009
|
async __deleteEntity(request, requestOptions) {
|
|
7993
|
-
const { namespace, id } = request;
|
|
8010
|
+
const { namespace, id, contextId } = request;
|
|
8011
|
+
const _queryParams = {
|
|
8012
|
+
contextId
|
|
8013
|
+
};
|
|
7994
8014
|
const _authRequest = await this._options.authProvider.getAuthRequest();
|
|
7995
8015
|
const _headers = mergeHeaders(
|
|
7996
8016
|
_authRequest.headers,
|
|
@@ -8004,7 +8024,7 @@ var IdentityClient = class {
|
|
|
8004
8024
|
),
|
|
8005
8025
|
method: "DELETE",
|
|
8006
8026
|
headers: _headers,
|
|
8007
|
-
queryString: url_exports.queryBuilder().mergeAdditional(requestOptions?.queryParams).build(),
|
|
8027
|
+
queryString: url_exports.queryBuilder().addMany(_queryParams).mergeAdditional(requestOptions?.queryParams).build(),
|
|
8008
8028
|
timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1e3,
|
|
8009
8029
|
maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries,
|
|
8010
8030
|
abortSignal: requestOptions?.abortSignal,
|
|
@@ -8047,6 +8067,7 @@ var IdentityClient = class {
|
|
|
8047
8067
|
* @example
|
|
8048
8068
|
* await client.identity.lookupEntities({
|
|
8049
8069
|
* namespace: "team",
|
|
8070
|
+
* contextId: "myapp",
|
|
8050
8071
|
* body: {
|
|
8051
8072
|
* type: "person_v1",
|
|
8052
8073
|
* field: "ssn"
|
|
@@ -8057,7 +8078,10 @@ var IdentityClient = class {
|
|
|
8057
8078
|
return HttpResponsePromise.fromPromise(this.__lookupEntities(request, requestOptions));
|
|
8058
8079
|
}
|
|
8059
8080
|
async __lookupEntities(request, requestOptions) {
|
|
8060
|
-
const { namespace, body: _body } = request;
|
|
8081
|
+
const { namespace, contextId, body: _body } = request;
|
|
8082
|
+
const _queryParams = {
|
|
8083
|
+
contextId
|
|
8084
|
+
};
|
|
8061
8085
|
const _authRequest = await this._options.authProvider.getAuthRequest();
|
|
8062
8086
|
const _headers = mergeHeaders(
|
|
8063
8087
|
_authRequest.headers,
|
|
@@ -8072,7 +8096,7 @@ var IdentityClient = class {
|
|
|
8072
8096
|
method: "POST",
|
|
8073
8097
|
headers: _headers,
|
|
8074
8098
|
contentType: "application/json",
|
|
8075
|
-
queryString: url_exports.queryBuilder().mergeAdditional(requestOptions?.queryParams).build(),
|
|
8099
|
+
queryString: url_exports.queryBuilder().addMany(_queryParams).mergeAdditional(requestOptions?.queryParams).build(),
|
|
8076
8100
|
requestType: "json",
|
|
8077
8101
|
body: _body,
|
|
8078
8102
|
timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1e3,
|
|
@@ -8116,15 +8140,17 @@ var IdentityClient = class {
|
|
|
8116
8140
|
* @example
|
|
8117
8141
|
* await client.identity.getEntityVersions({
|
|
8118
8142
|
* namespace: "team",
|
|
8119
|
-
* id: "id"
|
|
8143
|
+
* id: "id",
|
|
8144
|
+
* contextId: "myapp"
|
|
8120
8145
|
* })
|
|
8121
8146
|
*/
|
|
8122
8147
|
getEntityVersions(request, requestOptions) {
|
|
8123
8148
|
return HttpResponsePromise.fromPromise(this.__getEntityVersions(request, requestOptions));
|
|
8124
8149
|
}
|
|
8125
8150
|
async __getEntityVersions(request, requestOptions) {
|
|
8126
|
-
const { namespace, id, startFrom } = request;
|
|
8151
|
+
const { namespace, id, contextId, startFrom } = request;
|
|
8127
8152
|
const _queryParams = {
|
|
8153
|
+
contextId,
|
|
8128
8154
|
startFrom
|
|
8129
8155
|
};
|
|
8130
8156
|
const _authRequest = await this._options.authProvider.getAuthRequest();
|
|
@@ -8186,7 +8212,10 @@ var IdentityClient = class {
|
|
|
8186
8212
|
return HttpResponsePromise.fromPromise(this.__getNamespace(request, requestOptions));
|
|
8187
8213
|
}
|
|
8188
8214
|
async __getNamespace(request, requestOptions) {
|
|
8189
|
-
const { namespace } = request;
|
|
8215
|
+
const { namespace, contextId } = request;
|
|
8216
|
+
const _queryParams = {
|
|
8217
|
+
contextId
|
|
8218
|
+
};
|
|
8190
8219
|
const _authRequest = await this._options.authProvider.getAuthRequest();
|
|
8191
8220
|
const _headers = mergeHeaders(
|
|
8192
8221
|
_authRequest.headers,
|
|
@@ -8200,7 +8229,7 @@ var IdentityClient = class {
|
|
|
8200
8229
|
),
|
|
8201
8230
|
method: "GET",
|
|
8202
8231
|
headers: _headers,
|
|
8203
|
-
queryString: url_exports.queryBuilder().mergeAdditional(requestOptions?.queryParams).build(),
|
|
8232
|
+
queryString: url_exports.queryBuilder().addMany(_queryParams).mergeAdditional(requestOptions?.queryParams).build(),
|
|
8204
8233
|
timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1e3,
|
|
8205
8234
|
maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries,
|
|
8206
8235
|
abortSignal: requestOptions?.abortSignal,
|
|
@@ -8225,7 +8254,7 @@ var IdentityClient = class {
|
|
|
8225
8254
|
return handleNonStatusCodeError(_response.error, _response.rawResponse, "GET", "/v1/namespaces/{namespace}");
|
|
8226
8255
|
}
|
|
8227
8256
|
/**
|
|
8228
|
-
* 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.
|
|
8229
8258
|
*
|
|
8230
8259
|
* @param {Vectros.UpdateNamespaceRequest} request
|
|
8231
8260
|
* @param {IdentityClient.RequestOptions} requestOptions - Request-specific configuration.
|
|
@@ -8247,7 +8276,10 @@ var IdentityClient = class {
|
|
|
8247
8276
|
return HttpResponsePromise.fromPromise(this.__updateNamespace(request, requestOptions));
|
|
8248
8277
|
}
|
|
8249
8278
|
async __updateNamespace(request, requestOptions) {
|
|
8250
|
-
const { namespace, body: _body } = request;
|
|
8279
|
+
const { namespace, contextId, body: _body } = request;
|
|
8280
|
+
const _queryParams = {
|
|
8281
|
+
contextId
|
|
8282
|
+
};
|
|
8251
8283
|
const _authRequest = await this._options.authProvider.getAuthRequest();
|
|
8252
8284
|
const _headers = mergeHeaders(
|
|
8253
8285
|
_authRequest.headers,
|
|
@@ -8262,7 +8294,7 @@ var IdentityClient = class {
|
|
|
8262
8294
|
method: "PUT",
|
|
8263
8295
|
headers: _headers,
|
|
8264
8296
|
contentType: "application/json",
|
|
8265
|
-
queryString: url_exports.queryBuilder().mergeAdditional(requestOptions?.queryParams).build(),
|
|
8297
|
+
queryString: url_exports.queryBuilder().addMany(_queryParams).mergeAdditional(requestOptions?.queryParams).build(),
|
|
8266
8298
|
requestType: "json",
|
|
8267
8299
|
body: _body,
|
|
8268
8300
|
timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1e3,
|
|
@@ -8293,7 +8325,7 @@ var IdentityClient = class {
|
|
|
8293
8325
|
return handleNonStatusCodeError(_response.error, _response.rawResponse, "PUT", "/v1/namespaces/{namespace}");
|
|
8294
8326
|
}
|
|
8295
8327
|
/**
|
|
8296
|
-
* 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.
|
|
8297
8329
|
*
|
|
8298
8330
|
* @param {Vectros.DeleteNamespaceRequest} request
|
|
8299
8331
|
* @param {IdentityClient.RequestOptions} requestOptions - Request-specific configuration.
|
|
@@ -8311,7 +8343,10 @@ var IdentityClient = class {
|
|
|
8311
8343
|
return HttpResponsePromise.fromPromise(this.__deleteNamespace(request, requestOptions));
|
|
8312
8344
|
}
|
|
8313
8345
|
async __deleteNamespace(request, requestOptions) {
|
|
8314
|
-
const { namespace } = request;
|
|
8346
|
+
const { namespace, contextId } = request;
|
|
8347
|
+
const _queryParams = {
|
|
8348
|
+
contextId
|
|
8349
|
+
};
|
|
8315
8350
|
const _authRequest = await this._options.authProvider.getAuthRequest();
|
|
8316
8351
|
const _headers = mergeHeaders(
|
|
8317
8352
|
_authRequest.headers,
|
|
@@ -8325,7 +8360,7 @@ var IdentityClient = class {
|
|
|
8325
8360
|
),
|
|
8326
8361
|
method: "DELETE",
|
|
8327
8362
|
headers: _headers,
|
|
8328
|
-
queryString: url_exports.queryBuilder().mergeAdditional(requestOptions?.queryParams).build(),
|
|
8363
|
+
queryString: url_exports.queryBuilder().addMany(_queryParams).mergeAdditional(requestOptions?.queryParams).build(),
|
|
8329
8364
|
timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1e3,
|
|
8330
8365
|
maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries,
|
|
8331
8366
|
abortSignal: requestOptions?.abortSignal,
|
|
@@ -8354,7 +8389,7 @@ var IdentityClient = class {
|
|
|
8354
8389
|
return handleNonStatusCodeError(_response.error, _response.rawResponse, "DELETE", "/v1/namespaces/{namespace}");
|
|
8355
8390
|
}
|
|
8356
8391
|
/**
|
|
8357
|
-
* 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.
|
|
8358
8393
|
*
|
|
8359
8394
|
* @param {Vectros.ListNamespacesRequest} request
|
|
8360
8395
|
* @param {IdentityClient.RequestOptions} requestOptions - Request-specific configuration.
|
|
@@ -8366,10 +8401,11 @@ var IdentityClient = class {
|
|
|
8366
8401
|
return HttpResponsePromise.fromPromise(this.__listNamespaces(request, requestOptions));
|
|
8367
8402
|
}
|
|
8368
8403
|
async __listNamespaces(request = {}, requestOptions) {
|
|
8369
|
-
const { startFrom, limit } = request;
|
|
8404
|
+
const { startFrom, limit, contextId } = request;
|
|
8370
8405
|
const _queryParams = {
|
|
8371
8406
|
startFrom,
|
|
8372
|
-
limit
|
|
8407
|
+
limit,
|
|
8408
|
+
contextId
|
|
8373
8409
|
};
|
|
8374
8410
|
const _authRequest = await this._options.authProvider.getAuthRequest();
|
|
8375
8411
|
const _headers = mergeHeaders(
|
|
@@ -8404,9 +8440,9 @@ var IdentityClient = class {
|
|
|
8404
8440
|
return handleNonStatusCodeError(_response.error, _response.rawResponse, "GET", "/v1/namespaces");
|
|
8405
8441
|
}
|
|
8406
8442
|
/**
|
|
8407
|
-
* 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.
|
|
8408
8444
|
*
|
|
8409
|
-
* @param {Vectros.
|
|
8445
|
+
* @param {Vectros.RegisterNamespaceRequest} request
|
|
8410
8446
|
* @param {IdentityClient.RequestOptions} requestOptions - Request-specific configuration.
|
|
8411
8447
|
*
|
|
8412
8448
|
* @throws {@link Vectros.BadRequestError}
|
|
@@ -8415,14 +8451,20 @@ var IdentityClient = class {
|
|
|
8415
8451
|
*
|
|
8416
8452
|
* @example
|
|
8417
8453
|
* await client.identity.registerNamespace({
|
|
8418
|
-
*
|
|
8419
|
-
*
|
|
8454
|
+
* body: {
|
|
8455
|
+
* namespace: "team",
|
|
8456
|
+
* specificityRank: 1500
|
|
8457
|
+
* }
|
|
8420
8458
|
* })
|
|
8421
8459
|
*/
|
|
8422
8460
|
registerNamespace(request, requestOptions) {
|
|
8423
8461
|
return HttpResponsePromise.fromPromise(this.__registerNamespace(request, requestOptions));
|
|
8424
8462
|
}
|
|
8425
8463
|
async __registerNamespace(request, requestOptions) {
|
|
8464
|
+
const { contextId, body: _body } = request;
|
|
8465
|
+
const _queryParams = {
|
|
8466
|
+
contextId
|
|
8467
|
+
};
|
|
8426
8468
|
const _authRequest = await this._options.authProvider.getAuthRequest();
|
|
8427
8469
|
const _headers = mergeHeaders(
|
|
8428
8470
|
_authRequest.headers,
|
|
@@ -8437,9 +8479,9 @@ var IdentityClient = class {
|
|
|
8437
8479
|
method: "POST",
|
|
8438
8480
|
headers: _headers,
|
|
8439
8481
|
contentType: "application/json",
|
|
8440
|
-
queryString: url_exports.queryBuilder().mergeAdditional(requestOptions?.queryParams).build(),
|
|
8482
|
+
queryString: url_exports.queryBuilder().addMany(_queryParams).mergeAdditional(requestOptions?.queryParams).build(),
|
|
8441
8483
|
requestType: "json",
|
|
8442
|
-
body:
|
|
8484
|
+
body: _body,
|
|
8443
8485
|
timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1e3,
|
|
8444
8486
|
maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries,
|
|
8445
8487
|
abortSignal: requestOptions?.abortSignal,
|
|
@@ -8724,12 +8766,13 @@ var IdentityClient = class {
|
|
|
8724
8766
|
return handleNonStatusCodeError(_response.error, _response.rawResponse, "PUT", "/v1/users/{id}");
|
|
8725
8767
|
}
|
|
8726
8768
|
/**
|
|
8727
|
-
* 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.
|
|
8728
8770
|
*
|
|
8729
8771
|
* @param {Vectros.DeleteUserRequest} request
|
|
8730
8772
|
* @param {IdentityClient.RequestOptions} requestOptions - Request-specific configuration.
|
|
8731
8773
|
*
|
|
8732
8774
|
* @throws {@link Vectros.NotFoundError}
|
|
8775
|
+
* @throws {@link Vectros.ConflictError}
|
|
8733
8776
|
* @throws {@link Vectros.TooManyRequestsError}
|
|
8734
8777
|
*
|
|
8735
8778
|
* @example
|
|
@@ -8769,6 +8812,8 @@ var IdentityClient = class {
|
|
|
8769
8812
|
switch (_response.error.statusCode) {
|
|
8770
8813
|
case 404:
|
|
8771
8814
|
throw new NotFoundError(_response.error.body, _response.rawResponse);
|
|
8815
|
+
case 409:
|
|
8816
|
+
throw new ConflictError(_response.error.body, _response.rawResponse);
|
|
8772
8817
|
case 429:
|
|
8773
8818
|
throw new TooManyRequestsError(_response.error.body, _response.rawResponse);
|
|
8774
8819
|
default:
|
|
@@ -10851,6 +10896,35 @@ var BlueprintSchemaSchema = import_zod.z.object({
|
|
|
10851
10896
|
});
|
|
10852
10897
|
}
|
|
10853
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
|
+
});
|
|
10854
10928
|
var BlueprintAccessProfileSchema = import_zod.z.object({
|
|
10855
10929
|
// Validated structurally here; the SCOPE GATE (in @vectros-ai/cli) is
|
|
10856
10930
|
// what enforces the data-plane-only security boundary.
|
|
@@ -10882,11 +10956,31 @@ var BlueprintAccessProfileSchema = import_zod.z.object({
|
|
|
10882
10956
|
// below (`lintIdentityOverrides`, placeholder-aware — see that function's
|
|
10883
10957
|
// docstring); this `.min(1)` is only the shape-level "non-blank" guard on
|
|
10884
10958
|
// the value, not the grammar check.
|
|
10885
|
-
identityOverrides: import_zod.z.record(import_zod.z.string().min(1)).optional()
|
|
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
|
|
10886
10979
|
}).strict();
|
|
10887
10980
|
var BlueprintRoleClauseSchema = import_zod.z.object({
|
|
10888
10981
|
allowedActions: import_zod.z.array(import_zod.z.string().min(1)).min(1),
|
|
10889
|
-
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
|
|
10890
10984
|
}).strict();
|
|
10891
10985
|
var BlueprintRolesSchema = import_zod.z.record(import_zod.z.array(BlueprintRoleClauseSchema).min(1));
|
|
10892
10986
|
var IDENTITY_NAMESPACE_RE = /^[a-z][a-z0-9_-]{1,31}$/;
|
|
@@ -10924,7 +11018,7 @@ var BlueprintServicePrincipalSchema = import_zod.z.object({
|
|
|
10924
11018
|
}).strict();
|
|
10925
11019
|
var ISSUER_ID_RE = CONTEXT_ID_RE;
|
|
10926
11020
|
var BlueprintIssuerSchema = import_zod.z.object({
|
|
10927
|
-
/** Short slug identifying this issuer within your
|
|
11021
|
+
/** Short slug identifying this issuer within your account. Immutable once registered. */
|
|
10928
11022
|
issuerId: import_zod.z.string().regex(ISSUER_ID_RE, {
|
|
10929
11023
|
message: "issuerId must be 3-31 chars, start with a lowercase letter, then lowercase letters/digits/dashes (e.g. 'auth0-prod')"
|
|
10930
11024
|
}),
|
|
@@ -10939,7 +11033,12 @@ var BlueprintIssuerSchema = import_zod.z.object({
|
|
|
10939
11033
|
* this as an ordinary per-API/application default).
|
|
10940
11034
|
*/
|
|
10941
11035
|
audience: import_zod.z.string().min(1),
|
|
10942
|
-
/**
|
|
11036
|
+
/**
|
|
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.
|
|
11041
|
+
*/
|
|
10943
11042
|
contextId: import_zod.z.string().regex(CONTEXT_ID_RE, {
|
|
10944
11043
|
message: "contextId must be 3-31 chars, start with a lowercase letter, then lowercase letters/digits/dashes (e.g. 'casework')"
|
|
10945
11044
|
}),
|
|
@@ -10966,6 +11065,151 @@ var BlueprintIssuerSchema = import_zod.z.object({
|
|
|
10966
11065
|
}).strict()
|
|
10967
11066
|
).optional()
|
|
10968
11067
|
}).strict();
|
|
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()
|
|
11106
|
+
}).strict();
|
|
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
|
+
}
|
|
10969
11213
|
var SeedCommonShape = {
|
|
10970
11214
|
/** The schema typeName this seed instantiates (should match a declared schema). */
|
|
10971
11215
|
typeName: import_zod.z.string().min(1),
|
|
@@ -11006,7 +11250,7 @@ var BlueprintSeedRecordSchema = import_zod.z.discriminatedUnion("surface", [
|
|
|
11006
11250
|
RecordSeedSchema,
|
|
11007
11251
|
DocumentSeedSchema
|
|
11008
11252
|
]);
|
|
11009
|
-
var SELF_TOKEN_RE = /\$\{\{\s*(?:self|under)\.[^}]*?\s*\}\}/;
|
|
11253
|
+
var SELF_TOKEN_RE = /\$\{\{\s*(?:self|under|member)\.[^}]*?\s*\}\}/;
|
|
11010
11254
|
function lintSelfTokenPlacement(value, ctx) {
|
|
11011
11255
|
const walk = (node, path4, inRoleDataScope) => {
|
|
11012
11256
|
if (typeof node === "string") {
|
|
@@ -11014,7 +11258,7 @@ function lintSelfTokenPlacement(value, ctx) {
|
|
|
11014
11258
|
ctx.addIssue({
|
|
11015
11259
|
code: import_zod.z.ZodIssueCode.custom,
|
|
11016
11260
|
path: path4,
|
|
11017
|
-
message: "'${{ self.* }}' is a runtime
|
|
11261
|
+
message: "'${{ self.* }}' / '${{ member.* }}' is a runtime placeholder \u2014 it is only valid inside a roles[].dataScope value"
|
|
11018
11262
|
});
|
|
11019
11263
|
}
|
|
11020
11264
|
return;
|
|
@@ -11214,9 +11458,36 @@ function lintSeedSurfaces(value, ctx) {
|
|
|
11214
11458
|
}
|
|
11215
11459
|
});
|
|
11216
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
|
+
}
|
|
11217
11485
|
var BLUEPRINT_FIELD_PHASES = Object.freeze({
|
|
11218
11486
|
servicePrincipal: "bootstrap",
|
|
11219
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",
|
|
11220
11491
|
schemas: "in-context",
|
|
11221
11492
|
accessProfile: "in-context",
|
|
11222
11493
|
roles: "in-context",
|
|
@@ -11247,7 +11518,16 @@ var BlueprintSchema = import_zod.z.object({
|
|
|
11247
11518
|
* service-principal creation — NOT the in-context load (see
|
|
11248
11519
|
* {@link BlueprintIssuerSchema}).
|
|
11249
11520
|
*/
|
|
11250
|
-
issuers: import_zod.z.array(BlueprintIssuerSchema).optional()
|
|
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()
|
|
11251
11531
|
}).strict().superRefine((bp, ctx) => {
|
|
11252
11532
|
lintSelfTokenPlacement(bp, ctx);
|
|
11253
11533
|
lintIdentityRefs(bp, ctx);
|
|
@@ -11255,6 +11535,9 @@ var BlueprintSchema = import_zod.z.object({
|
|
|
11255
11535
|
lintNamespacedScopeArrays(bp, ctx);
|
|
11256
11536
|
lintDataScopeKeys(bp, ctx);
|
|
11257
11537
|
lintSeedSurfaces(bp, ctx);
|
|
11538
|
+
lintIdentitySurfaces(bp, ctx);
|
|
11539
|
+
lintIssuerContext(bp, ctx);
|
|
11540
|
+
lintNamespaces(bp, ctx);
|
|
11258
11541
|
});
|
|
11259
11542
|
function formatIssuePath(path4) {
|
|
11260
11543
|
let out = "";
|
|
@@ -11409,9 +11692,11 @@ function deriveSuffix(contextId) {
|
|
|
11409
11692
|
var WHOLE_TOKEN_RE = /^\$\{\{\s*([A-Za-z_]\w*)\.([A-Za-z_]\w*)\s*\}\}$/;
|
|
11410
11693
|
var TOKEN_SCAN_RE = /\$\$\{\{|\$\{\{([\s\S]*?)\}\}|\$\{\{/g;
|
|
11411
11694
|
var INNER_REF_RE = /^([A-Za-z_]\w*)\.([A-Za-z_]\w*)$/;
|
|
11412
|
-
var DEFERRED_NAMESPACES = /* @__PURE__ */ new Set(["self", "identities", "under"]);
|
|
11695
|
+
var DEFERRED_NAMESPACES = /* @__PURE__ */ new Set(["self", "identities", "under", "member"]);
|
|
11413
11696
|
var DEFERRED_NS_ALTERNATION = [...DEFERRED_NAMESPACES].join("|");
|
|
11414
|
-
var DEFERRED_INNER_RE = new RegExp(
|
|
11697
|
+
var DEFERRED_INNER_RE = new RegExp(
|
|
11698
|
+
`^(?:${DEFERRED_NS_ALTERNATION})(?:\\.[A-Za-z_]\\w*)+(?::[A-Za-z_]\\w*)?$`
|
|
11699
|
+
);
|
|
11415
11700
|
function resolveRef(ns, name, ctx, path4) {
|
|
11416
11701
|
if (ns === "inputs") {
|
|
11417
11702
|
if (!(name in ctx.declared)) {
|
|
@@ -11710,9 +11995,11 @@ var taskManagement = {
|
|
|
11710
11995
|
// Least-privilege profile — MUST pass the CLI scope gate.
|
|
11711
11996
|
// r/c/u + search + schema discovery. NOT records:d (least privilege).
|
|
11712
11997
|
accessProfile: {
|
|
11713
|
-
allowedActions: ["records:r", "records:c", "records:u", "search:r", "schemas:r"]
|
|
11714
|
-
// dataScope
|
|
11715
|
-
//
|
|
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: {}
|
|
11716
12003
|
},
|
|
11717
12004
|
servicePrincipal: {
|
|
11718
12005
|
externalId: "task-management",
|
|
@@ -11735,250 +12022,6 @@ var taskManagement = {
|
|
|
11735
12022
|
]
|
|
11736
12023
|
};
|
|
11737
12024
|
var task_management_default = taskManagement;
|
|
11738
|
-
var codingAgentMemory = {
|
|
11739
|
-
name: "coding-agent-memory",
|
|
11740
|
-
version: "1.0.0",
|
|
11741
|
-
description: "Persistent project memory for a coding agent \u2014 decisions, conventions, and gotchas that survive across sessions.",
|
|
11742
|
-
contextId: "coding-memory",
|
|
11743
|
-
contextName: "Coding Agent \u2014 Project Memory",
|
|
11744
|
-
schemas: [
|
|
11745
|
-
{
|
|
11746
|
-
// A durable architectural/product decision: what was decided, and WHY.
|
|
11747
|
-
// The rationale is the high-value, RAG-able body — "why did we do X?"
|
|
11748
|
-
// is the question a cold-context agent most needs answered.
|
|
11749
|
-
typeName: "decision",
|
|
11750
|
-
displayName: "Decision",
|
|
11751
|
-
indexMode: "HYBRID",
|
|
11752
|
-
// keyword on titles + semantic on statement/rationale
|
|
11753
|
-
fields: [
|
|
11754
|
-
{
|
|
11755
|
-
fieldId: "title",
|
|
11756
|
-
fieldType: "string",
|
|
11757
|
-
required: true,
|
|
11758
|
-
searchable: true,
|
|
11759
|
-
validation: { minLength: 1, maxLength: 200 },
|
|
11760
|
-
renderHints: { label: "Title", widget: "text", order: 1, displayField: true }
|
|
11761
|
-
},
|
|
11762
|
-
{
|
|
11763
|
-
fieldId: "statement",
|
|
11764
|
-
fieldType: "string",
|
|
11765
|
-
searchable: true,
|
|
11766
|
-
description: "What was decided, in one or two sentences.",
|
|
11767
|
-
renderHints: { label: "Statement", widget: "textarea", order: 2 }
|
|
11768
|
-
},
|
|
11769
|
-
{
|
|
11770
|
-
fieldId: "rationale",
|
|
11771
|
-
fieldType: "string",
|
|
11772
|
-
searchable: true,
|
|
11773
|
-
description: "Why \u2014 the trade-offs and the context. The most-recalled field.",
|
|
11774
|
-
renderHints: { label: "Rationale", widget: "textarea", order: 3 }
|
|
11775
|
-
},
|
|
11776
|
-
{
|
|
11777
|
-
fieldId: "status",
|
|
11778
|
-
fieldType: "enum",
|
|
11779
|
-
filterable: true,
|
|
11780
|
-
enumValues: ["proposed", "active", "superseded"],
|
|
11781
|
-
renderHints: { label: "Status", widget: "select", order: 4 }
|
|
11782
|
-
},
|
|
11783
|
-
{
|
|
11784
|
-
fieldId: "area",
|
|
11785
|
-
fieldType: "string",
|
|
11786
|
-
filterable: true,
|
|
11787
|
-
description: 'Subsystem / module the decision applies to (e.g. "auth", "search").',
|
|
11788
|
-
renderHints: { label: "Area", widget: "text", order: 5 }
|
|
11789
|
-
},
|
|
11790
|
-
{
|
|
11791
|
-
fieldId: "decidedOn",
|
|
11792
|
-
fieldType: "date",
|
|
11793
|
-
description: 'ISO-8601 date. Range-queryable \u2014 "decisions made this quarter".',
|
|
11794
|
-
renderHints: { label: "Decided on", widget: "date", order: 6 }
|
|
11795
|
-
}
|
|
11796
|
-
// externalId is the record's FIRST-CLASS identifier (the dedup/upsert key + the
|
|
11797
|
-
// value a `reference` resolves against) — it is NOT a payload field, so it is not
|
|
11798
|
-
// declared here; the loader sends it top-level on the RecordRequest.
|
|
11799
|
-
],
|
|
11800
|
-
// `externalId` is a first-class identifier with its own finder (exact
|
|
11801
|
-
// get/upsert) — look it up directly, never redeclare it as a schema lookup.
|
|
11802
|
-
// The choice below is MIGRATION-LOCKED once the schema is live:
|
|
11803
|
-
// • `area`/`status` — EQUALITY (categorical): "list active decisions",
|
|
11804
|
-
// "all decisions in the auth area". 2 of the 7 fast index slots.
|
|
11805
|
-
// • `decidedOn` — RANGE: ordered `from`/`to`/`prefix` over the date.
|
|
11806
|
-
lookupFields: ["area", "status", { fieldName: "decidedOn", rangeEnabled: true }]
|
|
11807
|
-
},
|
|
11808
|
-
{
|
|
11809
|
-
// A coding convention the team follows — the agent reads these before it
|
|
11810
|
-
// writes code so it matches the surrounding style instead of inventing one.
|
|
11811
|
-
typeName: "convention",
|
|
11812
|
-
displayName: "Convention",
|
|
11813
|
-
indexMode: "HYBRID",
|
|
11814
|
-
fields: [
|
|
11815
|
-
{
|
|
11816
|
-
fieldId: "name",
|
|
11817
|
-
fieldType: "string",
|
|
11818
|
-
required: true,
|
|
11819
|
-
searchable: true,
|
|
11820
|
-
validation: { minLength: 1, maxLength: 200 },
|
|
11821
|
-
renderHints: { label: "Name", widget: "text", order: 1, displayField: true }
|
|
11822
|
-
},
|
|
11823
|
-
{
|
|
11824
|
-
fieldId: "rule",
|
|
11825
|
-
fieldType: "string",
|
|
11826
|
-
searchable: true,
|
|
11827
|
-
description: "The convention itself, stated as an imperative.",
|
|
11828
|
-
renderHints: { label: "Rule", widget: "textarea", order: 2 }
|
|
11829
|
-
},
|
|
11830
|
-
{
|
|
11831
|
-
fieldId: "area",
|
|
11832
|
-
fieldType: "string",
|
|
11833
|
-
filterable: true,
|
|
11834
|
-
renderHints: { label: "Area", widget: "text", order: 3 }
|
|
11835
|
-
},
|
|
11836
|
-
{
|
|
11837
|
-
// Parallels `decision.status` so a convention can be RETIRED rather than
|
|
11838
|
-
// deleted (the blueprint omits records:d — memory is superseded, not lost).
|
|
11839
|
-
fieldId: "status",
|
|
11840
|
-
fieldType: "enum",
|
|
11841
|
-
filterable: true,
|
|
11842
|
-
enumValues: ["active", "retired"],
|
|
11843
|
-
renderHints: { label: "Status", widget: "select", order: 4 }
|
|
11844
|
-
},
|
|
11845
|
-
{
|
|
11846
|
-
// A typed REFERENCE to the decision that established this convention — a
|
|
11847
|
-
// foreign-key link between record types (the knowledge-graph edge). The
|
|
11848
|
-
// platform requires both the target type AND its surface; the value is the
|
|
11849
|
-
// target decision's externalId (the default `targetField`). Provenance:
|
|
11850
|
-
// "why does this convention exist? → open the decision behind it." Not
|
|
11851
|
-
// searchable (a foreign-key id is search noise); declared as an equality
|
|
11852
|
-
// lookup below so you can also enumerate "conventions from decision X".
|
|
11853
|
-
fieldId: "establishedByDecision",
|
|
11854
|
-
fieldType: "reference",
|
|
11855
|
-
targetTypeName: "decision",
|
|
11856
|
-
targetSurface: "record",
|
|
11857
|
-
targetField: "externalId",
|
|
11858
|
-
cardinality: "one",
|
|
11859
|
-
renderHints: { label: "Established by (decision)", order: 5 }
|
|
11860
|
-
},
|
|
11861
|
-
{
|
|
11862
|
-
fieldId: "adoptedOn",
|
|
11863
|
-
fieldType: "date",
|
|
11864
|
-
description: "ISO-8601 \u2014 when this convention was adopted. Range-queryable.",
|
|
11865
|
-
renderHints: { label: "Adopted on", widget: "date", order: 6 }
|
|
11866
|
-
}
|
|
11867
|
-
// externalId: first-class identifier, not a payload field (see `decision`).
|
|
11868
|
-
],
|
|
11869
|
-
// externalId has its own first-class finder for exact get — look it up
|
|
11870
|
-
// directly, not as a schema lookup. Locked equality-vs-range choices:
|
|
11871
|
-
// • `area`/`status` — EQUALITY: "active conventions for auth".
|
|
11872
|
-
// • `establishedByDecision` — EQUALITY on the reference value: enumerate
|
|
11873
|
-
// "conventions established by decision X" (a forward link query; the
|
|
11874
|
-
// platform has no reverse-reference index on this surface).
|
|
11875
|
-
// • `adoptedOn` — RANGE over the adoption date.
|
|
11876
|
-
// 3 equality lookups (of 7 fast slots) + 1 range row.
|
|
11877
|
-
lookupFields: ["area", "status", "establishedByDecision", { fieldName: "adoptedOn", rangeEnabled: true }]
|
|
11878
|
-
},
|
|
11879
|
-
{
|
|
11880
|
-
// A gotcha / sharp edge: a symptom, its cause, and the fix. Saves the
|
|
11881
|
-
// agent (and the human) from re-discovering the same trap.
|
|
11882
|
-
typeName: "gotcha",
|
|
11883
|
-
displayName: "Gotcha",
|
|
11884
|
-
indexMode: "HYBRID",
|
|
11885
|
-
fields: [
|
|
11886
|
-
{
|
|
11887
|
-
fieldId: "symptom",
|
|
11888
|
-
fieldType: "string",
|
|
11889
|
-
required: true,
|
|
11890
|
-
searchable: true,
|
|
11891
|
-
validation: { minLength: 1, maxLength: 500 },
|
|
11892
|
-
renderHints: { label: "Symptom", widget: "textarea", order: 1, displayField: true }
|
|
11893
|
-
},
|
|
11894
|
-
{
|
|
11895
|
-
fieldId: "cause",
|
|
11896
|
-
fieldType: "string",
|
|
11897
|
-
searchable: true,
|
|
11898
|
-
renderHints: { label: "Cause", widget: "textarea", order: 2 }
|
|
11899
|
-
},
|
|
11900
|
-
{
|
|
11901
|
-
fieldId: "fix",
|
|
11902
|
-
fieldType: "string",
|
|
11903
|
-
searchable: true,
|
|
11904
|
-
renderHints: { label: "Fix", widget: "textarea", order: 3 }
|
|
11905
|
-
},
|
|
11906
|
-
{
|
|
11907
|
-
fieldId: "area",
|
|
11908
|
-
fieldType: "string",
|
|
11909
|
-
filterable: true,
|
|
11910
|
-
renderHints: { label: "Area", widget: "text", order: 4 }
|
|
11911
|
-
},
|
|
11912
|
-
{
|
|
11913
|
-
// A gotcha can be RETIRED once the underlying trap is fixed for good —
|
|
11914
|
-
// superseded, not deleted (the blueprint omits records:d).
|
|
11915
|
-
fieldId: "status",
|
|
11916
|
-
fieldType: "enum",
|
|
11917
|
-
filterable: true,
|
|
11918
|
-
enumValues: ["active", "retired"],
|
|
11919
|
-
renderHints: { label: "Status", widget: "select", order: 5 }
|
|
11920
|
-
},
|
|
11921
|
-
{
|
|
11922
|
-
fieldId: "discoveredOn",
|
|
11923
|
-
fieldType: "date",
|
|
11924
|
-
description: "ISO-8601 \u2014 when this gotcha was first hit. Range-queryable.",
|
|
11925
|
-
renderHints: { label: "Discovered on", widget: "date", order: 6 }
|
|
11926
|
-
}
|
|
11927
|
-
// externalId: first-class identifier, not a payload field (see `decision`).
|
|
11928
|
-
],
|
|
11929
|
-
// externalId has its own first-class finder for exact get — look it up
|
|
11930
|
-
// directly, not as a schema lookup. `area`/`status` EQUALITY ("active
|
|
11931
|
-
// gotchas in search"); `discoveredOn` RANGE over the discovery date.
|
|
11932
|
-
lookupFields: ["area", "status", { fieldName: "discoveredOn", rangeEnabled: true }]
|
|
11933
|
-
}
|
|
11934
|
-
],
|
|
11935
|
-
// Least-privilege profile — MUST pass the CLI scope gate.
|
|
11936
|
-
// r/c/u + search + schema discovery + inference:r (grounded "why did we do X?"
|
|
11937
|
-
// recall over the captured rationale). NOT records:d: memory is superseded,
|
|
11938
|
-
// not deleted, so the project's decision history stays auditable.
|
|
11939
|
-
accessProfile: {
|
|
11940
|
-
allowedActions: ["records:r", "records:c", "records:u", "search:r", "schemas:r", "inference:r"]
|
|
11941
|
-
},
|
|
11942
|
-
servicePrincipal: {
|
|
11943
|
-
externalId: "coding-agent-memory",
|
|
11944
|
-
displayName: "Coding Agent \u2014 Project Memory"
|
|
11945
|
-
},
|
|
11946
|
-
seed: [
|
|
11947
|
-
{
|
|
11948
|
-
// Seeded FIRST so the convention below resolves its reference to it — the
|
|
11949
|
-
// loader creates seeds in array order, and a reference target must exist when
|
|
11950
|
-
// the referencing record is written.
|
|
11951
|
-
surface: "record",
|
|
11952
|
-
typeName: "decision",
|
|
11953
|
-
externalId: "seed-use-vectros-for-memory",
|
|
11954
|
-
fields: {
|
|
11955
|
-
title: "Use Vectros as the coding agent\u2019s project memory",
|
|
11956
|
-
statement: "Persist decisions, conventions, and gotchas as Vectros records so they survive across agent sessions.",
|
|
11957
|
-
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.",
|
|
11958
|
-
status: "active",
|
|
11959
|
-
area: "getting-started",
|
|
11960
|
-
decidedOn: "2026-06-14"
|
|
11961
|
-
}
|
|
11962
|
-
},
|
|
11963
|
-
{
|
|
11964
|
-
// Demonstrates a live typed link at bootstrap: this convention references the
|
|
11965
|
-
// decision above by its externalId, so "open the decision behind this rule"
|
|
11966
|
-
// works the moment the blueprint is applied.
|
|
11967
|
-
surface: "record",
|
|
11968
|
-
typeName: "convention",
|
|
11969
|
-
externalId: "seed-record-the-why",
|
|
11970
|
-
fields: {
|
|
11971
|
-
name: "Record the why, not just the what",
|
|
11972
|
-
rule: "When you record a decision or convention, always capture the rationale \u2014 the trade-offs a future session cannot re-derive from the code.",
|
|
11973
|
-
area: "getting-started",
|
|
11974
|
-
status: "active",
|
|
11975
|
-
establishedByDecision: "seed-use-vectros-for-memory",
|
|
11976
|
-
adoptedOn: "2026-06-14"
|
|
11977
|
-
}
|
|
11978
|
-
}
|
|
11979
|
-
]
|
|
11980
|
-
};
|
|
11981
|
-
var coding_agent_memory_default = codingAgentMemory;
|
|
11982
12025
|
var DATA_PLANE_ACTIONS = [
|
|
11983
12026
|
"records:r",
|
|
11984
12027
|
"records:c",
|
|
@@ -13105,7 +13148,11 @@ var agenticSdlc = {
|
|
|
13105
13148
|
// mints for THIS blueprint's service principal (the MCP/API runtime). See
|
|
13106
13149
|
// DATA_PLANE_ACTIONS above for the action set + rationale.
|
|
13107
13150
|
accessProfile: {
|
|
13108
|
-
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: {}
|
|
13109
13156
|
},
|
|
13110
13157
|
// A reusable `editor` role for the HUMAN owner — DISTINCT from `accessProfile`
|
|
13111
13158
|
// (which scopes only the service-principal key). `bootstrap` provisions this
|
|
@@ -13125,7 +13172,9 @@ var agenticSdlc = {
|
|
|
13125
13172
|
// which includes members' private memory — the trusted-administrator tradeoff,
|
|
13126
13173
|
// called out in the bundled guide. Scope it down if your deployment wants
|
|
13127
13174
|
// curator access without private-memory visibility.
|
|
13128
|
-
|
|
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: {} }],
|
|
13129
13178
|
// A team member (or their agent) — TWO memory tiers as two UNIONed clauses
|
|
13130
13179
|
// (a record is accessible when ANY clause grants it; within one clause every
|
|
13131
13180
|
// dataScope dimension must match):
|
|
@@ -13145,7 +13194,9 @@ var agenticSdlc = {
|
|
|
13145
13194
|
// `vectros join agentic-sdlc --role member`; verify a binding with
|
|
13146
13195
|
// `vectros access explain --principal me --context agentic-sdlc`.
|
|
13147
13196
|
member: [
|
|
13148
|
-
|
|
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: {} },
|
|
13149
13200
|
{
|
|
13150
13201
|
// `inference:r` is IN this self-scoped clause (not only clause 1) so a
|
|
13151
13202
|
// member's `rag_ask` can ground on its OWN private memory — the read
|
|
@@ -13260,7 +13311,11 @@ var secondBrain = {
|
|
|
13260
13311
|
// (so the agent can `rag_ask` a grounded, cited question over your notes). No
|
|
13261
13312
|
// records:d: notes are archived (status → 'archived'), never deleted.
|
|
13262
13313
|
accessProfile: {
|
|
13263
|
-
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: {}
|
|
13264
13319
|
},
|
|
13265
13320
|
servicePrincipal: {
|
|
13266
13321
|
externalId: "second-brain",
|
|
@@ -13392,7 +13447,11 @@ var clinicalIntake = {
|
|
|
13392
13447
|
// reveal qualifier (the demo key literally cannot un-redact the PHI fields),
|
|
13393
13448
|
// and NO inference:r (we never point a RAG capability at a PHI corpus here).
|
|
13394
13449
|
accessProfile: {
|
|
13395
|
-
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: {}
|
|
13396
13455
|
},
|
|
13397
13456
|
servicePrincipal: {
|
|
13398
13457
|
externalId: "clinical-intake",
|
|
@@ -13421,7 +13480,6 @@ var clinicalIntake = {
|
|
|
13421
13480
|
var clinical_intake_default = clinicalIntake;
|
|
13422
13481
|
var BUNDLED_BLUEPRINTS = [
|
|
13423
13482
|
task_management_default,
|
|
13424
|
-
coding_agent_memory_default,
|
|
13425
13483
|
agentic_sdlc_default,
|
|
13426
13484
|
second_brain_default,
|
|
13427
13485
|
clinical_intake_default
|
|
@@ -13720,18 +13778,17 @@ async function mintBridgeToken(opts) {
|
|
|
13720
13778
|
ttlSeconds = DEFAULT_BRIDGE_TTL_SECONDS,
|
|
13721
13779
|
tenant = "live",
|
|
13722
13780
|
useScopedToken = false,
|
|
13781
|
+
contextId,
|
|
13723
13782
|
fetchImpl = fetch
|
|
13724
13783
|
} = opts;
|
|
13725
13784
|
const base = apiBaseUrl.replace(/\/$/, "");
|
|
13726
13785
|
const path4 = bridgeTokenPath(useScopedToken);
|
|
13727
|
-
|
|
13728
|
-
|
|
13729
|
-
|
|
13730
|
-
|
|
13731
|
-
|
|
13732
|
-
|
|
13733
|
-
(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)` : ""
|
|
13734
|
-
);
|
|
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
|
+
});
|
|
13735
13792
|
}
|
|
13736
13793
|
async function mintContextToken(opts) {
|
|
13737
13794
|
const {
|
|
@@ -14114,8 +14171,29 @@ var KNOWN_CONTROL_PLANE_RESOURCES = [
|
|
|
14114
14171
|
// control-plane, unlike per-namespace entity CRUD. (Cosmetic: any
|
|
14115
14172
|
// non-data-plane resource hard-rejects whether or not it is listed here;
|
|
14116
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.
|
|
14117
14186
|
"namespaces"
|
|
14118
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
|
+
]);
|
|
14119
14197
|
var VALID_OPS = /* @__PURE__ */ new Set(["c", "r", "u", "d"]);
|
|
14120
14198
|
var DATA_PLANE_SET = new Set(DATA_PLANE_ALLOWLIST);
|
|
14121
14199
|
var CONTROL_PLANE_SET = new Set(KNOWN_CONTROL_PLANE_RESOURCES);
|
|
@@ -14131,7 +14209,60 @@ ${summary}`
|
|
|
14131
14209
|
this.name = "ScopeGateError";
|
|
14132
14210
|
this.rejected = rejected;
|
|
14133
14211
|
}
|
|
14134
|
-
};
|
|
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
|
+
}
|
|
14135
14266
|
function evaluateScopeToken(action) {
|
|
14136
14267
|
const token = action.trim();
|
|
14137
14268
|
if (token.length === 0) {
|
|
@@ -14153,6 +14284,48 @@ function evaluateScopeToken(action) {
|
|
|
14153
14284
|
};
|
|
14154
14285
|
}
|
|
14155
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
|
+
}
|
|
14156
14329
|
if (CONTROL_PLANE_SET.has(resource)) {
|
|
14157
14330
|
return {
|
|
14158
14331
|
action,
|
|
@@ -14167,13 +14340,12 @@ function evaluateScopeToken(action) {
|
|
|
14167
14340
|
if (ops === "*") {
|
|
14168
14341
|
return null;
|
|
14169
14342
|
}
|
|
14170
|
-
|
|
14171
|
-
|
|
14172
|
-
|
|
14173
|
-
|
|
14174
|
-
|
|
14175
|
-
|
|
14176
|
-
}
|
|
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
|
+
};
|
|
14177
14349
|
}
|
|
14178
14350
|
return null;
|
|
14179
14351
|
}
|
|
@@ -14196,6 +14368,16 @@ function assertScopeGate(allowedActions) {
|
|
|
14196
14368
|
throw new ScopeGateError(result.rejected);
|
|
14197
14369
|
}
|
|
14198
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
|
+
}
|
|
14199
14381
|
|
|
14200
14382
|
// src/machine-id.ts
|
|
14201
14383
|
var import_node_fs = require("fs");
|
|
@@ -14300,6 +14482,68 @@ async function resolveApplyTimeIdentities(blueprint, client, log) {
|
|
|
14300
14482
|
function actionOf(created) {
|
|
14301
14483
|
return created ? "created" : "reconciled";
|
|
14302
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
|
+
}
|
|
14303
14547
|
var LostKeyError = class extends Error {
|
|
14304
14548
|
constructor(keyName) {
|
|
14305
14549
|
super(
|
|
@@ -14310,6 +14554,20 @@ var LostKeyError = class extends Error {
|
|
|
14310
14554
|
}
|
|
14311
14555
|
keyName;
|
|
14312
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
|
+
};
|
|
14313
14571
|
function mapEnumValues(enumValues) {
|
|
14314
14572
|
if (!enumValues) return void 0;
|
|
14315
14573
|
return enumValues.map((value) => ({ value }));
|
|
@@ -14384,7 +14642,8 @@ function toCapabilities(capabilities) {
|
|
|
14384
14642
|
function toRoleScopeClauses(clauses) {
|
|
14385
14643
|
return clauses.map((c) => ({
|
|
14386
14644
|
allowed_actions: c.allowedActions,
|
|
14387
|
-
data_scope: c.dataScope
|
|
14645
|
+
data_scope: c.dataScope,
|
|
14646
|
+
granted_capabilities: c.capabilities
|
|
14388
14647
|
}));
|
|
14389
14648
|
}
|
|
14390
14649
|
var UnresolvedIdentitiesError = class extends Error {
|
|
@@ -14404,11 +14663,18 @@ function assertNoUnresolvedIdentities(blueprint) {
|
|
|
14404
14663
|
}
|
|
14405
14664
|
}
|
|
14406
14665
|
async function runLoader(client, input) {
|
|
14407
|
-
const {
|
|
14408
|
-
|
|
14409
|
-
|
|
14410
|
-
|
|
14411
|
-
|
|
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);
|
|
14412
14678
|
assertNoUnresolvedIdentities(blueprint);
|
|
14413
14679
|
const ctxResp = await client.auth.createAppContext({ body: { contextId: blueprint.contextId, name: contextNameOf(blueprint) } });
|
|
14414
14680
|
const contextAction = actionOf(ctxResp.created);
|
|
@@ -14426,10 +14692,42 @@ async function runLoader(client, input) {
|
|
|
14426
14692
|
if (!userId) {
|
|
14427
14693
|
throw new Error("Service-principal create returned no user id; cannot bind AccessProfile/key.");
|
|
14428
14694
|
}
|
|
14429
|
-
|
|
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
|
+
}
|
|
14430
14728
|
const issuerActions = [];
|
|
14431
14729
|
for (const issuer of blueprint.issuers ?? []) {
|
|
14432
|
-
const issuerResp = await
|
|
14730
|
+
const issuerResp = await ctxBootstrapClient.auth.registerIssuer({
|
|
14433
14731
|
issuerId: issuer.issuerId,
|
|
14434
14732
|
issuer: issuer.issuer,
|
|
14435
14733
|
jwksUri: issuer.jwksUri,
|
|
@@ -14499,7 +14797,10 @@ async function runLoader(client, input) {
|
|
|
14499
14797
|
// literal JSON null, which is exactly what the platform's scope
|
|
14500
14798
|
// matcher tests via `allowedValues.contains(null)`. The cast bridges
|
|
14501
14799
|
// to the SDK's loosely-typed Record<string, Record<string,unknown>>.
|
|
14502
|
-
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
|
|
14503
14804
|
}
|
|
14504
14805
|
],
|
|
14505
14806
|
// Optional identity overrides — scope values the service key STAMPS onto
|
|
@@ -14557,7 +14858,7 @@ async function runLoader(client, input) {
|
|
|
14557
14858
|
keyId = keyResp.keyId ?? "";
|
|
14558
14859
|
rawKey = keyResp.rawKey;
|
|
14559
14860
|
if (keyId) {
|
|
14560
|
-
await persistMintedKey(blueprint.contextId, { keyId, rawKey, keyName, principalId: userId
|
|
14861
|
+
await persistMintedKey(blueprint.contextId, { keyId, rawKey, keyName, principalId: `usr_${userId}`, revokedKeyId });
|
|
14561
14862
|
}
|
|
14562
14863
|
if (keyAction !== "rotated") keyAction = "created";
|
|
14563
14864
|
} else {
|
|
@@ -14631,10 +14932,12 @@ async function runLoader(client, input) {
|
|
|
14631
14932
|
keyName,
|
|
14632
14933
|
keyAction,
|
|
14633
14934
|
contextAction,
|
|
14935
|
+
userAction,
|
|
14634
14936
|
profileAction,
|
|
14635
14937
|
schemaActions,
|
|
14636
14938
|
roleActions,
|
|
14637
14939
|
issuerActions,
|
|
14940
|
+
namespaceActions,
|
|
14638
14941
|
rawKey,
|
|
14639
14942
|
keyId,
|
|
14640
14943
|
revokedKeyId,
|
|
@@ -14653,6 +14956,15 @@ function buildPlan(blueprint, opts = {}) {
|
|
|
14653
14956
|
kind: "principal",
|
|
14654
14957
|
summary: `principal '${blueprint.servicePrincipal.externalId}' (SERVICE)`
|
|
14655
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
|
+
}
|
|
14656
14968
|
for (const iss of blueprint.issuers ?? []) {
|
|
14657
14969
|
const selfSignup = iss.selfSignupPolicies?.length ? `, self-signup: ${iss.selfSignupPolicies.length} polic${iss.selfSignupPolicies.length === 1 ? "y" : "ies"}` : "";
|
|
14658
14970
|
steps.push({
|
|
@@ -14675,15 +14987,18 @@ function buildPlan(blueprint, opts = {}) {
|
|
|
14675
14987
|
}
|
|
14676
14988
|
const actions = blueprint.accessProfile.allowedActions;
|
|
14677
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(", ")}]` : "";
|
|
14678
14991
|
steps.push({
|
|
14679
14992
|
kind: "profile",
|
|
14680
|
-
summary: `profile scopes [${actions.join(", ")}] (data-plane \u2713)${scopeNote}`
|
|
14993
|
+
summary: `profile scopes [${actions.join(", ")}] (data-plane \u2713)${scopeNote}${capabilityNote}`
|
|
14681
14994
|
});
|
|
14682
14995
|
for (const [roleId, clauses] of Object.entries(blueprint.roles ?? {})) {
|
|
14683
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(", ")}]` : "";
|
|
14684
14999
|
steps.push({
|
|
14685
15000
|
kind: "role",
|
|
14686
|
-
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})`
|
|
14687
15002
|
});
|
|
14688
15003
|
}
|
|
14689
15004
|
steps.push({ kind: "key", summary: `key ${blueprint.name}-<machine> (mint 1 scoped ssk_*)` });
|
|
@@ -14710,6 +15025,13 @@ function renderPlan(blueprint, steps) {
|
|
|
14710
15025
|
` issuer is left unchanged, not reconciled to match.`
|
|
14711
15026
|
);
|
|
14712
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
|
+
}
|
|
14713
15035
|
const legend = legendLines.join("\n");
|
|
14714
15036
|
const lines = steps.map((s) => ` + ${s.summary}`);
|
|
14715
15037
|
return [header, legend, "", ...lines].join("\n");
|
|
@@ -14958,6 +15280,15 @@ var OsKeychainSecretStore = class {
|
|
|
14958
15280
|
* against the real Windows store on every MR — the dev-machine probe is a point
|
|
14959
15281
|
* measurement, not a regression gate. (Tracked internally; this file ships to
|
|
14960
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.
|
|
14961
15292
|
*/
|
|
14962
15293
|
async deleteSecret(ref) {
|
|
14963
15294
|
try {
|
|
@@ -15891,6 +16222,26 @@ The key itself WAS created server-side and is live \u2014 its id is ${entry.keyI
|
|
|
15891
16222
|
const entry = await this.getActive();
|
|
15892
16223
|
return entry ? this.readSecret(entry) : void 0;
|
|
15893
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
|
+
}
|
|
15894
16245
|
/**
|
|
15895
16246
|
* Read one entry's secret, moving it to this machine's preferred store if it is
|
|
15896
16247
|
* somewhere else.
|
|
@@ -16017,26 +16368,31 @@ var readline = __toESM(require("readline/promises"));
|
|
|
16017
16368
|
async function runKeyringList(opts, deps = {}) {
|
|
16018
16369
|
try {
|
|
16019
16370
|
const keyring = deps.keyring ?? await Keyring.open();
|
|
16020
|
-
const entries = await keyring.list();
|
|
16021
|
-
const active = await keyring.activeAlias();
|
|
16022
16371
|
if (opts.json) {
|
|
16372
|
+
const entries = await keyring.list();
|
|
16373
|
+
const active = await keyring.activeAlias();
|
|
16023
16374
|
process.stdout.write(`${formatJson({ active, entries })}
|
|
16024
16375
|
`);
|
|
16025
16376
|
return 0;
|
|
16026
16377
|
}
|
|
16027
|
-
|
|
16378
|
+
const { health } = await assessEntries(keyring);
|
|
16379
|
+
if (health.length === 0) {
|
|
16028
16380
|
process.stdout.write("\n (no keyring entries yet \u2014 run `vectros join <context>` to add one)\n");
|
|
16029
16381
|
return 0;
|
|
16030
16382
|
}
|
|
16031
16383
|
process.stdout.write(
|
|
16032
|
-
"\n" + formatTable(
|
|
16033
|
-
{ header: "", get: (
|
|
16034
|
-
{ header: "alias", get: (
|
|
16035
|
-
{ header: "context", get: (
|
|
16036
|
-
{ header: "principal", get: (
|
|
16037
|
-
{ header: "role", get: (
|
|
16038
|
-
|
|
16039
|
-
|
|
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 }
|
|
16040
16396
|
]) + "\n"
|
|
16041
16397
|
);
|
|
16042
16398
|
return 0;
|
|
@@ -16218,6 +16574,10 @@ async function assessEntries(keyring) {
|
|
|
16218
16574
|
}))
|
|
16219
16575
|
};
|
|
16220
16576
|
}
|
|
16577
|
+
function tenantCell(h, opts) {
|
|
16578
|
+
if (h.status !== "ok") return void 0;
|
|
16579
|
+
return h.tenant ?? (opts.upper ? "UNKNOWN" : "unknown");
|
|
16580
|
+
}
|
|
16221
16581
|
function describeEntry(h) {
|
|
16222
16582
|
return `${h.entry.alias} (${h.entry.context}, ${h.entry.keyId})`;
|
|
16223
16583
|
}
|
|
@@ -16362,9 +16722,7 @@ async function runKeyringDoctor(opts, deps = {}) {
|
|
|
16362
16722
|
// `unknown` can escalate; one that sees a confident wrong answer cannot.
|
|
16363
16723
|
store: storeKindOf(h.entry) ?? "unknown",
|
|
16364
16724
|
secret: h.status,
|
|
16365
|
-
|
|
16366
|
-
// live key" from "we could not read the shape of this one".
|
|
16367
|
-
tenant: h.status === "ok" ? h.tenant ?? "unknown" : void 0
|
|
16725
|
+
tenant: tenantCell(h, { upper: false })
|
|
16368
16726
|
})),
|
|
16369
16727
|
liveKeys: liveKeys.map((h) => h.entry.alias),
|
|
16370
16728
|
findings
|
|
@@ -16379,7 +16737,7 @@ async function runKeyringDoctor(opts, deps = {}) {
|
|
|
16379
16737
|
{ header: "", get: (h) => h.active ? "*" : "" },
|
|
16380
16738
|
{ header: "alias", get: (h) => h.entry.alias },
|
|
16381
16739
|
{ header: "context", get: (h) => h.entry.context },
|
|
16382
|
-
{ header: "tenant", get: (h) => h
|
|
16740
|
+
{ header: "tenant", get: (h) => tenantCell(h, { upper: true }) },
|
|
16383
16741
|
{ header: "key id", get: (h) => h.entry.keyId },
|
|
16384
16742
|
{ header: "secret", get: (h) => h.status === "ok" ? "ok" : h.status.toUpperCase() }
|
|
16385
16743
|
]) + "\n"
|
|
@@ -16573,6 +16931,47 @@ function keyringAliasFor(alias, tenant) {
|
|
|
16573
16931
|
}
|
|
16574
16932
|
return tenant === "test" ? `${alias}.test` : alias;
|
|
16575
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
|
+
}
|
|
16576
16975
|
function nowIso() {
|
|
16577
16976
|
return (/* @__PURE__ */ new Date()).toISOString();
|
|
16578
16977
|
}
|
|
@@ -16669,10 +17068,7 @@ async function storeMintedKey(keyring, binding, secret, opts = {}) {
|
|
|
16669
17068
|
// src/cli-bootstrap.ts
|
|
16670
17069
|
function validateBlueprint(input) {
|
|
16671
17070
|
const blueprint = parseBlueprint(input);
|
|
16672
|
-
|
|
16673
|
-
for (const clauses of Object.values(blueprint.roles ?? {})) {
|
|
16674
|
-
for (const clause of clauses) assertScopeGate(clause.allowedActions);
|
|
16675
|
-
}
|
|
17071
|
+
assertBlueprintGates(blueprint);
|
|
16676
17072
|
return blueprint;
|
|
16677
17073
|
}
|
|
16678
17074
|
var BLANK_SLATE = {
|
|
@@ -16694,6 +17090,7 @@ function parseBootstrapArgs(argv) {
|
|
|
16694
17090
|
yes: false,
|
|
16695
17091
|
noMerge: false,
|
|
16696
17092
|
rotate: false,
|
|
17093
|
+
confirmExistingPrincipal: false,
|
|
16697
17094
|
useScopedToken: false,
|
|
16698
17095
|
blankSlate: false,
|
|
16699
17096
|
noSeed: false,
|
|
@@ -16745,6 +17142,9 @@ function parseBootstrapArgs(argv) {
|
|
|
16745
17142
|
case "--rotate":
|
|
16746
17143
|
args.rotate = true;
|
|
16747
17144
|
break;
|
|
17145
|
+
case "--confirm-existing-principal":
|
|
17146
|
+
args.confirmExistingPrincipal = true;
|
|
17147
|
+
break;
|
|
16748
17148
|
case "--use-scoped-token":
|
|
16749
17149
|
args.useScopedToken = true;
|
|
16750
17150
|
break;
|
|
@@ -16766,6 +17166,12 @@ function parseBootstrapArgs(argv) {
|
|
|
16766
17166
|
case "--test":
|
|
16767
17167
|
args.tenant = "test";
|
|
16768
17168
|
break;
|
|
17169
|
+
case "--activate":
|
|
17170
|
+
args.activate = true;
|
|
17171
|
+
break;
|
|
17172
|
+
case "--no-activate":
|
|
17173
|
+
args.activate = false;
|
|
17174
|
+
break;
|
|
16769
17175
|
case "--help":
|
|
16770
17176
|
case "-h":
|
|
16771
17177
|
args.help = true;
|
|
@@ -16783,6 +17189,26 @@ function seedDeclined(answer) {
|
|
|
16783
17189
|
const a = answer.trim().toLowerCase();
|
|
16784
17190
|
return a === "n" || a === "no";
|
|
16785
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
|
+
}
|
|
16786
17212
|
function seedSummaryLine(opts) {
|
|
16787
17213
|
if (opts.noSeed) {
|
|
16788
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";
|
|
@@ -16819,16 +17245,28 @@ function issuerTally(items) {
|
|
|
16819
17245
|
if (unchanged) seg.push(`${unchanged} already registered \u2014 unchanged (issuers cannot be updated once registered)`);
|
|
16820
17246
|
return `${items.length} issuer${items.length === 1 ? "" : "s"} (${seg.join(", ")})`;
|
|
16821
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
|
+
}
|
|
16822
17257
|
function applyOutcomeSummary(result) {
|
|
16823
17258
|
const parts = [
|
|
16824
17259
|
reconcileTally(result.schemaActions, "schema"),
|
|
16825
17260
|
reconcileTally(result.roleActions, "role"),
|
|
16826
17261
|
issuerTally(result.issuerActions),
|
|
17262
|
+
namespaceTally(result.namespaceActions),
|
|
16827
17263
|
`profile ${result.profileAction}`
|
|
16828
17264
|
].filter((p) => p !== null);
|
|
16829
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
|
+
` : "";
|
|
16830
17268
|
return ` \u2714 context '${result.contextId}' ${result.contextAction}${detail}
|
|
16831
|
-
`;
|
|
17269
|
+
${principalNote}`;
|
|
16832
17270
|
}
|
|
16833
17271
|
var BOOTSTRAP_HELP = `vectros bootstrap \u2014 provision a least-privilege MCP credential.
|
|
16834
17272
|
|
|
@@ -16836,7 +17274,7 @@ Usage:
|
|
|
16836
17274
|
vectros bootstrap [--blueprint <name> | --blank] [--no-seed]
|
|
16837
17275
|
[--set name=value ...] [--values <file>]
|
|
16838
17276
|
[--tenant test|live | --test] [--client desktop|code]
|
|
16839
|
-
[--yes] [--no-merge] [--rotate]
|
|
17277
|
+
[--yes] [--no-merge] [--rotate] [--confirm-existing-principal]
|
|
16840
17278
|
[--token <t> | env VECTROS_BOOTSTRAP_TOKEN]
|
|
16841
17279
|
[--base-url <url>] [--use-scoped-token]
|
|
16842
17280
|
|
|
@@ -16859,6 +17297,10 @@ Options:
|
|
|
16859
17297
|
The tenant rides the key (ssk_test_* vs ssk_live_*), so
|
|
16860
17298
|
the MCP/client config is otherwise identical.
|
|
16861
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.
|
|
16862
17304
|
--base-url <url> Override the API base URL (beats VECTROS_API_BASE_URL).
|
|
16863
17305
|
--client <desktop|code> MCP host to configure (default: desktop). 'desktop' merges
|
|
16864
17306
|
claude_desktop_config.json (also works for Cursor/Cline);
|
|
@@ -16873,6 +17315,13 @@ Options:
|
|
|
16873
17315
|
print the config snippet instead of merging it into your client's
|
|
16874
17316
|
config file. Not a dry run; see 'blueprint plan' for that.
|
|
16875
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.
|
|
16876
17325
|
--use-scoped-token Legacy fallback.
|
|
16877
17326
|
NOTE: it mints a token WITHOUT the provisioning
|
|
16878
17327
|
capability, so it cannot create a blueprint's
|
|
@@ -16960,6 +17409,22 @@ function buildRemintForContext(opts) {
|
|
|
16960
17409
|
});
|
|
16961
17410
|
};
|
|
16962
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
|
+
}
|
|
16963
17428
|
async function runBootstrapCli(argv, deps = {}) {
|
|
16964
17429
|
const log = deps.log ?? createLogger2();
|
|
16965
17430
|
let args;
|
|
@@ -16993,6 +17458,15 @@ ${BOOTSTRAP_HELP}
|
|
|
16993
17458
|
\u2716 Scope gate refused blueprint '${blueprint.name}':
|
|
16994
17459
|
`);
|
|
16995
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}
|
|
16996
17470
|
`);
|
|
16997
17471
|
process.stderr.write(" Minting nothing.\n");
|
|
16998
17472
|
return 2;
|
|
@@ -17004,6 +17478,11 @@ ${BOOTSTRAP_HELP}
|
|
|
17004
17478
|
Blueprint: ${blueprint.name}
|
|
17005
17479
|
\u2714 Scope gate: [${gate.accepted.join(", ")}] \u2014 all data-plane \u2713
|
|
17006
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
|
+
}
|
|
17007
17486
|
let noSeed = args.noSeed;
|
|
17008
17487
|
let seedDeclinedInteractively = false;
|
|
17009
17488
|
if (shouldOfferSeedOptOut(args, blueprint, isTty)) {
|
|
@@ -17077,6 +17556,7 @@ ${renderPlan(blueprint, planSteps)}
|
|
|
17077
17556
|
keyringOpenError = err;
|
|
17078
17557
|
}
|
|
17079
17558
|
let storedFreshInKeyring = false;
|
|
17559
|
+
const lastResolvedSecret = /* @__PURE__ */ new Map();
|
|
17080
17560
|
const readExistingKey = async (contextId) => {
|
|
17081
17561
|
if (!keyring) return void 0;
|
|
17082
17562
|
const alias = keyringAliasFor(contextId, args.tenant);
|
|
@@ -17084,7 +17564,8 @@ ${renderPlan(blueprint, planSteps)}
|
|
|
17084
17564
|
const existing = await keyring.get(alias);
|
|
17085
17565
|
if (!existing?.keyId) return void 0;
|
|
17086
17566
|
const rawKey = await keyring.resolveSecret(alias);
|
|
17087
|
-
|
|
17567
|
+
lastResolvedSecret.set(alias, rawKey);
|
|
17568
|
+
return rawKey ? { keyId: existing.keyId, rawKey, principalId: existing.principalId } : void 0;
|
|
17088
17569
|
} catch {
|
|
17089
17570
|
return void 0;
|
|
17090
17571
|
}
|
|
@@ -17103,6 +17584,11 @@ ${renderPlan(blueprint, planSteps)}
|
|
|
17103
17584
|
const alias = keyringAliasFor(contextId, args.tenant);
|
|
17104
17585
|
try {
|
|
17105
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
|
+
});
|
|
17106
17592
|
await storeMintedKey(
|
|
17107
17593
|
keyring,
|
|
17108
17594
|
{
|
|
@@ -17117,11 +17603,14 @@ ${renderPlan(blueprint, planSteps)}
|
|
|
17117
17603
|
env: deriveEnvironment(baseUrl) ?? args.env
|
|
17118
17604
|
},
|
|
17119
17605
|
record.rawKey,
|
|
17120
|
-
{ activate:
|
|
17606
|
+
{ activate: willActivate, revokedKeyId: record.revokedKeyId }
|
|
17121
17607
|
);
|
|
17122
17608
|
if (record.revokedKeyId) {
|
|
17123
17609
|
await warnOtherAliasesNaming(keyring, record.revokedKeyId, alias);
|
|
17124
17610
|
}
|
|
17611
|
+
process.stdout.write(
|
|
17612
|
+
tenantActivateMessage({ alias, tenant: args.tenant, willActivate, explicitlyDeclined: args.activate === false })
|
|
17613
|
+
);
|
|
17125
17614
|
storedFreshInKeyring = true;
|
|
17126
17615
|
} catch (err) {
|
|
17127
17616
|
process.stderr.write(
|
|
@@ -17142,12 +17631,20 @@ ${renderPlan(blueprint, planSteps)}
|
|
|
17142
17631
|
skipSeed: noSeed,
|
|
17143
17632
|
readExistingKey,
|
|
17144
17633
|
persistMintedKey,
|
|
17634
|
+
remintBootstrapForContext: buildRemintBootstrapForContext({
|
|
17635
|
+
cognitoToken,
|
|
17636
|
+
apiBaseUrl: baseUrl,
|
|
17637
|
+
tenant: args.tenant,
|
|
17638
|
+
useScopedToken: args.useScopedToken,
|
|
17639
|
+
mintBridge
|
|
17640
|
+
}),
|
|
17145
17641
|
remintForContext: buildRemintForContext({
|
|
17146
17642
|
cognitoToken,
|
|
17147
17643
|
apiBaseUrl: baseUrl,
|
|
17148
17644
|
tenant: args.tenant,
|
|
17149
17645
|
mintContext
|
|
17150
17646
|
}),
|
|
17647
|
+
confirmExistingPrincipal: buildConfirmExistingPrincipal(args, promptCtx.prompt, isTty),
|
|
17151
17648
|
log
|
|
17152
17649
|
});
|
|
17153
17650
|
process.stdout.write(
|
|
@@ -17225,6 +17722,18 @@ ${codeAddHint}`);
|
|
|
17225
17722
|
`);
|
|
17226
17723
|
return 3;
|
|
17227
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
|
+
}
|
|
17228
17737
|
if (err instanceof BlueprintIdentityError || err instanceof UnresolvedIdentitiesError) {
|
|
17229
17738
|
process.stderr.write(`
|
|
17230
17739
|
\u2716 ${err.message}
|
|
@@ -17445,6 +17954,10 @@ function lintBlueprint(bp) {
|
|
|
17445
17954
|
}
|
|
17446
17955
|
}
|
|
17447
17956
|
}
|
|
17957
|
+
{
|
|
17958
|
+
const reached = ownerScopedReachWithoutDataScope(bp.accessProfile.allowedActions, ds);
|
|
17959
|
+
if (reached.length > 0) findings.push(unconstrainedReachFinding("accessProfile", reached));
|
|
17960
|
+
}
|
|
17448
17961
|
for (const roleId of Object.keys(bp.roles ?? {})) {
|
|
17449
17962
|
if (!ROLE_ID_RE.test(roleId)) {
|
|
17450
17963
|
findings.push({
|
|
@@ -17457,6 +17970,10 @@ function lintBlueprint(bp) {
|
|
|
17457
17970
|
for (const [roleId, clauses] of Object.entries(bp.roles ?? {})) {
|
|
17458
17971
|
clauses.forEach((clause, i) => {
|
|
17459
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
|
+
}
|
|
17460
17977
|
if (!cds) return;
|
|
17461
17978
|
for (const [key, values] of Object.entries(cds)) {
|
|
17462
17979
|
for (const value of values) {
|
|
@@ -17509,6 +18026,25 @@ function placeholderFinding(where, key, value) {
|
|
|
17509
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.`
|
|
17510
18027
|
};
|
|
17511
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
|
+
}
|
|
17512
18048
|
function lintPasses(findings) {
|
|
17513
18049
|
return !findings.some((f) => f.severity === "error");
|
|
17514
18050
|
}
|
|
@@ -17547,8 +18083,12 @@ schemas:
|
|
|
17547
18083
|
lookupFields: [title] # extra O(1) exact-match lookup indexes (max 10)
|
|
17548
18084
|
|
|
17549
18085
|
accessProfile:
|
|
17550
|
-
# SCOPE GATE: data-plane only (records/schemas/search/documents/folders/inference/entities)
|
|
17551
|
-
#
|
|
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/*.
|
|
17552
18092
|
# Ops: c|r|u|d, combinable (records:cru). Prefer the narrowest set (least privilege).
|
|
17553
18093
|
allowedActions: [records:r, records:c, records:u, search:r, schemas:r]
|
|
17554
18094
|
# dataScope binds the key to specific owners. Include null to ALSO see
|
|
@@ -17556,6 +18096,17 @@ accessProfile:
|
|
|
17556
18096
|
# owners' records, not shared/seed data. Keys are userId (the principal) and
|
|
17557
18097
|
# namespaced scopes (scope:org, scope:client, ...):
|
|
17558
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]
|
|
17559
18110
|
|
|
17560
18111
|
servicePrincipal:
|
|
17561
18112
|
externalId: ${name}-sp # stable; the principal the profile + key bind to
|
|
@@ -17675,15 +18226,19 @@ async function resolveAndValidate(arg, verb, supplied) {
|
|
|
17675
18226
|
return null;
|
|
17676
18227
|
}
|
|
17677
18228
|
try {
|
|
17678
|
-
|
|
17679
|
-
for (const clauses of Object.values(blueprint.roles ?? {})) {
|
|
17680
|
-
for (const clause of clauses) assertScopeGate(clause.allowedActions);
|
|
17681
|
-
}
|
|
18229
|
+
assertBlueprintGates(blueprint);
|
|
17682
18230
|
} catch (err) {
|
|
17683
18231
|
if (err instanceof ScopeGateError) {
|
|
17684
18232
|
process.stderr.write(` \u2716 Scope gate rejected '${blueprint.name}':
|
|
17685
18233
|
`);
|
|
17686
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}
|
|
17687
18242
|
`);
|
|
17688
18243
|
return null;
|
|
17689
18244
|
}
|
|
@@ -17715,9 +18270,10 @@ async function runValidate(args) {
|
|
|
17715
18270
|
const blueprint = await resolveAndValidate(args.file, "validate", supplied);
|
|
17716
18271
|
if (!blueprint) return 2;
|
|
17717
18272
|
const gate = evaluateScopeGate(blueprint.accessProfile.allowedActions);
|
|
18273
|
+
const capGate = evaluateCapabilityGate(blueprint.accessProfile.capabilities);
|
|
17718
18274
|
const warnCount = lintBlueprint(blueprint).length;
|
|
17719
18275
|
process.stdout.write(
|
|
17720
|
-
` \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"
|
|
17721
18277
|
);
|
|
17722
18278
|
return 0;
|
|
17723
18279
|
}
|
|
@@ -18048,7 +18604,14 @@ async function resolveKeyDestination(opts, deps) {
|
|
|
18048
18604
|
`);
|
|
18049
18605
|
return 2;
|
|
18050
18606
|
}
|
|
18051
|
-
|
|
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
|
+
}
|
|
18052
18615
|
if (opts.activate === false && await keyring.activeAlias() === alias) {
|
|
18053
18616
|
process.stderr.write(
|
|
18054
18617
|
` \u2716 --no-activate cannot be used on '${alias}': it is your ACTIVE identity, and storing
|
|
@@ -18069,6 +18632,8 @@ async function storeAndReport(dest, key, auth, opts) {
|
|
|
18069
18632
|
return 1;
|
|
18070
18633
|
}
|
|
18071
18634
|
const principalId = opts.principalId;
|
|
18635
|
+
const tenant = keyTenant(key.rawKey) ?? "test";
|
|
18636
|
+
const willActivate = await determineActivateDecision(dest.keyring, { tenant, explicitActivate: opts.activate });
|
|
18072
18637
|
let entry;
|
|
18073
18638
|
try {
|
|
18074
18639
|
warnReplacingAlias(await dest.keyring.get(dest.alias), dest.alias, opts.revokedKeyId);
|
|
@@ -18083,7 +18648,7 @@ async function storeAndReport(dest, key, auth, opts) {
|
|
|
18083
18648
|
env: auth.env
|
|
18084
18649
|
},
|
|
18085
18650
|
key.rawKey,
|
|
18086
|
-
{ activate:
|
|
18651
|
+
{ activate: willActivate, revokedKeyId: opts.revokedKeyId }
|
|
18087
18652
|
));
|
|
18088
18653
|
} catch (err) {
|
|
18089
18654
|
process.stderr.write(
|
|
@@ -18095,7 +18660,6 @@ async function storeAndReport(dest, key, auth, opts) {
|
|
|
18095
18660
|
);
|
|
18096
18661
|
return 1;
|
|
18097
18662
|
}
|
|
18098
|
-
const active = opts.activate !== false;
|
|
18099
18663
|
process.stdout.write(
|
|
18100
18664
|
`
|
|
18101
18665
|
\u2714 Stored the new key in your ${describeEntryStoreNoun(entry)} \u2014 it was not printed.
|
|
@@ -18105,9 +18669,19 @@ async function storeAndReport(dest, key, auth, opts) {
|
|
|
18105
18669
|
["principal", entry.principalId || void 0],
|
|
18106
18670
|
["key id", entry.keyId],
|
|
18107
18671
|
["env", entry.env],
|
|
18108
|
-
["active",
|
|
18109
|
-
]) + "\n\n" +
|
|
18110
|
-
|
|
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
|
+
})
|
|
18111
18685
|
);
|
|
18112
18686
|
return 0;
|
|
18113
18687
|
}
|
|
@@ -19641,6 +20215,19 @@ async function runAssertions(client, bootstrapClient, deps, blueprint, tenantId,
|
|
|
19641
20215
|
assertions.push({ name: `issuer:${issuerId}`, ok: false, detail: `getIssuer(${issuerId}) failed: ${errMsg(e)}` });
|
|
19642
20216
|
}
|
|
19643
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
|
+
}
|
|
19644
20231
|
return assertions;
|
|
19645
20232
|
}
|
|
19646
20233
|
async function runTeardown(client, bootstrapClient, blueprint, machineId, snapshot, result) {
|
|
@@ -19753,10 +20340,18 @@ async function runTeardown(client, bootstrapClient, blueprint, machineId, snapsh
|
|
|
19753
20340
|
steps.push({ step: `delete issuer ${issuerId}`, ok: false, error: errMsg(e) });
|
|
19754
20341
|
}
|
|
19755
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
|
+
}
|
|
19756
20351
|
return steps;
|
|
19757
20352
|
}
|
|
19758
20353
|
async function runBlueprintHarness(client, input, deps) {
|
|
19759
|
-
const { tenantId, machineId, keep = false, log, remintForContext } = input;
|
|
20354
|
+
const { tenantId, machineId, keep = false, log, remintForContext, remintBootstrapForContext } = input;
|
|
19760
20355
|
const readExistingKey = async () => void 0;
|
|
19761
20356
|
const persistMintedKey = async () => {
|
|
19762
20357
|
};
|
|
@@ -19766,6 +20361,9 @@ async function runBlueprintHarness(client, input, deps) {
|
|
|
19766
20361
|
log
|
|
19767
20362
|
);
|
|
19768
20363
|
const remint = remintForContext ?? (async () => client);
|
|
20364
|
+
const remintBootstrap = remintBootstrapForContext ?? (async () => client);
|
|
20365
|
+
const hasIssuers = (blueprint.issuers ?? []).length > 0;
|
|
20366
|
+
const hasNamespaces = (blueprint.namespaces ?? []).length > 0;
|
|
19769
20367
|
let contextSnapshot;
|
|
19770
20368
|
if (remintForContext) {
|
|
19771
20369
|
try {
|
|
@@ -19781,7 +20379,18 @@ async function runBlueprintHarness(client, input, deps) {
|
|
|
19781
20379
|
} else {
|
|
19782
20380
|
contextSnapshot = await captureSnapshot(client, blueprint);
|
|
19783
20381
|
}
|
|
19784
|
-
|
|
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);
|
|
20393
|
+
}
|
|
19785
20394
|
const snapshot = {
|
|
19786
20395
|
...contextSnapshot,
|
|
19787
20396
|
existingIssuerIds: issuerSnapshot.existingIssuerIds,
|
|
@@ -19791,9 +20400,25 @@ async function runBlueprintHarness(client, input, deps) {
|
|
|
19791
20400
|
let assertions = [];
|
|
19792
20401
|
let applyError;
|
|
19793
20402
|
try {
|
|
19794
|
-
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
|
+
});
|
|
19795
20413
|
log.debug({ component: "harness", contextId: result.contextId, keyName: result.keyName }, "applied");
|
|
19796
|
-
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
|
+
);
|
|
19797
20422
|
} catch (e) {
|
|
19798
20423
|
applyError = errMsg(e);
|
|
19799
20424
|
log.warn({ component: "harness", err: applyError }, "apply phase failed");
|
|
@@ -19805,7 +20430,14 @@ async function runBlueprintHarness(client, input, deps) {
|
|
|
19805
20430
|
tdClient = await remint(blueprint.contextId);
|
|
19806
20431
|
} catch {
|
|
19807
20432
|
}
|
|
19808
|
-
|
|
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);
|
|
19809
20441
|
}
|
|
19810
20442
|
const passed = applyError === void 0 && assertions.length > 0 && assertions.every((a) => a.ok);
|
|
19811
20443
|
return {
|
|
@@ -19879,6 +20511,13 @@ async function runBlueprintTest(fileOrName, opts, deps = {}) {
|
|
|
19879
20511
|
process.stderr.write(` \u2716 Scope gate refused blueprint:
|
|
19880
20512
|
`);
|
|
19881
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}
|
|
19882
20521
|
`);
|
|
19883
20522
|
return 2;
|
|
19884
20523
|
}
|
|
@@ -19914,9 +20553,22 @@ async function runBlueprintTest(fileOrName, opts, deps = {}) {
|
|
|
19914
20553
|
environment: auth.baseUrl
|
|
19915
20554
|
});
|
|
19916
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
|
+
};
|
|
19917
20569
|
const report = await runBlueprintHarness(
|
|
19918
20570
|
client,
|
|
19919
|
-
{ blueprint, tenantId, machineId, keep: opts.keep, log, remintForContext },
|
|
20571
|
+
{ blueprint, tenantId, machineId, keep: opts.keep, log, remintForContext, remintBootstrapForContext },
|
|
19920
20572
|
{ pingWithKey }
|
|
19921
20573
|
);
|
|
19922
20574
|
if (opts.json) {
|
|
@@ -19997,9 +20649,10 @@ async function runJoin(opts, deps = {}) {
|
|
|
19997
20649
|
return 2;
|
|
19998
20650
|
}
|
|
19999
20651
|
const context = opts.contextArg;
|
|
20652
|
+
const tenant = opts.tenant ?? "live";
|
|
20000
20653
|
let alias;
|
|
20001
20654
|
try {
|
|
20002
|
-
alias = keyringAliasFor(opts.alias ?? context,
|
|
20655
|
+
alias = keyringAliasFor(opts.alias ?? context, tenant);
|
|
20003
20656
|
} catch (err) {
|
|
20004
20657
|
process.stderr.write(` \u2716 ${formatError(err)}
|
|
20005
20658
|
`);
|
|
@@ -20034,8 +20687,23 @@ async function runJoin(opts, deps = {}) {
|
|
|
20034
20687
|
const heldSecret = existingEntry ? await keyring.resolveSecret(alias) : void 0;
|
|
20035
20688
|
const canReuse = existingEntry && heldSecret && existingEntry.context === context && existingEntry.principalId === principalId && existingEntry.env === auth.env;
|
|
20036
20689
|
if (canReuse && existingEntry) {
|
|
20037
|
-
await keyring
|
|
20038
|
-
|
|
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
|
+
});
|
|
20039
20707
|
return 0;
|
|
20040
20708
|
}
|
|
20041
20709
|
warnReplacingAlias(existingEntry, alias);
|
|
@@ -20047,13 +20715,24 @@ async function runJoin(opts, deps = {}) {
|
|
|
20047
20715
|
);
|
|
20048
20716
|
}
|
|
20049
20717
|
if (revokedKeyId) await warnOtherAliasesNaming(keyring, revokedKeyId, alias);
|
|
20718
|
+
const willActivate = await determineActivateDecision(keyring, { tenant, explicitActivate: opts.activate });
|
|
20050
20719
|
const { entry } = await storeMintedKey(
|
|
20051
20720
|
keyring,
|
|
20052
20721
|
{ alias, context, principalId, role: opts.role, keyId: key.keyId, env: auth.env },
|
|
20053
20722
|
key.rawKey,
|
|
20054
|
-
{ revokedKeyId }
|
|
20723
|
+
{ activate: willActivate, revokedKeyId }
|
|
20055
20724
|
);
|
|
20056
|
-
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
|
+
});
|
|
20057
20736
|
return 0;
|
|
20058
20737
|
} catch (err) {
|
|
20059
20738
|
process.stderr.write(formatCliError(err, { action: "join failed" }));
|
|
@@ -20061,27 +20740,34 @@ async function runJoin(opts, deps = {}) {
|
|
|
20061
20740
|
}
|
|
20062
20741
|
});
|
|
20063
20742
|
}
|
|
20064
|
-
function emitJoined(
|
|
20065
|
-
|
|
20066
|
-
|
|
20067
|
-
|
|
20068
|
-
|
|
20069
|
-
|
|
20070
|
-
|
|
20071
|
-
|
|
20072
|
-
|
|
20073
|
-
|
|
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 ? `
|
|
20074
20753
|
|
|
20075
|
-
Now active. Use \`vectros switch ${alias}\` to return to it, \`vectros keyring show --format env\` to export the key for the MCP server.
|
|
20076
|
-
`
|
|
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
|
+
})
|
|
20077
20763
|
);
|
|
20078
20764
|
}
|
|
20079
20765
|
|
|
20080
20766
|
// src/build-info.ts
|
|
20081
20767
|
var BUILD_INFO = {
|
|
20082
|
-
cli: true ? "0.
|
|
20083
|
-
sdk: true ? "0.
|
|
20084
|
-
blueprints: true ? "0.
|
|
20768
|
+
cli: true ? "0.16.0" : "dev",
|
|
20769
|
+
sdk: true ? "0.40.0" : "dev",
|
|
20770
|
+
blueprints: true ? "0.13.0" : "dev"
|
|
20085
20771
|
};
|
|
20086
20772
|
function formatBuildInfo() {
|
|
20087
20773
|
return `vectros ${BUILD_INFO.cli} (sdk ${BUILD_INFO.sdk}, blueprints ${BUILD_INFO.blueprints})`;
|
|
@@ -20105,6 +20791,14 @@ function addCommonAuthOptions(cmd) {
|
|
|
20105
20791
|
"emit machine-readable JSON instead of the human-readable view"
|
|
20106
20792
|
);
|
|
20107
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
|
+
}
|
|
20108
20802
|
function toCommonOptions(flags) {
|
|
20109
20803
|
return {
|
|
20110
20804
|
env: flags.env,
|
|
@@ -20158,7 +20852,10 @@ function buildProgram(deps = {}) {
|
|
|
20158
20852
|
return program;
|
|
20159
20853
|
}
|
|
20160
20854
|
function registerJoinCommand(program, deps) {
|
|
20161
|
-
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");
|
|
20162
20859
|
addAuthOptions(join3).action(
|
|
20163
20860
|
async (contextArg, flags) => {
|
|
20164
20861
|
if (flags.tenant !== void 0 && flags.tenant !== "test" && flags.tenant !== "live") {
|
|
@@ -20176,7 +20873,8 @@ function registerJoinCommand(program, deps) {
|
|
|
20176
20873
|
as: flags.as,
|
|
20177
20874
|
scope: flags.scope,
|
|
20178
20875
|
alias: flags.alias,
|
|
20179
|
-
tenant: flags.tenant
|
|
20876
|
+
tenant: flags.tenant,
|
|
20877
|
+
activate: flags.activate
|
|
20180
20878
|
},
|
|
20181
20879
|
deps
|
|
20182
20880
|
)
|
|
@@ -20423,9 +21121,10 @@ function toKeyDestination(flags) {
|
|
|
20423
21121
|
function registerKeyCommands(program, deps) {
|
|
20424
21122
|
const key = program.command("key").description("Manage scoped API keys (ssk_*) \u2014 issue, list, revoke, rotate.");
|
|
20425
21123
|
const issue = addKeyDestinationOptions(
|
|
20426
|
-
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)")
|
|
20427
21125
|
);
|
|
20428
21126
|
addAuthOptions(issue).action(async (flags) => {
|
|
21127
|
+
const tenant = parseTenantFlag(flags.tenant);
|
|
20429
21128
|
process.exit(
|
|
20430
21129
|
await runKeyIssue({
|
|
20431
21130
|
...toCommonOptions(flags),
|
|
@@ -20433,6 +21132,7 @@ function registerKeyCommands(program, deps) {
|
|
|
20433
21132
|
context: flags.context,
|
|
20434
21133
|
name: flags.name,
|
|
20435
21134
|
label: flags.label,
|
|
21135
|
+
tenant,
|
|
20436
21136
|
...toKeyDestination(flags)
|
|
20437
21137
|
}, deps)
|
|
20438
21138
|
);
|
|
@@ -20440,37 +21140,50 @@ function registerKeyCommands(program, deps) {
|
|
|
20440
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(
|
|
20441
21141
|
"--all-contexts",
|
|
20442
21142
|
"list every key across ALL your contexts (owner-only) \u2014 the tenant-wide view for the key limit"
|
|
20443
|
-
);
|
|
21143
|
+
).option("--tenant <tenant>", "tenant to list: 'test' or 'live' (default live; not combinable with --all-contexts)");
|
|
20444
21144
|
addCommonAuthOptions(list).action(
|
|
20445
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);
|
|
20446
21151
|
process.exit(
|
|
20447
21152
|
await runKeyList(
|
|
20448
21153
|
{
|
|
20449
21154
|
...toCommonOptions(flags),
|
|
20450
21155
|
principal: flags.principal,
|
|
20451
21156
|
context: flags.context,
|
|
20452
|
-
allContexts: Boolean(flags.allContexts)
|
|
21157
|
+
allContexts: Boolean(flags.allContexts),
|
|
21158
|
+
tenant
|
|
20453
21159
|
},
|
|
20454
21160
|
deps
|
|
20455
21161
|
)
|
|
20456
21162
|
);
|
|
20457
21163
|
}
|
|
20458
21164
|
);
|
|
20459
|
-
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)");
|
|
20460
|
-
addCommonAuthOptions(get).action(
|
|
20461
|
-
|
|
20462
|
-
|
|
20463
|
-
|
|
20464
|
-
|
|
20465
|
-
|
|
20466
|
-
|
|
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
|
+
);
|
|
20467
21179
|
const rotate = addKeyDestinationOptions(
|
|
20468
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(
|
|
20469
21181
|
"--force-new",
|
|
20470
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)"
|
|
20471
|
-
)
|
|
21183
|
+
).option("--tenant <tenant>", "tenant the key lives in: 'test' or 'live' (default live)")
|
|
20472
21184
|
);
|
|
20473
21185
|
addAuthOptions(rotate).action(async (flags) => {
|
|
21186
|
+
const tenant = parseTenantFlag(flags.tenant);
|
|
20474
21187
|
process.exit(
|
|
20475
21188
|
await runKeyRotate({
|
|
20476
21189
|
...toCommonOptions(flags),
|
|
@@ -20478,6 +21191,7 @@ function registerKeyCommands(program, deps) {
|
|
|
20478
21191
|
context: flags.context,
|
|
20479
21192
|
name: flags.name,
|
|
20480
21193
|
forceNew: flags.forceNew,
|
|
21194
|
+
tenant,
|
|
20481
21195
|
...toKeyDestination(flags)
|
|
20482
21196
|
}, deps)
|
|
20483
21197
|
);
|