@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.mjs
CHANGED
|
@@ -4440,7 +4440,7 @@ var AuthClient = class {
|
|
|
4440
4440
|
);
|
|
4441
4441
|
}
|
|
4442
4442
|
/**
|
|
4443
|
-
* Creates a new access profile under the given app context. This call is idempotent by `principalId`: if a profile with the same `principalId` already exists, the existing profile is returned (with status 200) instead of creating a duplicate. The response's `created` field (and the HTTP status — 201 when created, 200 when an existing profile was returned) tells the two apart. To overwrite an existing profile's `scopes`/`roleId`, `identityOverrides`, and `status` instead of returning it unchanged, set `?upsert=true` (this also requires the `profiles:u` scope, and applies the same `identityOverrides` bounds the update endpoint documents — a scoped credential may not repoint or clear an identity value it does not itself hold). You must provide exactly one of `scopes` (an inline list of scopes) or `roleId` (a reference to a role); supplying both, or neither, is rejected. `identityOverrides` is keyed by ownership namespace in `scope:<namespace>` form — `scope:org` and `scope:client` for the reserved namespaces, or any namespace you have registered — and may name at most two; any other key (including the account identifier or `userId`) is rejected. If you use a scoped credential, the profile's effective scopes may not exceed your own; a root API key (`sk_`) is exempt. Requires the `profiles:c` scope.
|
|
4443
|
+
* Creates a new access profile under the given app context. This call is idempotent by `principalId`: if a profile with the same `principalId` already exists, the existing profile is returned (with status 200) instead of creating a duplicate. The response's `created` field (and the HTTP status — 201 when created, 200 when an existing profile was returned) tells the two apart. To overwrite an existing profile's `scopes`/`roleId`, `identityOverrides`, and `status` instead of returning it unchanged, set `?upsert=true` (this also requires the `profiles:u` scope, and applies the same `identityOverrides` bounds the update endpoint documents — a scoped credential may not repoint or clear an identity value it does not itself hold). The `principalId` must name a principal that already exists: a `usr_` principal must be a live user in your tenant, so create the user before granting it a profile. A `usr_` id that names no such user is rejected, and no profile is created. `key_` principals are not checked this way. You must provide exactly one of `scopes` (an inline list of scopes) or `roleId` (a reference to a role); supplying both, or neither, is rejected. `identityOverrides` is keyed by ownership namespace in `scope:<namespace>` form — `scope:org` and `scope:client` for the reserved namespaces, or any namespace you have registered — and may name at most two; any other key (including the account identifier or `userId`) is rejected. If you use a scoped credential, the profile's effective scopes may not exceed your own; a root API key (`sk_`) is exempt. Requires the `profiles:c` scope.
|
|
4444
4444
|
*
|
|
4445
4445
|
* @param {Vectros.CreateAccessProfileRequest} request
|
|
4446
4446
|
* @param {AuthClient.RequestOptions} requestOptions - Request-specific configuration.
|
|
@@ -5541,7 +5541,7 @@ var AuthClient = class {
|
|
|
5541
5541
|
);
|
|
5542
5542
|
}
|
|
5543
5543
|
/**
|
|
5544
|
-
* Returns full usage detail for the requested calendar month, broken down by category (search, documents, and records) with per-category credit estimates and a split between your live and test environments. Defaults to the current month when `year` and `month` are omitted. Requires the `billing:r` scope on scoped tokens; API keys always have access.
|
|
5544
|
+
* Returns full usage detail for the requested calendar month, broken down by category (search, documents, and records) with per-category credit estimates and a split between your live and test environments. Defaults to the current month when `year` and `month` are omitted. Requires the `billing:r` scope on scoped tokens; API keys always have access. **A token confined to a single app context sees only that context's usage**: totals, the environment split, and the `contexts` breakdown narrow to it, and the environment your context is not bound to is omitted (`null`), not zeroed. Only a token with cross-context reach sees your full account-wide totals. Two exceptions to the narrowing, since they have no per-context breakdown to narrow to: `reads.calls.used`/`reads.dataOut.bytes` (metered per account, not per context) read as `0` for a confined token rather than a narrowed figure — the corresponding overage-credit charge fields narrow correctly; and `credits.limit` stays your whole plan's ceiling, so `credits.remaining` may overstate the account's true remaining room.
|
|
5545
5545
|
*
|
|
5546
5546
|
* @param {Vectros.GetUsageRequest} request
|
|
5547
5547
|
* @param {AuthClient.RequestOptions} requestOptions - Request-specific configuration.
|
|
@@ -5604,7 +5604,7 @@ var AuthClient = class {
|
|
|
5604
5604
|
return handleNonStatusCodeError(_response.error, _response.rawResponse, "GET", "/v1/usage");
|
|
5605
5605
|
}
|
|
5606
5606
|
/**
|
|
5607
|
-
* Retrieves a single registered issuer by issuerId. Requires a root API key or the bootstrap's provisioning capability.
|
|
5607
|
+
* Retrieves a single registered issuer by issuerId. Requires a root API key or the bootstrap's provisioning capability. A credential confined to one app context sees only an issuer registered in that context; naming one registered in another context returns 404, identically to a nonexistent issuerId. A root API key sees every context.
|
|
5608
5608
|
*
|
|
5609
5609
|
* @param {Vectros.GetIssuerRequest} request
|
|
5610
5610
|
* @param {AuthClient.RequestOptions} requestOptions - Request-specific configuration.
|
|
@@ -5662,13 +5662,14 @@ var AuthClient = class {
|
|
|
5662
5662
|
return handleNonStatusCodeError(_response.error, _response.rawResponse, "GET", "/v1/auth/issuers/{issuerId}");
|
|
5663
5663
|
}
|
|
5664
5664
|
/**
|
|
5665
|
-
* Deregisters a trusted third-party IdP issuer. Requires a root API key or the bootstrap's provisioning capability.
|
|
5665
|
+
* Deregisters a trusted third-party IdP issuer. Requires a root API key or the bootstrap's provisioning capability. A credential confined to one app context may only deregister an issuer registered in that context; naming one registered in another context returns 404, identically to a nonexistent issuerId. A root API key may deregister any issuer. Refused if any user account was ever created or matched via this issuer (by a prior self-signup or accepted invite, through `POST /v1/auth/token/exchange`) — that access cannot be silently orphaned. Deactivate the affected users first if you intend to cut off their access, or register a replacement issuer before removing this one. An issuer that has never been used for an exchange (no bound users yet) can always be deregistered.
|
|
5666
5666
|
*
|
|
5667
5667
|
* @param {Vectros.DeleteIssuerRequest} request
|
|
5668
5668
|
* @param {AuthClient.RequestOptions} requestOptions - Request-specific configuration.
|
|
5669
5669
|
*
|
|
5670
5670
|
* @throws {@link Vectros.ForbiddenError}
|
|
5671
5671
|
* @throws {@link Vectros.NotFoundError}
|
|
5672
|
+
* @throws {@link Vectros.ConflictError}
|
|
5672
5673
|
* @throws {@link Vectros.TooManyRequestsError}
|
|
5673
5674
|
*
|
|
5674
5675
|
* @example
|
|
@@ -5710,6 +5711,8 @@ var AuthClient = class {
|
|
|
5710
5711
|
throw new ForbiddenError(_response.error.body, _response.rawResponse);
|
|
5711
5712
|
case 404:
|
|
5712
5713
|
throw new NotFoundError(_response.error.body, _response.rawResponse);
|
|
5714
|
+
case 409:
|
|
5715
|
+
throw new ConflictError(_response.error.body, _response.rawResponse);
|
|
5713
5716
|
case 429:
|
|
5714
5717
|
throw new TooManyRequestsError(_response.error.body, _response.rawResponse);
|
|
5715
5718
|
default:
|
|
@@ -5728,7 +5731,7 @@ var AuthClient = class {
|
|
|
5728
5731
|
);
|
|
5729
5732
|
}
|
|
5730
5733
|
/**
|
|
5731
|
-
* Returns the issuers registered in your tenant. Requires a root API key or the bootstrap's provisioning capability. Returns a `{data, nextCursor}` envelope.
|
|
5734
|
+
* Returns the issuers registered in your tenant. Requires a root API key or the bootstrap's provisioning capability. A credential confined to one app context sees only the issuers registered in that context; a root API key sees every context. Returns a `{data, nextCursor}` envelope.
|
|
5732
5735
|
*
|
|
5733
5736
|
* @param {Vectros.ListIssuersRequest} request
|
|
5734
5737
|
* @param {AuthClient.RequestOptions} requestOptions - Request-specific configuration.
|
|
@@ -5785,7 +5788,7 @@ var AuthClient = class {
|
|
|
5785
5788
|
return handleNonStatusCodeError(_response.error, _response.rawResponse, "GET", "/v1/auth/issuers");
|
|
5786
5789
|
}
|
|
5787
5790
|
/**
|
|
5788
|
-
* Registers a trusted third-party IdP issuer that BYO-IdP token exchange (`POST /v1/auth/token/exchange`) may accept a `subject_token` from. Requires a root API key or the CLI bootstrap's provisioning capability — never an ordinary partner-grantable scope. Idempotent by `issuerId` within your tenant; the `(issuer, audience)` pair must not already be registered by a different issuerId/tenant.
|
|
5791
|
+
* Registers a trusted third-party IdP issuer that BYO-IdP token exchange (`POST /v1/auth/token/exchange`) may accept a `subject_token` from. Requires a root API key or the CLI bootstrap's provisioning capability — never an ordinary partner-grantable scope. Idempotent by `issuerId` within your tenant; the `(issuer, audience)` pair must not already be registered by a different issuerId/tenant. An app context may have at most one active issuer — deregister the existing one first if you need to replace it. One issuer MAY serve several contexts today, each via its own registration row with a distinct `audience`.
|
|
5789
5792
|
*
|
|
5790
5793
|
* @param {Vectros.IssuerRequest} request
|
|
5791
5794
|
* @param {AuthClient.RequestOptions} requestOptions - Request-specific configuration.
|
|
@@ -5896,7 +5899,7 @@ var AuthClient = class {
|
|
|
5896
5899
|
return handleNonStatusCodeError(_response.error, _response.rawResponse, "GET", "/v1/ping");
|
|
5897
5900
|
}
|
|
5898
5901
|
/**
|
|
5899
|
-
* Returns the access profiles for the given principal across
|
|
5902
|
+
* Returns the access profiles for the given principal. Looking up your OWN principal — or holding the `context-directory-read` capability — returns the profiles across ALL of your contexts, letting you answer questions like "which apps does this user have access to?". A context-bound credential looking up a DIFFERENT principal instead sees only that principal's profile in your credential's own context (at most one result), never across contexts it has no authority over. Results are always confined to your account. Requires the `profiles:r` scope.
|
|
5900
5903
|
*
|
|
5901
5904
|
* @param {Vectros.ListProfilesForPrincipalRequest} request
|
|
5902
5905
|
* @param {AuthClient.RequestOptions} requestOptions - Request-specific configuration.
|
|
@@ -6175,7 +6178,7 @@ var AuthClient = class {
|
|
|
6175
6178
|
return handleNonStatusCodeError(_response.error, _response.rawResponse, "POST", "/v1/users/invite/resend");
|
|
6176
6179
|
}
|
|
6177
6180
|
/**
|
|
6178
|
-
* RFC 8693 OAuth 2.0 Token Exchange. Trades a JWT issued by a third-party identity provider you've registered (`POST /v1/auth/issuers`) for a Vectros `st_*` scoped bearer token — no Vectros credential required to call this endpoint. The exchanged token's scope is resolved entirely server-side from the matched user's access profile; this endpoint accepts no caller-supplied scope, resource, or audience parameter (RFC 8693 §2.1's `resource`/`audience`/`scope` are not used in v1 — the registered `(issuer, audience)` pair alone pins the target tenant and app context). On a first-time login (no existing Vectros identity for this subject), two opt-in binding paths exist: `invite_token` (a `PENDING` sub-user invitation), and — if the registration declares one or more self-signup policies — `signup_type` (a brand-new user is created and bound to the policy's configured role). If `invite_token` is present at all, it is the ONLY path tried — a failed invite never falls through to self-signup. Neither field is required for a subject with an existing identity. Uses the OAuth-standard error envelope (`{"error":..., "error_description":...}`, RFC 6749 §5.2), NOT this API's usual `{"message":...}` shape — its client is generic OAuth tooling, not the Vectros SDK.
|
|
6181
|
+
* RFC 8693 OAuth 2.0 Token Exchange. Trades a JWT issued by a third-party identity provider you've registered (`POST /v1/auth/issuers`) for a Vectros `st_*` scoped bearer token — no Vectros credential required to call this endpoint. The exchanged token's scope is resolved entirely server-side from the matched user's access profile; this endpoint accepts no caller-supplied scope, resource, or audience parameter (RFC 8693 §2.1's `resource`/`audience`/`scope` are not used in v1 — the registered `(issuer, audience)` pair alone pins the target tenant and app context). On a first-time login (no existing Vectros identity for this subject), two opt-in binding paths exist: `invite_token` (a `PENDING` sub-user invitation), and — if the registration declares one or more self-signup policies — `signup_type` (a brand-new user is created and bound to the policy's configured role). If `invite_token` is present at all, it is the ONLY path tried — a failed invite never falls through to self-signup. Neither field is required for a subject with an existing identity. If your issuer is registered against more than one app context (each via its own audience), `context_id` selects which one to target; omit it when your token's `aud` claim matches only one registered context — the common case, unaffected by this field. Uses the OAuth-standard error envelope (`{"error":..., "error_description":...}`, RFC 6749 §5.2), NOT this API's usual `{"message":...}` shape — its client is generic OAuth tooling, not the Vectros SDK.
|
|
6179
6182
|
*
|
|
6180
6183
|
* @param {Vectros.TokenExchangeRequest} request
|
|
6181
6184
|
* @param {AuthClient.RequestOptions} requestOptions - Request-specific configuration.
|
|
@@ -7717,6 +7720,7 @@ var IdentityClient = class {
|
|
|
7717
7720
|
* @example
|
|
7718
7721
|
* await client.identity.listEntities({
|
|
7719
7722
|
* namespace: "team",
|
|
7723
|
+
* contextId: "myapp",
|
|
7720
7724
|
* startFrom: "b3BhcXVlLWN1cnNvci1mcm9tLXRoZS1wcmV2aW91cy1wYWdl"
|
|
7721
7725
|
* })
|
|
7722
7726
|
*/
|
|
@@ -7726,6 +7730,7 @@ var IdentityClient = class {
|
|
|
7726
7730
|
async __listEntities(request, requestOptions) {
|
|
7727
7731
|
const {
|
|
7728
7732
|
namespace,
|
|
7733
|
+
contextId,
|
|
7729
7734
|
userId,
|
|
7730
7735
|
externalId,
|
|
7731
7736
|
scope,
|
|
@@ -7740,6 +7745,7 @@ var IdentityClient = class {
|
|
|
7740
7745
|
limit
|
|
7741
7746
|
} = request;
|
|
7742
7747
|
const _queryParams = {
|
|
7748
|
+
contextId,
|
|
7743
7749
|
userId,
|
|
7744
7750
|
externalId,
|
|
7745
7751
|
scope,
|
|
@@ -7798,6 +7804,7 @@ var IdentityClient = class {
|
|
|
7798
7804
|
* @example
|
|
7799
7805
|
* await client.identity.createEntity({
|
|
7800
7806
|
* namespace: "team",
|
|
7807
|
+
* contextId: "myapp",
|
|
7801
7808
|
* body: {
|
|
7802
7809
|
* externalId: "team_eng_platform"
|
|
7803
7810
|
* }
|
|
@@ -7807,9 +7814,10 @@ var IdentityClient = class {
|
|
|
7807
7814
|
return HttpResponsePromise.fromPromise(this.__createEntity(request, requestOptions));
|
|
7808
7815
|
}
|
|
7809
7816
|
async __createEntity(request, requestOptions) {
|
|
7810
|
-
const { namespace, upsert, body: _body } = request;
|
|
7817
|
+
const { namespace, upsert, contextId, body: _body } = request;
|
|
7811
7818
|
const _queryParams = {
|
|
7812
|
-
upsert
|
|
7819
|
+
upsert,
|
|
7820
|
+
contextId
|
|
7813
7821
|
};
|
|
7814
7822
|
const _authRequest = await this._options.authProvider.getAuthRequest();
|
|
7815
7823
|
const _headers = mergeHeaders(
|
|
@@ -7866,14 +7874,18 @@ var IdentityClient = class {
|
|
|
7866
7874
|
* @example
|
|
7867
7875
|
* await client.identity.getEntity({
|
|
7868
7876
|
* namespace: "team",
|
|
7869
|
-
* id: "6ba7b810-9dad-11d1-80b4-00c04fd430c8"
|
|
7877
|
+
* id: "6ba7b810-9dad-11d1-80b4-00c04fd430c8",
|
|
7878
|
+
* contextId: "myapp"
|
|
7870
7879
|
* })
|
|
7871
7880
|
*/
|
|
7872
7881
|
getEntity(request, requestOptions) {
|
|
7873
7882
|
return HttpResponsePromise.fromPromise(this.__getEntity(request, requestOptions));
|
|
7874
7883
|
}
|
|
7875
7884
|
async __getEntity(request, requestOptions) {
|
|
7876
|
-
const { namespace, id } = request;
|
|
7885
|
+
const { namespace, id, contextId } = request;
|
|
7886
|
+
const _queryParams = {
|
|
7887
|
+
contextId
|
|
7888
|
+
};
|
|
7877
7889
|
const _authRequest = await this._options.authProvider.getAuthRequest();
|
|
7878
7890
|
const _headers = mergeHeaders(
|
|
7879
7891
|
_authRequest.headers,
|
|
@@ -7887,7 +7899,7 @@ var IdentityClient = class {
|
|
|
7887
7899
|
),
|
|
7888
7900
|
method: "GET",
|
|
7889
7901
|
headers: _headers,
|
|
7890
|
-
queryString: url_exports.queryBuilder().mergeAdditional(requestOptions?.queryParams).build(),
|
|
7902
|
+
queryString: url_exports.queryBuilder().addMany(_queryParams).mergeAdditional(requestOptions?.queryParams).build(),
|
|
7891
7903
|
timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1e3,
|
|
7892
7904
|
maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries,
|
|
7893
7905
|
abortSignal: requestOptions?.abortSignal,
|
|
@@ -7924,6 +7936,7 @@ var IdentityClient = class {
|
|
|
7924
7936
|
* await client.identity.updateEntity({
|
|
7925
7937
|
* namespace: "team",
|
|
7926
7938
|
* id: "id",
|
|
7939
|
+
* contextId: "myapp",
|
|
7927
7940
|
* body: {
|
|
7928
7941
|
* externalId: "team_eng_platform"
|
|
7929
7942
|
* }
|
|
@@ -7933,7 +7946,10 @@ var IdentityClient = class {
|
|
|
7933
7946
|
return HttpResponsePromise.fromPromise(this.__updateEntity(request, requestOptions));
|
|
7934
7947
|
}
|
|
7935
7948
|
async __updateEntity(request, requestOptions) {
|
|
7936
|
-
const { namespace, id, body: _body } = request;
|
|
7949
|
+
const { namespace, id, contextId, body: _body } = request;
|
|
7950
|
+
const _queryParams = {
|
|
7951
|
+
contextId
|
|
7952
|
+
};
|
|
7937
7953
|
const _authRequest = await this._options.authProvider.getAuthRequest();
|
|
7938
7954
|
const _headers = mergeHeaders(
|
|
7939
7955
|
_authRequest.headers,
|
|
@@ -7948,7 +7964,7 @@ var IdentityClient = class {
|
|
|
7948
7964
|
method: "PUT",
|
|
7949
7965
|
headers: _headers,
|
|
7950
7966
|
contentType: "application/json",
|
|
7951
|
-
queryString: url_exports.queryBuilder().mergeAdditional(requestOptions?.queryParams).build(),
|
|
7967
|
+
queryString: url_exports.queryBuilder().addMany(_queryParams).mergeAdditional(requestOptions?.queryParams).build(),
|
|
7952
7968
|
requestType: "json",
|
|
7953
7969
|
body: _body,
|
|
7954
7970
|
timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1e3,
|
|
@@ -7988,14 +8004,18 @@ var IdentityClient = class {
|
|
|
7988
8004
|
* @example
|
|
7989
8005
|
* await client.identity.deleteEntity({
|
|
7990
8006
|
* namespace: "team",
|
|
7991
|
-
* id: "id"
|
|
8007
|
+
* id: "id",
|
|
8008
|
+
* contextId: "myapp"
|
|
7992
8009
|
* })
|
|
7993
8010
|
*/
|
|
7994
8011
|
deleteEntity(request, requestOptions) {
|
|
7995
8012
|
return HttpResponsePromise.fromPromise(this.__deleteEntity(request, requestOptions));
|
|
7996
8013
|
}
|
|
7997
8014
|
async __deleteEntity(request, requestOptions) {
|
|
7998
|
-
const { namespace, id } = request;
|
|
8015
|
+
const { namespace, id, contextId } = request;
|
|
8016
|
+
const _queryParams = {
|
|
8017
|
+
contextId
|
|
8018
|
+
};
|
|
7999
8019
|
const _authRequest = await this._options.authProvider.getAuthRequest();
|
|
8000
8020
|
const _headers = mergeHeaders(
|
|
8001
8021
|
_authRequest.headers,
|
|
@@ -8009,7 +8029,7 @@ var IdentityClient = class {
|
|
|
8009
8029
|
),
|
|
8010
8030
|
method: "DELETE",
|
|
8011
8031
|
headers: _headers,
|
|
8012
|
-
queryString: url_exports.queryBuilder().mergeAdditional(requestOptions?.queryParams).build(),
|
|
8032
|
+
queryString: url_exports.queryBuilder().addMany(_queryParams).mergeAdditional(requestOptions?.queryParams).build(),
|
|
8013
8033
|
timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1e3,
|
|
8014
8034
|
maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries,
|
|
8015
8035
|
abortSignal: requestOptions?.abortSignal,
|
|
@@ -8052,6 +8072,7 @@ var IdentityClient = class {
|
|
|
8052
8072
|
* @example
|
|
8053
8073
|
* await client.identity.lookupEntities({
|
|
8054
8074
|
* namespace: "team",
|
|
8075
|
+
* contextId: "myapp",
|
|
8055
8076
|
* body: {
|
|
8056
8077
|
* type: "person_v1",
|
|
8057
8078
|
* field: "ssn"
|
|
@@ -8062,7 +8083,10 @@ var IdentityClient = class {
|
|
|
8062
8083
|
return HttpResponsePromise.fromPromise(this.__lookupEntities(request, requestOptions));
|
|
8063
8084
|
}
|
|
8064
8085
|
async __lookupEntities(request, requestOptions) {
|
|
8065
|
-
const { namespace, body: _body } = request;
|
|
8086
|
+
const { namespace, contextId, body: _body } = request;
|
|
8087
|
+
const _queryParams = {
|
|
8088
|
+
contextId
|
|
8089
|
+
};
|
|
8066
8090
|
const _authRequest = await this._options.authProvider.getAuthRequest();
|
|
8067
8091
|
const _headers = mergeHeaders(
|
|
8068
8092
|
_authRequest.headers,
|
|
@@ -8077,7 +8101,7 @@ var IdentityClient = class {
|
|
|
8077
8101
|
method: "POST",
|
|
8078
8102
|
headers: _headers,
|
|
8079
8103
|
contentType: "application/json",
|
|
8080
|
-
queryString: url_exports.queryBuilder().mergeAdditional(requestOptions?.queryParams).build(),
|
|
8104
|
+
queryString: url_exports.queryBuilder().addMany(_queryParams).mergeAdditional(requestOptions?.queryParams).build(),
|
|
8081
8105
|
requestType: "json",
|
|
8082
8106
|
body: _body,
|
|
8083
8107
|
timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1e3,
|
|
@@ -8121,15 +8145,17 @@ var IdentityClient = class {
|
|
|
8121
8145
|
* @example
|
|
8122
8146
|
* await client.identity.getEntityVersions({
|
|
8123
8147
|
* namespace: "team",
|
|
8124
|
-
* id: "id"
|
|
8148
|
+
* id: "id",
|
|
8149
|
+
* contextId: "myapp"
|
|
8125
8150
|
* })
|
|
8126
8151
|
*/
|
|
8127
8152
|
getEntityVersions(request, requestOptions) {
|
|
8128
8153
|
return HttpResponsePromise.fromPromise(this.__getEntityVersions(request, requestOptions));
|
|
8129
8154
|
}
|
|
8130
8155
|
async __getEntityVersions(request, requestOptions) {
|
|
8131
|
-
const { namespace, id, startFrom } = request;
|
|
8156
|
+
const { namespace, id, contextId, startFrom } = request;
|
|
8132
8157
|
const _queryParams = {
|
|
8158
|
+
contextId,
|
|
8133
8159
|
startFrom
|
|
8134
8160
|
};
|
|
8135
8161
|
const _authRequest = await this._options.authProvider.getAuthRequest();
|
|
@@ -8191,7 +8217,10 @@ var IdentityClient = class {
|
|
|
8191
8217
|
return HttpResponsePromise.fromPromise(this.__getNamespace(request, requestOptions));
|
|
8192
8218
|
}
|
|
8193
8219
|
async __getNamespace(request, requestOptions) {
|
|
8194
|
-
const { namespace } = request;
|
|
8220
|
+
const { namespace, contextId } = request;
|
|
8221
|
+
const _queryParams = {
|
|
8222
|
+
contextId
|
|
8223
|
+
};
|
|
8195
8224
|
const _authRequest = await this._options.authProvider.getAuthRequest();
|
|
8196
8225
|
const _headers = mergeHeaders(
|
|
8197
8226
|
_authRequest.headers,
|
|
@@ -8205,7 +8234,7 @@ var IdentityClient = class {
|
|
|
8205
8234
|
),
|
|
8206
8235
|
method: "GET",
|
|
8207
8236
|
headers: _headers,
|
|
8208
|
-
queryString: url_exports.queryBuilder().mergeAdditional(requestOptions?.queryParams).build(),
|
|
8237
|
+
queryString: url_exports.queryBuilder().addMany(_queryParams).mergeAdditional(requestOptions?.queryParams).build(),
|
|
8209
8238
|
timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1e3,
|
|
8210
8239
|
maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries,
|
|
8211
8240
|
abortSignal: requestOptions?.abortSignal,
|
|
@@ -8230,7 +8259,7 @@ var IdentityClient = class {
|
|
|
8230
8259
|
return handleNonStatusCodeError(_response.error, _response.rawResponse, "GET", "/v1/namespaces/{namespace}");
|
|
8231
8260
|
}
|
|
8232
8261
|
/**
|
|
8233
|
-
* Updates the mutable fields (`entityBacked`, `defaultSchemaId`, `specificityRank`) of a registered namespace. The namespace name
|
|
8262
|
+
* Updates the mutable fields (`entityBacked`, `defaultSchemaId`, `specificityRank`) of a registered namespace. The namespace name and its `contextId` (which row is selected) are both immutable. Requires a root API key. `org` and `client` are updatable like any other namespace — there is no reserved-built-in exception.
|
|
8234
8263
|
*
|
|
8235
8264
|
* @param {Vectros.UpdateNamespaceRequest} request
|
|
8236
8265
|
* @param {IdentityClient.RequestOptions} requestOptions - Request-specific configuration.
|
|
@@ -8252,7 +8281,10 @@ var IdentityClient = class {
|
|
|
8252
8281
|
return HttpResponsePromise.fromPromise(this.__updateNamespace(request, requestOptions));
|
|
8253
8282
|
}
|
|
8254
8283
|
async __updateNamespace(request, requestOptions) {
|
|
8255
|
-
const { namespace, body: _body } = request;
|
|
8284
|
+
const { namespace, contextId, body: _body } = request;
|
|
8285
|
+
const _queryParams = {
|
|
8286
|
+
contextId
|
|
8287
|
+
};
|
|
8256
8288
|
const _authRequest = await this._options.authProvider.getAuthRequest();
|
|
8257
8289
|
const _headers = mergeHeaders(
|
|
8258
8290
|
_authRequest.headers,
|
|
@@ -8267,7 +8299,7 @@ var IdentityClient = class {
|
|
|
8267
8299
|
method: "PUT",
|
|
8268
8300
|
headers: _headers,
|
|
8269
8301
|
contentType: "application/json",
|
|
8270
|
-
queryString: url_exports.queryBuilder().mergeAdditional(requestOptions?.queryParams).build(),
|
|
8302
|
+
queryString: url_exports.queryBuilder().addMany(_queryParams).mergeAdditional(requestOptions?.queryParams).build(),
|
|
8271
8303
|
requestType: "json",
|
|
8272
8304
|
body: _body,
|
|
8273
8305
|
timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1e3,
|
|
@@ -8298,7 +8330,7 @@ var IdentityClient = class {
|
|
|
8298
8330
|
return handleNonStatusCodeError(_response.error, _response.rawResponse, "PUT", "/v1/namespaces/{namespace}");
|
|
8299
8331
|
}
|
|
8300
8332
|
/**
|
|
8301
|
-
* Deletes a registered scope namespace. Requires a root API key.
|
|
8333
|
+
* Deletes a registered scope namespace. Requires a root API key. `org` and `client` are deletable like any other namespace — there is no reserved-built-in exception. A namespace that still has entities cannot be deleted (409) — delete its entities first; this keeps them reachable by the account-teardown and erasure sweeps.
|
|
8302
8334
|
*
|
|
8303
8335
|
* @param {Vectros.DeleteNamespaceRequest} request
|
|
8304
8336
|
* @param {IdentityClient.RequestOptions} requestOptions - Request-specific configuration.
|
|
@@ -8316,7 +8348,10 @@ var IdentityClient = class {
|
|
|
8316
8348
|
return HttpResponsePromise.fromPromise(this.__deleteNamespace(request, requestOptions));
|
|
8317
8349
|
}
|
|
8318
8350
|
async __deleteNamespace(request, requestOptions) {
|
|
8319
|
-
const { namespace } = request;
|
|
8351
|
+
const { namespace, contextId } = request;
|
|
8352
|
+
const _queryParams = {
|
|
8353
|
+
contextId
|
|
8354
|
+
};
|
|
8320
8355
|
const _authRequest = await this._options.authProvider.getAuthRequest();
|
|
8321
8356
|
const _headers = mergeHeaders(
|
|
8322
8357
|
_authRequest.headers,
|
|
@@ -8330,7 +8365,7 @@ var IdentityClient = class {
|
|
|
8330
8365
|
),
|
|
8331
8366
|
method: "DELETE",
|
|
8332
8367
|
headers: _headers,
|
|
8333
|
-
queryString: url_exports.queryBuilder().mergeAdditional(requestOptions?.queryParams).build(),
|
|
8368
|
+
queryString: url_exports.queryBuilder().addMany(_queryParams).mergeAdditional(requestOptions?.queryParams).build(),
|
|
8334
8369
|
timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1e3,
|
|
8335
8370
|
maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries,
|
|
8336
8371
|
abortSignal: requestOptions?.abortSignal,
|
|
@@ -8359,7 +8394,7 @@ var IdentityClient = class {
|
|
|
8359
8394
|
return handleNonStatusCodeError(_response.error, _response.rawResponse, "DELETE", "/v1/namespaces/{namespace}");
|
|
8360
8395
|
}
|
|
8361
8396
|
/**
|
|
8362
|
-
* Returns the scope namespaces
|
|
8397
|
+
* Returns the scope namespaces in your account, including `org` and `client`, which are provisioned for you when the account is created. Returns a `{data, nextCursor}` envelope.
|
|
8363
8398
|
*
|
|
8364
8399
|
* @param {Vectros.ListNamespacesRequest} request
|
|
8365
8400
|
* @param {IdentityClient.RequestOptions} requestOptions - Request-specific configuration.
|
|
@@ -8371,10 +8406,11 @@ var IdentityClient = class {
|
|
|
8371
8406
|
return HttpResponsePromise.fromPromise(this.__listNamespaces(request, requestOptions));
|
|
8372
8407
|
}
|
|
8373
8408
|
async __listNamespaces(request = {}, requestOptions) {
|
|
8374
|
-
const { startFrom, limit } = request;
|
|
8409
|
+
const { startFrom, limit, contextId } = request;
|
|
8375
8410
|
const _queryParams = {
|
|
8376
8411
|
startFrom,
|
|
8377
|
-
limit
|
|
8412
|
+
limit,
|
|
8413
|
+
contextId
|
|
8378
8414
|
};
|
|
8379
8415
|
const _authRequest = await this._options.authProvider.getAuthRequest();
|
|
8380
8416
|
const _headers = mergeHeaders(
|
|
@@ -8409,9 +8445,9 @@ var IdentityClient = class {
|
|
|
8409
8445
|
return handleNonStatusCodeError(_response.error, _response.rawResponse, "GET", "/v1/namespaces");
|
|
8410
8446
|
}
|
|
8411
8447
|
/**
|
|
8412
|
-
* Registers a new scope namespace and declares whether its values resolve to identity entities (`entityBacked`). Also requires `specificityRank`, an explicit, account-unique position in the specificity order used to break recordType schema-resolution ties. Requires a root API key
|
|
8448
|
+
* Registers a new scope namespace and declares whether its values resolve to identity entities (`entityBacked`). Also requires `specificityRank`, an explicit, account-unique position in the specificity order used to break recordType schema-resolution ties. Requires a root API key or the CLI bootstrap's provisioning capability — never an ordinary partner-grantable scope. `org` and `client` are provisioned when your account is created, so registering either returns the already-registered error.
|
|
8413
8449
|
*
|
|
8414
|
-
* @param {Vectros.
|
|
8450
|
+
* @param {Vectros.RegisterNamespaceRequest} request
|
|
8415
8451
|
* @param {IdentityClient.RequestOptions} requestOptions - Request-specific configuration.
|
|
8416
8452
|
*
|
|
8417
8453
|
* @throws {@link Vectros.BadRequestError}
|
|
@@ -8420,14 +8456,20 @@ var IdentityClient = class {
|
|
|
8420
8456
|
*
|
|
8421
8457
|
* @example
|
|
8422
8458
|
* await client.identity.registerNamespace({
|
|
8423
|
-
*
|
|
8424
|
-
*
|
|
8459
|
+
* body: {
|
|
8460
|
+
* namespace: "team",
|
|
8461
|
+
* specificityRank: 1500
|
|
8462
|
+
* }
|
|
8425
8463
|
* })
|
|
8426
8464
|
*/
|
|
8427
8465
|
registerNamespace(request, requestOptions) {
|
|
8428
8466
|
return HttpResponsePromise.fromPromise(this.__registerNamespace(request, requestOptions));
|
|
8429
8467
|
}
|
|
8430
8468
|
async __registerNamespace(request, requestOptions) {
|
|
8469
|
+
const { contextId, body: _body } = request;
|
|
8470
|
+
const _queryParams = {
|
|
8471
|
+
contextId
|
|
8472
|
+
};
|
|
8431
8473
|
const _authRequest = await this._options.authProvider.getAuthRequest();
|
|
8432
8474
|
const _headers = mergeHeaders(
|
|
8433
8475
|
_authRequest.headers,
|
|
@@ -8442,9 +8484,9 @@ var IdentityClient = class {
|
|
|
8442
8484
|
method: "POST",
|
|
8443
8485
|
headers: _headers,
|
|
8444
8486
|
contentType: "application/json",
|
|
8445
|
-
queryString: url_exports.queryBuilder().mergeAdditional(requestOptions?.queryParams).build(),
|
|
8487
|
+
queryString: url_exports.queryBuilder().addMany(_queryParams).mergeAdditional(requestOptions?.queryParams).build(),
|
|
8446
8488
|
requestType: "json",
|
|
8447
|
-
body:
|
|
8489
|
+
body: _body,
|
|
8448
8490
|
timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1e3,
|
|
8449
8491
|
maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries,
|
|
8450
8492
|
abortSignal: requestOptions?.abortSignal,
|
|
@@ -8729,12 +8771,13 @@ var IdentityClient = class {
|
|
|
8729
8771
|
return handleNonStatusCodeError(_response.error, _response.rawResponse, "PUT", "/v1/users/{id}");
|
|
8730
8772
|
}
|
|
8731
8773
|
/**
|
|
8732
|
-
* Permanently deletes a user identity. This cannot be undone. If the user is a pending invitation, the associated access profile created for that invitation is also removed. Requires the `users:d` scope.
|
|
8774
|
+
* Permanently deletes a user identity. This cannot be undone. If the user is a pending invitation, the associated access profile created for that invitation is also removed. Requires the `users:d` scope. Deleting your account's last OWNER is refused (409) — an account must always retain at least one owner.
|
|
8733
8775
|
*
|
|
8734
8776
|
* @param {Vectros.DeleteUserRequest} request
|
|
8735
8777
|
* @param {IdentityClient.RequestOptions} requestOptions - Request-specific configuration.
|
|
8736
8778
|
*
|
|
8737
8779
|
* @throws {@link Vectros.NotFoundError}
|
|
8780
|
+
* @throws {@link Vectros.ConflictError}
|
|
8738
8781
|
* @throws {@link Vectros.TooManyRequestsError}
|
|
8739
8782
|
*
|
|
8740
8783
|
* @example
|
|
@@ -8774,6 +8817,8 @@ var IdentityClient = class {
|
|
|
8774
8817
|
switch (_response.error.statusCode) {
|
|
8775
8818
|
case 404:
|
|
8776
8819
|
throw new NotFoundError(_response.error.body, _response.rawResponse);
|
|
8820
|
+
case 409:
|
|
8821
|
+
throw new ConflictError(_response.error.body, _response.rawResponse);
|
|
8777
8822
|
case 429:
|
|
8778
8823
|
throw new TooManyRequestsError(_response.error.body, _response.rawResponse);
|
|
8779
8824
|
default:
|
|
@@ -10856,6 +10901,35 @@ var BlueprintSchemaSchema = z.object({
|
|
|
10856
10901
|
});
|
|
10857
10902
|
}
|
|
10858
10903
|
});
|
|
10904
|
+
var CAPABILITY_NAME_RE = /^[a-z][a-z0-9-]*$/;
|
|
10905
|
+
var GrantedCapabilitiesSchema = z.array(z.string().min(1)).optional().superRefine((caps, ctx) => {
|
|
10906
|
+
if (!caps) return;
|
|
10907
|
+
const seen = /* @__PURE__ */ new Set();
|
|
10908
|
+
caps.forEach((name, i) => {
|
|
10909
|
+
if (name === "") return;
|
|
10910
|
+
if (name === "*") {
|
|
10911
|
+
ctx.addIssue({
|
|
10912
|
+
code: z.ZodIssueCode.custom,
|
|
10913
|
+
path: [i],
|
|
10914
|
+
message: "'*' is not a capability \u2014 granted_capabilities has no wildcard, and a '*' in allowedActions confers zero capabilities. Name each capability explicitly."
|
|
10915
|
+
});
|
|
10916
|
+
} else if (!CAPABILITY_NAME_RE.test(name)) {
|
|
10917
|
+
ctx.addIssue({
|
|
10918
|
+
code: z.ZodIssueCode.custom,
|
|
10919
|
+
path: [i],
|
|
10920
|
+
message: `'${name}' is not a valid capability name (expected lowercase letters, digits and hyphens, starting with a letter, and no ':' \u2014 a capability is not an action)`
|
|
10921
|
+
});
|
|
10922
|
+
}
|
|
10923
|
+
if (seen.has(name)) {
|
|
10924
|
+
ctx.addIssue({
|
|
10925
|
+
code: z.ZodIssueCode.custom,
|
|
10926
|
+
path: [i],
|
|
10927
|
+
message: `'${name}' is declared more than once`
|
|
10928
|
+
});
|
|
10929
|
+
}
|
|
10930
|
+
seen.add(name);
|
|
10931
|
+
});
|
|
10932
|
+
});
|
|
10859
10933
|
var BlueprintAccessProfileSchema = z.object({
|
|
10860
10934
|
// Validated structurally here; the SCOPE GATE (in @vectros-ai/cli) is
|
|
10861
10935
|
// what enforces the data-plane-only security boundary.
|
|
@@ -10887,11 +10961,31 @@ var BlueprintAccessProfileSchema = z.object({
|
|
|
10887
10961
|
// below (`lintIdentityOverrides`, placeholder-aware — see that function's
|
|
10888
10962
|
// docstring); this `.min(1)` is only the shape-level "non-blank" guard on
|
|
10889
10963
|
// the value, not the grammar check.
|
|
10890
|
-
identityOverrides: z.record(z.string().min(1)).optional()
|
|
10964
|
+
identityOverrides: z.record(z.string().min(1)).optional(),
|
|
10965
|
+
// Optional capabilities granted to the blueprint's own service-principal
|
|
10966
|
+
// profile. DECISION: allowed here, not role-clause-only. On the platform,
|
|
10967
|
+
// an access profile's scopes are themselves a list of clauses — the exact
|
|
10968
|
+
// same wire shape a role clause carries — and the loader already builds
|
|
10969
|
+
// this profile as ONE such clause. Refusing `capabilities` here while
|
|
10970
|
+
// allowing it on a role clause would be an asymmetry this FORMAT package
|
|
10971
|
+
// has no basis to invent: nothing on the platform side distinguishes the
|
|
10972
|
+
// two, and whether granting a capability to a blueprint's own service key
|
|
10973
|
+
// is sound POLICY is exactly the question the CLI's scope gate exists to
|
|
10974
|
+
// answer (a separate package, out of scope for this format layer) — the
|
|
10975
|
+
// same posture this package already takes with `allowedActions` above.
|
|
10976
|
+
//
|
|
10977
|
+
// Worth naming for whoever writes that gate: this profile is a long-lived,
|
|
10978
|
+
// non-human-attributable machine credential (typically stored in CI/CD
|
|
10979
|
+
// secrets), not an ephemeral human session — a materially different
|
|
10980
|
+
// blast-radius question from granting the same capability to a role that
|
|
10981
|
+
// a person is bound to. The gate should weigh the two separately rather
|
|
10982
|
+
// than reuse one policy for both.
|
|
10983
|
+
capabilities: GrantedCapabilitiesSchema
|
|
10891
10984
|
}).strict();
|
|
10892
10985
|
var BlueprintRoleClauseSchema = z.object({
|
|
10893
10986
|
allowedActions: z.array(z.string().min(1)).min(1),
|
|
10894
|
-
dataScope: z.record(z.array(z.union([z.string().min(1), z.null()]))).optional()
|
|
10987
|
+
dataScope: z.record(z.array(z.union([z.string().min(1), z.null()]))).optional(),
|
|
10988
|
+
capabilities: GrantedCapabilitiesSchema
|
|
10895
10989
|
}).strict();
|
|
10896
10990
|
var BlueprintRolesSchema = z.record(z.array(BlueprintRoleClauseSchema).min(1));
|
|
10897
10991
|
var IDENTITY_NAMESPACE_RE = /^[a-z][a-z0-9_-]{1,31}$/;
|
|
@@ -10929,7 +11023,7 @@ var BlueprintServicePrincipalSchema = z.object({
|
|
|
10929
11023
|
}).strict();
|
|
10930
11024
|
var ISSUER_ID_RE = CONTEXT_ID_RE;
|
|
10931
11025
|
var BlueprintIssuerSchema = z.object({
|
|
10932
|
-
/** Short slug identifying this issuer within your
|
|
11026
|
+
/** Short slug identifying this issuer within your account. Immutable once registered. */
|
|
10933
11027
|
issuerId: z.string().regex(ISSUER_ID_RE, {
|
|
10934
11028
|
message: "issuerId must be 3-31 chars, start with a lowercase letter, then lowercase letters/digits/dashes (e.g. 'auth0-prod')"
|
|
10935
11029
|
}),
|
|
@@ -10944,7 +11038,12 @@ var BlueprintIssuerSchema = z.object({
|
|
|
10944
11038
|
* this as an ordinary per-API/application default).
|
|
10945
11039
|
*/
|
|
10946
11040
|
audience: z.string().min(1),
|
|
10947
|
-
/**
|
|
11041
|
+
/**
|
|
11042
|
+
* Which app context an exchanged token targets. **Must equal the blueprint's own
|
|
11043
|
+
* `contextId`** — an issuer is a trust anchor, so a blueprint may only attach one to the
|
|
11044
|
+
* context it actually provisions. One IdP account serving several contexts needs one entry
|
|
11045
|
+
* per context, each in that context's own blueprint.
|
|
11046
|
+
*/
|
|
10948
11047
|
contextId: z.string().regex(CONTEXT_ID_RE, {
|
|
10949
11048
|
message: "contextId must be 3-31 chars, start with a lowercase letter, then lowercase letters/digits/dashes (e.g. 'casework')"
|
|
10950
11049
|
}),
|
|
@@ -10971,6 +11070,151 @@ var BlueprintIssuerSchema = z.object({
|
|
|
10971
11070
|
}).strict()
|
|
10972
11071
|
).optional()
|
|
10973
11072
|
}).strict();
|
|
11073
|
+
var BlueprintNamespaceSchema = z.object({
|
|
11074
|
+
/** The namespace to register — same grammar as a `scope:<namespace>` key. */
|
|
11075
|
+
namespace: z.string().min(1),
|
|
11076
|
+
/**
|
|
11077
|
+
* When `true`, every value in this namespace must resolve to an existing
|
|
11078
|
+
* identity entity (create via `entities:c` at the in-context phase, or a
|
|
11079
|
+
* declared `identities:` entry). When `false`/omitted, values are
|
|
11080
|
+
* free-form strings validated by grammar only.
|
|
11081
|
+
*/
|
|
11082
|
+
entityBacked: z.boolean().optional(),
|
|
11083
|
+
/**
|
|
11084
|
+
* This namespace's position in your account's specificity order (used to
|
|
11085
|
+
* break a tie when a caller holds two scope dimensions at once). Required
|
|
11086
|
+
* by the platform on registration; MUST be unique across every namespace
|
|
11087
|
+
* in the account, including `org` (1000) and `client` (2000) — pick a
|
|
11088
|
+
* value outside that pair, and outside any other namespace this same
|
|
11089
|
+
* account might register (its own, or another blueprint's).
|
|
11090
|
+
*/
|
|
11091
|
+
specificityRank: z.number().int().min(0).max(1e6),
|
|
11092
|
+
/**
|
|
11093
|
+
* Optional membership backing — declares which record type + field hold
|
|
11094
|
+
* grants of THIS namespace's values. Supply together with
|
|
11095
|
+
* `membershipTargetField`, or omit both. Declaring this grants nobody
|
|
11096
|
+
* anything on its own: a role opts in explicitly with
|
|
11097
|
+
* `${{ member.scope.<namespace> }}` in its `dataScope`.
|
|
11098
|
+
*/
|
|
11099
|
+
membershipRecordType: z.string().min(1).optional(),
|
|
11100
|
+
/** The field on `membershipRecordType` naming the user a grant is FOR. Required alongside `membershipRecordType`. */
|
|
11101
|
+
membershipTargetField: z.string().min(1).optional(),
|
|
11102
|
+
/**
|
|
11103
|
+
* The field on `membershipRecordType` naming a grant's LEVEL, so the same
|
|
11104
|
+
* user can hold different levels in different values of this namespace.
|
|
11105
|
+
* Supply together with `membershipLevels`, or omit both for plain
|
|
11106
|
+
* in-or-out membership.
|
|
11107
|
+
*/
|
|
11108
|
+
membershipLevelField: z.string().min(1).optional(),
|
|
11109
|
+
/** The complete set of level labels this namespace allows. Required alongside `membershipLevelField`. */
|
|
11110
|
+
membershipLevels: z.array(z.string().min(1)).optional()
|
|
11111
|
+
}).strict();
|
|
11112
|
+
var MEMBERSHIP_FIELD_NAME_RE = /^[A-Za-z0-9_-]{1,64}$/;
|
|
11113
|
+
function lintNamespaces(value, ctx) {
|
|
11114
|
+
if (!value || typeof value !== "object" || !("namespaces" in value)) return;
|
|
11115
|
+
const namespaces = value.namespaces;
|
|
11116
|
+
if (!Array.isArray(namespaces)) return;
|
|
11117
|
+
const rawSchemas = value.schemas;
|
|
11118
|
+
const declaredSchemaTypes = new Set(
|
|
11119
|
+
(Array.isArray(rawSchemas) ? rawSchemas : []).map((s) => s && typeof s === "object" && "typeName" in s ? s.typeName : void 0).filter((t) => typeof t === "string")
|
|
11120
|
+
);
|
|
11121
|
+
const seenNames = /* @__PURE__ */ new Set();
|
|
11122
|
+
const seenRanks = /* @__PURE__ */ new Map();
|
|
11123
|
+
namespaces.forEach((entry, i) => {
|
|
11124
|
+
if (!entry || typeof entry !== "object") return;
|
|
11125
|
+
const ns = entry;
|
|
11126
|
+
const path4 = ["namespaces", i];
|
|
11127
|
+
if (typeof ns.namespace === "string") {
|
|
11128
|
+
const err = scopeNamespaceGrammarError(ns.namespace);
|
|
11129
|
+
if (err) ctx.addIssue({ code: z.ZodIssueCode.custom, path: [...path4, "namespace"], message: err });
|
|
11130
|
+
if (seenNames.has(ns.namespace)) {
|
|
11131
|
+
ctx.addIssue({
|
|
11132
|
+
code: z.ZodIssueCode.custom,
|
|
11133
|
+
path: [...path4, "namespace"],
|
|
11134
|
+
message: `'${ns.namespace}' is declared more than once in namespaces[]`
|
|
11135
|
+
});
|
|
11136
|
+
}
|
|
11137
|
+
seenNames.add(ns.namespace);
|
|
11138
|
+
}
|
|
11139
|
+
if (typeof ns.specificityRank === "number") {
|
|
11140
|
+
const prior = seenRanks.get(ns.specificityRank);
|
|
11141
|
+
if (prior !== void 0) {
|
|
11142
|
+
ctx.addIssue({
|
|
11143
|
+
code: z.ZodIssueCode.custom,
|
|
11144
|
+
path: [...path4, "specificityRank"],
|
|
11145
|
+
message: `specificityRank ${ns.specificityRank} is already used by namespaces[${prior}] \u2014 every namespace's rank must be unique`
|
|
11146
|
+
});
|
|
11147
|
+
}
|
|
11148
|
+
seenRanks.set(ns.specificityRank, i);
|
|
11149
|
+
}
|
|
11150
|
+
const hasRecordType = ns.membershipRecordType !== void 0;
|
|
11151
|
+
const hasTargetField = ns.membershipTargetField !== void 0;
|
|
11152
|
+
if (hasRecordType !== hasTargetField) {
|
|
11153
|
+
ctx.addIssue({
|
|
11154
|
+
code: z.ZodIssueCode.custom,
|
|
11155
|
+
path: [...path4, "membershipRecordType"],
|
|
11156
|
+
message: "membershipRecordType and membershipTargetField must be declared together, or neither"
|
|
11157
|
+
});
|
|
11158
|
+
}
|
|
11159
|
+
if (typeof ns.membershipRecordType === "string" && !declaredSchemaTypes.has(ns.membershipRecordType)) {
|
|
11160
|
+
ctx.addIssue({
|
|
11161
|
+
code: z.ZodIssueCode.custom,
|
|
11162
|
+
path: [...path4, "membershipRecordType"],
|
|
11163
|
+
message: `membershipRecordType '${ns.membershipRecordType}' must name a typeName this blueprint declares in schemas[] \u2014 a namespace's membership can only resolve over a record type the blueprint itself ships, never a type that merely happens to already exist in the target context.`
|
|
11164
|
+
});
|
|
11165
|
+
}
|
|
11166
|
+
for (const [field, value2] of [
|
|
11167
|
+
["membershipRecordType", ns.membershipRecordType],
|
|
11168
|
+
["membershipTargetField", ns.membershipTargetField],
|
|
11169
|
+
["membershipLevelField", ns.membershipLevelField]
|
|
11170
|
+
]) {
|
|
11171
|
+
if (typeof value2 === "string" && !MEMBERSHIP_FIELD_NAME_RE.test(value2)) {
|
|
11172
|
+
ctx.addIssue({
|
|
11173
|
+
code: z.ZodIssueCode.custom,
|
|
11174
|
+
path: [...path4, field],
|
|
11175
|
+
message: `${field} '${value2}' is invalid \u2014 must be 1-64 characters of letters, digits, underscores, or hyphens`
|
|
11176
|
+
});
|
|
11177
|
+
}
|
|
11178
|
+
}
|
|
11179
|
+
const hasLevelField = ns.membershipLevelField !== void 0;
|
|
11180
|
+
const hasLevels = Array.isArray(ns.membershipLevels) && ns.membershipLevels.length > 0;
|
|
11181
|
+
if (hasLevelField !== hasLevels) {
|
|
11182
|
+
ctx.addIssue({
|
|
11183
|
+
code: z.ZodIssueCode.custom,
|
|
11184
|
+
path: [...path4, "membershipLevelField"],
|
|
11185
|
+
message: "membershipLevelField and a non-empty membershipLevels must be declared together, or neither"
|
|
11186
|
+
});
|
|
11187
|
+
}
|
|
11188
|
+
if ((hasLevelField || hasLevels) && !hasRecordType) {
|
|
11189
|
+
ctx.addIssue({
|
|
11190
|
+
code: z.ZodIssueCode.custom,
|
|
11191
|
+
path: [...path4, "membershipLevelField"],
|
|
11192
|
+
message: "membershipLevelField/membershipLevels require membershipRecordType + membershipTargetField too"
|
|
11193
|
+
});
|
|
11194
|
+
}
|
|
11195
|
+
if (Array.isArray(ns.membershipLevels)) {
|
|
11196
|
+
const seenLevels = /* @__PURE__ */ new Set();
|
|
11197
|
+
for (const level of ns.membershipLevels) {
|
|
11198
|
+
if (typeof level !== "string") continue;
|
|
11199
|
+
const err = scopeNamespaceGrammarError(level);
|
|
11200
|
+
if (err) {
|
|
11201
|
+
ctx.addIssue({
|
|
11202
|
+
code: z.ZodIssueCode.custom,
|
|
11203
|
+
path: [...path4, "membershipLevels"],
|
|
11204
|
+
message: `level '${level}': ${err}`
|
|
11205
|
+
});
|
|
11206
|
+
} else if (seenLevels.has(level)) {
|
|
11207
|
+
ctx.addIssue({
|
|
11208
|
+
code: z.ZodIssueCode.custom,
|
|
11209
|
+
path: [...path4, "membershipLevels"],
|
|
11210
|
+
message: `membershipLevels contains '${level}' twice`
|
|
11211
|
+
});
|
|
11212
|
+
}
|
|
11213
|
+
seenLevels.add(level);
|
|
11214
|
+
}
|
|
11215
|
+
}
|
|
11216
|
+
});
|
|
11217
|
+
}
|
|
10974
11218
|
var SeedCommonShape = {
|
|
10975
11219
|
/** The schema typeName this seed instantiates (should match a declared schema). */
|
|
10976
11220
|
typeName: z.string().min(1),
|
|
@@ -11011,7 +11255,7 @@ var BlueprintSeedRecordSchema = z.discriminatedUnion("surface", [
|
|
|
11011
11255
|
RecordSeedSchema,
|
|
11012
11256
|
DocumentSeedSchema
|
|
11013
11257
|
]);
|
|
11014
|
-
var SELF_TOKEN_RE = /\$\{\{\s*(?:self|under)\.[^}]*?\s*\}\}/;
|
|
11258
|
+
var SELF_TOKEN_RE = /\$\{\{\s*(?:self|under|member)\.[^}]*?\s*\}\}/;
|
|
11015
11259
|
function lintSelfTokenPlacement(value, ctx) {
|
|
11016
11260
|
const walk = (node, path4, inRoleDataScope) => {
|
|
11017
11261
|
if (typeof node === "string") {
|
|
@@ -11019,7 +11263,7 @@ function lintSelfTokenPlacement(value, ctx) {
|
|
|
11019
11263
|
ctx.addIssue({
|
|
11020
11264
|
code: z.ZodIssueCode.custom,
|
|
11021
11265
|
path: path4,
|
|
11022
|
-
message: "'${{ self.* }}' is a runtime
|
|
11266
|
+
message: "'${{ self.* }}' / '${{ member.* }}' is a runtime placeholder \u2014 it is only valid inside a roles[].dataScope value"
|
|
11023
11267
|
});
|
|
11024
11268
|
}
|
|
11025
11269
|
return;
|
|
@@ -11219,9 +11463,36 @@ function lintSeedSurfaces(value, ctx) {
|
|
|
11219
11463
|
}
|
|
11220
11464
|
});
|
|
11221
11465
|
}
|
|
11466
|
+
function lintIdentitySurfaces(value, ctx) {
|
|
11467
|
+
const bp = value;
|
|
11468
|
+
if (!Array.isArray(bp.schemas)) return;
|
|
11469
|
+
bp.schemas.forEach((schema, i) => {
|
|
11470
|
+
if (!schema.allowedSurfaces?.includes("user")) return;
|
|
11471
|
+
ctx.addIssue({
|
|
11472
|
+
code: z.ZodIssueCode.custom,
|
|
11473
|
+
path: ["schemas", i, "allowedSurfaces"],
|
|
11474
|
+
message: `schema '${schema.typeName}': allowedSurfaces cannot include 'user' \u2014 a user-surfaced schema is account-global and requires a root API key, which no blueprint credential ever holds, so this would fail at apply with a 403. Model the data as a 'record' scoped to the user, or as an 'entity' schema (which a blueprint CAN provision, in its own app context).`
|
|
11475
|
+
});
|
|
11476
|
+
});
|
|
11477
|
+
}
|
|
11478
|
+
function lintIssuerContext(value, ctx) {
|
|
11479
|
+
const bp = value;
|
|
11480
|
+
if (!Array.isArray(bp.issuers)) return;
|
|
11481
|
+
bp.issuers.forEach((issuer, i) => {
|
|
11482
|
+
if (issuer.contextId === void 0 || issuer.contextId === bp.contextId) return;
|
|
11483
|
+
ctx.addIssue({
|
|
11484
|
+
code: z.ZodIssueCode.custom,
|
|
11485
|
+
path: ["issuers", i, "contextId"],
|
|
11486
|
+
message: `issuer '${issuer.issuerId}' targets app context '${issuer.contextId}', but this blueprint provisions '${bp.contextId}'. An issuer must target the blueprint's own context \u2014 everything else a blueprint creates does, and an issuer that points elsewhere attaches an identity provider to an app context the blueprint never mentions. Set it to '${bp.contextId}', or move this issuer into a blueprint for '${issuer.contextId}'.`
|
|
11487
|
+
});
|
|
11488
|
+
});
|
|
11489
|
+
}
|
|
11222
11490
|
var BLUEPRINT_FIELD_PHASES = Object.freeze({
|
|
11223
11491
|
servicePrincipal: "bootstrap",
|
|
11224
11492
|
issuers: "bootstrap",
|
|
11493
|
+
// Registration requires the platform's provisioning:c capability, which
|
|
11494
|
+
// only the bootstrap credential carries — same reasoning as issuers.
|
|
11495
|
+
namespaces: "bootstrap",
|
|
11225
11496
|
schemas: "in-context",
|
|
11226
11497
|
accessProfile: "in-context",
|
|
11227
11498
|
roles: "in-context",
|
|
@@ -11252,7 +11523,16 @@ var BlueprintSchema = z.object({
|
|
|
11252
11523
|
* service-principal creation — NOT the in-context load (see
|
|
11253
11524
|
* {@link BlueprintIssuerSchema}).
|
|
11254
11525
|
*/
|
|
11255
|
-
issuers: z.array(BlueprintIssuerSchema).optional()
|
|
11526
|
+
issuers: z.array(BlueprintIssuerSchema).optional(),
|
|
11527
|
+
/**
|
|
11528
|
+
* Optional namespaces to register before anything else needs them — see
|
|
11529
|
+
* {@link BlueprintNamespaceSchema}. Applied in the BOOTSTRAP-token phase,
|
|
11530
|
+
* alongside app-context/service-principal creation and issuers, BEFORE
|
|
11531
|
+
* schemas/seed (an `entityBacked` namespace's entities, and an
|
|
11532
|
+
* `allowedSurfaces: ['entity']` schema, both need the registration to
|
|
11533
|
+
* already exist). `org`/`client` need no entry — they exist already.
|
|
11534
|
+
*/
|
|
11535
|
+
namespaces: z.array(BlueprintNamespaceSchema).optional()
|
|
11256
11536
|
}).strict().superRefine((bp, ctx) => {
|
|
11257
11537
|
lintSelfTokenPlacement(bp, ctx);
|
|
11258
11538
|
lintIdentityRefs(bp, ctx);
|
|
@@ -11260,6 +11540,9 @@ var BlueprintSchema = z.object({
|
|
|
11260
11540
|
lintNamespacedScopeArrays(bp, ctx);
|
|
11261
11541
|
lintDataScopeKeys(bp, ctx);
|
|
11262
11542
|
lintSeedSurfaces(bp, ctx);
|
|
11543
|
+
lintIdentitySurfaces(bp, ctx);
|
|
11544
|
+
lintIssuerContext(bp, ctx);
|
|
11545
|
+
lintNamespaces(bp, ctx);
|
|
11263
11546
|
});
|
|
11264
11547
|
function formatIssuePath(path4) {
|
|
11265
11548
|
let out = "";
|
|
@@ -11414,9 +11697,11 @@ function deriveSuffix(contextId) {
|
|
|
11414
11697
|
var WHOLE_TOKEN_RE = /^\$\{\{\s*([A-Za-z_]\w*)\.([A-Za-z_]\w*)\s*\}\}$/;
|
|
11415
11698
|
var TOKEN_SCAN_RE = /\$\$\{\{|\$\{\{([\s\S]*?)\}\}|\$\{\{/g;
|
|
11416
11699
|
var INNER_REF_RE = /^([A-Za-z_]\w*)\.([A-Za-z_]\w*)$/;
|
|
11417
|
-
var DEFERRED_NAMESPACES = /* @__PURE__ */ new Set(["self", "identities", "under"]);
|
|
11700
|
+
var DEFERRED_NAMESPACES = /* @__PURE__ */ new Set(["self", "identities", "under", "member"]);
|
|
11418
11701
|
var DEFERRED_NS_ALTERNATION = [...DEFERRED_NAMESPACES].join("|");
|
|
11419
|
-
var DEFERRED_INNER_RE = new RegExp(
|
|
11702
|
+
var DEFERRED_INNER_RE = new RegExp(
|
|
11703
|
+
`^(?:${DEFERRED_NS_ALTERNATION})(?:\\.[A-Za-z_]\\w*)+(?::[A-Za-z_]\\w*)?$`
|
|
11704
|
+
);
|
|
11420
11705
|
function resolveRef(ns, name, ctx, path4) {
|
|
11421
11706
|
if (ns === "inputs") {
|
|
11422
11707
|
if (!(name in ctx.declared)) {
|
|
@@ -11715,9 +12000,11 @@ var taskManagement = {
|
|
|
11715
12000
|
// Least-privilege profile — MUST pass the CLI scope gate.
|
|
11716
12001
|
// r/c/u + search + schema discovery. NOT records:d (least privilege).
|
|
11717
12002
|
accessProfile: {
|
|
11718
|
-
allowedActions: ["records:r", "records:c", "records:u", "search:r", "schemas:r"]
|
|
11719
|
-
// dataScope
|
|
11720
|
-
//
|
|
12003
|
+
allowedActions: ["records:r", "records:c", "records:u", "search:r", "schemas:r"],
|
|
12004
|
+
// dataScope: {} (present, empty) is the deliberate marker for a
|
|
12005
|
+
// tenant-level shared tracker — every task visible to every holder of
|
|
12006
|
+
// this credential. Add a `scope:org` entry instead for per-org isolation.
|
|
12007
|
+
dataScope: {}
|
|
11721
12008
|
},
|
|
11722
12009
|
servicePrincipal: {
|
|
11723
12010
|
externalId: "task-management",
|
|
@@ -11740,250 +12027,6 @@ var taskManagement = {
|
|
|
11740
12027
|
]
|
|
11741
12028
|
};
|
|
11742
12029
|
var task_management_default = taskManagement;
|
|
11743
|
-
var codingAgentMemory = {
|
|
11744
|
-
name: "coding-agent-memory",
|
|
11745
|
-
version: "1.0.0",
|
|
11746
|
-
description: "Persistent project memory for a coding agent \u2014 decisions, conventions, and gotchas that survive across sessions.",
|
|
11747
|
-
contextId: "coding-memory",
|
|
11748
|
-
contextName: "Coding Agent \u2014 Project Memory",
|
|
11749
|
-
schemas: [
|
|
11750
|
-
{
|
|
11751
|
-
// A durable architectural/product decision: what was decided, and WHY.
|
|
11752
|
-
// The rationale is the high-value, RAG-able body — "why did we do X?"
|
|
11753
|
-
// is the question a cold-context agent most needs answered.
|
|
11754
|
-
typeName: "decision",
|
|
11755
|
-
displayName: "Decision",
|
|
11756
|
-
indexMode: "HYBRID",
|
|
11757
|
-
// keyword on titles + semantic on statement/rationale
|
|
11758
|
-
fields: [
|
|
11759
|
-
{
|
|
11760
|
-
fieldId: "title",
|
|
11761
|
-
fieldType: "string",
|
|
11762
|
-
required: true,
|
|
11763
|
-
searchable: true,
|
|
11764
|
-
validation: { minLength: 1, maxLength: 200 },
|
|
11765
|
-
renderHints: { label: "Title", widget: "text", order: 1, displayField: true }
|
|
11766
|
-
},
|
|
11767
|
-
{
|
|
11768
|
-
fieldId: "statement",
|
|
11769
|
-
fieldType: "string",
|
|
11770
|
-
searchable: true,
|
|
11771
|
-
description: "What was decided, in one or two sentences.",
|
|
11772
|
-
renderHints: { label: "Statement", widget: "textarea", order: 2 }
|
|
11773
|
-
},
|
|
11774
|
-
{
|
|
11775
|
-
fieldId: "rationale",
|
|
11776
|
-
fieldType: "string",
|
|
11777
|
-
searchable: true,
|
|
11778
|
-
description: "Why \u2014 the trade-offs and the context. The most-recalled field.",
|
|
11779
|
-
renderHints: { label: "Rationale", widget: "textarea", order: 3 }
|
|
11780
|
-
},
|
|
11781
|
-
{
|
|
11782
|
-
fieldId: "status",
|
|
11783
|
-
fieldType: "enum",
|
|
11784
|
-
filterable: true,
|
|
11785
|
-
enumValues: ["proposed", "active", "superseded"],
|
|
11786
|
-
renderHints: { label: "Status", widget: "select", order: 4 }
|
|
11787
|
-
},
|
|
11788
|
-
{
|
|
11789
|
-
fieldId: "area",
|
|
11790
|
-
fieldType: "string",
|
|
11791
|
-
filterable: true,
|
|
11792
|
-
description: 'Subsystem / module the decision applies to (e.g. "auth", "search").',
|
|
11793
|
-
renderHints: { label: "Area", widget: "text", order: 5 }
|
|
11794
|
-
},
|
|
11795
|
-
{
|
|
11796
|
-
fieldId: "decidedOn",
|
|
11797
|
-
fieldType: "date",
|
|
11798
|
-
description: 'ISO-8601 date. Range-queryable \u2014 "decisions made this quarter".',
|
|
11799
|
-
renderHints: { label: "Decided on", widget: "date", order: 6 }
|
|
11800
|
-
}
|
|
11801
|
-
// externalId is the record's FIRST-CLASS identifier (the dedup/upsert key + the
|
|
11802
|
-
// value a `reference` resolves against) — it is NOT a payload field, so it is not
|
|
11803
|
-
// declared here; the loader sends it top-level on the RecordRequest.
|
|
11804
|
-
],
|
|
11805
|
-
// `externalId` is a first-class identifier with its own finder (exact
|
|
11806
|
-
// get/upsert) — look it up directly, never redeclare it as a schema lookup.
|
|
11807
|
-
// The choice below is MIGRATION-LOCKED once the schema is live:
|
|
11808
|
-
// • `area`/`status` — EQUALITY (categorical): "list active decisions",
|
|
11809
|
-
// "all decisions in the auth area". 2 of the 7 fast index slots.
|
|
11810
|
-
// • `decidedOn` — RANGE: ordered `from`/`to`/`prefix` over the date.
|
|
11811
|
-
lookupFields: ["area", "status", { fieldName: "decidedOn", rangeEnabled: true }]
|
|
11812
|
-
},
|
|
11813
|
-
{
|
|
11814
|
-
// A coding convention the team follows — the agent reads these before it
|
|
11815
|
-
// writes code so it matches the surrounding style instead of inventing one.
|
|
11816
|
-
typeName: "convention",
|
|
11817
|
-
displayName: "Convention",
|
|
11818
|
-
indexMode: "HYBRID",
|
|
11819
|
-
fields: [
|
|
11820
|
-
{
|
|
11821
|
-
fieldId: "name",
|
|
11822
|
-
fieldType: "string",
|
|
11823
|
-
required: true,
|
|
11824
|
-
searchable: true,
|
|
11825
|
-
validation: { minLength: 1, maxLength: 200 },
|
|
11826
|
-
renderHints: { label: "Name", widget: "text", order: 1, displayField: true }
|
|
11827
|
-
},
|
|
11828
|
-
{
|
|
11829
|
-
fieldId: "rule",
|
|
11830
|
-
fieldType: "string",
|
|
11831
|
-
searchable: true,
|
|
11832
|
-
description: "The convention itself, stated as an imperative.",
|
|
11833
|
-
renderHints: { label: "Rule", widget: "textarea", order: 2 }
|
|
11834
|
-
},
|
|
11835
|
-
{
|
|
11836
|
-
fieldId: "area",
|
|
11837
|
-
fieldType: "string",
|
|
11838
|
-
filterable: true,
|
|
11839
|
-
renderHints: { label: "Area", widget: "text", order: 3 }
|
|
11840
|
-
},
|
|
11841
|
-
{
|
|
11842
|
-
// Parallels `decision.status` so a convention can be RETIRED rather than
|
|
11843
|
-
// deleted (the blueprint omits records:d — memory is superseded, not lost).
|
|
11844
|
-
fieldId: "status",
|
|
11845
|
-
fieldType: "enum",
|
|
11846
|
-
filterable: true,
|
|
11847
|
-
enumValues: ["active", "retired"],
|
|
11848
|
-
renderHints: { label: "Status", widget: "select", order: 4 }
|
|
11849
|
-
},
|
|
11850
|
-
{
|
|
11851
|
-
// A typed REFERENCE to the decision that established this convention — a
|
|
11852
|
-
// foreign-key link between record types (the knowledge-graph edge). The
|
|
11853
|
-
// platform requires both the target type AND its surface; the value is the
|
|
11854
|
-
// target decision's externalId (the default `targetField`). Provenance:
|
|
11855
|
-
// "why does this convention exist? → open the decision behind it." Not
|
|
11856
|
-
// searchable (a foreign-key id is search noise); declared as an equality
|
|
11857
|
-
// lookup below so you can also enumerate "conventions from decision X".
|
|
11858
|
-
fieldId: "establishedByDecision",
|
|
11859
|
-
fieldType: "reference",
|
|
11860
|
-
targetTypeName: "decision",
|
|
11861
|
-
targetSurface: "record",
|
|
11862
|
-
targetField: "externalId",
|
|
11863
|
-
cardinality: "one",
|
|
11864
|
-
renderHints: { label: "Established by (decision)", order: 5 }
|
|
11865
|
-
},
|
|
11866
|
-
{
|
|
11867
|
-
fieldId: "adoptedOn",
|
|
11868
|
-
fieldType: "date",
|
|
11869
|
-
description: "ISO-8601 \u2014 when this convention was adopted. Range-queryable.",
|
|
11870
|
-
renderHints: { label: "Adopted on", widget: "date", order: 6 }
|
|
11871
|
-
}
|
|
11872
|
-
// externalId: first-class identifier, not a payload field (see `decision`).
|
|
11873
|
-
],
|
|
11874
|
-
// externalId has its own first-class finder for exact get — look it up
|
|
11875
|
-
// directly, not as a schema lookup. Locked equality-vs-range choices:
|
|
11876
|
-
// • `area`/`status` — EQUALITY: "active conventions for auth".
|
|
11877
|
-
// • `establishedByDecision` — EQUALITY on the reference value: enumerate
|
|
11878
|
-
// "conventions established by decision X" (a forward link query; the
|
|
11879
|
-
// platform has no reverse-reference index on this surface).
|
|
11880
|
-
// • `adoptedOn` — RANGE over the adoption date.
|
|
11881
|
-
// 3 equality lookups (of 7 fast slots) + 1 range row.
|
|
11882
|
-
lookupFields: ["area", "status", "establishedByDecision", { fieldName: "adoptedOn", rangeEnabled: true }]
|
|
11883
|
-
},
|
|
11884
|
-
{
|
|
11885
|
-
// A gotcha / sharp edge: a symptom, its cause, and the fix. Saves the
|
|
11886
|
-
// agent (and the human) from re-discovering the same trap.
|
|
11887
|
-
typeName: "gotcha",
|
|
11888
|
-
displayName: "Gotcha",
|
|
11889
|
-
indexMode: "HYBRID",
|
|
11890
|
-
fields: [
|
|
11891
|
-
{
|
|
11892
|
-
fieldId: "symptom",
|
|
11893
|
-
fieldType: "string",
|
|
11894
|
-
required: true,
|
|
11895
|
-
searchable: true,
|
|
11896
|
-
validation: { minLength: 1, maxLength: 500 },
|
|
11897
|
-
renderHints: { label: "Symptom", widget: "textarea", order: 1, displayField: true }
|
|
11898
|
-
},
|
|
11899
|
-
{
|
|
11900
|
-
fieldId: "cause",
|
|
11901
|
-
fieldType: "string",
|
|
11902
|
-
searchable: true,
|
|
11903
|
-
renderHints: { label: "Cause", widget: "textarea", order: 2 }
|
|
11904
|
-
},
|
|
11905
|
-
{
|
|
11906
|
-
fieldId: "fix",
|
|
11907
|
-
fieldType: "string",
|
|
11908
|
-
searchable: true,
|
|
11909
|
-
renderHints: { label: "Fix", widget: "textarea", order: 3 }
|
|
11910
|
-
},
|
|
11911
|
-
{
|
|
11912
|
-
fieldId: "area",
|
|
11913
|
-
fieldType: "string",
|
|
11914
|
-
filterable: true,
|
|
11915
|
-
renderHints: { label: "Area", widget: "text", order: 4 }
|
|
11916
|
-
},
|
|
11917
|
-
{
|
|
11918
|
-
// A gotcha can be RETIRED once the underlying trap is fixed for good —
|
|
11919
|
-
// superseded, not deleted (the blueprint omits records:d).
|
|
11920
|
-
fieldId: "status",
|
|
11921
|
-
fieldType: "enum",
|
|
11922
|
-
filterable: true,
|
|
11923
|
-
enumValues: ["active", "retired"],
|
|
11924
|
-
renderHints: { label: "Status", widget: "select", order: 5 }
|
|
11925
|
-
},
|
|
11926
|
-
{
|
|
11927
|
-
fieldId: "discoveredOn",
|
|
11928
|
-
fieldType: "date",
|
|
11929
|
-
description: "ISO-8601 \u2014 when this gotcha was first hit. Range-queryable.",
|
|
11930
|
-
renderHints: { label: "Discovered on", widget: "date", order: 6 }
|
|
11931
|
-
}
|
|
11932
|
-
// externalId: first-class identifier, not a payload field (see `decision`).
|
|
11933
|
-
],
|
|
11934
|
-
// externalId has its own first-class finder for exact get — look it up
|
|
11935
|
-
// directly, not as a schema lookup. `area`/`status` EQUALITY ("active
|
|
11936
|
-
// gotchas in search"); `discoveredOn` RANGE over the discovery date.
|
|
11937
|
-
lookupFields: ["area", "status", { fieldName: "discoveredOn", rangeEnabled: true }]
|
|
11938
|
-
}
|
|
11939
|
-
],
|
|
11940
|
-
// Least-privilege profile — MUST pass the CLI scope gate.
|
|
11941
|
-
// r/c/u + search + schema discovery + inference:r (grounded "why did we do X?"
|
|
11942
|
-
// recall over the captured rationale). NOT records:d: memory is superseded,
|
|
11943
|
-
// not deleted, so the project's decision history stays auditable.
|
|
11944
|
-
accessProfile: {
|
|
11945
|
-
allowedActions: ["records:r", "records:c", "records:u", "search:r", "schemas:r", "inference:r"]
|
|
11946
|
-
},
|
|
11947
|
-
servicePrincipal: {
|
|
11948
|
-
externalId: "coding-agent-memory",
|
|
11949
|
-
displayName: "Coding Agent \u2014 Project Memory"
|
|
11950
|
-
},
|
|
11951
|
-
seed: [
|
|
11952
|
-
{
|
|
11953
|
-
// Seeded FIRST so the convention below resolves its reference to it — the
|
|
11954
|
-
// loader creates seeds in array order, and a reference target must exist when
|
|
11955
|
-
// the referencing record is written.
|
|
11956
|
-
surface: "record",
|
|
11957
|
-
typeName: "decision",
|
|
11958
|
-
externalId: "seed-use-vectros-for-memory",
|
|
11959
|
-
fields: {
|
|
11960
|
-
title: "Use Vectros as the coding agent\u2019s project memory",
|
|
11961
|
-
statement: "Persist decisions, conventions, and gotchas as Vectros records so they survive across agent sessions.",
|
|
11962
|
-
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.",
|
|
11963
|
-
status: "active",
|
|
11964
|
-
area: "getting-started",
|
|
11965
|
-
decidedOn: "2026-06-14"
|
|
11966
|
-
}
|
|
11967
|
-
},
|
|
11968
|
-
{
|
|
11969
|
-
// Demonstrates a live typed link at bootstrap: this convention references the
|
|
11970
|
-
// decision above by its externalId, so "open the decision behind this rule"
|
|
11971
|
-
// works the moment the blueprint is applied.
|
|
11972
|
-
surface: "record",
|
|
11973
|
-
typeName: "convention",
|
|
11974
|
-
externalId: "seed-record-the-why",
|
|
11975
|
-
fields: {
|
|
11976
|
-
name: "Record the why, not just the what",
|
|
11977
|
-
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.",
|
|
11978
|
-
area: "getting-started",
|
|
11979
|
-
status: "active",
|
|
11980
|
-
establishedByDecision: "seed-use-vectros-for-memory",
|
|
11981
|
-
adoptedOn: "2026-06-14"
|
|
11982
|
-
}
|
|
11983
|
-
}
|
|
11984
|
-
]
|
|
11985
|
-
};
|
|
11986
|
-
var coding_agent_memory_default = codingAgentMemory;
|
|
11987
12030
|
var DATA_PLANE_ACTIONS = [
|
|
11988
12031
|
"records:r",
|
|
11989
12032
|
"records:c",
|
|
@@ -13110,7 +13153,11 @@ var agenticSdlc = {
|
|
|
13110
13153
|
// mints for THIS blueprint's service principal (the MCP/API runtime). See
|
|
13111
13154
|
// DATA_PLANE_ACTIONS above for the action set + rationale.
|
|
13112
13155
|
accessProfile: {
|
|
13113
|
-
allowedActions: DATA_PLANE_ACTIONS
|
|
13156
|
+
allowedActions: DATA_PLANE_ACTIONS,
|
|
13157
|
+
// dataScope: {} (present, empty) is the deliberate marker for tenant-wide
|
|
13158
|
+
// reach — this service key IS the runtime's own access to the whole KB,
|
|
13159
|
+
// not a multi-owner resource to isolate.
|
|
13160
|
+
dataScope: {}
|
|
13114
13161
|
},
|
|
13115
13162
|
// A reusable `editor` role for the HUMAN owner — DISTINCT from `accessProfile`
|
|
13116
13163
|
// (which scopes only the service-principal key). `bootstrap` provisions this
|
|
@@ -13130,7 +13177,9 @@ var agenticSdlc = {
|
|
|
13130
13177
|
// which includes members' private memory — the trusted-administrator tradeoff,
|
|
13131
13178
|
// called out in the bundled guide. Scope it down if your deployment wants
|
|
13132
13179
|
// curator access without private-memory visibility.
|
|
13133
|
-
|
|
13180
|
+
// dataScope: {} echoes the "deliberately UNSCOPED (whole-context)" call-out
|
|
13181
|
+
// above as an explicit marker, not just prose — see it for the tradeoff.
|
|
13182
|
+
editor: [{ allowedActions: EDITOR_ACTIONS, dataScope: {} }],
|
|
13134
13183
|
// A team member (or their agent) — TWO memory tiers as two UNIONed clauses
|
|
13135
13184
|
// (a record is accessible when ANY clause grants it; within one clause every
|
|
13136
13185
|
// dataScope dimension must match):
|
|
@@ -13150,7 +13199,9 @@ var agenticSdlc = {
|
|
|
13150
13199
|
// `vectros join agentic-sdlc --role member`; verify a binding with
|
|
13151
13200
|
// `vectros access explain --principal me --context agentic-sdlc`.
|
|
13152
13201
|
member: [
|
|
13153
|
-
|
|
13202
|
+
// dataScope: {} echoes clause 1's own "UNSCOPED, but safe" call-out above
|
|
13203
|
+
// as an explicit marker, not just prose.
|
|
13204
|
+
{ allowedActions: MEMBER_SHARED_READ_ACTIONS, dataScope: {} },
|
|
13154
13205
|
{
|
|
13155
13206
|
// `inference:r` is IN this self-scoped clause (not only clause 1) so a
|
|
13156
13207
|
// member's `rag_ask` can ground on its OWN private memory — the read
|
|
@@ -13265,7 +13316,11 @@ var secondBrain = {
|
|
|
13265
13316
|
// (so the agent can `rag_ask` a grounded, cited question over your notes). No
|
|
13266
13317
|
// records:d: notes are archived (status → 'archived'), never deleted.
|
|
13267
13318
|
accessProfile: {
|
|
13268
|
-
allowedActions: ["records:r", "records:c", "records:u", "search:r", "schemas:r", "inference:r"]
|
|
13319
|
+
allowedActions: ["records:r", "records:c", "records:u", "search:r", "schemas:r", "inference:r"],
|
|
13320
|
+
// dataScope: {} (present, empty) is the deliberate marker for tenant-wide
|
|
13321
|
+
// reach — this is a single personal-knowledge-base credential with one
|
|
13322
|
+
// holder, not a multi-owner shared resource to isolate.
|
|
13323
|
+
dataScope: {}
|
|
13269
13324
|
},
|
|
13270
13325
|
servicePrincipal: {
|
|
13271
13326
|
externalId: "second-brain",
|
|
@@ -13397,7 +13452,11 @@ var clinicalIntake = {
|
|
|
13397
13452
|
// reveal qualifier (the demo key literally cannot un-redact the PHI fields),
|
|
13398
13453
|
// and NO inference:r (we never point a RAG capability at a PHI corpus here).
|
|
13399
13454
|
accessProfile: {
|
|
13400
|
-
allowedActions: ["records:r", "records:c", "records:u", "search:r", "schemas:r"]
|
|
13455
|
+
allowedActions: ["records:r", "records:c", "records:u", "search:r", "schemas:r"],
|
|
13456
|
+
// dataScope: {} (present, empty) is the deliberate marker for tenant-wide
|
|
13457
|
+
// reach, unchanged from this demo's original intent — a real clinical
|
|
13458
|
+
// deployment should scope this (e.g. `scope:org`) before handling real PHI.
|
|
13459
|
+
dataScope: {}
|
|
13401
13460
|
},
|
|
13402
13461
|
servicePrincipal: {
|
|
13403
13462
|
externalId: "clinical-intake",
|
|
@@ -13426,7 +13485,6 @@ var clinicalIntake = {
|
|
|
13426
13485
|
var clinical_intake_default = clinicalIntake;
|
|
13427
13486
|
var BUNDLED_BLUEPRINTS = [
|
|
13428
13487
|
task_management_default,
|
|
13429
|
-
coding_agent_memory_default,
|
|
13430
13488
|
agentic_sdlc_default,
|
|
13431
13489
|
second_brain_default,
|
|
13432
13490
|
clinical_intake_default
|
|
@@ -13725,18 +13783,17 @@ async function mintBridgeToken(opts) {
|
|
|
13725
13783
|
ttlSeconds = DEFAULT_BRIDGE_TTL_SECONDS,
|
|
13726
13784
|
tenant = "live",
|
|
13727
13785
|
useScopedToken = false,
|
|
13786
|
+
contextId,
|
|
13728
13787
|
fetchImpl = fetch
|
|
13729
13788
|
} = opts;
|
|
13730
13789
|
const base = apiBaseUrl.replace(/\/$/, "");
|
|
13731
13790
|
const path4 = bridgeTokenPath(useScopedToken);
|
|
13732
|
-
|
|
13733
|
-
|
|
13734
|
-
|
|
13735
|
-
|
|
13736
|
-
|
|
13737
|
-
|
|
13738
|
-
(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)` : ""
|
|
13739
|
-
);
|
|
13791
|
+
let url = `${base}${path4}?ttl=${encodeURIComponent(String(ttlSeconds))}&tenant=${encodeURIComponent(tenant)}`;
|
|
13792
|
+
if (contextId) url += `&context=${encodeURIComponent(contextId)}`;
|
|
13793
|
+
return fetchStToken(url, cognitoToken, fetchImpl, path4, (status) => {
|
|
13794
|
+
if (status !== 404 || useScopedToken || contextId) return "";
|
|
13795
|
+
return ` (the dedicated ${CLI_BOOTSTRAP_TOKEN_PATH} endpoint may not be deployed yet \u2014 retry with --use-scoped-token for the fallback path)`;
|
|
13796
|
+
});
|
|
13740
13797
|
}
|
|
13741
13798
|
async function mintContextToken(opts) {
|
|
13742
13799
|
const {
|
|
@@ -14119,8 +14176,29 @@ var KNOWN_CONTROL_PLANE_RESOURCES = [
|
|
|
14119
14176
|
// control-plane, unlike per-namespace entity CRUD. (Cosmetic: any
|
|
14120
14177
|
// non-data-plane resource hard-rejects whether or not it is listed here;
|
|
14121
14178
|
// this list only sharpens the error message.)
|
|
14179
|
+
//
|
|
14180
|
+
// ⚠️ This bars `namespaces` as a SCOPE — no partner-authored role can ever
|
|
14181
|
+
// be granted it. It does NOT bar a blueprint's own top-level `namespaces[]`
|
|
14182
|
+
// FORMAT field (@vectros-ai/blueprints), which deliberately DOES write this
|
|
14183
|
+
// same registry — the same "DISCRETIONARY vs UNCONDITIONAL" distinction
|
|
14184
|
+
// {@link PARTIALLY_ADMITTED_OPS} draws for `entities`, applied here: the
|
|
14185
|
+
// write rides `provisioning:c` (non-grantable — no partner role can ever
|
|
14186
|
+
// hold it, the same bearer `keys`/`app-contexts` writes already require)
|
|
14187
|
+
// and the platform confines it to the blueprint's OWN context
|
|
14188
|
+
// unconditionally, never tenant-wide, so the reach is bounded to what the
|
|
14189
|
+
// blueprint already provisions, never the pack's say-so alone. See
|
|
14190
|
+
// `@vectros-ai/blueprints`' `namespaces[]` doc for the full design.
|
|
14122
14191
|
"namespaces"
|
|
14123
14192
|
];
|
|
14193
|
+
var PARTIALLY_ADMITTED_OPS = /* @__PURE__ */ new Map([
|
|
14194
|
+
["profiles", /* @__PURE__ */ new Set(["r", "u", "d"])]
|
|
14195
|
+
]);
|
|
14196
|
+
var SELF_QUALIFIED_OPS = /* @__PURE__ */ new Map([
|
|
14197
|
+
["profiles", /* @__PURE__ */ new Set(["c"])]
|
|
14198
|
+
]);
|
|
14199
|
+
var UNQUALIFIABLE_WITH_ANY_TAIL = /* @__PURE__ */ new Map([
|
|
14200
|
+
["profiles", /* @__PURE__ */ new Set(["r"])]
|
|
14201
|
+
]);
|
|
14124
14202
|
var VALID_OPS = /* @__PURE__ */ new Set(["c", "r", "u", "d"]);
|
|
14125
14203
|
var DATA_PLANE_SET = new Set(DATA_PLANE_ALLOWLIST);
|
|
14126
14204
|
var CONTROL_PLANE_SET = new Set(KNOWN_CONTROL_PLANE_RESOURCES);
|
|
@@ -14136,7 +14214,60 @@ ${summary}`
|
|
|
14136
14214
|
this.name = "ScopeGateError";
|
|
14137
14215
|
this.rejected = rejected;
|
|
14138
14216
|
}
|
|
14139
|
-
};
|
|
14217
|
+
};
|
|
14218
|
+
var GRANTED_CAPABILITIES_ALLOWLIST = ["member-lifecycle"];
|
|
14219
|
+
var GRANTED_CAPABILITIES_SET = new Set(GRANTED_CAPABILITIES_ALLOWLIST);
|
|
14220
|
+
var CAPABILITIES_HINT = GRANTED_CAPABILITIES_ALLOWLIST.join("/");
|
|
14221
|
+
var CapabilityGateError = class extends Error {
|
|
14222
|
+
rejected;
|
|
14223
|
+
constructor(rejected) {
|
|
14224
|
+
const summary = rejected.map((r) => ` \u2022 ${r.action}: ${r.reason}`).join("\n");
|
|
14225
|
+
super(
|
|
14226
|
+
`Capability gate rejected ${rejected.length} requested capability(ies); the bootstrap loader grants (${CAPABILITIES_HINT}) only and mints nothing on reject:
|
|
14227
|
+
${summary}`
|
|
14228
|
+
);
|
|
14229
|
+
this.name = "CapabilityGateError";
|
|
14230
|
+
this.rejected = rejected;
|
|
14231
|
+
}
|
|
14232
|
+
};
|
|
14233
|
+
function evaluateCapabilityToken(name) {
|
|
14234
|
+
const token = name.trim();
|
|
14235
|
+
if (token.length === 0) {
|
|
14236
|
+
return { action: name, reason: "empty capability name" };
|
|
14237
|
+
}
|
|
14238
|
+
if (!GRANTED_CAPABILITIES_SET.has(token)) {
|
|
14239
|
+
return {
|
|
14240
|
+
action: name,
|
|
14241
|
+
reason: `requests capability '${token}', which the bootstrap loader does not grant (${CAPABILITIES_HINT} only). A capability outside this list must be requested deliberately in the developer portal, never via the bootstrap loader`
|
|
14242
|
+
};
|
|
14243
|
+
}
|
|
14244
|
+
return null;
|
|
14245
|
+
}
|
|
14246
|
+
function evaluateCapabilityGate(capabilities) {
|
|
14247
|
+
const accepted = [];
|
|
14248
|
+
const rejected = [];
|
|
14249
|
+
for (const name of capabilities ?? []) {
|
|
14250
|
+
const rejection = evaluateCapabilityToken(name);
|
|
14251
|
+
if (rejection) {
|
|
14252
|
+
rejected.push(rejection);
|
|
14253
|
+
} else {
|
|
14254
|
+
accepted.push(name);
|
|
14255
|
+
}
|
|
14256
|
+
}
|
|
14257
|
+
return { ok: rejected.length === 0, accepted, rejected };
|
|
14258
|
+
}
|
|
14259
|
+
function assertCapabilityGate(capabilities) {
|
|
14260
|
+
const result = evaluateCapabilityGate(capabilities);
|
|
14261
|
+
if (!result.ok) {
|
|
14262
|
+
throw new CapabilityGateError(result.rejected);
|
|
14263
|
+
}
|
|
14264
|
+
}
|
|
14265
|
+
function firstInvalidOp(ops) {
|
|
14266
|
+
for (const ch of ops) {
|
|
14267
|
+
if (!VALID_OPS.has(ch)) return ch;
|
|
14268
|
+
}
|
|
14269
|
+
return void 0;
|
|
14270
|
+
}
|
|
14140
14271
|
function evaluateScopeToken(action) {
|
|
14141
14272
|
const token = action.trim();
|
|
14142
14273
|
if (token.length === 0) {
|
|
@@ -14158,6 +14289,48 @@ function evaluateScopeToken(action) {
|
|
|
14158
14289
|
};
|
|
14159
14290
|
}
|
|
14160
14291
|
if (!DATA_PLANE_SET.has(resource)) {
|
|
14292
|
+
const admittedOps = PARTIALLY_ADMITTED_OPS.get(resource);
|
|
14293
|
+
const selfOnlyOps = SELF_QUALIFIED_OPS.get(resource);
|
|
14294
|
+
if (admittedOps || selfOnlyOps) {
|
|
14295
|
+
const admittedHint = [.../* @__PURE__ */ new Set([...admittedOps ?? [], ...selfOnlyOps ?? []])].sort().join("/");
|
|
14296
|
+
if (ops === "*") {
|
|
14297
|
+
return {
|
|
14298
|
+
action,
|
|
14299
|
+
reason: `requests '${resource}:*', which includes operation(s) outside the narrow allowlist (${admittedHint}) admitted for this control-plane resource. Request only the specific operations you need`
|
|
14300
|
+
};
|
|
14301
|
+
}
|
|
14302
|
+
const invalidOp2 = firstInvalidOp(ops);
|
|
14303
|
+
if (invalidOp2) {
|
|
14304
|
+
return {
|
|
14305
|
+
action,
|
|
14306
|
+
reason: `invalid operation character '${invalidOp2}' (valid operations are c, r, u, d, or '*')`
|
|
14307
|
+
};
|
|
14308
|
+
}
|
|
14309
|
+
const qualifier = parts.length > 2 ? parts.slice(2).join(":") : void 0;
|
|
14310
|
+
const isSelfQualified = qualifier === "self";
|
|
14311
|
+
const unqualifiableOps = UNQUALIFIABLE_WITH_ANY_TAIL.get(resource);
|
|
14312
|
+
for (const ch of ops) {
|
|
14313
|
+
if (qualifier !== void 0 && unqualifiableOps?.has(ch)) {
|
|
14314
|
+
return {
|
|
14315
|
+
action,
|
|
14316
|
+
reason: `requests '${resource}:${ch}' alongside a qualifier ('${qualifier}'), but the platform does not resolve a qualifier for '${ch}' on '${resource}' at all \u2014 it will reject the WHOLE entry as malformed, not just ignore the qualifier. Split into separate entries (e.g. '${resource}:${ch}' + the other op(s) with their qualifier).`
|
|
14317
|
+
};
|
|
14318
|
+
}
|
|
14319
|
+
if (admittedOps?.has(ch)) continue;
|
|
14320
|
+
if (selfOnlyOps?.has(ch)) {
|
|
14321
|
+
if (isSelfQualified) continue;
|
|
14322
|
+
return {
|
|
14323
|
+
action,
|
|
14324
|
+
reason: `requests control-plane scope '${resource}:${ch}' without the required ':self' qualifier. A bare or arbitrarily-qualified '${resource}:${ch}' is outside the narrow allowlist (${admittedHint}) admitted for this resource \u2014 only '${resource}:${ch}:self' is. That operation must otherwise be done deliberately in the developer portal, never via the bootstrap loader`
|
|
14325
|
+
};
|
|
14326
|
+
}
|
|
14327
|
+
return {
|
|
14328
|
+
action,
|
|
14329
|
+
reason: `requests control-plane scope '${resource}:${ch}', which is outside the narrow allowlist (${admittedHint}) admitted for this resource. That operation must be done deliberately in the developer portal, never via the bootstrap loader`
|
|
14330
|
+
};
|
|
14331
|
+
}
|
|
14332
|
+
return null;
|
|
14333
|
+
}
|
|
14161
14334
|
if (CONTROL_PLANE_SET.has(resource)) {
|
|
14162
14335
|
return {
|
|
14163
14336
|
action,
|
|
@@ -14172,13 +14345,12 @@ function evaluateScopeToken(action) {
|
|
|
14172
14345
|
if (ops === "*") {
|
|
14173
14346
|
return null;
|
|
14174
14347
|
}
|
|
14175
|
-
|
|
14176
|
-
|
|
14177
|
-
|
|
14178
|
-
|
|
14179
|
-
|
|
14180
|
-
|
|
14181
|
-
}
|
|
14348
|
+
const invalidOp = firstInvalidOp(ops);
|
|
14349
|
+
if (invalidOp) {
|
|
14350
|
+
return {
|
|
14351
|
+
action,
|
|
14352
|
+
reason: `invalid operation character '${invalidOp}' (valid operations are c, r, u, d, or '*')`
|
|
14353
|
+
};
|
|
14182
14354
|
}
|
|
14183
14355
|
return null;
|
|
14184
14356
|
}
|
|
@@ -14201,6 +14373,16 @@ function assertScopeGate(allowedActions) {
|
|
|
14201
14373
|
throw new ScopeGateError(result.rejected);
|
|
14202
14374
|
}
|
|
14203
14375
|
}
|
|
14376
|
+
function assertBlueprintGates(blueprint) {
|
|
14377
|
+
assertScopeGate(blueprint.accessProfile.allowedActions);
|
|
14378
|
+
assertCapabilityGate(blueprint.accessProfile.capabilities);
|
|
14379
|
+
for (const clauses of Object.values(blueprint.roles ?? {})) {
|
|
14380
|
+
for (const clause of clauses) {
|
|
14381
|
+
assertScopeGate(clause.allowedActions);
|
|
14382
|
+
assertCapabilityGate(clause.capabilities);
|
|
14383
|
+
}
|
|
14384
|
+
}
|
|
14385
|
+
}
|
|
14204
14386
|
|
|
14205
14387
|
// src/machine-id.ts
|
|
14206
14388
|
import { promises as fs } from "fs";
|
|
@@ -14305,6 +14487,68 @@ async function resolveApplyTimeIdentities(blueprint, client, log) {
|
|
|
14305
14487
|
function actionOf(created) {
|
|
14306
14488
|
return created ? "created" : "reconciled";
|
|
14307
14489
|
}
|
|
14490
|
+
async function registerNamespaceIdempotently(client, contextIdForRequest, decl) {
|
|
14491
|
+
const body = {
|
|
14492
|
+
namespace: decl.namespace,
|
|
14493
|
+
entityBacked: decl.entityBacked,
|
|
14494
|
+
specificityRank: decl.specificityRank,
|
|
14495
|
+
membershipRecordType: decl.membershipRecordType,
|
|
14496
|
+
membershipTargetField: decl.membershipTargetField,
|
|
14497
|
+
membershipLevelField: decl.membershipLevelField,
|
|
14498
|
+
membershipLevels: decl.membershipLevels
|
|
14499
|
+
};
|
|
14500
|
+
try {
|
|
14501
|
+
await client.registerNamespace({ contextId: contextIdForRequest, body });
|
|
14502
|
+
return "created";
|
|
14503
|
+
} catch (err) {
|
|
14504
|
+
const info = extractServerError(err);
|
|
14505
|
+
if (info.statusCode !== 400) throw err;
|
|
14506
|
+
let existing;
|
|
14507
|
+
try {
|
|
14508
|
+
existing = await client.getNamespace({ namespace: decl.namespace, contextId: contextIdForRequest });
|
|
14509
|
+
} catch {
|
|
14510
|
+
throw err;
|
|
14511
|
+
}
|
|
14512
|
+
if (existing.contextId !== contextIdForRequest) throw err;
|
|
14513
|
+
const mismatch = describeNamespaceMismatch(decl, existing);
|
|
14514
|
+
if (mismatch) {
|
|
14515
|
+
throw new Error(
|
|
14516
|
+
`namespace '${decl.namespace}' is already registered with different settings than this blueprint declares (${mismatch}) \u2014 this looks like a collision with another blueprint or a manual registration, not a benign re-apply. Resolve the disagreement (or rename this blueprint's namespace) before re-applying.`
|
|
14517
|
+
);
|
|
14518
|
+
}
|
|
14519
|
+
return "reconciled";
|
|
14520
|
+
}
|
|
14521
|
+
}
|
|
14522
|
+
function describeNamespaceMismatch(decl, existing) {
|
|
14523
|
+
const mismatches = [];
|
|
14524
|
+
const declaredEntityBacked = decl.entityBacked ?? false;
|
|
14525
|
+
const existingEntityBacked = existing.entityBacked ?? false;
|
|
14526
|
+
if (declaredEntityBacked !== existingEntityBacked) {
|
|
14527
|
+
mismatches.push(
|
|
14528
|
+
`entityBacked: declared ${JSON.stringify(declaredEntityBacked)}, registered ${JSON.stringify(existingEntityBacked)}`
|
|
14529
|
+
);
|
|
14530
|
+
}
|
|
14531
|
+
const fields = [
|
|
14532
|
+
"specificityRank",
|
|
14533
|
+
"membershipRecordType",
|
|
14534
|
+
"membershipTargetField",
|
|
14535
|
+
"membershipLevelField"
|
|
14536
|
+
];
|
|
14537
|
+
for (const field of fields) {
|
|
14538
|
+
const declared = decl[field];
|
|
14539
|
+
const existingValue = existing[field];
|
|
14540
|
+
if (declared === void 0 && (existingValue === void 0 || existingValue === null)) continue;
|
|
14541
|
+
if (declared === existingValue) continue;
|
|
14542
|
+
mismatches.push(`${field}: declared ${JSON.stringify(declared)}, registered ${JSON.stringify(existingValue)}`);
|
|
14543
|
+
}
|
|
14544
|
+
const declaredLevels = decl.membershipLevels;
|
|
14545
|
+
const existingLevels = existing.membershipLevels;
|
|
14546
|
+
const levelsDiffer = declaredLevels === void 0 ? existingLevels != null && existingLevels.length > 0 : JSON.stringify([...declaredLevels].sort()) !== JSON.stringify([...existingLevels ?? []].sort());
|
|
14547
|
+
if (levelsDiffer) {
|
|
14548
|
+
mismatches.push(`membershipLevels: declared ${JSON.stringify(declaredLevels)}, registered ${JSON.stringify(existingLevels)}`);
|
|
14549
|
+
}
|
|
14550
|
+
return mismatches.length > 0 ? mismatches.join("; ") : null;
|
|
14551
|
+
}
|
|
14308
14552
|
var LostKeyError = class extends Error {
|
|
14309
14553
|
constructor(keyName) {
|
|
14310
14554
|
super(
|
|
@@ -14315,6 +14559,20 @@ var LostKeyError = class extends Error {
|
|
|
14315
14559
|
}
|
|
14316
14560
|
keyName;
|
|
14317
14561
|
};
|
|
14562
|
+
var ExistingPrincipalNotConfirmedError = class extends Error {
|
|
14563
|
+
constructor(contextId, userId, externalId) {
|
|
14564
|
+
super(
|
|
14565
|
+
`About to bind a NEW scoped key to a principal that already existed in this tenant before this run (externalId '${externalId}', userId '${userId}', context '${contextId}'). This is either an intentional multi-machine setup for an integration you already own, or this blueprint's servicePrincipal.externalId collided with an unrelated existing identity \u2014 confirm before a new credential is minted bound to it.`
|
|
14566
|
+
);
|
|
14567
|
+
this.contextId = contextId;
|
|
14568
|
+
this.userId = userId;
|
|
14569
|
+
this.externalId = externalId;
|
|
14570
|
+
this.name = "ExistingPrincipalNotConfirmedError";
|
|
14571
|
+
}
|
|
14572
|
+
contextId;
|
|
14573
|
+
userId;
|
|
14574
|
+
externalId;
|
|
14575
|
+
};
|
|
14318
14576
|
function mapEnumValues(enumValues) {
|
|
14319
14577
|
if (!enumValues) return void 0;
|
|
14320
14578
|
return enumValues.map((value) => ({ value }));
|
|
@@ -14389,7 +14647,8 @@ function toCapabilities(capabilities) {
|
|
|
14389
14647
|
function toRoleScopeClauses(clauses) {
|
|
14390
14648
|
return clauses.map((c) => ({
|
|
14391
14649
|
allowed_actions: c.allowedActions,
|
|
14392
|
-
data_scope: c.dataScope
|
|
14650
|
+
data_scope: c.dataScope,
|
|
14651
|
+
granted_capabilities: c.capabilities
|
|
14393
14652
|
}));
|
|
14394
14653
|
}
|
|
14395
14654
|
var UnresolvedIdentitiesError = class extends Error {
|
|
@@ -14409,11 +14668,18 @@ function assertNoUnresolvedIdentities(blueprint) {
|
|
|
14409
14668
|
}
|
|
14410
14669
|
}
|
|
14411
14670
|
async function runLoader(client, input) {
|
|
14412
|
-
const {
|
|
14413
|
-
|
|
14414
|
-
|
|
14415
|
-
|
|
14416
|
-
|
|
14671
|
+
const {
|
|
14672
|
+
blueprint,
|
|
14673
|
+
tenantId,
|
|
14674
|
+
machineId,
|
|
14675
|
+
rotate = false,
|
|
14676
|
+
skipSeed = false,
|
|
14677
|
+
readExistingKey,
|
|
14678
|
+
persistMintedKey,
|
|
14679
|
+
confirmExistingPrincipal,
|
|
14680
|
+
log
|
|
14681
|
+
} = input;
|
|
14682
|
+
assertBlueprintGates(blueprint);
|
|
14417
14683
|
assertNoUnresolvedIdentities(blueprint);
|
|
14418
14684
|
const ctxResp = await client.auth.createAppContext({ body: { contextId: blueprint.contextId, name: contextNameOf(blueprint) } });
|
|
14419
14685
|
const contextAction = actionOf(ctxResp.created);
|
|
@@ -14431,10 +14697,42 @@ async function runLoader(client, input) {
|
|
|
14431
14697
|
if (!userId) {
|
|
14432
14698
|
throw new Error("Service-principal create returned no user id; cannot bind AccessProfile/key.");
|
|
14433
14699
|
}
|
|
14434
|
-
|
|
14700
|
+
const userAction = actionOf(user.created);
|
|
14701
|
+
log.debug(
|
|
14702
|
+
{ component: "loader", externalId: blueprint.servicePrincipal.externalId, userId, userAction },
|
|
14703
|
+
"service principal ensured"
|
|
14704
|
+
);
|
|
14705
|
+
const localKeyForContext = await readExistingKey(blueprint.contextId);
|
|
14706
|
+
const storedPrincipalId = localKeyForContext?.principalId;
|
|
14707
|
+
const localKeyNamesThisPrincipal = storedPrincipalId === `usr_${userId}` || storedPrincipalId === userId;
|
|
14708
|
+
if (userAction === "reconciled" && !localKeyNamesThisPrincipal) {
|
|
14709
|
+
const proceed = confirmExistingPrincipal ? await confirmExistingPrincipal({
|
|
14710
|
+
contextId: blueprint.contextId,
|
|
14711
|
+
userId,
|
|
14712
|
+
externalId: blueprint.servicePrincipal.externalId
|
|
14713
|
+
}) : true;
|
|
14714
|
+
if (!proceed) {
|
|
14715
|
+
throw new ExistingPrincipalNotConfirmedError(blueprint.contextId, userId, blueprint.servicePrincipal.externalId);
|
|
14716
|
+
}
|
|
14717
|
+
log.debug(
|
|
14718
|
+
{ component: "loader", contextId: blueprint.contextId, userId },
|
|
14719
|
+
"existing-principal collision confirmed (or unwired) \u2014 proceeding with any further write to this principal"
|
|
14720
|
+
);
|
|
14721
|
+
}
|
|
14722
|
+
const needsContextPinnedBootstrap = (blueprint.issuers ?? []).length > 0 || (blueprint.namespaces ?? []).length > 0;
|
|
14723
|
+
const ctxBootstrapClient = needsContextPinnedBootstrap ? input.remintBootstrapForContext ? await input.remintBootstrapForContext(blueprint.contextId) : client : void 0;
|
|
14724
|
+
if (ctxBootstrapClient && input.remintBootstrapForContext) {
|
|
14725
|
+
log.debug({ component: "loader", contextId: blueprint.contextId }, "re-minted context-pinned bootstrap client");
|
|
14726
|
+
}
|
|
14727
|
+
const namespaceActions = [];
|
|
14728
|
+
for (const ns of blueprint.namespaces ?? []) {
|
|
14729
|
+
const action = await registerNamespaceIdempotently(ctxBootstrapClient.identity, blueprint.contextId, ns);
|
|
14730
|
+
namespaceActions.push({ namespace: ns.namespace, action });
|
|
14731
|
+
log.debug({ component: "loader", namespace: ns.namespace, contextId: blueprint.contextId, action }, "namespace ensured");
|
|
14732
|
+
}
|
|
14435
14733
|
const issuerActions = [];
|
|
14436
14734
|
for (const issuer of blueprint.issuers ?? []) {
|
|
14437
|
-
const issuerResp = await
|
|
14735
|
+
const issuerResp = await ctxBootstrapClient.auth.registerIssuer({
|
|
14438
14736
|
issuerId: issuer.issuerId,
|
|
14439
14737
|
issuer: issuer.issuer,
|
|
14440
14738
|
jwksUri: issuer.jwksUri,
|
|
@@ -14504,7 +14802,10 @@ async function runLoader(client, input) {
|
|
|
14504
14802
|
// literal JSON null, which is exactly what the platform's scope
|
|
14505
14803
|
// matcher tests via `allowedValues.contains(null)`. The cast bridges
|
|
14506
14804
|
// to the SDK's loosely-typed Record<string, Record<string,unknown>>.
|
|
14507
|
-
data_scope: blueprint.accessProfile.dataScope
|
|
14805
|
+
data_scope: blueprint.accessProfile.dataScope,
|
|
14806
|
+
// Already gate-validated (assertBlueprintGates, above this function's
|
|
14807
|
+
// call site) before this body is ever built.
|
|
14808
|
+
granted_capabilities: blueprint.accessProfile.capabilities
|
|
14508
14809
|
}
|
|
14509
14810
|
],
|
|
14510
14811
|
// Optional identity overrides — scope values the service key STAMPS onto
|
|
@@ -14562,7 +14863,7 @@ async function runLoader(client, input) {
|
|
|
14562
14863
|
keyId = keyResp.keyId ?? "";
|
|
14563
14864
|
rawKey = keyResp.rawKey;
|
|
14564
14865
|
if (keyId) {
|
|
14565
|
-
await persistMintedKey(blueprint.contextId, { keyId, rawKey, keyName, principalId: userId
|
|
14866
|
+
await persistMintedKey(blueprint.contextId, { keyId, rawKey, keyName, principalId: `usr_${userId}`, revokedKeyId });
|
|
14566
14867
|
}
|
|
14567
14868
|
if (keyAction !== "rotated") keyAction = "created";
|
|
14568
14869
|
} else {
|
|
@@ -14636,10 +14937,12 @@ async function runLoader(client, input) {
|
|
|
14636
14937
|
keyName,
|
|
14637
14938
|
keyAction,
|
|
14638
14939
|
contextAction,
|
|
14940
|
+
userAction,
|
|
14639
14941
|
profileAction,
|
|
14640
14942
|
schemaActions,
|
|
14641
14943
|
roleActions,
|
|
14642
14944
|
issuerActions,
|
|
14945
|
+
namespaceActions,
|
|
14643
14946
|
rawKey,
|
|
14644
14947
|
keyId,
|
|
14645
14948
|
revokedKeyId,
|
|
@@ -14658,6 +14961,15 @@ function buildPlan(blueprint, opts = {}) {
|
|
|
14658
14961
|
kind: "principal",
|
|
14659
14962
|
summary: `principal '${blueprint.servicePrincipal.externalId}' (SERVICE)`
|
|
14660
14963
|
});
|
|
14964
|
+
for (const ns of blueprint.namespaces ?? []) {
|
|
14965
|
+
const ownerNote = `owned by '${blueprint.contextId}'`;
|
|
14966
|
+
const entityNote = ns.entityBacked ? ", entity-backed" : "";
|
|
14967
|
+
const membershipNote = ns.membershipRecordType ? `, membership: ${ns.membershipRecordType}.${ns.membershipTargetField}` + (ns.membershipLevelField ? ` (levels: ${ns.membershipLevels?.join("/")})` : "") : "";
|
|
14968
|
+
steps.push({
|
|
14969
|
+
kind: "namespace",
|
|
14970
|
+
summary: `namespace '${ns.namespace}' (${ownerNote}${entityNote}${membershipNote}) [${BLUEPRINT_FIELD_PHASES.namespaces}-token phase]`
|
|
14971
|
+
});
|
|
14972
|
+
}
|
|
14661
14973
|
for (const iss of blueprint.issuers ?? []) {
|
|
14662
14974
|
const selfSignup = iss.selfSignupPolicies?.length ? `, self-signup: ${iss.selfSignupPolicies.length} polic${iss.selfSignupPolicies.length === 1 ? "y" : "ies"}` : "";
|
|
14663
14975
|
steps.push({
|
|
@@ -14680,15 +14992,18 @@ function buildPlan(blueprint, opts = {}) {
|
|
|
14680
14992
|
}
|
|
14681
14993
|
const actions = blueprint.accessProfile.allowedActions;
|
|
14682
14994
|
const scopeNote = blueprint.accessProfile.dataScope && Object.keys(blueprint.accessProfile.dataScope).length ? ` dataScope: ${Object.keys(blueprint.accessProfile.dataScope).join(", ")}` : "";
|
|
14995
|
+
const capabilityNote = blueprint.accessProfile.capabilities?.length ? ` capabilities: [${blueprint.accessProfile.capabilities.join(", ")}]` : "";
|
|
14683
14996
|
steps.push({
|
|
14684
14997
|
kind: "profile",
|
|
14685
|
-
summary: `profile scopes [${actions.join(", ")}] (data-plane \u2713)${scopeNote}`
|
|
14998
|
+
summary: `profile scopes [${actions.join(", ")}] (data-plane \u2713)${scopeNote}${capabilityNote}`
|
|
14686
14999
|
});
|
|
14687
15000
|
for (const [roleId, clauses] of Object.entries(blueprint.roles ?? {})) {
|
|
14688
15001
|
const actionSet = [...new Set(clauses.flatMap((c) => c.allowedActions))];
|
|
15002
|
+
const capSet = [...new Set(clauses.flatMap((c) => c.capabilities ?? []))];
|
|
15003
|
+
const roleCapabilityNote = capSet.length ? `, capabilities [${capSet.join(", ")}]` : "";
|
|
14689
15004
|
steps.push({
|
|
14690
15005
|
kind: "role",
|
|
14691
|
-
summary: `role '${roleId}' (${clauses.length} clause${clauses.length === 1 ? "" : "s"}, actions [${actionSet.join(", ")}])`
|
|
15006
|
+
summary: `role '${roleId}' (${clauses.length} clause${clauses.length === 1 ? "" : "s"}, actions [${actionSet.join(", ")}]${roleCapabilityNote})`
|
|
14692
15007
|
});
|
|
14693
15008
|
}
|
|
14694
15009
|
steps.push({ kind: "key", summary: `key ${blueprint.name}-<machine> (mint 1 scoped ssk_*)` });
|
|
@@ -14715,6 +15030,13 @@ function renderPlan(blueprint, steps) {
|
|
|
14715
15030
|
` issuer is left unchanged, not reconciled to match.`
|
|
14716
15031
|
);
|
|
14717
15032
|
}
|
|
15033
|
+
if (steps.some((s) => s.kind === "namespace")) {
|
|
15034
|
+
legendLines.push(
|
|
15035
|
+
` Exception: namespace registration is not idempotent \u2014 a re-apply matching`,
|
|
15036
|
+
` an existing registration converges silently, but one that disagrees FAILS`,
|
|
15037
|
+
` the apply rather than reconciling it.`
|
|
15038
|
+
);
|
|
15039
|
+
}
|
|
14718
15040
|
const legend = legendLines.join("\n");
|
|
14719
15041
|
const lines = steps.map((s) => ` + ${s.summary}`);
|
|
14720
15042
|
return [header, legend, "", ...lines].join("\n");
|
|
@@ -14963,6 +15285,15 @@ var OsKeychainSecretStore = class {
|
|
|
14963
15285
|
* against the real Windows store on every MR — the dev-machine probe is a point
|
|
14964
15286
|
* measurement, not a regression gate. (Tracked internally; this file ships to
|
|
14965
15287
|
* public npm, so no tracker number here.)
|
|
15288
|
+
*
|
|
15289
|
+
* ⚠️ THIS METHOD'S FAIL-CLOSED GUARANTEE IS AN UPSTREAM BEHAVIOURAL CONTRACT, NOT
|
|
15290
|
+
* SOMETHING THIS CODE ENFORCES. `@napi-rs/keyring` is pinned to an EXACT version
|
|
15291
|
+
* in `package.json` for exactly this reason: if a future release returned `[]`
|
|
15292
|
+
* from `findCredentials()` on a locked store instead of throwing, the `catch`
|
|
15293
|
+
* block above would never run — `!.some(...)` on an empty array reads as "gone",
|
|
15294
|
+
* `deleteSecret` returns `true`, and `remove`/`prune` drop the index row of a
|
|
15295
|
+
* still-live credential. A version bump of this dependency is a security review
|
|
15296
|
+
* of the paragraph above against the new release, not a routine dependency bump.
|
|
14966
15297
|
*/
|
|
14967
15298
|
async deleteSecret(ref) {
|
|
14968
15299
|
try {
|
|
@@ -15896,6 +16227,26 @@ The key itself WAS created server-side and is live \u2014 its id is ${entry.keyI
|
|
|
15896
16227
|
const entry = await this.getActive();
|
|
15897
16228
|
return entry ? this.readSecret(entry) : void 0;
|
|
15898
16229
|
}
|
|
16230
|
+
/**
|
|
16231
|
+
* The ACTIVE entry's tenant (test/live), or undefined if there is no active
|
|
16232
|
+
* entry or its tenant cannot be determined — WITHOUT migrating its secret.
|
|
16233
|
+
*
|
|
16234
|
+
* Deliberately NOT built on {@link resolveActiveSecret}/{@link readSecret}:
|
|
16235
|
+
* both migrate the secret to this machine's preferred store as a side effect
|
|
16236
|
+
* of reading it, and a caller that only wants to CLASSIFY the active identity
|
|
16237
|
+
* (e.g. "is it safe to activate a different one over it?") must not silently
|
|
16238
|
+
* relocate — and take the cross-process index lock for — a credential it has
|
|
16239
|
+
* no other reason to touch. Same non-migrating shape {@link assess} already
|
|
16240
|
+
* uses per entry (`store.getSecret` directly, never `readSecret`).
|
|
16241
|
+
*/
|
|
16242
|
+
async activeTenant() {
|
|
16243
|
+
const entry = await this.getActive();
|
|
16244
|
+
if (!entry) return void 0;
|
|
16245
|
+
const store = this.storeFor(entry);
|
|
16246
|
+
if (!store) return void 0;
|
|
16247
|
+
const secret = await store.getSecret(entry.secretRef);
|
|
16248
|
+
return secret !== void 0 ? keyTenant(secret) : void 0;
|
|
16249
|
+
}
|
|
15899
16250
|
/**
|
|
15900
16251
|
* Read one entry's secret, moving it to this machine's preferred store if it is
|
|
15901
16252
|
* somewhere else.
|
|
@@ -16022,26 +16373,31 @@ import * as readline from "readline/promises";
|
|
|
16022
16373
|
async function runKeyringList(opts, deps = {}) {
|
|
16023
16374
|
try {
|
|
16024
16375
|
const keyring = deps.keyring ?? await Keyring.open();
|
|
16025
|
-
const entries = await keyring.list();
|
|
16026
|
-
const active = await keyring.activeAlias();
|
|
16027
16376
|
if (opts.json) {
|
|
16377
|
+
const entries = await keyring.list();
|
|
16378
|
+
const active = await keyring.activeAlias();
|
|
16028
16379
|
process.stdout.write(`${formatJson({ active, entries })}
|
|
16029
16380
|
`);
|
|
16030
16381
|
return 0;
|
|
16031
16382
|
}
|
|
16032
|
-
|
|
16383
|
+
const { health } = await assessEntries(keyring);
|
|
16384
|
+
if (health.length === 0) {
|
|
16033
16385
|
process.stdout.write("\n (no keyring entries yet \u2014 run `vectros join <context>` to add one)\n");
|
|
16034
16386
|
return 0;
|
|
16035
16387
|
}
|
|
16036
16388
|
process.stdout.write(
|
|
16037
|
-
"\n" + formatTable(
|
|
16038
|
-
{ header: "", get: (
|
|
16039
|
-
{ header: "alias", get: (
|
|
16040
|
-
{ header: "context", get: (
|
|
16041
|
-
{ header: "principal", get: (
|
|
16042
|
-
{ header: "role", get: (
|
|
16043
|
-
|
|
16044
|
-
|
|
16389
|
+
"\n" + formatTable(health, [
|
|
16390
|
+
{ header: "", get: (h) => h.active ? "*" : "" },
|
|
16391
|
+
{ header: "alias", get: (h) => h.entry.alias },
|
|
16392
|
+
{ header: "context", get: (h) => h.entry.context },
|
|
16393
|
+
{ header: "principal", get: (h) => h.entry.principalId },
|
|
16394
|
+
{ header: "role", get: (h) => h.entry.role },
|
|
16395
|
+
// `tenant` (test/live) replaces the old `env` (staging/production) column
|
|
16396
|
+
// here: env is an internal-team concern an external developer's CLI never
|
|
16397
|
+
// sees, while tenant is the distinction every user routinely holds two
|
|
16398
|
+
// credentials side by side over.
|
|
16399
|
+
{ header: "tenant", get: (h) => tenantCell(h, { upper: true }) },
|
|
16400
|
+
{ header: "key id", get: (h) => h.entry.keyId }
|
|
16045
16401
|
]) + "\n"
|
|
16046
16402
|
);
|
|
16047
16403
|
return 0;
|
|
@@ -16223,6 +16579,10 @@ async function assessEntries(keyring) {
|
|
|
16223
16579
|
}))
|
|
16224
16580
|
};
|
|
16225
16581
|
}
|
|
16582
|
+
function tenantCell(h, opts) {
|
|
16583
|
+
if (h.status !== "ok") return void 0;
|
|
16584
|
+
return h.tenant ?? (opts.upper ? "UNKNOWN" : "unknown");
|
|
16585
|
+
}
|
|
16226
16586
|
function describeEntry(h) {
|
|
16227
16587
|
return `${h.entry.alias} (${h.entry.context}, ${h.entry.keyId})`;
|
|
16228
16588
|
}
|
|
@@ -16367,9 +16727,7 @@ async function runKeyringDoctor(opts, deps = {}) {
|
|
|
16367
16727
|
// `unknown` can escalate; one that sees a confident wrong answer cannot.
|
|
16368
16728
|
store: storeKindOf(h.entry) ?? "unknown",
|
|
16369
16729
|
secret: h.status,
|
|
16370
|
-
|
|
16371
|
-
// live key" from "we could not read the shape of this one".
|
|
16372
|
-
tenant: h.status === "ok" ? h.tenant ?? "unknown" : void 0
|
|
16730
|
+
tenant: tenantCell(h, { upper: false })
|
|
16373
16731
|
})),
|
|
16374
16732
|
liveKeys: liveKeys.map((h) => h.entry.alias),
|
|
16375
16733
|
findings
|
|
@@ -16384,7 +16742,7 @@ async function runKeyringDoctor(opts, deps = {}) {
|
|
|
16384
16742
|
{ header: "", get: (h) => h.active ? "*" : "" },
|
|
16385
16743
|
{ header: "alias", get: (h) => h.entry.alias },
|
|
16386
16744
|
{ header: "context", get: (h) => h.entry.context },
|
|
16387
|
-
{ header: "tenant", get: (h) => h
|
|
16745
|
+
{ header: "tenant", get: (h) => tenantCell(h, { upper: true }) },
|
|
16388
16746
|
{ header: "key id", get: (h) => h.entry.keyId },
|
|
16389
16747
|
{ header: "secret", get: (h) => h.status === "ok" ? "ok" : h.status.toUpperCase() }
|
|
16390
16748
|
]) + "\n"
|
|
@@ -16578,6 +16936,47 @@ function keyringAliasFor(alias, tenant) {
|
|
|
16578
16936
|
}
|
|
16579
16937
|
return tenant === "test" ? `${alias}.test` : alias;
|
|
16580
16938
|
}
|
|
16939
|
+
function resolveActivateDecision(opts) {
|
|
16940
|
+
if (opts.explicitActivate !== void 0) return opts.explicitActivate;
|
|
16941
|
+
if (opts.tenant === "live") return true;
|
|
16942
|
+
return opts.currentActiveTenant !== "live";
|
|
16943
|
+
}
|
|
16944
|
+
async function determineActivateDecision(keyring, opts) {
|
|
16945
|
+
if (opts.explicitActivate !== void 0 || opts.tenant === "live") {
|
|
16946
|
+
return resolveActivateDecision(opts);
|
|
16947
|
+
}
|
|
16948
|
+
let currentActiveTenant;
|
|
16949
|
+
try {
|
|
16950
|
+
if (opts.hint && opts.hint.alias === await keyring.activeAlias()) {
|
|
16951
|
+
currentActiveTenant = keyTenant(opts.hint.secret);
|
|
16952
|
+
} else {
|
|
16953
|
+
currentActiveTenant = await keyring.activeTenant();
|
|
16954
|
+
}
|
|
16955
|
+
} catch {
|
|
16956
|
+
}
|
|
16957
|
+
return resolveActivateDecision({ ...opts, currentActiveTenant });
|
|
16958
|
+
}
|
|
16959
|
+
function tenantActivateMessage(opts) {
|
|
16960
|
+
if (opts.willActivate) {
|
|
16961
|
+
if (opts.tenant !== "test") return "";
|
|
16962
|
+
return `
|
|
16963
|
+
\u26A0 '${opts.alias}' is now your ACTIVE identity \u2014 a TEST tenant credential. Anything
|
|
16964
|
+
resolving a key with no explicit --alias (including the MCP server) follows it.
|
|
16965
|
+
`;
|
|
16966
|
+
}
|
|
16967
|
+
if (opts.explicitlyDeclined) {
|
|
16968
|
+
return `
|
|
16969
|
+
\u26A0 Stored '${opts.alias}' (${opts.tenant} tenant) without activating it \u2014 you passed --no-activate.
|
|
16970
|
+
Run \`vectros switch ${opts.alias}\` when you're ready to use it.
|
|
16971
|
+
`;
|
|
16972
|
+
}
|
|
16973
|
+
return `
|
|
16974
|
+
\u26A0 Stored '${opts.alias}' (test tenant) WITHOUT activating it \u2014 your currently-active
|
|
16975
|
+
identity is a LIVE credential, and switching it out from under you silently is
|
|
16976
|
+
exactly the mistake this check exists to prevent. Run \`vectros switch ${opts.alias}\`
|
|
16977
|
+
to use the test key, or re-run with --activate to do so automatically.
|
|
16978
|
+
`;
|
|
16979
|
+
}
|
|
16581
16980
|
function nowIso() {
|
|
16582
16981
|
return (/* @__PURE__ */ new Date()).toISOString();
|
|
16583
16982
|
}
|
|
@@ -16674,10 +17073,7 @@ async function storeMintedKey(keyring, binding, secret, opts = {}) {
|
|
|
16674
17073
|
// src/cli-bootstrap.ts
|
|
16675
17074
|
function validateBlueprint(input) {
|
|
16676
17075
|
const blueprint = parseBlueprint(input);
|
|
16677
|
-
|
|
16678
|
-
for (const clauses of Object.values(blueprint.roles ?? {})) {
|
|
16679
|
-
for (const clause of clauses) assertScopeGate(clause.allowedActions);
|
|
16680
|
-
}
|
|
17076
|
+
assertBlueprintGates(blueprint);
|
|
16681
17077
|
return blueprint;
|
|
16682
17078
|
}
|
|
16683
17079
|
var BLANK_SLATE = {
|
|
@@ -16699,6 +17095,7 @@ function parseBootstrapArgs(argv) {
|
|
|
16699
17095
|
yes: false,
|
|
16700
17096
|
noMerge: false,
|
|
16701
17097
|
rotate: false,
|
|
17098
|
+
confirmExistingPrincipal: false,
|
|
16702
17099
|
useScopedToken: false,
|
|
16703
17100
|
blankSlate: false,
|
|
16704
17101
|
noSeed: false,
|
|
@@ -16750,6 +17147,9 @@ function parseBootstrapArgs(argv) {
|
|
|
16750
17147
|
case "--rotate":
|
|
16751
17148
|
args.rotate = true;
|
|
16752
17149
|
break;
|
|
17150
|
+
case "--confirm-existing-principal":
|
|
17151
|
+
args.confirmExistingPrincipal = true;
|
|
17152
|
+
break;
|
|
16753
17153
|
case "--use-scoped-token":
|
|
16754
17154
|
args.useScopedToken = true;
|
|
16755
17155
|
break;
|
|
@@ -16771,6 +17171,12 @@ function parseBootstrapArgs(argv) {
|
|
|
16771
17171
|
case "--test":
|
|
16772
17172
|
args.tenant = "test";
|
|
16773
17173
|
break;
|
|
17174
|
+
case "--activate":
|
|
17175
|
+
args.activate = true;
|
|
17176
|
+
break;
|
|
17177
|
+
case "--no-activate":
|
|
17178
|
+
args.activate = false;
|
|
17179
|
+
break;
|
|
16774
17180
|
case "--help":
|
|
16775
17181
|
case "-h":
|
|
16776
17182
|
args.help = true;
|
|
@@ -16788,6 +17194,26 @@ function seedDeclined(answer) {
|
|
|
16788
17194
|
const a = answer.trim().toLowerCase();
|
|
16789
17195
|
return a === "n" || a === "no";
|
|
16790
17196
|
}
|
|
17197
|
+
function existingPrincipalConfirmed(answer) {
|
|
17198
|
+
const a = answer.trim().toLowerCase();
|
|
17199
|
+
return a === "y" || a === "yes";
|
|
17200
|
+
}
|
|
17201
|
+
function buildConfirmExistingPrincipal(args, prompt, isTty) {
|
|
17202
|
+
return async (details) => {
|
|
17203
|
+
if (!args.yes && isTty) {
|
|
17204
|
+
const ans = await prompt(
|
|
17205
|
+
`
|
|
17206
|
+
\u26A0 '${details.externalId}' already exists in your tenant (context '${details.contextId}') \u2014 this key will bind to that EXISTING identity, not a new one.
|
|
17207
|
+
Expected if you're re-applying a blueprint from a new machine, or setting up a second
|
|
17208
|
+
credential for an integration you already own. If you don't recognize this identity,
|
|
17209
|
+
do NOT proceed \u2014 this may be a name collision with something you don't control.
|
|
17210
|
+
Proceed and bind to the existing identity? [y/N]: `
|
|
17211
|
+
);
|
|
17212
|
+
return existingPrincipalConfirmed(ans);
|
|
17213
|
+
}
|
|
17214
|
+
return args.confirmExistingPrincipal;
|
|
17215
|
+
};
|
|
17216
|
+
}
|
|
16791
17217
|
function seedSummaryLine(opts) {
|
|
16792
17218
|
if (opts.noSeed) {
|
|
16793
17219
|
return opts.declinedInteractively ? " \u2714 Seeded 0 record(s) \u2014 skipped (you declined sample data)\n" : " \u2714 Seeded 0 record(s) \u2014 skipped (--no-seed)\n";
|
|
@@ -16824,16 +17250,28 @@ function issuerTally(items) {
|
|
|
16824
17250
|
if (unchanged) seg.push(`${unchanged} already registered \u2014 unchanged (issuers cannot be updated once registered)`);
|
|
16825
17251
|
return `${items.length} issuer${items.length === 1 ? "" : "s"} (${seg.join(", ")})`;
|
|
16826
17252
|
}
|
|
17253
|
+
function namespaceTally(items) {
|
|
17254
|
+
if (items.length === 0) return null;
|
|
17255
|
+
const created = items.filter((i) => i.action === "created").length;
|
|
17256
|
+
const matched = items.length - created;
|
|
17257
|
+
const seg = [];
|
|
17258
|
+
if (created) seg.push(`${created} created`);
|
|
17259
|
+
if (matched) seg.push(`${matched} already registered \u2014 confirmed matching`);
|
|
17260
|
+
return `${items.length} namespace${items.length === 1 ? "" : "s"} (${seg.join(", ")})`;
|
|
17261
|
+
}
|
|
16827
17262
|
function applyOutcomeSummary(result) {
|
|
16828
17263
|
const parts = [
|
|
16829
17264
|
reconcileTally(result.schemaActions, "schema"),
|
|
16830
17265
|
reconcileTally(result.roleActions, "role"),
|
|
16831
17266
|
issuerTally(result.issuerActions),
|
|
17267
|
+
namespaceTally(result.namespaceActions),
|
|
16832
17268
|
`profile ${result.profileAction}`
|
|
16833
17269
|
].filter((p) => p !== null);
|
|
16834
17270
|
const detail = parts.length ? ` \u2014 ${parts.join("; ")}` : "";
|
|
17271
|
+
const principalNote = result.userAction === "reconciled" ? ` \u26A0 service principal already existed in this tenant before this run (not newly created)
|
|
17272
|
+
` : "";
|
|
16835
17273
|
return ` \u2714 context '${result.contextId}' ${result.contextAction}${detail}
|
|
16836
|
-
`;
|
|
17274
|
+
${principalNote}`;
|
|
16837
17275
|
}
|
|
16838
17276
|
var BOOTSTRAP_HELP = `vectros bootstrap \u2014 provision a least-privilege MCP credential.
|
|
16839
17277
|
|
|
@@ -16841,7 +17279,7 @@ Usage:
|
|
|
16841
17279
|
vectros bootstrap [--blueprint <name> | --blank] [--no-seed]
|
|
16842
17280
|
[--set name=value ...] [--values <file>]
|
|
16843
17281
|
[--tenant test|live | --test] [--client desktop|code]
|
|
16844
|
-
[--yes] [--no-merge] [--rotate]
|
|
17282
|
+
[--yes] [--no-merge] [--rotate] [--confirm-existing-principal]
|
|
16845
17283
|
[--token <t> | env VECTROS_BOOTSTRAP_TOKEN]
|
|
16846
17284
|
[--base-url <url>] [--use-scoped-token]
|
|
16847
17285
|
|
|
@@ -16864,6 +17302,10 @@ Options:
|
|
|
16864
17302
|
The tenant rides the key (ssk_test_* vs ssk_live_*), so
|
|
16865
17303
|
the MCP/client config is otherwise identical.
|
|
16866
17304
|
--test Shorthand for --tenant test.
|
|
17305
|
+
--activate Force the freshly-stored key to become your active identity, even
|
|
17306
|
+
if that would switch away from a currently-active LIVE credential
|
|
17307
|
+
(the one case --tenant test does NOT do this automatically).
|
|
17308
|
+
--no-activate Store the key without activating it, regardless of tenant.
|
|
16867
17309
|
--base-url <url> Override the API base URL (beats VECTROS_API_BASE_URL).
|
|
16868
17310
|
--client <desktop|code> MCP host to configure (default: desktop). 'desktop' merges
|
|
16869
17311
|
claude_desktop_config.json (also works for Cursor/Cline);
|
|
@@ -16878,6 +17320,13 @@ Options:
|
|
|
16878
17320
|
print the config snippet instead of merging it into your client's
|
|
16879
17321
|
config file. Not a dry run; see 'blueprint plan' for that.
|
|
16880
17322
|
--rotate Revoke + re-mint THIS machine's key.
|
|
17323
|
+
--confirm-existing-principal
|
|
17324
|
+
Required (with --yes, or with no TTY) to mint a NEW key bound to a
|
|
17325
|
+
service principal that already existed in your tenant before this run
|
|
17326
|
+
\u2014 e.g. a deliberate second machine for an integration you already own.
|
|
17327
|
+
Refused by default so an unexpected collision halts a CI run loudly
|
|
17328
|
+
instead of silently minting a credential bound to the wrong identity.
|
|
17329
|
+
Never needed for --rotate or an ordinary same-machine re-apply.
|
|
16881
17330
|
--use-scoped-token Legacy fallback.
|
|
16882
17331
|
NOTE: it mints a token WITHOUT the provisioning
|
|
16883
17332
|
capability, so it cannot create a blueprint's
|
|
@@ -16965,6 +17414,22 @@ function buildRemintForContext(opts) {
|
|
|
16965
17414
|
});
|
|
16966
17415
|
};
|
|
16967
17416
|
}
|
|
17417
|
+
function buildRemintBootstrapForContext(opts) {
|
|
17418
|
+
const mintBridge = opts.mintBridge ?? mintBridgeToken;
|
|
17419
|
+
return async (contextId) => {
|
|
17420
|
+
const ctxToken = await mintBridge({
|
|
17421
|
+
cognitoToken: opts.cognitoToken,
|
|
17422
|
+
apiBaseUrl: opts.apiBaseUrl,
|
|
17423
|
+
contextId,
|
|
17424
|
+
tenant: opts.tenant,
|
|
17425
|
+
useScopedToken: opts.useScopedToken
|
|
17426
|
+
});
|
|
17427
|
+
return new VectrosClient({
|
|
17428
|
+
token: ctxToken.token,
|
|
17429
|
+
environment: opts.apiBaseUrl
|
|
17430
|
+
});
|
|
17431
|
+
};
|
|
17432
|
+
}
|
|
16968
17433
|
async function runBootstrapCli(argv, deps = {}) {
|
|
16969
17434
|
const log = deps.log ?? createLogger2();
|
|
16970
17435
|
let args;
|
|
@@ -16998,6 +17463,15 @@ ${BOOTSTRAP_HELP}
|
|
|
16998
17463
|
\u2716 Scope gate refused blueprint '${blueprint.name}':
|
|
16999
17464
|
`);
|
|
17000
17465
|
for (const r of err.rejected) process.stderr.write(` \u2022 ${r.action}: ${r.reason}
|
|
17466
|
+
`);
|
|
17467
|
+
process.stderr.write(" Minting nothing.\n");
|
|
17468
|
+
return 2;
|
|
17469
|
+
}
|
|
17470
|
+
if (err instanceof CapabilityGateError) {
|
|
17471
|
+
process.stderr.write(`
|
|
17472
|
+
\u2716 Capability gate refused blueprint '${blueprint.name}':
|
|
17473
|
+
`);
|
|
17474
|
+
for (const r of err.rejected) process.stderr.write(` \u2022 ${r.action}: ${r.reason}
|
|
17001
17475
|
`);
|
|
17002
17476
|
process.stderr.write(" Minting nothing.\n");
|
|
17003
17477
|
return 2;
|
|
@@ -17009,6 +17483,11 @@ ${BOOTSTRAP_HELP}
|
|
|
17009
17483
|
Blueprint: ${blueprint.name}
|
|
17010
17484
|
\u2714 Scope gate: [${gate.accepted.join(", ")}] \u2014 all data-plane \u2713
|
|
17011
17485
|
`);
|
|
17486
|
+
if (blueprint.accessProfile.capabilities?.length) {
|
|
17487
|
+
const capGate = evaluateCapabilityGate(blueprint.accessProfile.capabilities);
|
|
17488
|
+
process.stdout.write(` \u2714 Capability gate: [${capGate.accepted.join(", ")}] \u2014 granted \u2713
|
|
17489
|
+
`);
|
|
17490
|
+
}
|
|
17012
17491
|
let noSeed = args.noSeed;
|
|
17013
17492
|
let seedDeclinedInteractively = false;
|
|
17014
17493
|
if (shouldOfferSeedOptOut(args, blueprint, isTty)) {
|
|
@@ -17082,6 +17561,7 @@ ${renderPlan(blueprint, planSteps)}
|
|
|
17082
17561
|
keyringOpenError = err;
|
|
17083
17562
|
}
|
|
17084
17563
|
let storedFreshInKeyring = false;
|
|
17564
|
+
const lastResolvedSecret = /* @__PURE__ */ new Map();
|
|
17085
17565
|
const readExistingKey = async (contextId) => {
|
|
17086
17566
|
if (!keyring) return void 0;
|
|
17087
17567
|
const alias = keyringAliasFor(contextId, args.tenant);
|
|
@@ -17089,7 +17569,8 @@ ${renderPlan(blueprint, planSteps)}
|
|
|
17089
17569
|
const existing = await keyring.get(alias);
|
|
17090
17570
|
if (!existing?.keyId) return void 0;
|
|
17091
17571
|
const rawKey = await keyring.resolveSecret(alias);
|
|
17092
|
-
|
|
17572
|
+
lastResolvedSecret.set(alias, rawKey);
|
|
17573
|
+
return rawKey ? { keyId: existing.keyId, rawKey, principalId: existing.principalId } : void 0;
|
|
17093
17574
|
} catch {
|
|
17094
17575
|
return void 0;
|
|
17095
17576
|
}
|
|
@@ -17108,6 +17589,11 @@ ${renderPlan(blueprint, planSteps)}
|
|
|
17108
17589
|
const alias = keyringAliasFor(contextId, args.tenant);
|
|
17109
17590
|
try {
|
|
17110
17591
|
warnReplacingAlias(await keyring.get(alias), alias, record.revokedKeyId);
|
|
17592
|
+
const willActivate = await determineActivateDecision(keyring, {
|
|
17593
|
+
tenant: args.tenant,
|
|
17594
|
+
explicitActivate: args.activate,
|
|
17595
|
+
hint: lastResolvedSecret.has(alias) ? { alias, secret: lastResolvedSecret.get(alias) } : void 0
|
|
17596
|
+
});
|
|
17111
17597
|
await storeMintedKey(
|
|
17112
17598
|
keyring,
|
|
17113
17599
|
{
|
|
@@ -17122,11 +17608,14 @@ ${renderPlan(blueprint, planSteps)}
|
|
|
17122
17608
|
env: deriveEnvironment(baseUrl) ?? args.env
|
|
17123
17609
|
},
|
|
17124
17610
|
record.rawKey,
|
|
17125
|
-
{ activate:
|
|
17611
|
+
{ activate: willActivate, revokedKeyId: record.revokedKeyId }
|
|
17126
17612
|
);
|
|
17127
17613
|
if (record.revokedKeyId) {
|
|
17128
17614
|
await warnOtherAliasesNaming(keyring, record.revokedKeyId, alias);
|
|
17129
17615
|
}
|
|
17616
|
+
process.stdout.write(
|
|
17617
|
+
tenantActivateMessage({ alias, tenant: args.tenant, willActivate, explicitlyDeclined: args.activate === false })
|
|
17618
|
+
);
|
|
17130
17619
|
storedFreshInKeyring = true;
|
|
17131
17620
|
} catch (err) {
|
|
17132
17621
|
process.stderr.write(
|
|
@@ -17147,12 +17636,20 @@ ${renderPlan(blueprint, planSteps)}
|
|
|
17147
17636
|
skipSeed: noSeed,
|
|
17148
17637
|
readExistingKey,
|
|
17149
17638
|
persistMintedKey,
|
|
17639
|
+
remintBootstrapForContext: buildRemintBootstrapForContext({
|
|
17640
|
+
cognitoToken,
|
|
17641
|
+
apiBaseUrl: baseUrl,
|
|
17642
|
+
tenant: args.tenant,
|
|
17643
|
+
useScopedToken: args.useScopedToken,
|
|
17644
|
+
mintBridge
|
|
17645
|
+
}),
|
|
17150
17646
|
remintForContext: buildRemintForContext({
|
|
17151
17647
|
cognitoToken,
|
|
17152
17648
|
apiBaseUrl: baseUrl,
|
|
17153
17649
|
tenant: args.tenant,
|
|
17154
17650
|
mintContext
|
|
17155
17651
|
}),
|
|
17652
|
+
confirmExistingPrincipal: buildConfirmExistingPrincipal(args, promptCtx.prompt, isTty),
|
|
17156
17653
|
log
|
|
17157
17654
|
});
|
|
17158
17655
|
process.stdout.write(
|
|
@@ -17230,6 +17727,18 @@ ${codeAddHint}`);
|
|
|
17230
17727
|
`);
|
|
17231
17728
|
return 3;
|
|
17232
17729
|
}
|
|
17730
|
+
if (err instanceof ExistingPrincipalNotConfirmedError) {
|
|
17731
|
+
process.stderr.write(
|
|
17732
|
+
`
|
|
17733
|
+
\u2716 ${err.message}
|
|
17734
|
+
|
|
17735
|
+
If this is expected (a re-apply, or a deliberate second machine for an integration you already own),
|
|
17736
|
+
re-run with --confirm-existing-principal, or confirm interactively.
|
|
17737
|
+
If you don't recognize this identity, investigate the blueprint's servicePrincipal.externalId before re-running.
|
|
17738
|
+
`
|
|
17739
|
+
);
|
|
17740
|
+
return 3;
|
|
17741
|
+
}
|
|
17233
17742
|
if (err instanceof BlueprintIdentityError || err instanceof UnresolvedIdentitiesError) {
|
|
17234
17743
|
process.stderr.write(`
|
|
17235
17744
|
\u2716 ${err.message}
|
|
@@ -17450,6 +17959,10 @@ function lintBlueprint(bp) {
|
|
|
17450
17959
|
}
|
|
17451
17960
|
}
|
|
17452
17961
|
}
|
|
17962
|
+
{
|
|
17963
|
+
const reached = ownerScopedReachWithoutDataScope(bp.accessProfile.allowedActions, ds);
|
|
17964
|
+
if (reached.length > 0) findings.push(unconstrainedReachFinding("accessProfile", reached));
|
|
17965
|
+
}
|
|
17453
17966
|
for (const roleId of Object.keys(bp.roles ?? {})) {
|
|
17454
17967
|
if (!ROLE_ID_RE.test(roleId)) {
|
|
17455
17968
|
findings.push({
|
|
@@ -17462,6 +17975,10 @@ function lintBlueprint(bp) {
|
|
|
17462
17975
|
for (const [roleId, clauses] of Object.entries(bp.roles ?? {})) {
|
|
17463
17976
|
clauses.forEach((clause, i) => {
|
|
17464
17977
|
const cds = clause.dataScope;
|
|
17978
|
+
const reached = ownerScopedReachWithoutDataScope(clause.allowedActions, cds);
|
|
17979
|
+
if (reached.length > 0) {
|
|
17980
|
+
findings.push(unconstrainedReachFinding(`role '${roleId}' clause ${i}`, reached));
|
|
17981
|
+
}
|
|
17465
17982
|
if (!cds) return;
|
|
17466
17983
|
for (const [key, values] of Object.entries(cds)) {
|
|
17467
17984
|
for (const value of values) {
|
|
@@ -17514,6 +18031,25 @@ function placeholderFinding(where, key, value) {
|
|
|
17514
18031
|
message: `${where}: dataScope.${key} value '${value}' is not a placeholder the platform recognizes \u2014 it will be rejected at apply. Valid: \${{ any }}, \${{ self.userId }}, \${{ self.scope.<namespace> }}, \${{ under.self.userId }}, \${{ under.self.scope.<namespace> }}. Spelling is exact and lowercase; use a plain value for a literal owner id.`
|
|
17515
18032
|
};
|
|
17516
18033
|
}
|
|
18034
|
+
var OWNER_SCOPED_RESOURCES = /* @__PURE__ */ new Set(["records", "documents", "entities"]);
|
|
18035
|
+
var UNCONSTRAINED_REACH_OPS = /* @__PURE__ */ new Set(["r", "u", "d"]);
|
|
18036
|
+
function ownerScopedReachWithoutDataScope(allowedActions, dataScope) {
|
|
18037
|
+
const reached = /* @__PURE__ */ new Set();
|
|
18038
|
+
for (const action of allowedActions) {
|
|
18039
|
+
const [resource, ops] = action.split(":", 2);
|
|
18040
|
+
if (!resource || !ops || !OWNER_SCOPED_RESOURCES.has(resource)) continue;
|
|
18041
|
+
if (ops === "*" || [...ops].some((op) => UNCONSTRAINED_REACH_OPS.has(op))) reached.add(resource);
|
|
18042
|
+
}
|
|
18043
|
+
if (dataScope !== void 0) return [];
|
|
18044
|
+
return [...reached].sort();
|
|
18045
|
+
}
|
|
18046
|
+
function unconstrainedReachFinding(where, resources) {
|
|
18047
|
+
return {
|
|
18048
|
+
severity: "warning",
|
|
18049
|
+
rule: "datascope-absent-unconstrained-reach",
|
|
18050
|
+
message: `${where}: no dataScope at all, but allowedActions grants r/u/d on ${resources.join("/")} \u2014 this reaches EVERY row of EVERY owner on ${resources.length > 1 ? "those resources" : "that resource"}, not just "your own org" or any other value you may have intended. If that's deliberate (a genuinely shared/tenant-wide resource), you can ignore this; otherwise add a dataScope entry.`
|
|
18051
|
+
};
|
|
18052
|
+
}
|
|
17517
18053
|
function lintPasses(findings) {
|
|
17518
18054
|
return !findings.some((f) => f.severity === "error");
|
|
17519
18055
|
}
|
|
@@ -17552,8 +18088,12 @@ schemas:
|
|
|
17552
18088
|
lookupFields: [title] # extra O(1) exact-match lookup indexes (max 10)
|
|
17553
18089
|
|
|
17554
18090
|
accessProfile:
|
|
17555
|
-
# SCOPE GATE: data-plane only (records/schemas/search/documents/folders/inference/entities)
|
|
17556
|
-
#
|
|
18091
|
+
# SCOPE GATE: data-plane only (records/schemas/search/documents/folders/inference/entities),
|
|
18092
|
+
# PLUS profiles:r/u/d (bounded by the platform to redistributing access you already have,
|
|
18093
|
+
# never granting more) and profiles:c:self (create a profile for yourself only \u2014 the
|
|
18094
|
+
# platform resolves "self" from the caller, not from anything you declare here). A bare
|
|
18095
|
+
# profiles:c, or profiles:c:usr_<id> naming another principal, is HARD-REJECTED \u2014 by
|
|
18096
|
+
# design, no override. Same for keys/users/admin/namespaces/*.
|
|
17557
18097
|
# Ops: c|r|u|d, combinable (records:cru). Prefer the narrowest set (least privilege).
|
|
17558
18098
|
allowedActions: [records:r, records:c, records:u, search:r, schemas:r]
|
|
17559
18099
|
# dataScope binds the key to specific owners. Include null to ALSO see
|
|
@@ -17561,6 +18101,17 @@ accessProfile:
|
|
|
17561
18101
|
# owners' records, not shared/seed data. Keys are userId (the principal) and
|
|
17562
18102
|
# namespaced scopes (scope:org, scope:client, ...):
|
|
17563
18103
|
# dataScope: { "scope:org": [<org-uuid>, null] }
|
|
18104
|
+
# Left empty ({}) below \u2014 the explicit marker for "deliberately tenant-wide,
|
|
18105
|
+
# every record visible" ('blueprint validate' warns on a MISSING dataScope
|
|
18106
|
+
# key, but not on this one). This is a SCAFFOLD PLACEHOLDER, not a
|
|
18107
|
+
# recommendation: it also grants records:c/u tenant-wide, so before this
|
|
18108
|
+
# profile touches real data, replace it with a real scope (the example
|
|
18109
|
+
# above) unless every owner genuinely sharing every record is what you want.
|
|
18110
|
+
dataScope: {}
|
|
18111
|
+
# capabilities is a SEPARATE gated dimension from allowedActions \u2014 named platform
|
|
18112
|
+
# effects a resource:ops verb can't express. Only 'member-lifecycle' is admitted
|
|
18113
|
+
# today; every other name is HARD-REJECTED, same posture as the scope gate above.
|
|
18114
|
+
# capabilities: [member-lifecycle]
|
|
17564
18115
|
|
|
17565
18116
|
servicePrincipal:
|
|
17566
18117
|
externalId: ${name}-sp # stable; the principal the profile + key bind to
|
|
@@ -17680,15 +18231,19 @@ async function resolveAndValidate(arg, verb, supplied) {
|
|
|
17680
18231
|
return null;
|
|
17681
18232
|
}
|
|
17682
18233
|
try {
|
|
17683
|
-
|
|
17684
|
-
for (const clauses of Object.values(blueprint.roles ?? {})) {
|
|
17685
|
-
for (const clause of clauses) assertScopeGate(clause.allowedActions);
|
|
17686
|
-
}
|
|
18234
|
+
assertBlueprintGates(blueprint);
|
|
17687
18235
|
} catch (err) {
|
|
17688
18236
|
if (err instanceof ScopeGateError) {
|
|
17689
18237
|
process.stderr.write(` \u2716 Scope gate rejected '${blueprint.name}':
|
|
17690
18238
|
`);
|
|
17691
18239
|
for (const r of err.rejected) process.stderr.write(` \u2022 ${r.action}: ${r.reason}
|
|
18240
|
+
`);
|
|
18241
|
+
return null;
|
|
18242
|
+
}
|
|
18243
|
+
if (err instanceof CapabilityGateError) {
|
|
18244
|
+
process.stderr.write(` \u2716 Capability gate rejected '${blueprint.name}':
|
|
18245
|
+
`);
|
|
18246
|
+
for (const r of err.rejected) process.stderr.write(` \u2022 ${r.action}: ${r.reason}
|
|
17692
18247
|
`);
|
|
17693
18248
|
return null;
|
|
17694
18249
|
}
|
|
@@ -17720,9 +18275,10 @@ async function runValidate(args) {
|
|
|
17720
18275
|
const blueprint = await resolveAndValidate(args.file, "validate", supplied);
|
|
17721
18276
|
if (!blueprint) return 2;
|
|
17722
18277
|
const gate = evaluateScopeGate(blueprint.accessProfile.allowedActions);
|
|
18278
|
+
const capGate = evaluateCapabilityGate(blueprint.accessProfile.capabilities);
|
|
17723
18279
|
const warnCount = lintBlueprint(blueprint).length;
|
|
17724
18280
|
process.stdout.write(
|
|
17725
|
-
` \u2714 '${blueprint.name}' is valid \u2014 structural \u2713, scope gate \u2713 [${gate.accepted.join(", ")}], lint \u2713` + (warnCount ? ` (${warnCount} warning${warnCount === 1 ? "" : "s"})` : "") + "\n"
|
|
18281
|
+
` \u2714 '${blueprint.name}' is valid \u2014 structural \u2713, scope gate \u2713 [${gate.accepted.join(", ")}]` + (capGate.accepted.length ? `, capability gate \u2713 [${capGate.accepted.join(", ")}]` : "") + `, lint \u2713` + (warnCount ? ` (${warnCount} warning${warnCount === 1 ? "" : "s"})` : "") + "\n"
|
|
17726
18282
|
);
|
|
17727
18283
|
return 0;
|
|
17728
18284
|
}
|
|
@@ -18053,7 +18609,14 @@ async function resolveKeyDestination(opts, deps) {
|
|
|
18053
18609
|
`);
|
|
18054
18610
|
return 2;
|
|
18055
18611
|
}
|
|
18056
|
-
|
|
18612
|
+
let alias;
|
|
18613
|
+
try {
|
|
18614
|
+
alias = keyringAliasFor(opts.alias ?? opts.context, opts.tenant ?? "live");
|
|
18615
|
+
} catch (err) {
|
|
18616
|
+
process.stderr.write(` \u2716 ${formatError(err)}
|
|
18617
|
+
`);
|
|
18618
|
+
return 2;
|
|
18619
|
+
}
|
|
18057
18620
|
if (opts.activate === false && await keyring.activeAlias() === alias) {
|
|
18058
18621
|
process.stderr.write(
|
|
18059
18622
|
` \u2716 --no-activate cannot be used on '${alias}': it is your ACTIVE identity, and storing
|
|
@@ -18074,6 +18637,8 @@ async function storeAndReport(dest, key, auth, opts) {
|
|
|
18074
18637
|
return 1;
|
|
18075
18638
|
}
|
|
18076
18639
|
const principalId = opts.principalId;
|
|
18640
|
+
const tenant = keyTenant(key.rawKey) ?? "test";
|
|
18641
|
+
const willActivate = await determineActivateDecision(dest.keyring, { tenant, explicitActivate: opts.activate });
|
|
18077
18642
|
let entry;
|
|
18078
18643
|
try {
|
|
18079
18644
|
warnReplacingAlias(await dest.keyring.get(dest.alias), dest.alias, opts.revokedKeyId);
|
|
@@ -18088,7 +18653,7 @@ async function storeAndReport(dest, key, auth, opts) {
|
|
|
18088
18653
|
env: auth.env
|
|
18089
18654
|
},
|
|
18090
18655
|
key.rawKey,
|
|
18091
|
-
{ activate:
|
|
18656
|
+
{ activate: willActivate, revokedKeyId: opts.revokedKeyId }
|
|
18092
18657
|
));
|
|
18093
18658
|
} catch (err) {
|
|
18094
18659
|
process.stderr.write(
|
|
@@ -18100,7 +18665,6 @@ async function storeAndReport(dest, key, auth, opts) {
|
|
|
18100
18665
|
);
|
|
18101
18666
|
return 1;
|
|
18102
18667
|
}
|
|
18103
|
-
const active = opts.activate !== false;
|
|
18104
18668
|
process.stdout.write(
|
|
18105
18669
|
`
|
|
18106
18670
|
\u2714 Stored the new key in your ${describeEntryStoreNoun(entry)} \u2014 it was not printed.
|
|
@@ -18110,9 +18674,19 @@ async function storeAndReport(dest, key, auth, opts) {
|
|
|
18110
18674
|
["principal", entry.principalId || void 0],
|
|
18111
18675
|
["key id", entry.keyId],
|
|
18112
18676
|
["env", entry.env],
|
|
18113
|
-
["active",
|
|
18114
|
-
]) + "\n\n" +
|
|
18115
|
-
|
|
18677
|
+
["active", willActivate ? "yes" : "no"]
|
|
18678
|
+
]) + "\n\n" + // The plain "now active" note fires only on the activated path. When it
|
|
18679
|
+
// did NOT activate, tenantActivateMessage below is the ONLY explanation —
|
|
18680
|
+
// it already says to run `switch`, AND (unlike a bare "Not active") says
|
|
18681
|
+
// WHY: an explicit --no-activate vs. this call's tenant-safety default.
|
|
18682
|
+
// Saying both here would print two different "run switch" instructions
|
|
18683
|
+
// for one outcome.
|
|
18684
|
+
(willActivate ? " Anything that resolves a key through the CLI \u2014 the MCP server included \u2014 now\n reads this one. No VECTROS_API_KEY needed.\n" : "") + tenantActivateMessage({
|
|
18685
|
+
alias: entry.alias,
|
|
18686
|
+
tenant,
|
|
18687
|
+
willActivate,
|
|
18688
|
+
explicitlyDeclined: opts.activate === false
|
|
18689
|
+
})
|
|
18116
18690
|
);
|
|
18117
18691
|
return 0;
|
|
18118
18692
|
}
|
|
@@ -19646,6 +20220,19 @@ async function runAssertions(client, bootstrapClient, deps, blueprint, tenantId,
|
|
|
19646
20220
|
assertions.push({ name: `issuer:${issuerId}`, ok: false, detail: `getIssuer(${issuerId}) failed: ${errMsg(e)}` });
|
|
19647
20221
|
}
|
|
19648
20222
|
}
|
|
20223
|
+
for (const { namespace } of result.namespaceActions) {
|
|
20224
|
+
try {
|
|
20225
|
+
const ns = await bootstrapClient.identity.getNamespace({ namespace, contextId: blueprint.contextId });
|
|
20226
|
+
const ok = ns.namespace === namespace && ns.contextId === blueprint.contextId;
|
|
20227
|
+
assertions.push({
|
|
20228
|
+
name: `namespace:${namespace}`,
|
|
20229
|
+
ok,
|
|
20230
|
+
detail: ok ? `namespace '${namespace}' registered + readable in context '${blueprint.contextId}'` : `getNamespace(${namespace}) returned namespace '${ns.namespace ?? "(none)"}' in context '${ns.contextId ?? "(tenant-wide)"}', expected '${blueprint.contextId}'`
|
|
20231
|
+
});
|
|
20232
|
+
} catch (e) {
|
|
20233
|
+
assertions.push({ name: `namespace:${namespace}`, ok: false, detail: `getNamespace(${namespace}) failed: ${errMsg(e)}` });
|
|
20234
|
+
}
|
|
20235
|
+
}
|
|
19649
20236
|
return assertions;
|
|
19650
20237
|
}
|
|
19651
20238
|
async function runTeardown(client, bootstrapClient, blueprint, machineId, snapshot, result) {
|
|
@@ -19758,10 +20345,18 @@ async function runTeardown(client, bootstrapClient, blueprint, machineId, snapsh
|
|
|
19758
20345
|
steps.push({ step: `delete issuer ${issuerId}`, ok: false, error: errMsg(e) });
|
|
19759
20346
|
}
|
|
19760
20347
|
}
|
|
20348
|
+
for (const { namespace } of result.namespaceActions) {
|
|
20349
|
+
steps.push({
|
|
20350
|
+
step: `delete namespace ${namespace}`,
|
|
20351
|
+
ok: true,
|
|
20352
|
+
skipped: true,
|
|
20353
|
+
error: "namespace deletion requires a root API key (sk_*) \u2014 blueprint-test never holds one; the registration is left in place"
|
|
20354
|
+
});
|
|
20355
|
+
}
|
|
19761
20356
|
return steps;
|
|
19762
20357
|
}
|
|
19763
20358
|
async function runBlueprintHarness(client, input, deps) {
|
|
19764
|
-
const { tenantId, machineId, keep = false, log, remintForContext } = input;
|
|
20359
|
+
const { tenantId, machineId, keep = false, log, remintForContext, remintBootstrapForContext } = input;
|
|
19765
20360
|
const readExistingKey = async () => void 0;
|
|
19766
20361
|
const persistMintedKey = async () => {
|
|
19767
20362
|
};
|
|
@@ -19771,6 +20366,9 @@ async function runBlueprintHarness(client, input, deps) {
|
|
|
19771
20366
|
log
|
|
19772
20367
|
);
|
|
19773
20368
|
const remint = remintForContext ?? (async () => client);
|
|
20369
|
+
const remintBootstrap = remintBootstrapForContext ?? (async () => client);
|
|
20370
|
+
const hasIssuers = (blueprint.issuers ?? []).length > 0;
|
|
20371
|
+
const hasNamespaces = (blueprint.namespaces ?? []).length > 0;
|
|
19774
20372
|
let contextSnapshot;
|
|
19775
20373
|
if (remintForContext) {
|
|
19776
20374
|
try {
|
|
@@ -19786,7 +20384,18 @@ async function runBlueprintHarness(client, input, deps) {
|
|
|
19786
20384
|
} else {
|
|
19787
20385
|
contextSnapshot = await captureSnapshot(client, blueprint);
|
|
19788
20386
|
}
|
|
19789
|
-
|
|
20387
|
+
let issuerSnapshot;
|
|
20388
|
+
if (!hasIssuers) {
|
|
20389
|
+
issuerSnapshot = { existingIssuerIds: /* @__PURE__ */ new Set(), failed: false };
|
|
20390
|
+
} else if (remintBootstrapForContext) {
|
|
20391
|
+
try {
|
|
20392
|
+
issuerSnapshot = await captureIssuerSnapshot(await remintBootstrapForContext(blueprint.contextId));
|
|
20393
|
+
} catch (e) {
|
|
20394
|
+
issuerSnapshot = { existingIssuerIds: /* @__PURE__ */ new Set(), failed: !isNotFound(e) };
|
|
20395
|
+
}
|
|
20396
|
+
} else {
|
|
20397
|
+
issuerSnapshot = await captureIssuerSnapshot(client);
|
|
20398
|
+
}
|
|
19790
20399
|
const snapshot = {
|
|
19791
20400
|
...contextSnapshot,
|
|
19792
20401
|
existingIssuerIds: issuerSnapshot.existingIssuerIds,
|
|
@@ -19796,9 +20405,25 @@ async function runBlueprintHarness(client, input, deps) {
|
|
|
19796
20405
|
let assertions = [];
|
|
19797
20406
|
let applyError;
|
|
19798
20407
|
try {
|
|
19799
|
-
result = await runLoader(client, {
|
|
20408
|
+
result = await runLoader(client, {
|
|
20409
|
+
blueprint,
|
|
20410
|
+
tenantId,
|
|
20411
|
+
machineId,
|
|
20412
|
+
readExistingKey,
|
|
20413
|
+
persistMintedKey,
|
|
20414
|
+
log,
|
|
20415
|
+
remintForContext,
|
|
20416
|
+
remintBootstrapForContext
|
|
20417
|
+
});
|
|
19800
20418
|
log.debug({ component: "harness", contextId: result.contextId, keyName: result.keyName }, "applied");
|
|
19801
|
-
assertions = await runAssertions(
|
|
20419
|
+
assertions = await runAssertions(
|
|
20420
|
+
await remint(blueprint.contextId),
|
|
20421
|
+
hasIssuers || hasNamespaces ? await remintBootstrap(blueprint.contextId) : client,
|
|
20422
|
+
deps,
|
|
20423
|
+
blueprint,
|
|
20424
|
+
tenantId,
|
|
20425
|
+
result
|
|
20426
|
+
);
|
|
19802
20427
|
} catch (e) {
|
|
19803
20428
|
applyError = errMsg(e);
|
|
19804
20429
|
log.warn({ component: "harness", err: applyError }, "apply phase failed");
|
|
@@ -19810,7 +20435,14 @@ async function runBlueprintHarness(client, input, deps) {
|
|
|
19810
20435
|
tdClient = await remint(blueprint.contextId);
|
|
19811
20436
|
} catch {
|
|
19812
20437
|
}
|
|
19813
|
-
|
|
20438
|
+
let tdBootstrapClient = client;
|
|
20439
|
+
if (hasIssuers) {
|
|
20440
|
+
try {
|
|
20441
|
+
tdBootstrapClient = await remintBootstrap(blueprint.contextId);
|
|
20442
|
+
} catch {
|
|
20443
|
+
}
|
|
20444
|
+
}
|
|
20445
|
+
teardown = await runTeardown(tdClient, tdBootstrapClient, blueprint, machineId, snapshot, result);
|
|
19814
20446
|
}
|
|
19815
20447
|
const passed = applyError === void 0 && assertions.length > 0 && assertions.every((a) => a.ok);
|
|
19816
20448
|
return {
|
|
@@ -19884,6 +20516,13 @@ async function runBlueprintTest(fileOrName, opts, deps = {}) {
|
|
|
19884
20516
|
process.stderr.write(` \u2716 Scope gate refused blueprint:
|
|
19885
20517
|
`);
|
|
19886
20518
|
for (const r of err.rejected) process.stderr.write(` \u2022 ${r.action}: ${r.reason}
|
|
20519
|
+
`);
|
|
20520
|
+
return 2;
|
|
20521
|
+
}
|
|
20522
|
+
if (err instanceof CapabilityGateError) {
|
|
20523
|
+
process.stderr.write(` \u2716 Capability gate refused blueprint:
|
|
20524
|
+
`);
|
|
20525
|
+
for (const r of err.rejected) process.stderr.write(` \u2022 ${r.action}: ${r.reason}
|
|
19887
20526
|
`);
|
|
19888
20527
|
return 2;
|
|
19889
20528
|
}
|
|
@@ -19919,9 +20558,22 @@ async function runBlueprintTest(fileOrName, opts, deps = {}) {
|
|
|
19919
20558
|
environment: auth.baseUrl
|
|
19920
20559
|
});
|
|
19921
20560
|
};
|
|
20561
|
+
const remintBootstrapForContext = async (contextId) => {
|
|
20562
|
+
const ctxToken = await mintBridgeToken({
|
|
20563
|
+
cognitoToken: auth.cognitoToken,
|
|
20564
|
+
apiBaseUrl: auth.baseUrl,
|
|
20565
|
+
contextId,
|
|
20566
|
+
tenant: auth.tenant,
|
|
20567
|
+
useScopedToken: opts.useScopedToken
|
|
20568
|
+
});
|
|
20569
|
+
return new VectrosClient({
|
|
20570
|
+
token: ctxToken.token,
|
|
20571
|
+
environment: auth.baseUrl
|
|
20572
|
+
});
|
|
20573
|
+
};
|
|
19922
20574
|
const report = await runBlueprintHarness(
|
|
19923
20575
|
client,
|
|
19924
|
-
{ blueprint, tenantId, machineId, keep: opts.keep, log, remintForContext },
|
|
20576
|
+
{ blueprint, tenantId, machineId, keep: opts.keep, log, remintForContext, remintBootstrapForContext },
|
|
19925
20577
|
{ pingWithKey }
|
|
19926
20578
|
);
|
|
19927
20579
|
if (opts.json) {
|
|
@@ -20002,9 +20654,10 @@ async function runJoin(opts, deps = {}) {
|
|
|
20002
20654
|
return 2;
|
|
20003
20655
|
}
|
|
20004
20656
|
const context = opts.contextArg;
|
|
20657
|
+
const tenant = opts.tenant ?? "live";
|
|
20005
20658
|
let alias;
|
|
20006
20659
|
try {
|
|
20007
|
-
alias = keyringAliasFor(opts.alias ?? context,
|
|
20660
|
+
alias = keyringAliasFor(opts.alias ?? context, tenant);
|
|
20008
20661
|
} catch (err) {
|
|
20009
20662
|
process.stderr.write(` \u2716 ${formatError(err)}
|
|
20010
20663
|
`);
|
|
@@ -20039,8 +20692,23 @@ async function runJoin(opts, deps = {}) {
|
|
|
20039
20692
|
const heldSecret = existingEntry ? await keyring.resolveSecret(alias) : void 0;
|
|
20040
20693
|
const canReuse = existingEntry && heldSecret && existingEntry.context === context && existingEntry.principalId === principalId && existingEntry.env === auth.env;
|
|
20041
20694
|
if (canReuse && existingEntry) {
|
|
20042
|
-
await keyring
|
|
20043
|
-
|
|
20695
|
+
const willActivate2 = await determineActivateDecision(keyring, {
|
|
20696
|
+
tenant,
|
|
20697
|
+
explicitActivate: opts.activate,
|
|
20698
|
+
hint: { alias, secret: heldSecret }
|
|
20699
|
+
});
|
|
20700
|
+
if (willActivate2) await keyring.setActive(alias);
|
|
20701
|
+
emitJoined({
|
|
20702
|
+
alias,
|
|
20703
|
+
context,
|
|
20704
|
+
principalId,
|
|
20705
|
+
role: opts.role,
|
|
20706
|
+
keyId: existingEntry.keyId,
|
|
20707
|
+
reused: true,
|
|
20708
|
+
tenant,
|
|
20709
|
+
willActivate: willActivate2,
|
|
20710
|
+
explicitlyDeclined: opts.activate === false
|
|
20711
|
+
});
|
|
20044
20712
|
return 0;
|
|
20045
20713
|
}
|
|
20046
20714
|
warnReplacingAlias(existingEntry, alias);
|
|
@@ -20052,13 +20720,24 @@ async function runJoin(opts, deps = {}) {
|
|
|
20052
20720
|
);
|
|
20053
20721
|
}
|
|
20054
20722
|
if (revokedKeyId) await warnOtherAliasesNaming(keyring, revokedKeyId, alias);
|
|
20723
|
+
const willActivate = await determineActivateDecision(keyring, { tenant, explicitActivate: opts.activate });
|
|
20055
20724
|
const { entry } = await storeMintedKey(
|
|
20056
20725
|
keyring,
|
|
20057
20726
|
{ alias, context, principalId, role: opts.role, keyId: key.keyId, env: auth.env },
|
|
20058
20727
|
key.rawKey,
|
|
20059
|
-
{ revokedKeyId }
|
|
20728
|
+
{ activate: willActivate, revokedKeyId }
|
|
20060
20729
|
);
|
|
20061
|
-
emitJoined(
|
|
20730
|
+
emitJoined({
|
|
20731
|
+
alias,
|
|
20732
|
+
context,
|
|
20733
|
+
principalId,
|
|
20734
|
+
role: opts.role,
|
|
20735
|
+
keyId: entry.keyId,
|
|
20736
|
+
reused: false,
|
|
20737
|
+
tenant,
|
|
20738
|
+
willActivate,
|
|
20739
|
+
explicitlyDeclined: opts.activate === false
|
|
20740
|
+
});
|
|
20062
20741
|
return 0;
|
|
20063
20742
|
} catch (err) {
|
|
20064
20743
|
process.stderr.write(formatCliError(err, { action: "join failed" }));
|
|
@@ -20066,27 +20745,34 @@ async function runJoin(opts, deps = {}) {
|
|
|
20066
20745
|
}
|
|
20067
20746
|
});
|
|
20068
20747
|
}
|
|
20069
|
-
function emitJoined(
|
|
20070
|
-
|
|
20071
|
-
|
|
20072
|
-
|
|
20073
|
-
|
|
20074
|
-
|
|
20075
|
-
|
|
20076
|
-
|
|
20077
|
-
|
|
20078
|
-
|
|
20748
|
+
function emitJoined(opts) {
|
|
20749
|
+
const header = `
|
|
20750
|
+
\u2714 Joined '${opts.context}' as ${opts.principalId}` + (opts.reused ? " (existing key reused)" : "") + "\n" + formatKeyValues([
|
|
20751
|
+
["alias", opts.alias],
|
|
20752
|
+
["context", opts.context],
|
|
20753
|
+
["principal", opts.principalId],
|
|
20754
|
+
["role", opts.role],
|
|
20755
|
+
["key id", opts.keyId]
|
|
20756
|
+
]);
|
|
20757
|
+
const activationNote = opts.willActivate ? `
|
|
20079
20758
|
|
|
20080
|
-
Now active. Use \`vectros switch ${alias}\` to return to it, \`vectros keyring show --format env\` to export the key for the MCP server.
|
|
20081
|
-
`
|
|
20759
|
+
Now active. Use \`vectros switch ${opts.alias}\` to return to it, \`vectros keyring show --format env\` to export the key for the MCP server.
|
|
20760
|
+
` : "";
|
|
20761
|
+
process.stdout.write(
|
|
20762
|
+
header + activationNote + tenantActivateMessage({
|
|
20763
|
+
alias: opts.alias,
|
|
20764
|
+
tenant: opts.tenant,
|
|
20765
|
+
willActivate: opts.willActivate,
|
|
20766
|
+
explicitlyDeclined: opts.explicitlyDeclined
|
|
20767
|
+
})
|
|
20082
20768
|
);
|
|
20083
20769
|
}
|
|
20084
20770
|
|
|
20085
20771
|
// src/build-info.ts
|
|
20086
20772
|
var BUILD_INFO = {
|
|
20087
|
-
cli: true ? "0.
|
|
20088
|
-
sdk: true ? "0.
|
|
20089
|
-
blueprints: true ? "0.
|
|
20773
|
+
cli: true ? "0.16.0" : "dev",
|
|
20774
|
+
sdk: true ? "0.40.0" : "dev",
|
|
20775
|
+
blueprints: true ? "0.13.0" : "dev"
|
|
20090
20776
|
};
|
|
20091
20777
|
function formatBuildInfo() {
|
|
20092
20778
|
return `vectros ${BUILD_INFO.cli} (sdk ${BUILD_INFO.sdk}, blueprints ${BUILD_INFO.blueprints})`;
|
|
@@ -20110,6 +20796,14 @@ function addCommonAuthOptions(cmd) {
|
|
|
20110
20796
|
"emit machine-readable JSON instead of the human-readable view"
|
|
20111
20797
|
);
|
|
20112
20798
|
}
|
|
20799
|
+
function parseTenantFlag(raw) {
|
|
20800
|
+
if (raw !== void 0 && raw !== "test" && raw !== "live") {
|
|
20801
|
+
process.stderr.write(` \u2716 --tenant must be 'test' or 'live' (got '${raw}')
|
|
20802
|
+
`);
|
|
20803
|
+
process.exit(2);
|
|
20804
|
+
}
|
|
20805
|
+
return raw;
|
|
20806
|
+
}
|
|
20113
20807
|
function toCommonOptions(flags) {
|
|
20114
20808
|
return {
|
|
20115
20809
|
env: flags.env,
|
|
@@ -20163,7 +20857,10 @@ function buildProgram(deps = {}) {
|
|
|
20163
20857
|
return program;
|
|
20164
20858
|
}
|
|
20165
20859
|
function registerJoinCommand(program, deps) {
|
|
20166
|
-
const join3 = program.command("join").description("Enroll a principal in a context (grant + key) and keep the key in the keyring.").argument("<context>", "the app context to join").option("--role <roleId>", "reference a Role to grant (XOR --actions)").option("--actions <csv>", "inline single-clause actions to grant, comma-separated (XOR --role)").option("--as <principal>", "who to enroll: 'me' (default) or an explicit 'usr_<id>'", "me").option("--scope <ns:value>", "identity override: a scope the member acts under (repeatable, e.g. org:<id>)", collect, []).option("--alias <name>", "keyring entry name (defaults to the context id)").option("--tenant <tenant>", "target tenant: 'test' or 'live' (default live)")
|
|
20860
|
+
const join3 = program.command("join").description("Enroll a principal in a context (grant + key) and keep the key in the keyring.").argument("<context>", "the app context to join").option("--role <roleId>", "reference a Role to grant (XOR --actions)").option("--actions <csv>", "inline single-clause actions to grant, comma-separated (XOR --role)").option("--as <principal>", "who to enroll: 'me' (default) or an explicit 'usr_<id>'", "me").option("--scope <ns:value>", "identity override: a scope the member acts under (repeatable, e.g. org:<id>)", collect, []).option("--alias <name>", "keyring entry name (defaults to the context id)").option("--tenant <tenant>", "target tenant: 'test' or 'live' (default live)").option(
|
|
20861
|
+
"--activate",
|
|
20862
|
+
"force the freshly-stored key to become your active identity, even over a currently-active LIVE credential (the one case --tenant test does NOT do automatically)"
|
|
20863
|
+
).option("--no-activate", "store the key without activating it, regardless of tenant");
|
|
20167
20864
|
addAuthOptions(join3).action(
|
|
20168
20865
|
async (contextArg, flags) => {
|
|
20169
20866
|
if (flags.tenant !== void 0 && flags.tenant !== "test" && flags.tenant !== "live") {
|
|
@@ -20181,7 +20878,8 @@ function registerJoinCommand(program, deps) {
|
|
|
20181
20878
|
as: flags.as,
|
|
20182
20879
|
scope: flags.scope,
|
|
20183
20880
|
alias: flags.alias,
|
|
20184
|
-
tenant: flags.tenant
|
|
20881
|
+
tenant: flags.tenant,
|
|
20882
|
+
activate: flags.activate
|
|
20185
20883
|
},
|
|
20186
20884
|
deps
|
|
20187
20885
|
)
|
|
@@ -20428,9 +21126,10 @@ function toKeyDestination(flags) {
|
|
|
20428
21126
|
function registerKeyCommands(program, deps) {
|
|
20429
21127
|
const key = program.command("key").description("Manage scoped API keys (ssk_*) \u2014 issue, list, revoke, rotate.");
|
|
20430
21128
|
const issue = addKeyDestinationOptions(
|
|
20431
|
-
key.command("issue").description("Mint an ssk_* for a principal that already has a profile in the context.").requiredOption("--principal <p>", "principal (usr_<id> or <id>) with an AccessProfile in the context").requiredOption("--context <c>", "app context the key operates in").option("--name <name>", "key name (idempotency tuple component)", DEFAULT_KEY_NAME).option("--label <label>", "optional principal label (surfaced in /v1/ping)")
|
|
21129
|
+
key.command("issue").description("Mint an ssk_* for a principal that already has a profile in the context.").requiredOption("--principal <p>", "principal (usr_<id> or <id>) with an AccessProfile in the context").requiredOption("--context <c>", "app context the key operates in").option("--name <name>", "key name (idempotency tuple component)", DEFAULT_KEY_NAME).option("--label <label>", "optional principal label (surfaced in /v1/ping)").option("--tenant <tenant>", "tenant the key lives in: 'test' or 'live' (default live)")
|
|
20432
21130
|
);
|
|
20433
21131
|
addAuthOptions(issue).action(async (flags) => {
|
|
21132
|
+
const tenant = parseTenantFlag(flags.tenant);
|
|
20434
21133
|
process.exit(
|
|
20435
21134
|
await runKeyIssue({
|
|
20436
21135
|
...toCommonOptions(flags),
|
|
@@ -20438,6 +21137,7 @@ function registerKeyCommands(program, deps) {
|
|
|
20438
21137
|
context: flags.context,
|
|
20439
21138
|
name: flags.name,
|
|
20440
21139
|
label: flags.label,
|
|
21140
|
+
tenant,
|
|
20441
21141
|
...toKeyDestination(flags)
|
|
20442
21142
|
}, deps)
|
|
20443
21143
|
);
|
|
@@ -20445,37 +21145,50 @@ function registerKeyCommands(program, deps) {
|
|
|
20445
21145
|
const list = key.command("list").description("List active scoped keys in one context (default, or --context <id>); --all-contexts for every context.").option("--principal <p>", "only keys for this principal (usr_<id> or <id>)").option("--context <c>", "only keys in this context (default: the default context)").option(
|
|
20446
21146
|
"--all-contexts",
|
|
20447
21147
|
"list every key across ALL your contexts (owner-only) \u2014 the tenant-wide view for the key limit"
|
|
20448
|
-
);
|
|
21148
|
+
).option("--tenant <tenant>", "tenant to list: 'test' or 'live' (default live; not combinable with --all-contexts)");
|
|
20449
21149
|
addCommonAuthOptions(list).action(
|
|
20450
21150
|
async (flags) => {
|
|
21151
|
+
if (flags.allContexts && flags.tenant !== void 0) {
|
|
21152
|
+
process.stderr.write(" \u2716 --tenant is not combinable with --all-contexts (which already spans both tenants).\n");
|
|
21153
|
+
process.exit(2);
|
|
21154
|
+
}
|
|
21155
|
+
const tenant = parseTenantFlag(flags.tenant);
|
|
20451
21156
|
process.exit(
|
|
20452
21157
|
await runKeyList(
|
|
20453
21158
|
{
|
|
20454
21159
|
...toCommonOptions(flags),
|
|
20455
21160
|
principal: flags.principal,
|
|
20456
21161
|
context: flags.context,
|
|
20457
|
-
allContexts: Boolean(flags.allContexts)
|
|
21162
|
+
allContexts: Boolean(flags.allContexts),
|
|
21163
|
+
tenant
|
|
20458
21164
|
},
|
|
20459
21165
|
deps
|
|
20460
21166
|
)
|
|
20461
21167
|
);
|
|
20462
21168
|
}
|
|
20463
21169
|
);
|
|
20464
|
-
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)");
|
|
20465
|
-
addCommonAuthOptions(get).action(
|
|
20466
|
-
|
|
20467
|
-
|
|
20468
|
-
|
|
20469
|
-
|
|
20470
|
-
|
|
20471
|
-
|
|
21170
|
+
const get = key.command("get").description("Show one key\u2019s metadata (no secret).").argument("<keyId>").option("--context <c>", "context the key is bound to (default: the default context)").option("--tenant <tenant>", "tenant the key lives in: 'test' or 'live' (default live)");
|
|
21171
|
+
addCommonAuthOptions(get).action(
|
|
21172
|
+
async (keyId, flags) => {
|
|
21173
|
+
const tenant = parseTenantFlag(flags.tenant);
|
|
21174
|
+
process.exit(await runKeyGet(keyId, { ...toCommonOptions(flags), context: flags.context, tenant }, deps));
|
|
21175
|
+
}
|
|
21176
|
+
);
|
|
21177
|
+
const revoke = key.command("revoke").description("Revoke a key (soft-delete; ~5 min to take effect).").argument("<keyId>").option("--context <c>", "context the key is bound to (default: the default context)").option("--tenant <tenant>", "tenant the key lives in: 'test' or 'live' (default live)");
|
|
21178
|
+
addAuthOptions(revoke).action(
|
|
21179
|
+
async (keyId, flags) => {
|
|
21180
|
+
const tenant = parseTenantFlag(flags.tenant);
|
|
21181
|
+
process.exit(await runKeyRevoke(keyId, { ...toCommonOptions(flags), context: flags.context, tenant }, deps));
|
|
21182
|
+
}
|
|
21183
|
+
);
|
|
20472
21184
|
const rotate = addKeyDestinationOptions(
|
|
20473
21185
|
key.command("rotate").description("Revoke the matching active key and mint a fresh secret.").requiredOption("--principal <p>", "principal (usr_<id> or <id>)").requiredOption("--context <c>", "app context").option("--name <name>", "key name to rotate", DEFAULT_KEY_NAME).option(
|
|
20474
21186
|
"--force-new",
|
|
20475
21187
|
"mint a new key even though other active keys already exist for this principal/context under a different name (bypasses the default-name-mismatch guard)"
|
|
20476
|
-
)
|
|
21188
|
+
).option("--tenant <tenant>", "tenant the key lives in: 'test' or 'live' (default live)")
|
|
20477
21189
|
);
|
|
20478
21190
|
addAuthOptions(rotate).action(async (flags) => {
|
|
21191
|
+
const tenant = parseTenantFlag(flags.tenant);
|
|
20479
21192
|
process.exit(
|
|
20480
21193
|
await runKeyRotate({
|
|
20481
21194
|
...toCommonOptions(flags),
|
|
@@ -20483,6 +21196,7 @@ function registerKeyCommands(program, deps) {
|
|
|
20483
21196
|
context: flags.context,
|
|
20484
21197
|
name: flags.name,
|
|
20485
21198
|
forceNew: flags.forceNew,
|
|
21199
|
+
tenant,
|
|
20486
21200
|
...toKeyDestination(flags)
|
|
20487
21201
|
}, deps)
|
|
20488
21202
|
);
|