@vectros-ai/cli 0.15.0 → 0.19.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 +179 -8
- package/CHANGELOG.md +339 -0
- package/README.md +253 -2
- package/THIRD-PARTY-NOTICES.md +4 -4
- package/dist/cli.js +2018 -570
- package/dist/cli.js.map +1 -1
- package/dist/cli.mjs +2018 -570
- package/dist/cli.mjs.map +1 -1
- package/package.json +4 -4
package/dist/cli.js
CHANGED
|
@@ -4103,7 +4103,7 @@ var AuthClient = class {
|
|
|
4103
4103
|
return handleNonStatusCodeError(_response.error, _response.rawResponse, "GET", "/v1/admin/keys/scoped");
|
|
4104
4104
|
}
|
|
4105
4105
|
/**
|
|
4106
|
-
* Creates a scoped API key (an `ssk_*` secret) that inherits its permissions from an existing access profile in your account. The call is idempotent on the combination of tenant, context, user, and key name: re-issuing the same request returns the existing key WITHOUT re-disclosing its raw secret. The raw key is returned ONLY in this response — store it securely, as it cannot be retrieved again. Requires the `keys:c` scope. If you use a scoped credential, `keys:c` alone is not sufficient: because the minted key is durably bound to the profile you name, the profile's effective scopes may not exceed your own, and you may only mint against a profile whose `identityOverrides` values your own identity holds. A root API key (`sk_`) is exempt from
|
|
4106
|
+
* Creates a scoped API key (an `ssk_*` secret) that inherits its permissions from an existing access profile in your account. The call is idempotent on the combination of tenant, context, user, and key name: re-issuing the same request returns the existing key WITHOUT re-disclosing its raw secret. The raw key is returned ONLY in this response — store it securely, as it cannot be retrieved again. Requires the `keys:c` scope. If you use a scoped credential, `keys:c` alone is not sufficient: because the minted key is durably bound to the profile you name, the profile's effective scopes may not exceed your own, and you may only mint against a profile whose `identityOverrides` values your own identity holds. Minting a key bound to your OWN principal needs nothing further; minting one bound to a DIFFERENT principal additionally requires the `delegate-mint` capability (`granted_capabilities`) on your credential — without it the request is refused. A root API key (`sk_`) is exempt from all three bounds.
|
|
4107
4107
|
*
|
|
4108
4108
|
* @param {Vectros.CreateScopedKeyRequest} request
|
|
4109
4109
|
* @param {AuthClient.RequestOptions} requestOptions - Request-specific configuration.
|
|
@@ -4435,7 +4435,7 @@ var AuthClient = class {
|
|
|
4435
4435
|
);
|
|
4436
4436
|
}
|
|
4437
4437
|
/**
|
|
4438
|
-
* Creates a new access profile under the given app context. This call is idempotent by `principalId`: if a profile with the same `principalId` already exists, the existing profile is returned (with status 200) instead of creating a duplicate. The response's `created` field (and the HTTP status — 201 when created, 200 when an existing profile was returned) tells the two apart. To overwrite an existing profile's `scopes`/`roleId`, `identityOverrides`, and `status` instead of returning it unchanged, set `?upsert=true` (this also requires the `profiles:u` scope, and applies the same `identityOverrides` bounds the update endpoint documents — a scoped credential may not repoint or clear an identity value it does not itself hold). You must provide exactly one of `scopes` (an inline list of scopes) or `roleId` (a reference to a role); supplying both, or neither, is rejected. `identityOverrides` is keyed by ownership namespace in `scope:<namespace>` form — `scope:org` and `scope:client` for the reserved namespaces, or any namespace you have registered — and may name at most two; any other key (including the account identifier or `userId`) is rejected. If you use a scoped credential, the profile's effective scopes may not exceed your own; a root API key (`sk_`) is exempt. Requires the `profiles:c` scope.
|
|
4438
|
+
* Creates a new access profile under the given app context. This call is idempotent by `principalId`: if a profile with the same `principalId` already exists, the existing profile is returned (with status 200) instead of creating a duplicate. The response's `created` field (and the HTTP status — 201 when created, 200 when an existing profile was returned) tells the two apart. To overwrite an existing profile's `scopes`/`roleId`, `identityOverrides`, and `status` instead of returning it unchanged, set `?upsert=true` (this also requires the `profiles:u` scope, and applies the same `identityOverrides` bounds the update endpoint documents — a scoped credential may not repoint or clear an identity value it does not itself hold). The `principalId` must name a principal that already exists: a `usr_` principal must be a live user in your tenant, so create the user before granting it a profile. A `usr_` id that names no such user is rejected, and no profile is created. `key_` principals are not checked this way. You must provide exactly one of `scopes` (an inline list of scopes) or `roleId` (a reference to a role); supplying both, or neither, is rejected. `identityOverrides` is keyed by ownership namespace in `scope:<namespace>` form — `scope:org` and `scope:client` for the reserved namespaces, or any namespace you have registered — and may name at most two; any other key (including the account identifier or `userId`) is rejected. If you use a scoped credential, the profile's effective scopes may not exceed your own; a root API key (`sk_`) is exempt. Requires the `profiles:c` scope.
|
|
4439
4439
|
*
|
|
4440
4440
|
* @param {Vectros.CreateAccessProfileRequest} request
|
|
4441
4441
|
* @param {AuthClient.RequestOptions} requestOptions - Request-specific configuration.
|
|
@@ -5536,7 +5536,7 @@ var AuthClient = class {
|
|
|
5536
5536
|
);
|
|
5537
5537
|
}
|
|
5538
5538
|
/**
|
|
5539
|
-
* Returns full usage detail for the requested calendar month, broken down by category (search, documents, and records) with per-category credit estimates and a split between your live and test environments. Defaults to the current month when `year` and `month` are omitted. Requires the `billing:r` scope on scoped tokens; API keys always have access.
|
|
5539
|
+
* Returns full usage detail for the requested calendar month, broken down by category (search, documents, and records) with per-category credit estimates and a split between your live and test environments. Defaults to the current month when `year` and `month` are omitted. Requires the `billing:r` scope on scoped tokens; API keys always have access. **A token confined to a single app context sees only that context's usage**: totals, the environment split, and the `contexts` breakdown narrow to it, and the environment your context is not bound to is omitted (`null`), not zeroed. Only a token with cross-context reach sees your full account-wide totals. Two exceptions to the narrowing, since they have no per-context breakdown to narrow to: `reads.calls.used`/`reads.dataOut.bytes` (metered per account, not per context) read as `0` for a confined token rather than a narrowed figure — the corresponding overage-credit charge fields narrow correctly; and `credits.limit` stays your whole plan's ceiling, so `credits.remaining` may overstate the account's true remaining room.
|
|
5540
5540
|
*
|
|
5541
5541
|
* @param {Vectros.GetUsageRequest} request
|
|
5542
5542
|
* @param {AuthClient.RequestOptions} requestOptions - Request-specific configuration.
|
|
@@ -5599,7 +5599,7 @@ var AuthClient = class {
|
|
|
5599
5599
|
return handleNonStatusCodeError(_response.error, _response.rawResponse, "GET", "/v1/usage");
|
|
5600
5600
|
}
|
|
5601
5601
|
/**
|
|
5602
|
-
* Retrieves a single registered issuer by issuerId. Requires a root API key or the bootstrap's provisioning capability.
|
|
5602
|
+
* Retrieves a single registered issuer by issuerId. Requires a root API key or the bootstrap's provisioning capability. A credential confined to one app context sees only an issuer registered in that context; naming one registered in another context returns 404, identically to a nonexistent issuerId. A root API key sees every context. An ordinary bootstrap credential that didn't specify a context resolves to the `default` app context specifically, not every context — so this call returns 404 for an issuer registered under any other context unless you re-minted the bootstrap token pinned to that context.
|
|
5603
5603
|
*
|
|
5604
5604
|
* @param {Vectros.GetIssuerRequest} request
|
|
5605
5605
|
* @param {AuthClient.RequestOptions} requestOptions - Request-specific configuration.
|
|
@@ -5657,13 +5657,81 @@ var AuthClient = class {
|
|
|
5657
5657
|
return handleNonStatusCodeError(_response.error, _response.rawResponse, "GET", "/v1/auth/issuers/{issuerId}");
|
|
5658
5658
|
}
|
|
5659
5659
|
/**
|
|
5660
|
-
*
|
|
5660
|
+
* Updates the mutable fields of a registered issuer: `subClaim`, `emailClaim`, `status` (`active`/`suspended` — a suspended issuer's tokens are rejected identically to an unregistered issuer at exchange time), and `selfSignupPolicies`. Fields omitted from the body are left unchanged (partial update). `issuer`, `jwksUri`, `audience`, and `contextId` are trust-anchor / routing-pin fields and are immutable via this route — supplying a value that differs from the current registration is rejected with 400; supplying the current value back is a no-op. Rotating a trust anchor requires deleting and re-registering the issuer, which is itself refused while any user is bound through it. Requires a root API key or the bootstrap's provisioning capability, gated identically to every other operation on this surface. A credential confined to one app context may only update an issuer registered in that context; naming one registered in another context returns 404, identically to a nonexistent issuerId. A root API key may update any issuer.
|
|
5661
|
+
*
|
|
5662
|
+
* @param {Vectros.IssuerUpdateRequest} request
|
|
5663
|
+
* @param {AuthClient.RequestOptions} requestOptions - Request-specific configuration.
|
|
5664
|
+
*
|
|
5665
|
+
* @throws {@link Vectros.BadRequestError}
|
|
5666
|
+
* @throws {@link Vectros.ForbiddenError}
|
|
5667
|
+
* @throws {@link Vectros.NotFoundError}
|
|
5668
|
+
* @throws {@link Vectros.TooManyRequestsError}
|
|
5669
|
+
*
|
|
5670
|
+
* @example
|
|
5671
|
+
* await client.auth.updateIssuer({
|
|
5672
|
+
* issuerId: "auth0-prod"
|
|
5673
|
+
* })
|
|
5674
|
+
*/
|
|
5675
|
+
updateIssuer(request, requestOptions) {
|
|
5676
|
+
return HttpResponsePromise.fromPromise(this.__updateIssuer(request, requestOptions));
|
|
5677
|
+
}
|
|
5678
|
+
async __updateIssuer(request, requestOptions) {
|
|
5679
|
+
const { issuerId, ..._body } = request;
|
|
5680
|
+
const _authRequest = await this._options.authProvider.getAuthRequest();
|
|
5681
|
+
const _headers = mergeHeaders(
|
|
5682
|
+
_authRequest.headers,
|
|
5683
|
+
this._options?.headers,
|
|
5684
|
+
requestOptions?.headers
|
|
5685
|
+
);
|
|
5686
|
+
const _response = await fetcher({
|
|
5687
|
+
url: url_exports.join(
|
|
5688
|
+
await Supplier.get(this._options.baseUrl) ?? await Supplier.get(this._options.environment),
|
|
5689
|
+
`v1/auth/issuers/${url_exports.encodePathParam(issuerId)}`
|
|
5690
|
+
),
|
|
5691
|
+
method: "PUT",
|
|
5692
|
+
headers: _headers,
|
|
5693
|
+
contentType: "application/json",
|
|
5694
|
+
queryString: url_exports.queryBuilder().mergeAdditional(requestOptions?.queryParams).build(),
|
|
5695
|
+
requestType: "json",
|
|
5696
|
+
body: _body,
|
|
5697
|
+
timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1e3,
|
|
5698
|
+
maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries,
|
|
5699
|
+
abortSignal: requestOptions?.abortSignal,
|
|
5700
|
+
fetchFn: this._options?.fetch,
|
|
5701
|
+
logging: this._options.logging
|
|
5702
|
+
});
|
|
5703
|
+
if (_response.ok) {
|
|
5704
|
+
return { data: _response.body, rawResponse: _response.rawResponse };
|
|
5705
|
+
}
|
|
5706
|
+
if (_response.error.reason === "status-code") {
|
|
5707
|
+
switch (_response.error.statusCode) {
|
|
5708
|
+
case 400:
|
|
5709
|
+
throw new BadRequestError(_response.error.body, _response.rawResponse);
|
|
5710
|
+
case 403:
|
|
5711
|
+
throw new ForbiddenError(_response.error.body, _response.rawResponse);
|
|
5712
|
+
case 404:
|
|
5713
|
+
throw new NotFoundError(_response.error.body, _response.rawResponse);
|
|
5714
|
+
case 429:
|
|
5715
|
+
throw new TooManyRequestsError(_response.error.body, _response.rawResponse);
|
|
5716
|
+
default:
|
|
5717
|
+
throw new VectrosError({
|
|
5718
|
+
statusCode: _response.error.statusCode,
|
|
5719
|
+
body: _response.error.body,
|
|
5720
|
+
rawResponse: _response.rawResponse
|
|
5721
|
+
});
|
|
5722
|
+
}
|
|
5723
|
+
}
|
|
5724
|
+
return handleNonStatusCodeError(_response.error, _response.rawResponse, "PUT", "/v1/auth/issuers/{issuerId}");
|
|
5725
|
+
}
|
|
5726
|
+
/**
|
|
5727
|
+
* Deregisters a trusted third-party IdP issuer. Requires a root API key or the bootstrap's provisioning capability. A credential confined to one app context may only deregister an issuer registered in that context; naming one registered in another context returns 404, identically to a nonexistent issuerId. A root API key may deregister any issuer. Refused if any user account was ever created or matched via this issuer (by a prior self-signup or accepted invite, through `POST /v1/auth/token/exchange`) — that access cannot be silently orphaned. Deactivate the affected users first if you intend to cut off their access, or register a replacement issuer before removing this one. An issuer that has never been used for an exchange (no bound users yet) can always be deregistered.
|
|
5661
5728
|
*
|
|
5662
5729
|
* @param {Vectros.DeleteIssuerRequest} request
|
|
5663
5730
|
* @param {AuthClient.RequestOptions} requestOptions - Request-specific configuration.
|
|
5664
5731
|
*
|
|
5665
5732
|
* @throws {@link Vectros.ForbiddenError}
|
|
5666
5733
|
* @throws {@link Vectros.NotFoundError}
|
|
5734
|
+
* @throws {@link Vectros.ConflictError}
|
|
5667
5735
|
* @throws {@link Vectros.TooManyRequestsError}
|
|
5668
5736
|
*
|
|
5669
5737
|
* @example
|
|
@@ -5705,6 +5773,8 @@ var AuthClient = class {
|
|
|
5705
5773
|
throw new ForbiddenError(_response.error.body, _response.rawResponse);
|
|
5706
5774
|
case 404:
|
|
5707
5775
|
throw new NotFoundError(_response.error.body, _response.rawResponse);
|
|
5776
|
+
case 409:
|
|
5777
|
+
throw new ConflictError(_response.error.body, _response.rawResponse);
|
|
5708
5778
|
case 429:
|
|
5709
5779
|
throw new TooManyRequestsError(_response.error.body, _response.rawResponse);
|
|
5710
5780
|
default:
|
|
@@ -5723,7 +5793,7 @@ var AuthClient = class {
|
|
|
5723
5793
|
);
|
|
5724
5794
|
}
|
|
5725
5795
|
/**
|
|
5726
|
-
* Returns the issuers registered in your tenant. Requires a root API key or the bootstrap's provisioning capability. Returns a `{data, nextCursor}` envelope.
|
|
5796
|
+
* 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. An ordinary bootstrap credential that didn't specify a context resolves to the `default` app context specifically, not every context — so this call returns an empty page for a tenant whose issuers are all registered under a different context unless you re-minted the bootstrap token pinned to that context. Returns a `{data, nextCursor}` envelope.
|
|
5727
5797
|
*
|
|
5728
5798
|
* @param {Vectros.ListIssuersRequest} request
|
|
5729
5799
|
* @param {AuthClient.RequestOptions} requestOptions - Request-specific configuration.
|
|
@@ -5780,7 +5850,7 @@ var AuthClient = class {
|
|
|
5780
5850
|
return handleNonStatusCodeError(_response.error, _response.rawResponse, "GET", "/v1/auth/issuers");
|
|
5781
5851
|
}
|
|
5782
5852
|
/**
|
|
5783
|
-
* Registers a trusted third-party IdP issuer that BYO-IdP token exchange (`POST /v1/auth/token/exchange`) may accept a `subject_token` from. Requires a root API key or the CLI bootstrap's provisioning capability — never an ordinary partner-grantable scope. Idempotent by `issuerId` within your tenant; the `(issuer, audience)` pair must not already be registered by a different issuerId/tenant.
|
|
5853
|
+
* 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. A credential authorized only via the provisioning capability may register only against the app context it is bound to; naming a different one returns 403. A root API key is unaffected and may register against any of its contexts. Idempotent by `issuerId` within your tenant; the `(issuer, audience)` pair must not already be registered by a different issuerId/tenant. If `issuerId` collides with a registration owned by a different app context than the one you're confined to, the request fails with 400 rather than returning that context's configuration. An app context may have at most one active issuer — deregister the existing one first if you need to replace it. One issuer MAY serve several contexts today, each via its own registration row with a distinct `audience`.
|
|
5784
5854
|
*
|
|
5785
5855
|
* @param {Vectros.IssuerRequest} request
|
|
5786
5856
|
* @param {AuthClient.RequestOptions} requestOptions - Request-specific configuration.
|
|
@@ -5891,7 +5961,7 @@ var AuthClient = class {
|
|
|
5891
5961
|
return handleNonStatusCodeError(_response.error, _response.rawResponse, "GET", "/v1/ping");
|
|
5892
5962
|
}
|
|
5893
5963
|
/**
|
|
5894
|
-
* Returns the access profiles for the given principal across
|
|
5964
|
+
* Returns the access profiles for the given principal. Looking up your OWN principal — or holding the `context-directory-read` capability — returns the profiles across ALL of your contexts, letting you answer questions like "which apps does this user have access to?". A context-bound credential looking up a DIFFERENT principal instead sees only that principal's profile in your credential's own context (at most one result), never across contexts it has no authority over. Results are always confined to your account. Requires the `profiles:r` scope.
|
|
5895
5965
|
*
|
|
5896
5966
|
* @param {Vectros.ListProfilesForPrincipalRequest} request
|
|
5897
5967
|
* @param {AuthClient.RequestOptions} requestOptions - Request-specific configuration.
|
|
@@ -5965,6 +6035,7 @@ var AuthClient = class {
|
|
|
5965
6035
|
*
|
|
5966
6036
|
* @throws {@link Vectros.BadRequestError}
|
|
5967
6037
|
* @throws {@link Vectros.ForbiddenError}
|
|
6038
|
+
* @throws {@link Vectros.NotFoundError}
|
|
5968
6039
|
* @throws {@link Vectros.TooManyRequestsError}
|
|
5969
6040
|
*
|
|
5970
6041
|
* @example
|
|
@@ -6018,6 +6089,8 @@ var AuthClient = class {
|
|
|
6018
6089
|
throw new BadRequestError(_response.error.body, _response.rawResponse);
|
|
6019
6090
|
case 403:
|
|
6020
6091
|
throw new ForbiddenError(_response.error.body, _response.rawResponse);
|
|
6092
|
+
case 404:
|
|
6093
|
+
throw new NotFoundError(_response.error.body, _response.rawResponse);
|
|
6021
6094
|
case 429:
|
|
6022
6095
|
throw new TooManyRequestsError(_response.error.body, _response.rawResponse);
|
|
6023
6096
|
default:
|
|
@@ -6031,7 +6104,7 @@ var AuthClient = class {
|
|
|
6031
6104
|
return handleNonStatusCodeError(_response.error, _response.rawResponse, "POST", "/v1/auth/token");
|
|
6032
6105
|
}
|
|
6033
6106
|
/**
|
|
6034
|
-
* Invite a new member to one of your app contexts by email
|
|
6107
|
+
* Invite a new member to one of your app contexts by email, OR grant an existing member access to an additional app context by inviting their same email again. Idempotent on the combination of context and email: re-inviting the same email into the SAME context rotates the token and resends the invitation rather than creating a duplicate — this requires the `users:r` and `users:u` scopes in addition to `users:c`, because resending rotates a credential on an existing invitation and invalidates any link already sent. Without them the collision returns 409 instead, with no invitation details and no change to the outstanding invitation. Inviting the SAME email into a DIFFERENT app context in this tenant, where that email already resolves to an existing member: if that member is active AND already has (or, once accepted, will have) a credential that works for the new context's own identity provider, this immediately grants them access to the new context (no email is sent — there is nothing to accept, `emailSent` is false) — this additionally requires the `users:r` scope (no `users:u`, since nothing is mutated), because the response names the existing member's userId, a fact about them your credential could not otherwise learn through this endpoint. If that active member's ONLY existing credential is for a DIFFERENT identity provider than the one the new context uses, a normal, independent invitation is created instead (its own new member id, a real token/accept link) — attaching them silently would leave no way for them to ever actually sign in to that context. If the existing member's original invitation is still pending, this attaches the new context's access to that same outstanding invitation and rotates its token (`users:r`+`users:u`, same as an ordinary resend — both the disclosure and the credential rotation apply here). A SUSPENDED member's email does not get new-context access this way — reactivate them explicitly first. Returns HTTP 201 in every one of those cases. Returns 409 if that email already belongs to an active or suspended member of THIS specific app context, already has a PENDING invitation for THIS specific app context, or resolves to an existing member elsewhere in the tenant and your token lacks the additional scope the grant/attach requires (`users:r`, or `users:r`+`users:u` for the still-pending case). An email that already has an identity in your OTHER tenant (test vs. live) is not a collision either — it creates an additional, independent membership in this tenant for that same identity. When `sendEmail` is false, the response includes the raw token and a ready-to-use accept link so you can deliver the invitation through your own email provider. Requires the `users:c` scope.
|
|
6035
6108
|
*
|
|
6036
6109
|
* @param {Vectros.CreateInviteRequest} request
|
|
6037
6110
|
* @param {AuthClient.RequestOptions} requestOptions - Request-specific configuration.
|
|
@@ -6170,7 +6243,84 @@ var AuthClient = class {
|
|
|
6170
6243
|
return handleNonStatusCodeError(_response.error, _response.rawResponse, "POST", "/v1/users/invite/resend");
|
|
6171
6244
|
}
|
|
6172
6245
|
/**
|
|
6173
|
-
*
|
|
6246
|
+
* Re-mints the presented `st_*` scoped token with one or more `identity.<namespace>` values changed — for a caller whose ROLE explicitly grants assuming those values (an invited hr-admin, a multi-org case-handler) and needs to change which value new writes place records under. The request body names one or more namespaces in canonical `scope:<namespace>` form, e.g. `{"scope:org": "orgB"}` — each value must be a plain literal, never a `${{ ... }}` placeholder. When you name MORE THAN ONE namespace, a single one of your roles must grant all of them together: the combination is never assembled from two different roles, because no role author would have vouched for it. `st_*`-only — a root API key or `ssk_*` scoped API key gets 403; neither needs this (root already has full authority, and an `ssk_*`'s identity shape is not what this resolves against).
|
|
6247
|
+
*
|
|
6248
|
+
* **Only an original token may assume.** A token produced BY this endpoint cannot assume again (403) — every assume starts from the token you exchanged for, so the identity you end up with is always one a single role explicitly granted rather than a combination reached by chaining calls. Keep your original token if you need to switch more than once, or exchange for a new one.
|
|
6249
|
+
*
|
|
6250
|
+
* **Entitlement is checked LIVE, against your roles as they are right now** — not against a copy frozen into your token when it was minted. The requested value must be explicitly granted by a role's `assumable` field for that namespace: a POINT check against the one value requested, and a deliberately separate, explicitly-authored question from what the role's `data_scope` permits reading or writing. Holding broad `data_scope` reach in a namespace does NOT by itself grant assuming any value in it.
|
|
6251
|
+
*
|
|
6252
|
+
* **What is preserved, and what is not.** Every clause of your token that does not reference a requested namespace is preserved verbatim, as are all other claims (`partner_user_id`, `context_id`, mint attribution). Clauses that DO reference a requested namespace are kept only if they come from a role that authorized the new value. A role that does not authorize it loses all of its clauses touching that namespace — including any scoped to the value you already held. Assume into a value one role grants and you keep that role's reach, not the reach of roles that never vouched for it.
|
|
6253
|
+
*
|
|
6254
|
+
* The re-minted token's `exp` is IDENTICAL to the presented token's — this call can never extend a session's life. A fresh, independently-revocable `jti` is stamped on every call, and (except when the presented token predates jti support and has none to chain from) the token also carries a `root_jti` revocation-lineage claim so revoking the token you started from closes every value ever assumed from it. Uses the ordinary Vectros `{"message":...}` error shape, not the OAuth envelope `POST /v1/auth/token/exchange` uses — this endpoint's caller is always Vectros-SDK code already holding a bearer token, never generic OAuth tooling.
|
|
6255
|
+
*
|
|
6256
|
+
* @param {Record<string, unknown>} request
|
|
6257
|
+
* @param {AuthClient.RequestOptions} requestOptions - Request-specific configuration.
|
|
6258
|
+
*
|
|
6259
|
+
* @throws {@link Vectros.BadRequestError}
|
|
6260
|
+
* @throws {@link Vectros.UnauthorizedError}
|
|
6261
|
+
* @throws {@link Vectros.ForbiddenError}
|
|
6262
|
+
* @throws {@link Vectros.ConflictError}
|
|
6263
|
+
* @throws {@link Vectros.TooManyRequestsError}
|
|
6264
|
+
*
|
|
6265
|
+
* @example
|
|
6266
|
+
* await client.auth.assumeToken({
|
|
6267
|
+
* "scope:org": "orgB"
|
|
6268
|
+
* })
|
|
6269
|
+
*/
|
|
6270
|
+
assumeToken(request, requestOptions) {
|
|
6271
|
+
return HttpResponsePromise.fromPromise(this.__assumeToken(request, requestOptions));
|
|
6272
|
+
}
|
|
6273
|
+
async __assumeToken(request, requestOptions) {
|
|
6274
|
+
const _authRequest = await this._options.authProvider.getAuthRequest();
|
|
6275
|
+
const _headers = mergeHeaders(
|
|
6276
|
+
_authRequest.headers,
|
|
6277
|
+
this._options?.headers,
|
|
6278
|
+
requestOptions?.headers
|
|
6279
|
+
);
|
|
6280
|
+
const _response = await fetcher({
|
|
6281
|
+
url: url_exports.join(
|
|
6282
|
+
await Supplier.get(this._options.baseUrl) ?? await Supplier.get(this._options.environment),
|
|
6283
|
+
"v1/auth/token/assume"
|
|
6284
|
+
),
|
|
6285
|
+
method: "POST",
|
|
6286
|
+
headers: _headers,
|
|
6287
|
+
contentType: "application/json",
|
|
6288
|
+
queryString: url_exports.queryBuilder().mergeAdditional(requestOptions?.queryParams).build(),
|
|
6289
|
+
requestType: "json",
|
|
6290
|
+
body: request,
|
|
6291
|
+
timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1e3,
|
|
6292
|
+
maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries,
|
|
6293
|
+
abortSignal: requestOptions?.abortSignal,
|
|
6294
|
+
fetchFn: this._options?.fetch,
|
|
6295
|
+
logging: this._options.logging
|
|
6296
|
+
});
|
|
6297
|
+
if (_response.ok) {
|
|
6298
|
+
return { data: _response.body, rawResponse: _response.rawResponse };
|
|
6299
|
+
}
|
|
6300
|
+
if (_response.error.reason === "status-code") {
|
|
6301
|
+
switch (_response.error.statusCode) {
|
|
6302
|
+
case 400:
|
|
6303
|
+
throw new BadRequestError(_response.error.body, _response.rawResponse);
|
|
6304
|
+
case 401:
|
|
6305
|
+
throw new UnauthorizedError(_response.error.body, _response.rawResponse);
|
|
6306
|
+
case 403:
|
|
6307
|
+
throw new ForbiddenError(_response.error.body, _response.rawResponse);
|
|
6308
|
+
case 409:
|
|
6309
|
+
throw new ConflictError(_response.error.body, _response.rawResponse);
|
|
6310
|
+
case 429:
|
|
6311
|
+
throw new TooManyRequestsError(_response.error.body, _response.rawResponse);
|
|
6312
|
+
default:
|
|
6313
|
+
throw new VectrosError({
|
|
6314
|
+
statusCode: _response.error.statusCode,
|
|
6315
|
+
body: _response.error.body,
|
|
6316
|
+
rawResponse: _response.rawResponse
|
|
6317
|
+
});
|
|
6318
|
+
}
|
|
6319
|
+
}
|
|
6320
|
+
return handleNonStatusCodeError(_response.error, _response.rawResponse, "POST", "/v1/auth/token/assume");
|
|
6321
|
+
}
|
|
6322
|
+
/**
|
|
6323
|
+
* RFC 8693 OAuth 2.0 Token Exchange. Trades a JWT issued by a third-party identity provider you've registered (`POST /v1/auth/issuers`) for a Vectros `st_*` scoped bearer token — no Vectros credential required to call this endpoint. The exchanged token's scope is resolved entirely server-side from the matched user's access profile; this endpoint accepts no caller-supplied scope, resource, or audience parameter (RFC 8693 §2.1's `resource`/`audience`/`scope` are not used in v1 — the registered `(issuer, audience)` pair alone pins the target tenant and app context). On a first-time login (no existing Vectros identity for this subject), two opt-in binding paths exist: `invite_token` (a `PENDING` sub-user invitation), and — if the registration declares one or more self-signup policies — `signup_type` (a brand-new user is created and bound to the policy's configured role). If `invite_token` is present at all, it is the ONLY path tried — a failed invite never falls through to self-signup. Neither field is required for a subject with an existing identity. If your issuer is registered against more than one app context (each via its own audience), `context_id` selects which one to target; omit it when your token's `aud` claim matches only one registered context — the common case, unaffected by this field. Uses the OAuth-standard error envelope (`{"error":..., "error_description":...}`, RFC 6749 §5.2), NOT this API's usual `{"message":...}` shape — its client is generic OAuth tooling, not the Vectros SDK.
|
|
6174
6324
|
*
|
|
6175
6325
|
* @param {Vectros.TokenExchangeRequest} request
|
|
6176
6326
|
* @param {AuthClient.RequestOptions} requestOptions - Request-specific configuration.
|
|
@@ -6246,9 +6396,9 @@ var ComplianceClient = class {
|
|
|
6246
6396
|
* @param {Vectros.ErasureRequest} request
|
|
6247
6397
|
* @param {ComplianceClient.RequestOptions} requestOptions - Request-specific configuration.
|
|
6248
6398
|
*
|
|
6399
|
+
* @throws {@link Vectros.BadRequestError}
|
|
6249
6400
|
* @throws {@link Vectros.ForbiddenError}
|
|
6250
6401
|
* @throws {@link Vectros.TooManyRequestsError}
|
|
6251
|
-
* @throws {@link Vectros.NotImplementedError}
|
|
6252
6402
|
*
|
|
6253
6403
|
* @example
|
|
6254
6404
|
* await client.compliance.createErasureRequest({
|
|
@@ -6287,12 +6437,12 @@ var ComplianceClient = class {
|
|
|
6287
6437
|
}
|
|
6288
6438
|
if (_response.error.reason === "status-code") {
|
|
6289
6439
|
switch (_response.error.statusCode) {
|
|
6440
|
+
case 400:
|
|
6441
|
+
throw new BadRequestError(_response.error.body, _response.rawResponse);
|
|
6290
6442
|
case 403:
|
|
6291
6443
|
throw new ForbiddenError(_response.error.body, _response.rawResponse);
|
|
6292
6444
|
case 429:
|
|
6293
6445
|
throw new TooManyRequestsError(_response.error.body, _response.rawResponse);
|
|
6294
|
-
case 501:
|
|
6295
|
-
throw new NotImplementedError(_response.error.body, _response.rawResponse);
|
|
6296
6446
|
default:
|
|
6297
6447
|
throw new VectrosError({
|
|
6298
6448
|
statusCode: _response.error.statusCode,
|
|
@@ -6311,7 +6461,6 @@ var ComplianceClient = class {
|
|
|
6311
6461
|
*
|
|
6312
6462
|
* @throws {@link Vectros.ForbiddenError}
|
|
6313
6463
|
* @throws {@link Vectros.NotFoundError}
|
|
6314
|
-
* @throws {@link Vectros.NotImplementedError}
|
|
6315
6464
|
*
|
|
6316
6465
|
* @example
|
|
6317
6466
|
* await client.compliance.getErasureRequest({
|
|
@@ -6352,8 +6501,6 @@ var ComplianceClient = class {
|
|
|
6352
6501
|
throw new ForbiddenError(_response.error.body, _response.rawResponse);
|
|
6353
6502
|
case 404:
|
|
6354
6503
|
throw new NotFoundError(_response.error.body, _response.rawResponse);
|
|
6355
|
-
case 501:
|
|
6356
|
-
throw new NotImplementedError(_response.error.body, _response.rawResponse);
|
|
6357
6504
|
default:
|
|
6358
6505
|
throw new VectrosError({
|
|
6359
6506
|
statusCode: _response.error.statusCode,
|
|
@@ -7712,6 +7859,7 @@ var IdentityClient = class {
|
|
|
7712
7859
|
* @example
|
|
7713
7860
|
* await client.identity.listEntities({
|
|
7714
7861
|
* namespace: "team",
|
|
7862
|
+
* contextId: "myapp",
|
|
7715
7863
|
* startFrom: "b3BhcXVlLWN1cnNvci1mcm9tLXRoZS1wcmV2aW91cy1wYWdl"
|
|
7716
7864
|
* })
|
|
7717
7865
|
*/
|
|
@@ -7721,6 +7869,7 @@ var IdentityClient = class {
|
|
|
7721
7869
|
async __listEntities(request, requestOptions) {
|
|
7722
7870
|
const {
|
|
7723
7871
|
namespace,
|
|
7872
|
+
contextId,
|
|
7724
7873
|
userId,
|
|
7725
7874
|
externalId,
|
|
7726
7875
|
scope,
|
|
@@ -7735,6 +7884,7 @@ var IdentityClient = class {
|
|
|
7735
7884
|
limit
|
|
7736
7885
|
} = request;
|
|
7737
7886
|
const _queryParams = {
|
|
7887
|
+
contextId,
|
|
7738
7888
|
userId,
|
|
7739
7889
|
externalId,
|
|
7740
7890
|
scope,
|
|
@@ -7793,6 +7943,7 @@ var IdentityClient = class {
|
|
|
7793
7943
|
* @example
|
|
7794
7944
|
* await client.identity.createEntity({
|
|
7795
7945
|
* namespace: "team",
|
|
7946
|
+
* contextId: "myapp",
|
|
7796
7947
|
* body: {
|
|
7797
7948
|
* externalId: "team_eng_platform"
|
|
7798
7949
|
* }
|
|
@@ -7802,9 +7953,10 @@ var IdentityClient = class {
|
|
|
7802
7953
|
return HttpResponsePromise.fromPromise(this.__createEntity(request, requestOptions));
|
|
7803
7954
|
}
|
|
7804
7955
|
async __createEntity(request, requestOptions) {
|
|
7805
|
-
const { namespace, upsert, body: _body } = request;
|
|
7956
|
+
const { namespace, upsert, contextId, body: _body } = request;
|
|
7806
7957
|
const _queryParams = {
|
|
7807
|
-
upsert
|
|
7958
|
+
upsert,
|
|
7959
|
+
contextId
|
|
7808
7960
|
};
|
|
7809
7961
|
const _authRequest = await this._options.authProvider.getAuthRequest();
|
|
7810
7962
|
const _headers = mergeHeaders(
|
|
@@ -7861,14 +8013,18 @@ var IdentityClient = class {
|
|
|
7861
8013
|
* @example
|
|
7862
8014
|
* await client.identity.getEntity({
|
|
7863
8015
|
* namespace: "team",
|
|
7864
|
-
* id: "6ba7b810-9dad-11d1-80b4-00c04fd430c8"
|
|
8016
|
+
* id: "6ba7b810-9dad-11d1-80b4-00c04fd430c8",
|
|
8017
|
+
* contextId: "myapp"
|
|
7865
8018
|
* })
|
|
7866
8019
|
*/
|
|
7867
8020
|
getEntity(request, requestOptions) {
|
|
7868
8021
|
return HttpResponsePromise.fromPromise(this.__getEntity(request, requestOptions));
|
|
7869
8022
|
}
|
|
7870
8023
|
async __getEntity(request, requestOptions) {
|
|
7871
|
-
const { namespace, id } = request;
|
|
8024
|
+
const { namespace, id, contextId } = request;
|
|
8025
|
+
const _queryParams = {
|
|
8026
|
+
contextId
|
|
8027
|
+
};
|
|
7872
8028
|
const _authRequest = await this._options.authProvider.getAuthRequest();
|
|
7873
8029
|
const _headers = mergeHeaders(
|
|
7874
8030
|
_authRequest.headers,
|
|
@@ -7882,7 +8038,7 @@ var IdentityClient = class {
|
|
|
7882
8038
|
),
|
|
7883
8039
|
method: "GET",
|
|
7884
8040
|
headers: _headers,
|
|
7885
|
-
queryString: url_exports.queryBuilder().mergeAdditional(requestOptions?.queryParams).build(),
|
|
8041
|
+
queryString: url_exports.queryBuilder().addMany(_queryParams).mergeAdditional(requestOptions?.queryParams).build(),
|
|
7886
8042
|
timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1e3,
|
|
7887
8043
|
maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries,
|
|
7888
8044
|
abortSignal: requestOptions?.abortSignal,
|
|
@@ -7919,6 +8075,7 @@ var IdentityClient = class {
|
|
|
7919
8075
|
* await client.identity.updateEntity({
|
|
7920
8076
|
* namespace: "team",
|
|
7921
8077
|
* id: "id",
|
|
8078
|
+
* contextId: "myapp",
|
|
7922
8079
|
* body: {
|
|
7923
8080
|
* externalId: "team_eng_platform"
|
|
7924
8081
|
* }
|
|
@@ -7928,7 +8085,10 @@ var IdentityClient = class {
|
|
|
7928
8085
|
return HttpResponsePromise.fromPromise(this.__updateEntity(request, requestOptions));
|
|
7929
8086
|
}
|
|
7930
8087
|
async __updateEntity(request, requestOptions) {
|
|
7931
|
-
const { namespace, id, body: _body } = request;
|
|
8088
|
+
const { namespace, id, contextId, body: _body } = request;
|
|
8089
|
+
const _queryParams = {
|
|
8090
|
+
contextId
|
|
8091
|
+
};
|
|
7932
8092
|
const _authRequest = await this._options.authProvider.getAuthRequest();
|
|
7933
8093
|
const _headers = mergeHeaders(
|
|
7934
8094
|
_authRequest.headers,
|
|
@@ -7943,7 +8103,7 @@ var IdentityClient = class {
|
|
|
7943
8103
|
method: "PUT",
|
|
7944
8104
|
headers: _headers,
|
|
7945
8105
|
contentType: "application/json",
|
|
7946
|
-
queryString: url_exports.queryBuilder().mergeAdditional(requestOptions?.queryParams).build(),
|
|
8106
|
+
queryString: url_exports.queryBuilder().addMany(_queryParams).mergeAdditional(requestOptions?.queryParams).build(),
|
|
7947
8107
|
requestType: "json",
|
|
7948
8108
|
body: _body,
|
|
7949
8109
|
timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1e3,
|
|
@@ -7983,14 +8143,18 @@ var IdentityClient = class {
|
|
|
7983
8143
|
* @example
|
|
7984
8144
|
* await client.identity.deleteEntity({
|
|
7985
8145
|
* namespace: "team",
|
|
7986
|
-
* id: "id"
|
|
8146
|
+
* id: "id",
|
|
8147
|
+
* contextId: "myapp"
|
|
7987
8148
|
* })
|
|
7988
8149
|
*/
|
|
7989
8150
|
deleteEntity(request, requestOptions) {
|
|
7990
8151
|
return HttpResponsePromise.fromPromise(this.__deleteEntity(request, requestOptions));
|
|
7991
8152
|
}
|
|
7992
8153
|
async __deleteEntity(request, requestOptions) {
|
|
7993
|
-
const { namespace, id } = request;
|
|
8154
|
+
const { namespace, id, contextId } = request;
|
|
8155
|
+
const _queryParams = {
|
|
8156
|
+
contextId
|
|
8157
|
+
};
|
|
7994
8158
|
const _authRequest = await this._options.authProvider.getAuthRequest();
|
|
7995
8159
|
const _headers = mergeHeaders(
|
|
7996
8160
|
_authRequest.headers,
|
|
@@ -8004,7 +8168,7 @@ var IdentityClient = class {
|
|
|
8004
8168
|
),
|
|
8005
8169
|
method: "DELETE",
|
|
8006
8170
|
headers: _headers,
|
|
8007
|
-
queryString: url_exports.queryBuilder().mergeAdditional(requestOptions?.queryParams).build(),
|
|
8171
|
+
queryString: url_exports.queryBuilder().addMany(_queryParams).mergeAdditional(requestOptions?.queryParams).build(),
|
|
8008
8172
|
timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1e3,
|
|
8009
8173
|
maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries,
|
|
8010
8174
|
abortSignal: requestOptions?.abortSignal,
|
|
@@ -8047,6 +8211,7 @@ var IdentityClient = class {
|
|
|
8047
8211
|
* @example
|
|
8048
8212
|
* await client.identity.lookupEntities({
|
|
8049
8213
|
* namespace: "team",
|
|
8214
|
+
* contextId: "myapp",
|
|
8050
8215
|
* body: {
|
|
8051
8216
|
* type: "person_v1",
|
|
8052
8217
|
* field: "ssn"
|
|
@@ -8057,7 +8222,10 @@ var IdentityClient = class {
|
|
|
8057
8222
|
return HttpResponsePromise.fromPromise(this.__lookupEntities(request, requestOptions));
|
|
8058
8223
|
}
|
|
8059
8224
|
async __lookupEntities(request, requestOptions) {
|
|
8060
|
-
const { namespace, body: _body } = request;
|
|
8225
|
+
const { namespace, contextId, body: _body } = request;
|
|
8226
|
+
const _queryParams = {
|
|
8227
|
+
contextId
|
|
8228
|
+
};
|
|
8061
8229
|
const _authRequest = await this._options.authProvider.getAuthRequest();
|
|
8062
8230
|
const _headers = mergeHeaders(
|
|
8063
8231
|
_authRequest.headers,
|
|
@@ -8072,7 +8240,7 @@ var IdentityClient = class {
|
|
|
8072
8240
|
method: "POST",
|
|
8073
8241
|
headers: _headers,
|
|
8074
8242
|
contentType: "application/json",
|
|
8075
|
-
queryString: url_exports.queryBuilder().mergeAdditional(requestOptions?.queryParams).build(),
|
|
8243
|
+
queryString: url_exports.queryBuilder().addMany(_queryParams).mergeAdditional(requestOptions?.queryParams).build(),
|
|
8076
8244
|
requestType: "json",
|
|
8077
8245
|
body: _body,
|
|
8078
8246
|
timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1e3,
|
|
@@ -8116,15 +8284,17 @@ var IdentityClient = class {
|
|
|
8116
8284
|
* @example
|
|
8117
8285
|
* await client.identity.getEntityVersions({
|
|
8118
8286
|
* namespace: "team",
|
|
8119
|
-
* id: "id"
|
|
8287
|
+
* id: "id",
|
|
8288
|
+
* contextId: "myapp"
|
|
8120
8289
|
* })
|
|
8121
8290
|
*/
|
|
8122
8291
|
getEntityVersions(request, requestOptions) {
|
|
8123
8292
|
return HttpResponsePromise.fromPromise(this.__getEntityVersions(request, requestOptions));
|
|
8124
8293
|
}
|
|
8125
8294
|
async __getEntityVersions(request, requestOptions) {
|
|
8126
|
-
const { namespace, id, startFrom } = request;
|
|
8295
|
+
const { namespace, id, contextId, startFrom } = request;
|
|
8127
8296
|
const _queryParams = {
|
|
8297
|
+
contextId,
|
|
8128
8298
|
startFrom
|
|
8129
8299
|
};
|
|
8130
8300
|
const _authRequest = await this._options.authProvider.getAuthRequest();
|
|
@@ -8170,7 +8340,7 @@ var IdentityClient = class {
|
|
|
8170
8340
|
);
|
|
8171
8341
|
}
|
|
8172
8342
|
/**
|
|
8173
|
-
* Retrieves a single scope-namespace registration by name.
|
|
8343
|
+
* Retrieves a single scope-namespace registration by name.
|
|
8174
8344
|
*
|
|
8175
8345
|
* @param {Vectros.GetNamespaceRequest} request
|
|
8176
8346
|
* @param {IdentityClient.RequestOptions} requestOptions - Request-specific configuration.
|
|
@@ -8186,7 +8356,10 @@ var IdentityClient = class {
|
|
|
8186
8356
|
return HttpResponsePromise.fromPromise(this.__getNamespace(request, requestOptions));
|
|
8187
8357
|
}
|
|
8188
8358
|
async __getNamespace(request, requestOptions) {
|
|
8189
|
-
const { namespace } = request;
|
|
8359
|
+
const { namespace, contextId } = request;
|
|
8360
|
+
const _queryParams = {
|
|
8361
|
+
contextId
|
|
8362
|
+
};
|
|
8190
8363
|
const _authRequest = await this._options.authProvider.getAuthRequest();
|
|
8191
8364
|
const _headers = mergeHeaders(
|
|
8192
8365
|
_authRequest.headers,
|
|
@@ -8200,7 +8373,7 @@ var IdentityClient = class {
|
|
|
8200
8373
|
),
|
|
8201
8374
|
method: "GET",
|
|
8202
8375
|
headers: _headers,
|
|
8203
|
-
queryString: url_exports.queryBuilder().mergeAdditional(requestOptions?.queryParams).build(),
|
|
8376
|
+
queryString: url_exports.queryBuilder().addMany(_queryParams).mergeAdditional(requestOptions?.queryParams).build(),
|
|
8204
8377
|
timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1e3,
|
|
8205
8378
|
maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries,
|
|
8206
8379
|
abortSignal: requestOptions?.abortSignal,
|
|
@@ -8225,7 +8398,7 @@ var IdentityClient = class {
|
|
|
8225
8398
|
return handleNonStatusCodeError(_response.error, _response.rawResponse, "GET", "/v1/namespaces/{namespace}");
|
|
8226
8399
|
}
|
|
8227
8400
|
/**
|
|
8228
|
-
* Updates the mutable fields (`entityBacked`, `defaultSchemaId`, `specificityRank`) of a registered namespace. The namespace name
|
|
8401
|
+
* Updates the mutable fields (`entityBacked`, `defaultSchemaId`, `specificityRank`) of a registered namespace. The namespace name and its `contextId` (which row is selected) are both immutable. Requires a root API key. `org` and `client` are updatable like any other namespace — there is no reserved-built-in exception.
|
|
8229
8402
|
*
|
|
8230
8403
|
* @param {Vectros.UpdateNamespaceRequest} request
|
|
8231
8404
|
* @param {IdentityClient.RequestOptions} requestOptions - Request-specific configuration.
|
|
@@ -8247,7 +8420,10 @@ var IdentityClient = class {
|
|
|
8247
8420
|
return HttpResponsePromise.fromPromise(this.__updateNamespace(request, requestOptions));
|
|
8248
8421
|
}
|
|
8249
8422
|
async __updateNamespace(request, requestOptions) {
|
|
8250
|
-
const { namespace, body: _body } = request;
|
|
8423
|
+
const { namespace, contextId, body: _body } = request;
|
|
8424
|
+
const _queryParams = {
|
|
8425
|
+
contextId
|
|
8426
|
+
};
|
|
8251
8427
|
const _authRequest = await this._options.authProvider.getAuthRequest();
|
|
8252
8428
|
const _headers = mergeHeaders(
|
|
8253
8429
|
_authRequest.headers,
|
|
@@ -8262,7 +8438,7 @@ var IdentityClient = class {
|
|
|
8262
8438
|
method: "PUT",
|
|
8263
8439
|
headers: _headers,
|
|
8264
8440
|
contentType: "application/json",
|
|
8265
|
-
queryString: url_exports.queryBuilder().mergeAdditional(requestOptions?.queryParams).build(),
|
|
8441
|
+
queryString: url_exports.queryBuilder().addMany(_queryParams).mergeAdditional(requestOptions?.queryParams).build(),
|
|
8266
8442
|
requestType: "json",
|
|
8267
8443
|
body: _body,
|
|
8268
8444
|
timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1e3,
|
|
@@ -8293,7 +8469,7 @@ var IdentityClient = class {
|
|
|
8293
8469
|
return handleNonStatusCodeError(_response.error, _response.rawResponse, "PUT", "/v1/namespaces/{namespace}");
|
|
8294
8470
|
}
|
|
8295
8471
|
/**
|
|
8296
|
-
* Deletes a registered scope namespace. Requires a root API key.
|
|
8472
|
+
* Deletes a registered scope namespace. Requires a root API key. `org` and `client` are deletable like any other namespace — there is no reserved-built-in exception. A namespace that still has entities cannot be deleted (409) — delete its entities first; this keeps them reachable by the account-teardown and erasure sweeps.
|
|
8297
8473
|
*
|
|
8298
8474
|
* @param {Vectros.DeleteNamespaceRequest} request
|
|
8299
8475
|
* @param {IdentityClient.RequestOptions} requestOptions - Request-specific configuration.
|
|
@@ -8311,7 +8487,10 @@ var IdentityClient = class {
|
|
|
8311
8487
|
return HttpResponsePromise.fromPromise(this.__deleteNamespace(request, requestOptions));
|
|
8312
8488
|
}
|
|
8313
8489
|
async __deleteNamespace(request, requestOptions) {
|
|
8314
|
-
const { namespace } = request;
|
|
8490
|
+
const { namespace, contextId } = request;
|
|
8491
|
+
const _queryParams = {
|
|
8492
|
+
contextId
|
|
8493
|
+
};
|
|
8315
8494
|
const _authRequest = await this._options.authProvider.getAuthRequest();
|
|
8316
8495
|
const _headers = mergeHeaders(
|
|
8317
8496
|
_authRequest.headers,
|
|
@@ -8325,7 +8504,7 @@ var IdentityClient = class {
|
|
|
8325
8504
|
),
|
|
8326
8505
|
method: "DELETE",
|
|
8327
8506
|
headers: _headers,
|
|
8328
|
-
queryString: url_exports.queryBuilder().mergeAdditional(requestOptions?.queryParams).build(),
|
|
8507
|
+
queryString: url_exports.queryBuilder().addMany(_queryParams).mergeAdditional(requestOptions?.queryParams).build(),
|
|
8329
8508
|
timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1e3,
|
|
8330
8509
|
maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries,
|
|
8331
8510
|
abortSignal: requestOptions?.abortSignal,
|
|
@@ -8354,7 +8533,7 @@ var IdentityClient = class {
|
|
|
8354
8533
|
return handleNonStatusCodeError(_response.error, _response.rawResponse, "DELETE", "/v1/namespaces/{namespace}");
|
|
8355
8534
|
}
|
|
8356
8535
|
/**
|
|
8357
|
-
* Returns the scope namespaces registered in your account
|
|
8536
|
+
* Returns the scope namespaces registered in your account. Returns a `{data, nextCursor}` envelope.
|
|
8358
8537
|
*
|
|
8359
8538
|
* @param {Vectros.ListNamespacesRequest} request
|
|
8360
8539
|
* @param {IdentityClient.RequestOptions} requestOptions - Request-specific configuration.
|
|
@@ -8366,10 +8545,11 @@ var IdentityClient = class {
|
|
|
8366
8545
|
return HttpResponsePromise.fromPromise(this.__listNamespaces(request, requestOptions));
|
|
8367
8546
|
}
|
|
8368
8547
|
async __listNamespaces(request = {}, requestOptions) {
|
|
8369
|
-
const { startFrom, limit } = request;
|
|
8548
|
+
const { startFrom, limit, contextId } = request;
|
|
8370
8549
|
const _queryParams = {
|
|
8371
8550
|
startFrom,
|
|
8372
|
-
limit
|
|
8551
|
+
limit,
|
|
8552
|
+
contextId
|
|
8373
8553
|
};
|
|
8374
8554
|
const _authRequest = await this._options.authProvider.getAuthRequest();
|
|
8375
8555
|
const _headers = mergeHeaders(
|
|
@@ -8404,9 +8584,9 @@ var IdentityClient = class {
|
|
|
8404
8584
|
return handleNonStatusCodeError(_response.error, _response.rawResponse, "GET", "/v1/namespaces");
|
|
8405
8585
|
}
|
|
8406
8586
|
/**
|
|
8407
|
-
* Registers a new scope namespace and declares whether its values resolve to identity entities (`entityBacked`). Also requires `specificityRank`, an explicit, account-unique position in the specificity order used to break recordType schema-resolution ties. Requires a root API key.
|
|
8587
|
+
* 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 one of the CLI bootstrap's two provisioning capabilities — never an ordinary partner-grantable scope. A bootstrap credential's registration is confined to its own app context UNLESS it additionally holds the tenant-wide namespace-provisioning capability and `contextId` is omitted, in which case it may register a TENANT-WIDE namespace (visible to every context). `org` and `client` are reserved names, registered the same way as any other namespace.
|
|
8408
8588
|
*
|
|
8409
|
-
* @param {Vectros.
|
|
8589
|
+
* @param {Vectros.RegisterNamespaceRequest} request
|
|
8410
8590
|
* @param {IdentityClient.RequestOptions} requestOptions - Request-specific configuration.
|
|
8411
8591
|
*
|
|
8412
8592
|
* @throws {@link Vectros.BadRequestError}
|
|
@@ -8415,14 +8595,20 @@ var IdentityClient = class {
|
|
|
8415
8595
|
*
|
|
8416
8596
|
* @example
|
|
8417
8597
|
* await client.identity.registerNamespace({
|
|
8418
|
-
*
|
|
8419
|
-
*
|
|
8598
|
+
* body: {
|
|
8599
|
+
* namespace: "team",
|
|
8600
|
+
* specificityRank: 1500
|
|
8601
|
+
* }
|
|
8420
8602
|
* })
|
|
8421
8603
|
*/
|
|
8422
8604
|
registerNamespace(request, requestOptions) {
|
|
8423
8605
|
return HttpResponsePromise.fromPromise(this.__registerNamespace(request, requestOptions));
|
|
8424
8606
|
}
|
|
8425
8607
|
async __registerNamespace(request, requestOptions) {
|
|
8608
|
+
const { contextId, body: _body } = request;
|
|
8609
|
+
const _queryParams = {
|
|
8610
|
+
contextId
|
|
8611
|
+
};
|
|
8426
8612
|
const _authRequest = await this._options.authProvider.getAuthRequest();
|
|
8427
8613
|
const _headers = mergeHeaders(
|
|
8428
8614
|
_authRequest.headers,
|
|
@@ -8437,9 +8623,9 @@ var IdentityClient = class {
|
|
|
8437
8623
|
method: "POST",
|
|
8438
8624
|
headers: _headers,
|
|
8439
8625
|
contentType: "application/json",
|
|
8440
|
-
queryString: url_exports.queryBuilder().mergeAdditional(requestOptions?.queryParams).build(),
|
|
8626
|
+
queryString: url_exports.queryBuilder().addMany(_queryParams).mergeAdditional(requestOptions?.queryParams).build(),
|
|
8441
8627
|
requestType: "json",
|
|
8442
|
-
body:
|
|
8628
|
+
body: _body,
|
|
8443
8629
|
timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1e3,
|
|
8444
8630
|
maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries,
|
|
8445
8631
|
abortSignal: requestOptions?.abortSignal,
|
|
@@ -8468,7 +8654,7 @@ var IdentityClient = class {
|
|
|
8468
8654
|
return handleNonStatusCodeError(_response.error, _response.rawResponse, "POST", "/v1/namespaces");
|
|
8469
8655
|
}
|
|
8470
8656
|
/**
|
|
8471
|
-
* Returns a paginated list of the users in your account. Pass `externalId` to look up a single user by your own identifier. To filter on schema-declared lookup fields, supply `type` and `field` together with one lookup mode: `value` (exact match), `from`+`to` (range), or `prefix`. Requires the `users:r` scope.
|
|
8657
|
+
* Returns a paginated list of the users in your account. Pass `externalId` to look up a single user by your own identifier. To filter on schema-declared lookup fields, supply `type` and `field` together with one lookup mode: `value` (exact match), `from`+`to` (range), or `prefix`. Requires the `users:r` scope. A context-confined credential only sees users who hold an access profile in the credential's own app context — others are silently absent from the page, not an error.
|
|
8472
8658
|
*
|
|
8473
8659
|
* @param {Vectros.ListUsersRequest} request
|
|
8474
8660
|
* @param {IdentityClient.RequestOptions} requestOptions - Request-specific configuration.
|
|
@@ -8533,7 +8719,7 @@ var IdentityClient = class {
|
|
|
8533
8719
|
return handleNonStatusCodeError(_response.error, _response.rawResponse, "GET", "/v1/users");
|
|
8534
8720
|
}
|
|
8535
8721
|
/**
|
|
8536
|
-
* Creates a user identity in your account. The operation is idempotent on `externalId`: if a user with the same `externalId` already exists, the existing record is returned instead of creating a duplicate. The response's `created` field (and the HTTP status — 201 when created, 200 when an existing user was returned) tells the two apart. To overwrite an existing user's mutable fields (email, status, payload, schema binding) instead of returning it unchanged, set `?upsert=true` (this also requires the `users:u` scope). Requires the `users:c` scope to create. Being returned the existing user on a collision is a read of that user's data and additionally requires the `users:r` scope — a credential holding `users:c` alone receives a `400` ("already exists") on collision instead of the user.
|
|
8722
|
+
* Creates a user identity in your account. The operation is idempotent on `externalId`: if a user with the same `externalId` already exists, the existing record is returned instead of creating a duplicate. The response's `created` field (and the HTTP status — 201 when created, 200 when an existing user was returned) tells the two apart. To overwrite an existing user's mutable fields (email, status, payload, schema binding) instead of returning it unchanged, set `?upsert=true` (this also requires the `users:u` scope). Requires the `users:c` scope to create. Being returned the existing user on a collision is a read of that user's data and additionally requires the `users:r` scope — a credential holding `users:c` alone receives a `400` ("already exists") on collision instead of the user. A context-confined credential additionally receives the collision echo (or, with `?upsert=true`, is allowed to overwrite) only when the colliding user holds an access profile in the credential's own app context — a cross-context collision otherwise gets the same uniform `400` instead of the other context's user data. One narrow exception on the plain (non-upsert) echo only: the CLI's no-code bootstrap credential can be echoed a colliding user across app contexts, needed for a blueprint to idempotently re-apply against a service principal it provisioned in a non-default context; this does not extend to `?upsert=true`.
|
|
8537
8723
|
*
|
|
8538
8724
|
* @param {Vectros.CreateUserRequest} request
|
|
8539
8725
|
* @param {IdentityClient.RequestOptions} requestOptions - Request-specific configuration.
|
|
@@ -8657,7 +8843,7 @@ var IdentityClient = class {
|
|
|
8657
8843
|
return handleNonStatusCodeError(_response.error, _response.rawResponse, "GET", "/v1/users/{id}");
|
|
8658
8844
|
}
|
|
8659
8845
|
/**
|
|
8660
|
-
* Updates mutable fields on an existing user (such as email, status, payload, or schema binding). The `type` field is immutable after creation, and `email` cannot be changed while an invitation to that user is still outstanding — revoke the invitation, or invite the new address instead. This endpoint also activates an invited user: a PUT that moves a PENDING user to ACTIVE and carries `inviteToken`, `externalSubject`, and `emailVerifiedAttestation=true` completes the invitation. Requires the `users:u` scope.
|
|
8846
|
+
* Updates mutable fields on an existing user (such as email, status, payload, or schema binding). The `type` field is immutable after creation, and `email` cannot be changed while an invitation to that user is still outstanding — revoke the invitation, or invite the new address instead. This endpoint also activates an invited user: a PUT that moves a PENDING user to ACTIVE and carries `inviteToken`, `externalSubject`, and `emailVerifiedAttestation=true` completes the invitation. **`externalSubject` set this way is NOT an authentication binding and does not enable sign-in anywhere** — not the Vectros DevPortal/Admin App/web apps, and not `/v1/auth/token/exchange` either; both are granted only by the invitee authenticating themselves. If the invitee needs dev-portal/web-app access, send them the invitation link and let them accept it directly. If the invitee needs to sign in via token exchange, have them redeem `inviteToken` themselves at `/v1/auth/token/exchange` with their own credential — Vectros verifies it there and computes its own value for this field from the verified credential. The value you send here is stored exactly as-is, with no normalization, and does NOT automatically match or deduplicate against what a later token-exchange sign-in computes for the same real-world identity. Call this endpoint directly only if you run your own backend and want Vectros to record which of your own users a given account corresponds to — treat this field as informational for that purpose, not as a way to pre-authorize sign-in. Requires the `users:u` scope.
|
|
8661
8847
|
*
|
|
8662
8848
|
* @param {Vectros.UpdateUserRequest} request
|
|
8663
8849
|
* @param {IdentityClient.RequestOptions} requestOptions - Request-specific configuration.
|
|
@@ -8724,12 +8910,13 @@ var IdentityClient = class {
|
|
|
8724
8910
|
return handleNonStatusCodeError(_response.error, _response.rawResponse, "PUT", "/v1/users/{id}");
|
|
8725
8911
|
}
|
|
8726
8912
|
/**
|
|
8727
|
-
* Permanently deletes a user identity. This cannot be undone. If the user is a pending invitation, the associated access profile created for that invitation is also removed. Requires the `users:d` scope.
|
|
8913
|
+
* 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. Called by a context-confined credential, deletion is also refused (409) when the user holds access in an app context other than the caller's own — remove the user from the caller's own context first, or use a credential with cross-context reach.
|
|
8728
8914
|
*
|
|
8729
8915
|
* @param {Vectros.DeleteUserRequest} request
|
|
8730
8916
|
* @param {IdentityClient.RequestOptions} requestOptions - Request-specific configuration.
|
|
8731
8917
|
*
|
|
8732
8918
|
* @throws {@link Vectros.NotFoundError}
|
|
8919
|
+
* @throws {@link Vectros.ConflictError}
|
|
8733
8920
|
* @throws {@link Vectros.TooManyRequestsError}
|
|
8734
8921
|
*
|
|
8735
8922
|
* @example
|
|
@@ -8769,6 +8956,8 @@ var IdentityClient = class {
|
|
|
8769
8956
|
switch (_response.error.statusCode) {
|
|
8770
8957
|
case 404:
|
|
8771
8958
|
throw new NotFoundError(_response.error.body, _response.rawResponse);
|
|
8959
|
+
case 409:
|
|
8960
|
+
throw new ConflictError(_response.error.body, _response.rawResponse);
|
|
8772
8961
|
case 429:
|
|
8773
8962
|
throw new TooManyRequestsError(_response.error.body, _response.rawResponse);
|
|
8774
8963
|
default:
|
|
@@ -8782,7 +8971,7 @@ var IdentityClient = class {
|
|
|
8782
8971
|
return handleNonStatusCodeError(_response.error, _response.rawResponse, "DELETE", "/v1/users/{id}");
|
|
8783
8972
|
}
|
|
8784
8973
|
/**
|
|
8785
|
-
* Looks up users by a schema lookup field, with the query criteria carried in the request body rather than the URL. Use this when looking up by a sensitive (blind-indexed) field: the value is blind-indexed server-side and never appears in the URL, request logs, or proxies. The query semantics are identical to the GET /v1/users lookup, which rejects sensitive-field values and directs you here. Returns a page in the `{data, nextCursor}` envelope. Requires the `users:r` scope.
|
|
8974
|
+
* Looks up users by a schema lookup field, with the query criteria carried in the request body rather than the URL. Use this when looking up by a sensitive (blind-indexed) field: the value is blind-indexed server-side and never appears in the URL, request logs, or proxies. The query semantics are identical to the GET /v1/users lookup, which rejects sensitive-field values and directs you here. Returns a page in the `{data, nextCursor}` envelope. Requires the `users:r` scope. A context-confined credential only sees users who hold an access profile in the credential's own app context — others are silently absent from the page, not an error.
|
|
8786
8975
|
*
|
|
8787
8976
|
* @param {Vectros.IdentityLookupRequest} request
|
|
8788
8977
|
* @param {IdentityClient.RequestOptions} requestOptions - Request-specific configuration.
|
|
@@ -9225,14 +9414,14 @@ var RecordsClient = class {
|
|
|
9225
9414
|
this._options = normalizeClientOptionsWithAuth(options);
|
|
9226
9415
|
}
|
|
9227
9416
|
/**
|
|
9228
|
-
*
|
|
9417
|
+
* Fetches multiple records by ID in one call (`ids`, maximum 100). The response contains only the records you can access — any id that does not exist, belongs to another account/AppContext, or is outside your token's scope is silently omitted, with no per-id existence signal, matching the not-found behavior of the single-record GET. Payloads are hydrated the same way a by-id GET hydrates them (payloads externalized to object storage are rehydrated for this response). Requires the `records:r` scope (and, for a scoped token, `records:r:<type>` per record type).
|
|
9229
9418
|
*
|
|
9230
9419
|
* @param {Vectros.BatchGetRequest} request
|
|
9231
9420
|
* @param {RecordsClient.RequestOptions} requestOptions - Request-specific configuration.
|
|
9232
9421
|
*
|
|
9422
|
+
* @throws {@link Vectros.BadRequestError}
|
|
9233
9423
|
* @throws {@link Vectros.ForbiddenError}
|
|
9234
9424
|
* @throws {@link Vectros.TooManyRequestsError}
|
|
9235
|
-
* @throws {@link Vectros.NotImplementedError}
|
|
9236
9425
|
*
|
|
9237
9426
|
* @example
|
|
9238
9427
|
* await client.records.batchGetRecords()
|
|
@@ -9269,12 +9458,12 @@ var RecordsClient = class {
|
|
|
9269
9458
|
}
|
|
9270
9459
|
if (_response.error.reason === "status-code") {
|
|
9271
9460
|
switch (_response.error.statusCode) {
|
|
9461
|
+
case 400:
|
|
9462
|
+
throw new BadRequestError(_response.error.body, _response.rawResponse);
|
|
9272
9463
|
case 403:
|
|
9273
9464
|
throw new ForbiddenError(_response.error.body, _response.rawResponse);
|
|
9274
9465
|
case 429:
|
|
9275
9466
|
throw new TooManyRequestsError(_response.error.body, _response.rawResponse);
|
|
9276
|
-
case 501:
|
|
9277
|
-
throw new NotImplementedError(_response.error.body, _response.rawResponse);
|
|
9278
9467
|
default:
|
|
9279
9468
|
throw new VectrosError({
|
|
9280
9469
|
statusCode: _response.error.statusCode,
|
|
@@ -10088,7 +10277,7 @@ var SchemasClient = class {
|
|
|
10088
10277
|
this._options = normalizeClientOptionsWithAuth(options);
|
|
10089
10278
|
}
|
|
10090
10279
|
/**
|
|
10091
|
-
* Returns a paginated list of the record schemas defined in your account. Filter by `userId` or `scope` to scope to an owner, by `surface` to list the types bindable to one surface, or by `recordType` to resolve the single schema for a type directly. Filtering by
|
|
10280
|
+
* Returns a paginated list of the record schemas defined in your account. Filter by `userId` or `scope` to scope to an owner, by `surface` to list the types bindable to one surface, or by `recordType` to resolve the single schema for a type directly. Filtering by `surface=user` lists your account-wide identity schemas regardless of the calling context (a `user`-surfaced schema always has one tenant-wide home). Filtering by `surface=entity` reads your own app context's entity schemas AND the tenant-wide home together, newest first, through a single cursor — an entity schema may live in either home depending on where its namespace is placed (see `POST /v1/namespaces`). Filtering by `record` or `document` lists within the calling context only. Requires the `schemas:r` scope.
|
|
10092
10281
|
*
|
|
10093
10282
|
* @param {Vectros.ListSchemasRequest} request
|
|
10094
10283
|
* @param {SchemasClient.RequestOptions} requestOptions - Request-specific configuration.
|
|
@@ -10376,6 +10565,7 @@ var SchemasClient = class {
|
|
|
10376
10565
|
* @param {Vectros.DeleteSchemaRequest} request
|
|
10377
10566
|
* @param {SchemasClient.RequestOptions} requestOptions - Request-specific configuration.
|
|
10378
10567
|
*
|
|
10568
|
+
* @throws {@link Vectros.ForbiddenError}
|
|
10379
10569
|
* @throws {@link Vectros.NotFoundError}
|
|
10380
10570
|
* @throws {@link Vectros.ConflictError}
|
|
10381
10571
|
* @throws {@link Vectros.TooManyRequestsError}
|
|
@@ -10415,6 +10605,8 @@ var SchemasClient = class {
|
|
|
10415
10605
|
}
|
|
10416
10606
|
if (_response.error.reason === "status-code") {
|
|
10417
10607
|
switch (_response.error.statusCode) {
|
|
10608
|
+
case 403:
|
|
10609
|
+
throw new ForbiddenError(_response.error.body, _response.rawResponse);
|
|
10418
10610
|
case 404:
|
|
10419
10611
|
throw new NotFoundError(_response.error.body, _response.rawResponse);
|
|
10420
10612
|
case 409:
|
|
@@ -10851,10 +11043,58 @@ var BlueprintSchemaSchema = import_zod.z.object({
|
|
|
10851
11043
|
});
|
|
10852
11044
|
}
|
|
10853
11045
|
});
|
|
11046
|
+
var CAPABILITY_NAME_RE = /^[a-z][a-z0-9-]*$/;
|
|
11047
|
+
var GrantedCapabilitiesSchema = import_zod.z.array(import_zod.z.string().min(1)).optional().superRefine((caps, ctx) => {
|
|
11048
|
+
if (!caps) return;
|
|
11049
|
+
const seen = /* @__PURE__ */ new Set();
|
|
11050
|
+
caps.forEach((name, i) => {
|
|
11051
|
+
if (name === "") return;
|
|
11052
|
+
if (name === "*") {
|
|
11053
|
+
ctx.addIssue({
|
|
11054
|
+
code: import_zod.z.ZodIssueCode.custom,
|
|
11055
|
+
path: [i],
|
|
11056
|
+
message: "'*' is not a capability \u2014 granted_capabilities has no wildcard, and a '*' in allowedActions confers zero capabilities. Name each capability explicitly."
|
|
11057
|
+
});
|
|
11058
|
+
} else if (!CAPABILITY_NAME_RE.test(name)) {
|
|
11059
|
+
ctx.addIssue({
|
|
11060
|
+
code: import_zod.z.ZodIssueCode.custom,
|
|
11061
|
+
path: [i],
|
|
11062
|
+
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)`
|
|
11063
|
+
});
|
|
11064
|
+
}
|
|
11065
|
+
if (seen.has(name)) {
|
|
11066
|
+
ctx.addIssue({
|
|
11067
|
+
code: import_zod.z.ZodIssueCode.custom,
|
|
11068
|
+
path: [i],
|
|
11069
|
+
message: `'${name}' is declared more than once`
|
|
11070
|
+
});
|
|
11071
|
+
}
|
|
11072
|
+
seen.add(name);
|
|
11073
|
+
});
|
|
11074
|
+
});
|
|
10854
11075
|
var BlueprintAccessProfileSchema = import_zod.z.object({
|
|
10855
11076
|
// Validated structurally here; the SCOPE GATE (in @vectros-ai/cli) is
|
|
10856
11077
|
// what enforces the data-plane-only security boundary.
|
|
10857
|
-
|
|
11078
|
+
//
|
|
11079
|
+
// Optional now: mutually exclusive with `roleIds` below (the
|
|
11080
|
+
// `.superRefine` on this schema enforces the XOR, mirroring the platform's
|
|
11081
|
+
// own `AccessProfileDB.validateScopesXorRole`). Exactly one of the two
|
|
11082
|
+
// authors this profile's grant.
|
|
11083
|
+
allowedActions: import_zod.z.array(import_zod.z.string().min(1)).min(1).optional(),
|
|
11084
|
+
// Additive role composition: this profile's effective grant is
|
|
11085
|
+
// each named role's own clauses, concatenated in the order listed, instead of
|
|
11086
|
+
// an inline clause authored here. Every id must name a role this SAME
|
|
11087
|
+
// blueprint declares in `roles` (`lintAccessProfileRoleIds` below — the
|
|
11088
|
+
// authoring-time mirror of the platform's same-`(tenantId, contextId)`
|
|
11089
|
+
// check, trivially satisfied here since a blueprint's own `roles` are
|
|
11090
|
+
// always created in its own `contextId`); duplicates are rejected outright
|
|
11091
|
+
// rather than silently deduplicated. Mutually exclusive
|
|
11092
|
+
// with `allowedActions`/`dataScope`/`capabilities` — a role-referencing
|
|
11093
|
+
// profile has no inline clause of its own to carry a dataScope or
|
|
11094
|
+
// capability grant; author those on the referenced Role instead, where
|
|
11095
|
+
// every profile referencing it picks them up uniformly (mirrors how
|
|
11096
|
+
// `assumable` already works for a `roleId`-referencing profile).
|
|
11097
|
+
roleIds: import_zod.z.array(import_zod.z.string().min(1)).min(1).optional(),
|
|
10858
11098
|
// Optional ownership binding: { userId: [...], "scope:org": [...], "scope:client": [...] }.
|
|
10859
11099
|
// A `null` element in a value list is the documented NULL SENTINEL — it
|
|
10860
11100
|
// grants access to TENANT-LEVEL (owner-less) records IN ADDITION to the
|
|
@@ -10882,31 +11122,92 @@ var BlueprintAccessProfileSchema = import_zod.z.object({
|
|
|
10882
11122
|
// below (`lintIdentityOverrides`, placeholder-aware — see that function's
|
|
10883
11123
|
// docstring); this `.min(1)` is only the shape-level "non-blank" guard on
|
|
10884
11124
|
// the value, not the grammar check.
|
|
10885
|
-
identityOverrides: import_zod.z.record(import_zod.z.string().min(1)).optional()
|
|
10886
|
-
|
|
10887
|
-
|
|
10888
|
-
|
|
10889
|
-
|
|
10890
|
-
|
|
10891
|
-
|
|
10892
|
-
|
|
10893
|
-
|
|
10894
|
-
|
|
10895
|
-
|
|
10896
|
-
|
|
10897
|
-
|
|
10898
|
-
|
|
10899
|
-
|
|
10900
|
-
|
|
10901
|
-
|
|
11125
|
+
identityOverrides: import_zod.z.record(import_zod.z.string().min(1)).optional(),
|
|
11126
|
+
// Optional capabilities granted to the blueprint's own service-principal
|
|
11127
|
+
// profile. DECISION: allowed here, not role-clause-only. On the platform,
|
|
11128
|
+
// an access profile's scopes are themselves a list of clauses — the exact
|
|
11129
|
+
// same wire shape a role clause carries — and the loader already builds
|
|
11130
|
+
// this profile as ONE such clause. Refusing `capabilities` here while
|
|
11131
|
+
// allowing it on a role clause would be an asymmetry this FORMAT package
|
|
11132
|
+
// has no basis to invent: nothing on the platform side distinguishes the
|
|
11133
|
+
// two, and whether granting a capability to a blueprint's own service key
|
|
11134
|
+
// is sound POLICY is exactly the question the CLI's scope gate exists to
|
|
11135
|
+
// answer (a separate package, out of scope for this format layer) — the
|
|
11136
|
+
// same posture this package already takes with `allowedActions` above.
|
|
11137
|
+
//
|
|
11138
|
+
// Worth naming for whoever writes that gate: this profile is a long-lived,
|
|
11139
|
+
// non-human-attributable machine credential (typically stored in CI/CD
|
|
11140
|
+
// secrets), not an ephemeral human session — a materially different
|
|
11141
|
+
// blast-radius question from granting the same capability to a role that
|
|
11142
|
+
// a person is bound to. The gate should weigh the two separately rather
|
|
11143
|
+
// than reuse one policy for both.
|
|
11144
|
+
capabilities: GrantedCapabilitiesSchema
|
|
11145
|
+
}).strict().superRefine((ap, ctx) => {
|
|
11146
|
+
const hasInline = ap.allowedActions !== void 0;
|
|
11147
|
+
const hasRoleIds = ap.roleIds !== void 0;
|
|
11148
|
+
if (hasInline === hasRoleIds) {
|
|
10902
11149
|
ctx.addIssue({
|
|
10903
11150
|
code: import_zod.z.ZodIssueCode.custom,
|
|
10904
|
-
|
|
11151
|
+
path: ["roleIds"],
|
|
11152
|
+
message: hasInline ? "accessProfile carries both 'allowedActions' and 'roleIds' \u2014 provide exactly one: an inline clause (allowedActions, optionally dataScope/capabilities) OR a role composition (roleIds), never both" : "accessProfile carries neither 'allowedActions' nor 'roleIds' \u2014 provide exactly one: an inline clause (allowedActions, optionally dataScope/capabilities) OR a role composition (roleIds)"
|
|
10905
11153
|
});
|
|
11154
|
+
return;
|
|
10906
11155
|
}
|
|
10907
|
-
|
|
10908
|
-
|
|
10909
|
-
|
|
11156
|
+
if (hasRoleIds) {
|
|
11157
|
+
if (ap.dataScope !== void 0) {
|
|
11158
|
+
ctx.addIssue({
|
|
11159
|
+
code: import_zod.z.ZodIssueCode.custom,
|
|
11160
|
+
path: ["dataScope"],
|
|
11161
|
+
message: "accessProfile.dataScope is not valid alongside 'roleIds' \u2014 a role-composed profile has no inline clause of its own; author dataScope on the referenced role's clause instead"
|
|
11162
|
+
});
|
|
11163
|
+
}
|
|
11164
|
+
if (ap.capabilities !== void 0) {
|
|
11165
|
+
ctx.addIssue({
|
|
11166
|
+
code: import_zod.z.ZodIssueCode.custom,
|
|
11167
|
+
path: ["capabilities"],
|
|
11168
|
+
message: "accessProfile.capabilities is not valid alongside 'roleIds' \u2014 a role-composed profile has no inline clause of its own; author capabilities on the referenced role's clause instead"
|
|
11169
|
+
});
|
|
11170
|
+
}
|
|
11171
|
+
}
|
|
11172
|
+
});
|
|
11173
|
+
var BlueprintRoleClauseSchema = import_zod.z.object({
|
|
11174
|
+
allowedActions: import_zod.z.array(import_zod.z.string().min(1)).min(1),
|
|
11175
|
+
dataScope: import_zod.z.record(import_zod.z.array(import_zod.z.union([import_zod.z.string().min(1), import_zod.z.null()]))).optional(),
|
|
11176
|
+
dataScopeRef: import_zod.z.string().min(1).optional(),
|
|
11177
|
+
capabilities: GrantedCapabilitiesSchema
|
|
11178
|
+
}).strict().superRefine((c, ctx) => {
|
|
11179
|
+
if (c.dataScope !== void 0 && c.dataScopeRef !== void 0) {
|
|
11180
|
+
ctx.addIssue({
|
|
11181
|
+
code: import_zod.z.ZodIssueCode.custom,
|
|
11182
|
+
path: ["dataScopeRef"],
|
|
11183
|
+
message: "a clause may declare 'dataScope' OR 'dataScopeRef', not both \u2014 dataScopeRef is sugar for an inline dataScope fragment, so the two are alternatives for the same slot, not additive"
|
|
11184
|
+
});
|
|
11185
|
+
}
|
|
11186
|
+
});
|
|
11187
|
+
var BlueprintFragmentsSchema = import_zod.z.record(
|
|
11188
|
+
import_zod.z.record(import_zod.z.array(import_zod.z.union([import_zod.z.string().min(1), import_zod.z.null()])))
|
|
11189
|
+
);
|
|
11190
|
+
var BlueprintRolesSchema = import_zod.z.record(import_zod.z.array(BlueprintRoleClauseSchema).min(1));
|
|
11191
|
+
var BlueprintRoleAssumableSchema = import_zod.z.record(import_zod.z.array(import_zod.z.string().min(1)).min(1));
|
|
11192
|
+
var BlueprintRoleAssumableMapSchema = import_zod.z.record(BlueprintRoleAssumableSchema);
|
|
11193
|
+
var IDENTITY_NAMESPACE_RE = /^[a-z][a-z0-9_-]{1,31}$/;
|
|
11194
|
+
var FORBIDDEN_IDENTITY_NAMESPACES = /* @__PURE__ */ new Set(["record", "document", "entity", "user"]);
|
|
11195
|
+
var IdentityKindSchema = import_zod.z.string().min(1).superRefine((val, ctx) => {
|
|
11196
|
+
if (val === "user") return;
|
|
11197
|
+
if (!IDENTITY_NAMESPACE_RE.test(val)) {
|
|
11198
|
+
ctx.addIssue({
|
|
11199
|
+
code: import_zod.z.ZodIssueCode.custom,
|
|
11200
|
+
message: `kind must be 'user' or an entity namespace (2-32 chars, a lowercase letter first, then lowercase letters/digits/_/-), e.g. 'org', 'client', 'team'`
|
|
11201
|
+
});
|
|
11202
|
+
} else if (FORBIDDEN_IDENTITY_NAMESPACES.has(val)) {
|
|
11203
|
+
ctx.addIssue({
|
|
11204
|
+
code: import_zod.z.ZodIssueCode.custom,
|
|
11205
|
+
message: `'${val}' is a reserved surface name and cannot be an entity namespace \u2014 use 'user' for a user, or a namespace like 'org'/'client'/'team'`
|
|
11206
|
+
});
|
|
11207
|
+
}
|
|
11208
|
+
});
|
|
11209
|
+
var IdentityDeclSchema = import_zod.z.object({
|
|
11210
|
+
kind: IdentityKindSchema,
|
|
10910
11211
|
externalId: import_zod.z.string().min(1),
|
|
10911
11212
|
displayName: import_zod.z.string().min(1).optional(),
|
|
10912
11213
|
metadata: import_zod.z.record(import_zod.z.unknown()).optional()
|
|
@@ -10924,7 +11225,7 @@ var BlueprintServicePrincipalSchema = import_zod.z.object({
|
|
|
10924
11225
|
}).strict();
|
|
10925
11226
|
var ISSUER_ID_RE = CONTEXT_ID_RE;
|
|
10926
11227
|
var BlueprintIssuerSchema = import_zod.z.object({
|
|
10927
|
-
/** Short slug identifying this issuer within your
|
|
11228
|
+
/** Short slug identifying this issuer within your account. Immutable once registered. */
|
|
10928
11229
|
issuerId: import_zod.z.string().regex(ISSUER_ID_RE, {
|
|
10929
11230
|
message: "issuerId must be 3-31 chars, start with a lowercase letter, then lowercase letters/digits/dashes (e.g. 'auth0-prod')"
|
|
10930
11231
|
}),
|
|
@@ -10939,7 +11240,12 @@ var BlueprintIssuerSchema = import_zod.z.object({
|
|
|
10939
11240
|
* this as an ordinary per-API/application default).
|
|
10940
11241
|
*/
|
|
10941
11242
|
audience: import_zod.z.string().min(1),
|
|
10942
|
-
/**
|
|
11243
|
+
/**
|
|
11244
|
+
* Which app context an exchanged token targets. **Must equal the blueprint's own
|
|
11245
|
+
* `contextId`** — an issuer is a trust anchor, so a blueprint may only attach one to the
|
|
11246
|
+
* context it actually provisions. One IdP account serving several contexts needs one entry
|
|
11247
|
+
* per context, each in that context's own blueprint.
|
|
11248
|
+
*/
|
|
10943
11249
|
contextId: import_zod.z.string().regex(CONTEXT_ID_RE, {
|
|
10944
11250
|
message: "contextId must be 3-31 chars, start with a lowercase letter, then lowercase letters/digits/dashes (e.g. 'casework')"
|
|
10945
11251
|
}),
|
|
@@ -10947,6 +11253,14 @@ var BlueprintIssuerSchema = import_zod.z.object({
|
|
|
10947
11253
|
subClaim: import_zod.z.string().min(1).optional(),
|
|
10948
11254
|
/** The claim carrying the subject's email (first-login invite matching). Platform defaults to 'email' when omitted. */
|
|
10949
11255
|
emailClaim: import_zod.z.string().min(1).optional(),
|
|
11256
|
+
/**
|
|
11257
|
+
* The IdP's OIDC userinfo endpoint. Optional. Presented tokens are access tokens, which under
|
|
11258
|
+
* OIDC don't carry `email` unless the IdP was specifically configured to add it — if
|
|
11259
|
+
* `emailClaim` misses on the presented token, and `userinfoUri` is configured, Vectros falls
|
|
11260
|
+
* back to calling this endpoint (with the presented token as the bearer credential) and reads
|
|
11261
|
+
* `emailClaim` from its JSON response instead. Omit to leave the fallback disabled.
|
|
11262
|
+
*/
|
|
11263
|
+
userinfoUri: import_zod.z.string().min(1).optional(),
|
|
10950
11264
|
/**
|
|
10951
11265
|
* Opt-in self-service signup: a list of {signupType, roleId} pairs. When a first-time exchange
|
|
10952
11266
|
* caller presents no invite token but names a signupType matching one of these (or omits it and
|
|
@@ -10966,6 +11280,169 @@ var BlueprintIssuerSchema = import_zod.z.object({
|
|
|
10966
11280
|
}).strict()
|
|
10967
11281
|
).optional()
|
|
10968
11282
|
}).strict();
|
|
11283
|
+
var BlueprintNamespaceSchema = import_zod.z.object({
|
|
11284
|
+
/** The namespace to register — same grammar as a `scope:<namespace>` key. */
|
|
11285
|
+
namespace: import_zod.z.string().min(1),
|
|
11286
|
+
/**
|
|
11287
|
+
* When `true`, every value in this namespace must resolve to an existing
|
|
11288
|
+
* identity entity (create via `entities:c` at the in-context phase, or a
|
|
11289
|
+
* declared `identities:` entry). When `false`/omitted, values are
|
|
11290
|
+
* free-form strings validated by grammar only.
|
|
11291
|
+
*/
|
|
11292
|
+
entityBacked: import_zod.z.boolean().optional(),
|
|
11293
|
+
/**
|
|
11294
|
+
* Request the platform's TENANT-WIDE registration form instead of this
|
|
11295
|
+
* blueprint's own context — visible to every context in the account, not
|
|
11296
|
+
* just the one this blueprint provisions. `false`/omitted (the default)
|
|
11297
|
+
* is the ordinary, context-owned registration every other namespace here
|
|
11298
|
+
* gets. Mutually exclusive with any future context-targeting field this
|
|
11299
|
+
* schema may grow — there is exactly one way to say "not my own context"
|
|
11300
|
+
* today.
|
|
11301
|
+
*
|
|
11302
|
+
* A blueprint declaring this does NOT get it automatically: the applying
|
|
11303
|
+
* credential must separately hold a platform capability granted only to
|
|
11304
|
+
* an account OWNER, and the CLI refuses to even request it unless
|
|
11305
|
+
* explicitly told to (see `--allow-tenant-wide-namespaces`) — no flag,
|
|
11306
|
+
* no request, regardless of what this field says. Setting this to `true`
|
|
11307
|
+
* states the blueprint's INTENT; it is not, on its own, a grant of
|
|
11308
|
+
* anything.
|
|
11309
|
+
*/
|
|
11310
|
+
tenantWide: import_zod.z.boolean().optional(),
|
|
11311
|
+
/**
|
|
11312
|
+
* This namespace's position in your account's specificity order (used to
|
|
11313
|
+
* break a tie when a caller holds two scope dimensions at once). Required
|
|
11314
|
+
* by the platform on registration; MUST be unique across every namespace
|
|
11315
|
+
* in the account, including `org` (1000) and `client` (2000) — pick a
|
|
11316
|
+
* value outside that pair, and outside any other namespace this same
|
|
11317
|
+
* account might register (its own, or another blueprint's).
|
|
11318
|
+
*/
|
|
11319
|
+
specificityRank: import_zod.z.number().int().min(0).max(1e6),
|
|
11320
|
+
/**
|
|
11321
|
+
* Optional membership backing — declares which record type + field hold
|
|
11322
|
+
* grants of THIS namespace's values. Supply together with
|
|
11323
|
+
* `membershipTargetField`, or omit both. Declaring this grants nobody
|
|
11324
|
+
* anything on its own: a role opts in explicitly with
|
|
11325
|
+
* `${{ member.scope.<namespace> }}` in its `dataScope`.
|
|
11326
|
+
*/
|
|
11327
|
+
membershipRecordType: import_zod.z.string().min(1).optional(),
|
|
11328
|
+
/** The field on `membershipRecordType` naming the user a grant is FOR. Required alongside `membershipRecordType`. */
|
|
11329
|
+
membershipTargetField: import_zod.z.string().min(1).optional(),
|
|
11330
|
+
/**
|
|
11331
|
+
* The field on `membershipRecordType` naming a grant's LEVEL, so the same
|
|
11332
|
+
* user can hold different levels in different values of this namespace.
|
|
11333
|
+
* Supply together with `membershipLevels`, or omit both for plain
|
|
11334
|
+
* in-or-out membership.
|
|
11335
|
+
*/
|
|
11336
|
+
membershipLevelField: import_zod.z.string().min(1).optional(),
|
|
11337
|
+
/** The complete set of level labels this namespace allows. Required alongside `membershipLevelField`. */
|
|
11338
|
+
membershipLevels: import_zod.z.array(import_zod.z.string().min(1)).optional()
|
|
11339
|
+
}).strict();
|
|
11340
|
+
var MEMBERSHIP_FIELD_NAME_RE = /^[A-Za-z0-9_-]{1,64}$/;
|
|
11341
|
+
function lintNamespaces(value, ctx) {
|
|
11342
|
+
if (!value || typeof value !== "object" || !("namespaces" in value)) return;
|
|
11343
|
+
const namespaces = value.namespaces;
|
|
11344
|
+
if (!Array.isArray(namespaces)) return;
|
|
11345
|
+
const rawSchemas = value.schemas;
|
|
11346
|
+
const declaredSchemaTypes = new Set(
|
|
11347
|
+
(Array.isArray(rawSchemas) ? rawSchemas : []).map((s) => s && typeof s === "object" && "typeName" in s ? s.typeName : void 0).filter((t) => typeof t === "string")
|
|
11348
|
+
);
|
|
11349
|
+
const seenNames = /* @__PURE__ */ new Set();
|
|
11350
|
+
const seenRanks = /* @__PURE__ */ new Map();
|
|
11351
|
+
namespaces.forEach((entry, i) => {
|
|
11352
|
+
if (!entry || typeof entry !== "object") return;
|
|
11353
|
+
const ns = entry;
|
|
11354
|
+
const path4 = ["namespaces", i];
|
|
11355
|
+
if (typeof ns.namespace === "string") {
|
|
11356
|
+
const err = scopeNamespaceGrammarError(ns.namespace);
|
|
11357
|
+
if (err) ctx.addIssue({ code: import_zod.z.ZodIssueCode.custom, path: [...path4, "namespace"], message: err });
|
|
11358
|
+
if (seenNames.has(ns.namespace)) {
|
|
11359
|
+
ctx.addIssue({
|
|
11360
|
+
code: import_zod.z.ZodIssueCode.custom,
|
|
11361
|
+
path: [...path4, "namespace"],
|
|
11362
|
+
message: `'${ns.namespace}' is declared more than once in namespaces[]`
|
|
11363
|
+
});
|
|
11364
|
+
}
|
|
11365
|
+
seenNames.add(ns.namespace);
|
|
11366
|
+
}
|
|
11367
|
+
if (typeof ns.specificityRank === "number") {
|
|
11368
|
+
const prior = seenRanks.get(ns.specificityRank);
|
|
11369
|
+
if (prior !== void 0) {
|
|
11370
|
+
ctx.addIssue({
|
|
11371
|
+
code: import_zod.z.ZodIssueCode.custom,
|
|
11372
|
+
path: [...path4, "specificityRank"],
|
|
11373
|
+
message: `specificityRank ${ns.specificityRank} is already used by namespaces[${prior}] \u2014 every namespace's rank must be unique`
|
|
11374
|
+
});
|
|
11375
|
+
}
|
|
11376
|
+
seenRanks.set(ns.specificityRank, i);
|
|
11377
|
+
}
|
|
11378
|
+
const hasRecordType = ns.membershipRecordType !== void 0;
|
|
11379
|
+
const hasTargetField = ns.membershipTargetField !== void 0;
|
|
11380
|
+
if (hasRecordType !== hasTargetField) {
|
|
11381
|
+
ctx.addIssue({
|
|
11382
|
+
code: import_zod.z.ZodIssueCode.custom,
|
|
11383
|
+
path: [...path4, "membershipRecordType"],
|
|
11384
|
+
message: "membershipRecordType and membershipTargetField must be declared together, or neither"
|
|
11385
|
+
});
|
|
11386
|
+
}
|
|
11387
|
+
if (typeof ns.membershipRecordType === "string" && !declaredSchemaTypes.has(ns.membershipRecordType)) {
|
|
11388
|
+
ctx.addIssue({
|
|
11389
|
+
code: import_zod.z.ZodIssueCode.custom,
|
|
11390
|
+
path: [...path4, "membershipRecordType"],
|
|
11391
|
+
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.`
|
|
11392
|
+
});
|
|
11393
|
+
}
|
|
11394
|
+
for (const [field, value2] of [
|
|
11395
|
+
["membershipRecordType", ns.membershipRecordType],
|
|
11396
|
+
["membershipTargetField", ns.membershipTargetField],
|
|
11397
|
+
["membershipLevelField", ns.membershipLevelField]
|
|
11398
|
+
]) {
|
|
11399
|
+
if (typeof value2 === "string" && !MEMBERSHIP_FIELD_NAME_RE.test(value2)) {
|
|
11400
|
+
ctx.addIssue({
|
|
11401
|
+
code: import_zod.z.ZodIssueCode.custom,
|
|
11402
|
+
path: [...path4, field],
|
|
11403
|
+
message: `${field} '${value2}' is invalid \u2014 must be 1-64 characters of letters, digits, underscores, or hyphens`
|
|
11404
|
+
});
|
|
11405
|
+
}
|
|
11406
|
+
}
|
|
11407
|
+
const hasLevelField = ns.membershipLevelField !== void 0;
|
|
11408
|
+
const hasLevels = Array.isArray(ns.membershipLevels) && ns.membershipLevels.length > 0;
|
|
11409
|
+
if (hasLevelField !== hasLevels) {
|
|
11410
|
+
ctx.addIssue({
|
|
11411
|
+
code: import_zod.z.ZodIssueCode.custom,
|
|
11412
|
+
path: [...path4, "membershipLevelField"],
|
|
11413
|
+
message: "membershipLevelField and a non-empty membershipLevels must be declared together, or neither"
|
|
11414
|
+
});
|
|
11415
|
+
}
|
|
11416
|
+
if ((hasLevelField || hasLevels) && !hasRecordType) {
|
|
11417
|
+
ctx.addIssue({
|
|
11418
|
+
code: import_zod.z.ZodIssueCode.custom,
|
|
11419
|
+
path: [...path4, "membershipLevelField"],
|
|
11420
|
+
message: "membershipLevelField/membershipLevels require membershipRecordType + membershipTargetField too"
|
|
11421
|
+
});
|
|
11422
|
+
}
|
|
11423
|
+
if (Array.isArray(ns.membershipLevels)) {
|
|
11424
|
+
const seenLevels = /* @__PURE__ */ new Set();
|
|
11425
|
+
for (const level of ns.membershipLevels) {
|
|
11426
|
+
if (typeof level !== "string") continue;
|
|
11427
|
+
const err = scopeNamespaceGrammarError(level);
|
|
11428
|
+
if (err) {
|
|
11429
|
+
ctx.addIssue({
|
|
11430
|
+
code: import_zod.z.ZodIssueCode.custom,
|
|
11431
|
+
path: [...path4, "membershipLevels"],
|
|
11432
|
+
message: `level '${level}': ${err}`
|
|
11433
|
+
});
|
|
11434
|
+
} else if (seenLevels.has(level)) {
|
|
11435
|
+
ctx.addIssue({
|
|
11436
|
+
code: import_zod.z.ZodIssueCode.custom,
|
|
11437
|
+
path: [...path4, "membershipLevels"],
|
|
11438
|
+
message: `membershipLevels contains '${level}' twice`
|
|
11439
|
+
});
|
|
11440
|
+
}
|
|
11441
|
+
seenLevels.add(level);
|
|
11442
|
+
}
|
|
11443
|
+
}
|
|
11444
|
+
});
|
|
11445
|
+
}
|
|
10969
11446
|
var SeedCommonShape = {
|
|
10970
11447
|
/** The schema typeName this seed instantiates (should match a declared schema). */
|
|
10971
11448
|
typeName: import_zod.z.string().min(1),
|
|
@@ -11006,7 +11483,7 @@ var BlueprintSeedRecordSchema = import_zod.z.discriminatedUnion("surface", [
|
|
|
11006
11483
|
RecordSeedSchema,
|
|
11007
11484
|
DocumentSeedSchema
|
|
11008
11485
|
]);
|
|
11009
|
-
var SELF_TOKEN_RE = /\$\{\{\s*(?:self|under)\.[^}]*?\s*\}\}/;
|
|
11486
|
+
var SELF_TOKEN_RE = /\$\{\{\s*(?:self|under|member)\.[^}]*?\s*\}\}/;
|
|
11010
11487
|
function lintSelfTokenPlacement(value, ctx) {
|
|
11011
11488
|
const walk = (node, path4, inRoleDataScope) => {
|
|
11012
11489
|
if (typeof node === "string") {
|
|
@@ -11014,7 +11491,7 @@ function lintSelfTokenPlacement(value, ctx) {
|
|
|
11014
11491
|
ctx.addIssue({
|
|
11015
11492
|
code: import_zod.z.ZodIssueCode.custom,
|
|
11016
11493
|
path: path4,
|
|
11017
|
-
message: "'${{ self.* }}' is a runtime
|
|
11494
|
+
message: "'${{ self.* }}' / '${{ member.* }}' is a runtime placeholder \u2014 it is only valid inside a roles[].dataScope value, a roleAssumable[] value, or a fragments[] value"
|
|
11018
11495
|
});
|
|
11019
11496
|
}
|
|
11020
11497
|
return;
|
|
@@ -11025,7 +11502,10 @@ function lintSelfTokenPlacement(value, ctx) {
|
|
|
11025
11502
|
}
|
|
11026
11503
|
if (node && typeof node === "object") {
|
|
11027
11504
|
for (const [k, v] of Object.entries(node)) {
|
|
11028
|
-
const
|
|
11505
|
+
const inRoleDataScopeClause = path4.length === 3 && path4[0] === "roles" && k === "dataScope";
|
|
11506
|
+
const inRoleAssumable = path4.length === 0 && k === "roleAssumable";
|
|
11507
|
+
const inFragments = path4.length === 0 && k === "fragments";
|
|
11508
|
+
const entering = inRoleDataScope || inRoleDataScopeClause || inRoleAssumable || inFragments;
|
|
11029
11509
|
walk(v, [...path4, k], entering);
|
|
11030
11510
|
}
|
|
11031
11511
|
}
|
|
@@ -11196,6 +11676,140 @@ function lintDataScopeKeys(value, ctx) {
|
|
|
11196
11676
|
}
|
|
11197
11677
|
});
|
|
11198
11678
|
}
|
|
11679
|
+
for (const [name, fragment] of Object.entries(bp.fragments ?? {})) {
|
|
11680
|
+
for (const key of Object.keys(fragment ?? {})) {
|
|
11681
|
+
lintDataScopeKey(key, ["fragments", name, key], ctx);
|
|
11682
|
+
}
|
|
11683
|
+
}
|
|
11684
|
+
}
|
|
11685
|
+
function lintDataScopeRefs(value, ctx) {
|
|
11686
|
+
const bp = value;
|
|
11687
|
+
const declared = new Set(Object.keys(bp.fragments ?? {}));
|
|
11688
|
+
for (const [roleId, clauses] of Object.entries(bp.roles ?? {})) {
|
|
11689
|
+
clauses.forEach((clause, i) => {
|
|
11690
|
+
if (clause.dataScopeRef === void 0) return;
|
|
11691
|
+
if (!declared.has(clause.dataScopeRef)) {
|
|
11692
|
+
ctx.addIssue({
|
|
11693
|
+
code: import_zod.z.ZodIssueCode.custom,
|
|
11694
|
+
path: ["roles", roleId, i, "dataScopeRef"],
|
|
11695
|
+
message: `dataScopeRef '${clause.dataScopeRef}' is not declared in fragments \u2014 declared fragments: ` + (declared.size ? [...declared].join(", ") : "(none)")
|
|
11696
|
+
});
|
|
11697
|
+
}
|
|
11698
|
+
});
|
|
11699
|
+
}
|
|
11700
|
+
}
|
|
11701
|
+
var BARE_SELF_PLACEHOLDER_RE = /^\$\{\{\s*self\.[^}]*?\s*\}\}$/;
|
|
11702
|
+
var UNDER_SELF_PLACEHOLDER_RE = /^\$\{\{\s*under\.self\.userId\s*\}\}$/;
|
|
11703
|
+
var UNDER_SELF_SCOPE_PLACEHOLDER_RE = /^\$\{\{\s*under\.self\.scope\.[a-z][a-z0-9_-]{1,31}\s*\}\}$/;
|
|
11704
|
+
var MEMBER_SCOPE_PLACEHOLDER_RE = /^\$\{\{\s*member\.scope\.[a-z][a-z0-9_-]{1,31}(?::[a-z][a-z0-9_-]{1,31})?\s*\}\}$/;
|
|
11705
|
+
var ANY_PLACEHOLDER_RE = /^\$\{\{\s*any\s*\}\}$/;
|
|
11706
|
+
function assumableValueError(publicKey, v) {
|
|
11707
|
+
if (UNDER_SELF_PLACEHOLDER_RE.test(v) || MEMBER_SCOPE_PLACEHOLDER_RE.test(v)) return null;
|
|
11708
|
+
if (UNDER_SELF_SCOPE_PLACEHOLDER_RE.test(v)) {
|
|
11709
|
+
return `assumable['${publicKey}'] value '${v}' resolves against your own current value for that namespace, which an assume can itself change \u2014 so what this grant admits would depend on what was last assumed. Use '\${{ under.self.userId }}', '\${{ member.scope.<namespace>[:level] }}', or a plain literal, all of which mean the same thing before and after an assume. ('\${{ under.self.scope.<namespace> }}' remains valid in dataScope, where it is re-derived per write)`;
|
|
11710
|
+
}
|
|
11711
|
+
if (ANY_PLACEHOLDER_RE.test(v)) {
|
|
11712
|
+
return `assumable['${publicKey}'] value '${v}' is not a concrete value and cannot be authored as an assumable grant \u2014 name the specific value(s) holders of this role may become`;
|
|
11713
|
+
}
|
|
11714
|
+
if (BARE_SELF_PLACEHOLDER_RE.test(v)) {
|
|
11715
|
+
return `assumable['${publicKey}'] value '${v}' is the bare self placeholder, which is tautological here \u2014 it can only ever equal whatever identity currently is, never a distinct value to become. Use '\${{ under.self.userId }}', '\${{ member.scope.<namespace>[:level] }}', or a plain literal instead`;
|
|
11716
|
+
}
|
|
11717
|
+
if (SELF_TOKEN_RE.test(v)) {
|
|
11718
|
+
return `assumable['${publicKey}'] value '${v}' is not a recognized runtime placeholder \u2014 use '\${{ under.self.userId }}' or '\${{ member.scope.<namespace>[:level] }}'`;
|
|
11719
|
+
}
|
|
11720
|
+
if (!SCOPE_VALUE_RE.test(v)) {
|
|
11721
|
+
return `assumable['${publicKey}'] value '${v}' is not a valid scope value \u2014 1-128 characters, starting with a letter or digit and continuing with letters, digits, '_' or '-'`;
|
|
11722
|
+
}
|
|
11723
|
+
return null;
|
|
11724
|
+
}
|
|
11725
|
+
function lintAccessProfileRoleIds(value, ctx) {
|
|
11726
|
+
const bp = value;
|
|
11727
|
+
const roleIds = bp.accessProfile?.roleIds;
|
|
11728
|
+
if (!roleIds) return;
|
|
11729
|
+
const declaredRoles = new Set(Object.keys(bp.roles ?? {}));
|
|
11730
|
+
const seen = /* @__PURE__ */ new Set();
|
|
11731
|
+
roleIds.forEach((roleId, i) => {
|
|
11732
|
+
const path4 = ["accessProfile", "roleIds", i];
|
|
11733
|
+
if (!declaredRoles.has(roleId)) {
|
|
11734
|
+
ctx.addIssue({
|
|
11735
|
+
code: import_zod.z.ZodIssueCode.custom,
|
|
11736
|
+
path: path4,
|
|
11737
|
+
message: `accessProfile.roleIds names '${roleId}', which is not a role this blueprint declares in roles \u2014 ` + (declaredRoles.size ? `declared roles: ${[...declaredRoles].join(", ")}` : "declared roles: (none)")
|
|
11738
|
+
});
|
|
11739
|
+
}
|
|
11740
|
+
if (seen.has(roleId)) {
|
|
11741
|
+
ctx.addIssue({
|
|
11742
|
+
code: import_zod.z.ZodIssueCode.custom,
|
|
11743
|
+
path: path4,
|
|
11744
|
+
message: `accessProfile.roleIds names '${roleId}' more than once \u2014 remove the duplicate`
|
|
11745
|
+
});
|
|
11746
|
+
}
|
|
11747
|
+
seen.add(roleId);
|
|
11748
|
+
});
|
|
11749
|
+
}
|
|
11750
|
+
function lintRoleAssumable(value, ctx) {
|
|
11751
|
+
const bp = value;
|
|
11752
|
+
const declaredRoles = new Set(Object.keys(bp.roles ?? {}));
|
|
11753
|
+
for (const [roleId, assumable] of Object.entries(bp.roleAssumable ?? {})) {
|
|
11754
|
+
if (!declaredRoles.has(roleId)) {
|
|
11755
|
+
ctx.addIssue({
|
|
11756
|
+
code: import_zod.z.ZodIssueCode.custom,
|
|
11757
|
+
path: ["roleAssumable", roleId],
|
|
11758
|
+
message: `roleAssumable names '${roleId}', which is not a role this blueprint declares in roles \u2014 ` + (declaredRoles.size ? `declared roles: ${[...declaredRoles].join(", ")}` : "declared roles: (none)")
|
|
11759
|
+
});
|
|
11760
|
+
continue;
|
|
11761
|
+
}
|
|
11762
|
+
for (const [key, values] of Object.entries(assumable)) {
|
|
11763
|
+
const path4 = ["roleAssumable", roleId, key];
|
|
11764
|
+
if (key === "userId") {
|
|
11765
|
+
ctx.addIssue({
|
|
11766
|
+
code: import_zod.z.ZodIssueCode.custom,
|
|
11767
|
+
path: path4,
|
|
11768
|
+
message: "assumable cannot name the principal ('userId') \u2014 the principal is never assumable via /assume, and a grant for it would save but could never be exercised"
|
|
11769
|
+
});
|
|
11770
|
+
continue;
|
|
11771
|
+
}
|
|
11772
|
+
const ns = key.startsWith("scope:") ? key.slice("scope:".length) : null;
|
|
11773
|
+
const nsError = ns === null ? null : scopeNamespaceGrammarError(ns);
|
|
11774
|
+
if (ns === null || nsError !== null) {
|
|
11775
|
+
ctx.addIssue({
|
|
11776
|
+
code: import_zod.z.ZodIssueCode.custom,
|
|
11777
|
+
path: path4,
|
|
11778
|
+
message: ns === null ? `'${key}' is not allowed \u2014 assumable keys must be a namespaced 'scope:<namespace>' key (the principal cannot be named)` : `assumable['${key}'] ${nsError}`
|
|
11779
|
+
});
|
|
11780
|
+
continue;
|
|
11781
|
+
}
|
|
11782
|
+
if (!Array.isArray(values)) continue;
|
|
11783
|
+
values.forEach((v, i) => {
|
|
11784
|
+
if (typeof v !== "string") return;
|
|
11785
|
+
const err = assumableValueError(key, v);
|
|
11786
|
+
if (err) ctx.addIssue({ code: import_zod.z.ZodIssueCode.custom, path: [...path4, i], message: err });
|
|
11787
|
+
});
|
|
11788
|
+
}
|
|
11789
|
+
}
|
|
11790
|
+
}
|
|
11791
|
+
function expandDataScopeRefs(bp) {
|
|
11792
|
+
if (!bp.roles) return bp;
|
|
11793
|
+
const fragments = bp.fragments ?? {};
|
|
11794
|
+
let changedAnyRole = false;
|
|
11795
|
+
const roles = {};
|
|
11796
|
+
for (const [roleId, clauses] of Object.entries(bp.roles)) {
|
|
11797
|
+
let changedThisRole = false;
|
|
11798
|
+
const expandedClauses = clauses.map((clause) => {
|
|
11799
|
+
if (clause.dataScopeRef === void 0) return clause;
|
|
11800
|
+
changedThisRole = true;
|
|
11801
|
+
const { dataScopeRef, ...rest } = clause;
|
|
11802
|
+
if (!(dataScopeRef in fragments)) {
|
|
11803
|
+
throw new Error(
|
|
11804
|
+
`expandDataScopeRefs: dataScopeRef '${dataScopeRef}' does not resolve against fragments \u2014 this should have been caught by lintDataScopeRefs before parseBlueprint reaches this function`
|
|
11805
|
+
);
|
|
11806
|
+
}
|
|
11807
|
+
return { ...rest, dataScope: fragments[dataScopeRef] };
|
|
11808
|
+
});
|
|
11809
|
+
roles[roleId] = changedThisRole ? expandedClauses : clauses;
|
|
11810
|
+
changedAnyRole = changedAnyRole || changedThisRole;
|
|
11811
|
+
}
|
|
11812
|
+
return changedAnyRole ? { ...bp, roles } : bp;
|
|
11199
11813
|
}
|
|
11200
11814
|
function lintSeedSurfaces(value, ctx) {
|
|
11201
11815
|
const bp = value;
|
|
@@ -11214,9 +11828,36 @@ function lintSeedSurfaces(value, ctx) {
|
|
|
11214
11828
|
}
|
|
11215
11829
|
});
|
|
11216
11830
|
}
|
|
11831
|
+
function lintIdentitySurfaces(value, ctx) {
|
|
11832
|
+
const bp = value;
|
|
11833
|
+
if (!Array.isArray(bp.schemas)) return;
|
|
11834
|
+
bp.schemas.forEach((schema, i) => {
|
|
11835
|
+
if (!schema.allowedSurfaces?.includes("user")) return;
|
|
11836
|
+
ctx.addIssue({
|
|
11837
|
+
code: import_zod.z.ZodIssueCode.custom,
|
|
11838
|
+
path: ["schemas", i, "allowedSurfaces"],
|
|
11839
|
+
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).`
|
|
11840
|
+
});
|
|
11841
|
+
});
|
|
11842
|
+
}
|
|
11843
|
+
function lintIssuerContext(value, ctx) {
|
|
11844
|
+
const bp = value;
|
|
11845
|
+
if (!Array.isArray(bp.issuers)) return;
|
|
11846
|
+
bp.issuers.forEach((issuer, i) => {
|
|
11847
|
+
if (issuer.contextId === void 0 || issuer.contextId === bp.contextId) return;
|
|
11848
|
+
ctx.addIssue({
|
|
11849
|
+
code: import_zod.z.ZodIssueCode.custom,
|
|
11850
|
+
path: ["issuers", i, "contextId"],
|
|
11851
|
+
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}'.`
|
|
11852
|
+
});
|
|
11853
|
+
});
|
|
11854
|
+
}
|
|
11217
11855
|
var BLUEPRINT_FIELD_PHASES = Object.freeze({
|
|
11218
11856
|
servicePrincipal: "bootstrap",
|
|
11219
11857
|
issuers: "bootstrap",
|
|
11858
|
+
// Registration requires the platform's provisioning:c capability, which
|
|
11859
|
+
// only the bootstrap credential carries — same reasoning as issuers.
|
|
11860
|
+
namespaces: "bootstrap",
|
|
11220
11861
|
schemas: "in-context",
|
|
11221
11862
|
accessProfile: "in-context",
|
|
11222
11863
|
roles: "in-context",
|
|
@@ -11239,6 +11880,24 @@ var BlueprintSchema = import_zod.z.object({
|
|
|
11239
11880
|
seed: import_zod.z.array(BlueprintSeedRecordSchema).optional(),
|
|
11240
11881
|
/** Optional multi-clause roles, bound to principals via `access grant --role`. */
|
|
11241
11882
|
roles: BlueprintRolesSchema.optional(),
|
|
11883
|
+
/**
|
|
11884
|
+
* Optional roleId → `POST /v1/auth/token/assume`
|
|
11885
|
+
* entitlement grant, mirroring the platform's role create/update request
|
|
11886
|
+
* and response shape for this field. Kept separate from `roles` rather
|
|
11887
|
+
* than folded into a role entry — see {@link BlueprintRoleAssumableMapSchema}'s
|
|
11888
|
+
* own doc comment for why. Every key must name a role this blueprint
|
|
11889
|
+
* declares in `roles` (`lintRoleAssumable`).
|
|
11890
|
+
*/
|
|
11891
|
+
roleAssumable: BlueprintRoleAssumableMapSchema.optional(),
|
|
11892
|
+
/**
|
|
11893
|
+
* Optional named `dataScope` fragments, each
|
|
11894
|
+
* referenceable from a role clause via `dataScopeRef: <name>` instead of
|
|
11895
|
+
* repeating the same map inline. Purely local authoring sugar: not a
|
|
11896
|
+
* provisioned resource (absent from `BLUEPRINT_FIELD_PHASES`), and never
|
|
11897
|
+
* survives `parseBlueprint` as a live reference — see
|
|
11898
|
+
* {@link expandDataScopeRefs}.
|
|
11899
|
+
*/
|
|
11900
|
+
fragments: BlueprintFragmentsSchema.optional(),
|
|
11242
11901
|
/** Optional principals ensured-exist at apply; referenced via ${{ identities.* }}. */
|
|
11243
11902
|
identities: IdentitiesDeclSchema.optional(),
|
|
11244
11903
|
/**
|
|
@@ -11247,14 +11906,29 @@ var BlueprintSchema = import_zod.z.object({
|
|
|
11247
11906
|
* service-principal creation — NOT the in-context load (see
|
|
11248
11907
|
* {@link BlueprintIssuerSchema}).
|
|
11249
11908
|
*/
|
|
11250
|
-
issuers: import_zod.z.array(BlueprintIssuerSchema).optional()
|
|
11909
|
+
issuers: import_zod.z.array(BlueprintIssuerSchema).optional(),
|
|
11910
|
+
/**
|
|
11911
|
+
* Optional namespaces to register before anything else needs them — see
|
|
11912
|
+
* {@link BlueprintNamespaceSchema}. Applied in the BOOTSTRAP-token phase,
|
|
11913
|
+
* alongside app-context/service-principal creation and issuers, BEFORE
|
|
11914
|
+
* schemas/seed (an `entityBacked` namespace's entities, and an
|
|
11915
|
+
* `allowedSurfaces: ['entity']` schema, both need the registration to
|
|
11916
|
+
* already exist). `org`/`client` need no entry — they exist already.
|
|
11917
|
+
*/
|
|
11918
|
+
namespaces: import_zod.z.array(BlueprintNamespaceSchema).optional()
|
|
11251
11919
|
}).strict().superRefine((bp, ctx) => {
|
|
11252
11920
|
lintSelfTokenPlacement(bp, ctx);
|
|
11253
11921
|
lintIdentityRefs(bp, ctx);
|
|
11254
11922
|
lintIdentityOverrides(bp, ctx);
|
|
11255
11923
|
lintNamespacedScopeArrays(bp, ctx);
|
|
11256
11924
|
lintDataScopeKeys(bp, ctx);
|
|
11925
|
+
lintDataScopeRefs(bp, ctx);
|
|
11926
|
+
lintRoleAssumable(bp, ctx);
|
|
11927
|
+
lintAccessProfileRoleIds(bp, ctx);
|
|
11257
11928
|
lintSeedSurfaces(bp, ctx);
|
|
11929
|
+
lintIdentitySurfaces(bp, ctx);
|
|
11930
|
+
lintIssuerContext(bp, ctx);
|
|
11931
|
+
lintNamespaces(bp, ctx);
|
|
11258
11932
|
});
|
|
11259
11933
|
function formatIssuePath(path4) {
|
|
11260
11934
|
let out = "";
|
|
@@ -11290,7 +11964,7 @@ function parseBlueprint(input) {
|
|
|
11290
11964
|
throw new BlueprintValidationError(`Malformed blueprint:
|
|
11291
11965
|
${renderIssues(issues)}`, issues);
|
|
11292
11966
|
}
|
|
11293
|
-
return result.data;
|
|
11967
|
+
return expandDataScopeRefs(result.data);
|
|
11294
11968
|
}
|
|
11295
11969
|
function contextNameOf(blueprint) {
|
|
11296
11970
|
return blueprint.contextName ?? `MCP \u2014 ${blueprint.name}`;
|
|
@@ -11409,9 +12083,12 @@ function deriveSuffix(contextId) {
|
|
|
11409
12083
|
var WHOLE_TOKEN_RE = /^\$\{\{\s*([A-Za-z_]\w*)\.([A-Za-z_]\w*)\s*\}\}$/;
|
|
11410
12084
|
var TOKEN_SCAN_RE = /\$\$\{\{|\$\{\{([\s\S]*?)\}\}|\$\{\{/g;
|
|
11411
12085
|
var INNER_REF_RE = /^([A-Za-z_]\w*)\.([A-Za-z_]\w*)$/;
|
|
11412
|
-
var DEFERRED_NAMESPACES = /* @__PURE__ */ new Set(["self", "identities", "under"]);
|
|
12086
|
+
var DEFERRED_NAMESPACES = /* @__PURE__ */ new Set(["self", "identities", "under", "member"]);
|
|
12087
|
+
var DEFERRED_BARE_TOKENS = /* @__PURE__ */ new Set(["any"]);
|
|
11413
12088
|
var DEFERRED_NS_ALTERNATION = [...DEFERRED_NAMESPACES].join("|");
|
|
11414
|
-
var DEFERRED_INNER_RE = new RegExp(
|
|
12089
|
+
var DEFERRED_INNER_RE = new RegExp(
|
|
12090
|
+
`^(?:${DEFERRED_NS_ALTERNATION})(?:\\.[A-Za-z_]\\w*)+(?::[A-Za-z_]\\w*)?$`
|
|
12091
|
+
);
|
|
11415
12092
|
function resolveRef(ns, name, ctx, path4) {
|
|
11416
12093
|
if (ns === "inputs") {
|
|
11417
12094
|
if (!(name in ctx.declared)) {
|
|
@@ -11469,7 +12146,7 @@ function interpolate(str, ctx, path4) {
|
|
|
11469
12146
|
});
|
|
11470
12147
|
} else {
|
|
11471
12148
|
const inner = (m[1] ?? "").trim();
|
|
11472
|
-
if (DEFERRED_INNER_RE.test(inner)) {
|
|
12149
|
+
if (DEFERRED_INNER_RE.test(inner) || DEFERRED_BARE_TOKENS.has(inner)) {
|
|
11473
12150
|
out += m[0];
|
|
11474
12151
|
last = TOKEN_SCAN_RE.lastIndex;
|
|
11475
12152
|
continue;
|
|
@@ -11671,314 +12348,72 @@ var taskManagement = {
|
|
|
11671
12348
|
renderHints: { label: "Assignee", widget: "text", order: 5, section: "Tracking" }
|
|
11672
12349
|
},
|
|
11673
12350
|
{
|
|
11674
|
-
fieldId: "project",
|
|
11675
|
-
fieldType: "string",
|
|
11676
|
-
filterable: true,
|
|
11677
|
-
description: "Grouping key for cross-session continuity.",
|
|
11678
|
-
renderHints: { label: "Project", widget: "text", order: 6, section: "Tracking" }
|
|
11679
|
-
},
|
|
11680
|
-
{
|
|
11681
|
-
fieldId: "dueDate",
|
|
11682
|
-
fieldType: "date",
|
|
11683
|
-
description: "ISO-8601. Range-queryable (see lookupFields) \u2014 list tasks due in a window.",
|
|
11684
|
-
renderHints: { label: "Due date", widget: "date", order: 7, section: "Tracking" }
|
|
11685
|
-
},
|
|
11686
|
-
{
|
|
11687
|
-
fieldId: "tags",
|
|
11688
|
-
fieldType: "array",
|
|
11689
|
-
filterable: true,
|
|
11690
|
-
renderHints: { label: "Tags", order: 8, section: "Tracking" }
|
|
11691
|
-
}
|
|
11692
|
-
// externalId is the record's FIRST-CLASS identifier (the dedup/lookup key + the
|
|
11693
|
-
// value a `reference` resolves against) — not a payload field; the loader sends
|
|
11694
|
-
// it top-level on the RecordRequest.
|
|
11695
|
-
],
|
|
11696
|
-
// Two lookup shapes, chosen deliberately because the equality-vs-range choice
|
|
11697
|
-
// per field is MIGRATION-LOCKED once the schema is live (you cannot flip a
|
|
11698
|
-
// field slot↔range later, even by removing and re-adding it):
|
|
11699
|
-
// • `project` — EQUALITY (a grouping key, not ordered): enumerate one
|
|
11700
|
-
// project's tasks directly, no search. Each equality lookup uses 1 of the
|
|
11701
|
-
// schema's 7 fast index slots.
|
|
11702
|
-
// • `dueDate` — RANGE: ordered `from`/`to`/`prefix` queries ("tasks due this
|
|
11703
|
-
// week", "due in 2026-06"). Range lookups use a relationship row, not a
|
|
11704
|
-
// slot, and are billed at the range rate. ISO-8601 sorts chronologically,
|
|
11705
|
-
// so the order is correct.
|
|
11706
|
-
// `externalId` has a built-in first-class finder and must NOT be redeclared here.
|
|
11707
|
-
lookupFields: ["project", { fieldName: "dueDate", rangeEnabled: true }]
|
|
11708
|
-
}
|
|
11709
|
-
],
|
|
11710
|
-
// Least-privilege profile — MUST pass the CLI scope gate.
|
|
11711
|
-
// r/c/u + search + schema discovery. NOT records:d (least privilege).
|
|
11712
|
-
accessProfile: {
|
|
11713
|
-
allowedActions: ["records:r", "records:c", "records:u", "search:r", "schemas:r"]
|
|
11714
|
-
// dataScope omitted → tenant-level shared tracker. Add a `scope:org`
|
|
11715
|
-
// entry for per-org isolation.
|
|
11716
|
-
},
|
|
11717
|
-
servicePrincipal: {
|
|
11718
|
-
externalId: "task-management",
|
|
11719
|
-
displayName: "Task Management"
|
|
11720
|
-
},
|
|
11721
|
-
seed: [
|
|
11722
|
-
{
|
|
11723
|
-
surface: "record",
|
|
11724
|
-
typeName: "task",
|
|
11725
|
-
externalId: "seed-welcome",
|
|
11726
|
-
fields: {
|
|
11727
|
-
title: "Welcome to your Vectros task tracker",
|
|
11728
|
-
description: "Created by the bootstrap loader. Ask your agent to add tasks.",
|
|
11729
|
-
status: "todo",
|
|
11730
|
-
priority: "low",
|
|
11731
|
-
project: "getting-started",
|
|
11732
|
-
dueDate: "2026-06-30"
|
|
11733
|
-
}
|
|
11734
|
-
}
|
|
11735
|
-
]
|
|
11736
|
-
};
|
|
11737
|
-
var task_management_default = taskManagement;
|
|
11738
|
-
var codingAgentMemory = {
|
|
11739
|
-
name: "coding-agent-memory",
|
|
11740
|
-
version: "1.0.0",
|
|
11741
|
-
description: "Persistent project memory for a coding agent \u2014 decisions, conventions, and gotchas that survive across sessions.",
|
|
11742
|
-
contextId: "coding-memory",
|
|
11743
|
-
contextName: "Coding Agent \u2014 Project Memory",
|
|
11744
|
-
schemas: [
|
|
11745
|
-
{
|
|
11746
|
-
// A durable architectural/product decision: what was decided, and WHY.
|
|
11747
|
-
// The rationale is the high-value, RAG-able body — "why did we do X?"
|
|
11748
|
-
// is the question a cold-context agent most needs answered.
|
|
11749
|
-
typeName: "decision",
|
|
11750
|
-
displayName: "Decision",
|
|
11751
|
-
indexMode: "HYBRID",
|
|
11752
|
-
// keyword on titles + semantic on statement/rationale
|
|
11753
|
-
fields: [
|
|
11754
|
-
{
|
|
11755
|
-
fieldId: "title",
|
|
11756
|
-
fieldType: "string",
|
|
11757
|
-
required: true,
|
|
11758
|
-
searchable: true,
|
|
11759
|
-
validation: { minLength: 1, maxLength: 200 },
|
|
11760
|
-
renderHints: { label: "Title", widget: "text", order: 1, displayField: true }
|
|
11761
|
-
},
|
|
11762
|
-
{
|
|
11763
|
-
fieldId: "statement",
|
|
11764
|
-
fieldType: "string",
|
|
11765
|
-
searchable: true,
|
|
11766
|
-
description: "What was decided, in one or two sentences.",
|
|
11767
|
-
renderHints: { label: "Statement", widget: "textarea", order: 2 }
|
|
11768
|
-
},
|
|
11769
|
-
{
|
|
11770
|
-
fieldId: "rationale",
|
|
11771
|
-
fieldType: "string",
|
|
11772
|
-
searchable: true,
|
|
11773
|
-
description: "Why \u2014 the trade-offs and the context. The most-recalled field.",
|
|
11774
|
-
renderHints: { label: "Rationale", widget: "textarea", order: 3 }
|
|
11775
|
-
},
|
|
11776
|
-
{
|
|
11777
|
-
fieldId: "status",
|
|
11778
|
-
fieldType: "enum",
|
|
11779
|
-
filterable: true,
|
|
11780
|
-
enumValues: ["proposed", "active", "superseded"],
|
|
11781
|
-
renderHints: { label: "Status", widget: "select", order: 4 }
|
|
11782
|
-
},
|
|
11783
|
-
{
|
|
11784
|
-
fieldId: "area",
|
|
11785
|
-
fieldType: "string",
|
|
11786
|
-
filterable: true,
|
|
11787
|
-
description: 'Subsystem / module the decision applies to (e.g. "auth", "search").',
|
|
11788
|
-
renderHints: { label: "Area", widget: "text", order: 5 }
|
|
11789
|
-
},
|
|
11790
|
-
{
|
|
11791
|
-
fieldId: "decidedOn",
|
|
11792
|
-
fieldType: "date",
|
|
11793
|
-
description: 'ISO-8601 date. Range-queryable \u2014 "decisions made this quarter".',
|
|
11794
|
-
renderHints: { label: "Decided on", widget: "date", order: 6 }
|
|
11795
|
-
}
|
|
11796
|
-
// externalId is the record's FIRST-CLASS identifier (the dedup/upsert key + the
|
|
11797
|
-
// value a `reference` resolves against) — it is NOT a payload field, so it is not
|
|
11798
|
-
// declared here; the loader sends it top-level on the RecordRequest.
|
|
11799
|
-
],
|
|
11800
|
-
// `externalId` is a first-class identifier with its own finder (exact
|
|
11801
|
-
// get/upsert) — look it up directly, never redeclare it as a schema lookup.
|
|
11802
|
-
// The choice below is MIGRATION-LOCKED once the schema is live:
|
|
11803
|
-
// • `area`/`status` — EQUALITY (categorical): "list active decisions",
|
|
11804
|
-
// "all decisions in the auth area". 2 of the 7 fast index slots.
|
|
11805
|
-
// • `decidedOn` — RANGE: ordered `from`/`to`/`prefix` over the date.
|
|
11806
|
-
lookupFields: ["area", "status", { fieldName: "decidedOn", rangeEnabled: true }]
|
|
11807
|
-
},
|
|
11808
|
-
{
|
|
11809
|
-
// A coding convention the team follows — the agent reads these before it
|
|
11810
|
-
// writes code so it matches the surrounding style instead of inventing one.
|
|
11811
|
-
typeName: "convention",
|
|
11812
|
-
displayName: "Convention",
|
|
11813
|
-
indexMode: "HYBRID",
|
|
11814
|
-
fields: [
|
|
11815
|
-
{
|
|
11816
|
-
fieldId: "name",
|
|
11817
|
-
fieldType: "string",
|
|
11818
|
-
required: true,
|
|
11819
|
-
searchable: true,
|
|
11820
|
-
validation: { minLength: 1, maxLength: 200 },
|
|
11821
|
-
renderHints: { label: "Name", widget: "text", order: 1, displayField: true }
|
|
11822
|
-
},
|
|
11823
|
-
{
|
|
11824
|
-
fieldId: "rule",
|
|
11825
|
-
fieldType: "string",
|
|
11826
|
-
searchable: true,
|
|
11827
|
-
description: "The convention itself, stated as an imperative.",
|
|
11828
|
-
renderHints: { label: "Rule", widget: "textarea", order: 2 }
|
|
11829
|
-
},
|
|
11830
|
-
{
|
|
11831
|
-
fieldId: "area",
|
|
11832
|
-
fieldType: "string",
|
|
11833
|
-
filterable: true,
|
|
11834
|
-
renderHints: { label: "Area", widget: "text", order: 3 }
|
|
11835
|
-
},
|
|
11836
|
-
{
|
|
11837
|
-
// Parallels `decision.status` so a convention can be RETIRED rather than
|
|
11838
|
-
// deleted (the blueprint omits records:d — memory is superseded, not lost).
|
|
11839
|
-
fieldId: "status",
|
|
11840
|
-
fieldType: "enum",
|
|
11841
|
-
filterable: true,
|
|
11842
|
-
enumValues: ["active", "retired"],
|
|
11843
|
-
renderHints: { label: "Status", widget: "select", order: 4 }
|
|
11844
|
-
},
|
|
11845
|
-
{
|
|
11846
|
-
// A typed REFERENCE to the decision that established this convention — a
|
|
11847
|
-
// foreign-key link between record types (the knowledge-graph edge). The
|
|
11848
|
-
// platform requires both the target type AND its surface; the value is the
|
|
11849
|
-
// target decision's externalId (the default `targetField`). Provenance:
|
|
11850
|
-
// "why does this convention exist? → open the decision behind it." Not
|
|
11851
|
-
// searchable (a foreign-key id is search noise); declared as an equality
|
|
11852
|
-
// lookup below so you can also enumerate "conventions from decision X".
|
|
11853
|
-
fieldId: "establishedByDecision",
|
|
11854
|
-
fieldType: "reference",
|
|
11855
|
-
targetTypeName: "decision",
|
|
11856
|
-
targetSurface: "record",
|
|
11857
|
-
targetField: "externalId",
|
|
11858
|
-
cardinality: "one",
|
|
11859
|
-
renderHints: { label: "Established by (decision)", order: 5 }
|
|
11860
|
-
},
|
|
11861
|
-
{
|
|
11862
|
-
fieldId: "adoptedOn",
|
|
11863
|
-
fieldType: "date",
|
|
11864
|
-
description: "ISO-8601 \u2014 when this convention was adopted. Range-queryable.",
|
|
11865
|
-
renderHints: { label: "Adopted on", widget: "date", order: 6 }
|
|
11866
|
-
}
|
|
11867
|
-
// externalId: first-class identifier, not a payload field (see `decision`).
|
|
11868
|
-
],
|
|
11869
|
-
// externalId has its own first-class finder for exact get — look it up
|
|
11870
|
-
// directly, not as a schema lookup. Locked equality-vs-range choices:
|
|
11871
|
-
// • `area`/`status` — EQUALITY: "active conventions for auth".
|
|
11872
|
-
// • `establishedByDecision` — EQUALITY on the reference value: enumerate
|
|
11873
|
-
// "conventions established by decision X" (a forward link query; the
|
|
11874
|
-
// platform has no reverse-reference index on this surface).
|
|
11875
|
-
// • `adoptedOn` — RANGE over the adoption date.
|
|
11876
|
-
// 3 equality lookups (of 7 fast slots) + 1 range row.
|
|
11877
|
-
lookupFields: ["area", "status", "establishedByDecision", { fieldName: "adoptedOn", rangeEnabled: true }]
|
|
11878
|
-
},
|
|
11879
|
-
{
|
|
11880
|
-
// A gotcha / sharp edge: a symptom, its cause, and the fix. Saves the
|
|
11881
|
-
// agent (and the human) from re-discovering the same trap.
|
|
11882
|
-
typeName: "gotcha",
|
|
11883
|
-
displayName: "Gotcha",
|
|
11884
|
-
indexMode: "HYBRID",
|
|
11885
|
-
fields: [
|
|
11886
|
-
{
|
|
11887
|
-
fieldId: "symptom",
|
|
11888
|
-
fieldType: "string",
|
|
11889
|
-
required: true,
|
|
11890
|
-
searchable: true,
|
|
11891
|
-
validation: { minLength: 1, maxLength: 500 },
|
|
11892
|
-
renderHints: { label: "Symptom", widget: "textarea", order: 1, displayField: true }
|
|
11893
|
-
},
|
|
11894
|
-
{
|
|
11895
|
-
fieldId: "cause",
|
|
11896
|
-
fieldType: "string",
|
|
11897
|
-
searchable: true,
|
|
11898
|
-
renderHints: { label: "Cause", widget: "textarea", order: 2 }
|
|
11899
|
-
},
|
|
11900
|
-
{
|
|
11901
|
-
fieldId: "fix",
|
|
11902
|
-
fieldType: "string",
|
|
11903
|
-
searchable: true,
|
|
11904
|
-
renderHints: { label: "Fix", widget: "textarea", order: 3 }
|
|
11905
|
-
},
|
|
11906
|
-
{
|
|
11907
|
-
fieldId: "area",
|
|
11908
|
-
fieldType: "string",
|
|
11909
|
-
filterable: true,
|
|
11910
|
-
renderHints: { label: "Area", widget: "text", order: 4 }
|
|
11911
|
-
},
|
|
11912
|
-
{
|
|
11913
|
-
// A gotcha can be RETIRED once the underlying trap is fixed for good —
|
|
11914
|
-
// superseded, not deleted (the blueprint omits records:d).
|
|
11915
|
-
fieldId: "status",
|
|
11916
|
-
fieldType: "enum",
|
|
12351
|
+
fieldId: "project",
|
|
12352
|
+
fieldType: "string",
|
|
11917
12353
|
filterable: true,
|
|
11918
|
-
|
|
11919
|
-
renderHints: { label: "
|
|
12354
|
+
description: "Grouping key for cross-session continuity.",
|
|
12355
|
+
renderHints: { label: "Project", widget: "text", order: 6, section: "Tracking" }
|
|
11920
12356
|
},
|
|
11921
12357
|
{
|
|
11922
|
-
fieldId: "
|
|
12358
|
+
fieldId: "dueDate",
|
|
11923
12359
|
fieldType: "date",
|
|
11924
|
-
description: "ISO-8601 \u2014
|
|
11925
|
-
renderHints: { label: "
|
|
12360
|
+
description: "ISO-8601. Range-queryable (see lookupFields) \u2014 list tasks due in a window.",
|
|
12361
|
+
renderHints: { label: "Due date", widget: "date", order: 7, section: "Tracking" }
|
|
12362
|
+
},
|
|
12363
|
+
{
|
|
12364
|
+
fieldId: "tags",
|
|
12365
|
+
fieldType: "array",
|
|
12366
|
+
filterable: true,
|
|
12367
|
+
renderHints: { label: "Tags", order: 8, section: "Tracking" }
|
|
11926
12368
|
}
|
|
11927
|
-
// externalId
|
|
12369
|
+
// externalId is the record's FIRST-CLASS identifier (the dedup/lookup key + the
|
|
12370
|
+
// value a `reference` resolves against) — not a payload field; the loader sends
|
|
12371
|
+
// it top-level on the RecordRequest.
|
|
11928
12372
|
],
|
|
11929
|
-
//
|
|
11930
|
-
//
|
|
11931
|
-
//
|
|
11932
|
-
|
|
12373
|
+
// Two lookup shapes, chosen deliberately because the equality-vs-range choice
|
|
12374
|
+
// per field is MIGRATION-LOCKED once the schema is live (you cannot flip a
|
|
12375
|
+
// field slot↔range later, even by removing and re-adding it):
|
|
12376
|
+
// • `project` — EQUALITY (a grouping key, not ordered): enumerate one
|
|
12377
|
+
// project's tasks directly, no search. Each equality lookup uses 1 of the
|
|
12378
|
+
// schema's 7 fast index slots.
|
|
12379
|
+
// • `dueDate` — RANGE: ordered `from`/`to`/`prefix` queries ("tasks due this
|
|
12380
|
+
// week", "due in 2026-06"). Range lookups use a relationship row, not a
|
|
12381
|
+
// slot, and are billed at the range rate. ISO-8601 sorts chronologically,
|
|
12382
|
+
// so the order is correct.
|
|
12383
|
+
// `externalId` has a built-in first-class finder and must NOT be redeclared here.
|
|
12384
|
+
lookupFields: ["project", { fieldName: "dueDate", rangeEnabled: true }]
|
|
11933
12385
|
}
|
|
11934
12386
|
],
|
|
11935
12387
|
// Least-privilege profile — MUST pass the CLI scope gate.
|
|
11936
|
-
// r/c/u + search + schema discovery
|
|
11937
|
-
// recall over the captured rationale). NOT records:d: memory is superseded,
|
|
11938
|
-
// not deleted, so the project's decision history stays auditable.
|
|
12388
|
+
// r/c/u + search + schema discovery. NOT records:d (least privilege).
|
|
11939
12389
|
accessProfile: {
|
|
11940
|
-
allowedActions: ["records:r", "records:c", "records:u", "search:r", "schemas:r",
|
|
12390
|
+
allowedActions: ["records:r", "records:c", "records:u", "search:r", "schemas:r"],
|
|
12391
|
+
// dataScope: {} (present, empty) is the deliberate marker for a
|
|
12392
|
+
// tenant-level shared tracker — every task visible to every holder of
|
|
12393
|
+
// this credential. Add a `scope:org` entry instead for per-org isolation.
|
|
12394
|
+
dataScope: {}
|
|
11941
12395
|
},
|
|
11942
12396
|
servicePrincipal: {
|
|
11943
|
-
externalId: "
|
|
11944
|
-
displayName: "
|
|
12397
|
+
externalId: "task-management",
|
|
12398
|
+
displayName: "Task Management"
|
|
11945
12399
|
},
|
|
11946
12400
|
seed: [
|
|
11947
12401
|
{
|
|
11948
|
-
// Seeded FIRST so the convention below resolves its reference to it — the
|
|
11949
|
-
// loader creates seeds in array order, and a reference target must exist when
|
|
11950
|
-
// the referencing record is written.
|
|
11951
|
-
surface: "record",
|
|
11952
|
-
typeName: "decision",
|
|
11953
|
-
externalId: "seed-use-vectros-for-memory",
|
|
11954
|
-
fields: {
|
|
11955
|
-
title: "Use Vectros as the coding agent\u2019s project memory",
|
|
11956
|
-
statement: "Persist decisions, conventions, and gotchas as Vectros records so they survive across agent sessions.",
|
|
11957
|
-
rationale: "A coding agent loses context every cold start. A governed, searchable memory lets it recall prior decisions by meaning instead of re-asking \u2014 and the version history shows how the thinking evolved.",
|
|
11958
|
-
status: "active",
|
|
11959
|
-
area: "getting-started",
|
|
11960
|
-
decidedOn: "2026-06-14"
|
|
11961
|
-
}
|
|
11962
|
-
},
|
|
11963
|
-
{
|
|
11964
|
-
// Demonstrates a live typed link at bootstrap: this convention references the
|
|
11965
|
-
// decision above by its externalId, so "open the decision behind this rule"
|
|
11966
|
-
// works the moment the blueprint is applied.
|
|
11967
12402
|
surface: "record",
|
|
11968
|
-
typeName: "
|
|
11969
|
-
externalId: "seed-
|
|
12403
|
+
typeName: "task",
|
|
12404
|
+
externalId: "seed-welcome",
|
|
11970
12405
|
fields: {
|
|
11971
|
-
|
|
11972
|
-
|
|
11973
|
-
|
|
11974
|
-
|
|
11975
|
-
|
|
11976
|
-
|
|
12406
|
+
title: "Welcome to your Vectros task tracker",
|
|
12407
|
+
description: "Created by the bootstrap loader. Ask your agent to add tasks.",
|
|
12408
|
+
status: "todo",
|
|
12409
|
+
priority: "low",
|
|
12410
|
+
project: "getting-started",
|
|
12411
|
+
dueDate: "2026-06-30"
|
|
11977
12412
|
}
|
|
11978
12413
|
}
|
|
11979
12414
|
]
|
|
11980
12415
|
};
|
|
11981
|
-
var
|
|
12416
|
+
var task_management_default = taskManagement;
|
|
11982
12417
|
var DATA_PLANE_ACTIONS = [
|
|
11983
12418
|
"records:r",
|
|
11984
12419
|
"records:c",
|
|
@@ -13105,7 +13540,11 @@ var agenticSdlc = {
|
|
|
13105
13540
|
// mints for THIS blueprint's service principal (the MCP/API runtime). See
|
|
13106
13541
|
// DATA_PLANE_ACTIONS above for the action set + rationale.
|
|
13107
13542
|
accessProfile: {
|
|
13108
|
-
allowedActions: DATA_PLANE_ACTIONS
|
|
13543
|
+
allowedActions: DATA_PLANE_ACTIONS,
|
|
13544
|
+
// dataScope: {} (present, empty) is the deliberate marker for tenant-wide
|
|
13545
|
+
// reach — this service key IS the runtime's own access to the whole KB,
|
|
13546
|
+
// not a multi-owner resource to isolate.
|
|
13547
|
+
dataScope: {}
|
|
13109
13548
|
},
|
|
13110
13549
|
// A reusable `editor` role for the HUMAN owner — DISTINCT from `accessProfile`
|
|
13111
13550
|
// (which scopes only the service-principal key). `bootstrap` provisions this
|
|
@@ -13125,7 +13564,9 @@ var agenticSdlc = {
|
|
|
13125
13564
|
// which includes members' private memory — the trusted-administrator tradeoff,
|
|
13126
13565
|
// called out in the bundled guide. Scope it down if your deployment wants
|
|
13127
13566
|
// curator access without private-memory visibility.
|
|
13128
|
-
|
|
13567
|
+
// dataScope: {} echoes the "deliberately UNSCOPED (whole-context)" call-out
|
|
13568
|
+
// above as an explicit marker, not just prose — see it for the tradeoff.
|
|
13569
|
+
editor: [{ allowedActions: EDITOR_ACTIONS, dataScope: {} }],
|
|
13129
13570
|
// A team member (or their agent) — TWO memory tiers as two UNIONed clauses
|
|
13130
13571
|
// (a record is accessible when ANY clause grants it; within one clause every
|
|
13131
13572
|
// dataScope dimension must match):
|
|
@@ -13145,7 +13586,9 @@ var agenticSdlc = {
|
|
|
13145
13586
|
// `vectros join agentic-sdlc --role member`; verify a binding with
|
|
13146
13587
|
// `vectros access explain --principal me --context agentic-sdlc`.
|
|
13147
13588
|
member: [
|
|
13148
|
-
|
|
13589
|
+
// dataScope: {} echoes clause 1's own "UNSCOPED, but safe" call-out above
|
|
13590
|
+
// as an explicit marker, not just prose.
|
|
13591
|
+
{ allowedActions: MEMBER_SHARED_READ_ACTIONS, dataScope: {} },
|
|
13149
13592
|
{
|
|
13150
13593
|
// `inference:r` is IN this self-scoped clause (not only clause 1) so a
|
|
13151
13594
|
// member's `rag_ask` can ground on its OWN private memory — the read
|
|
@@ -13260,7 +13703,11 @@ var secondBrain = {
|
|
|
13260
13703
|
// (so the agent can `rag_ask` a grounded, cited question over your notes). No
|
|
13261
13704
|
// records:d: notes are archived (status → 'archived'), never deleted.
|
|
13262
13705
|
accessProfile: {
|
|
13263
|
-
allowedActions: ["records:r", "records:c", "records:u", "search:r", "schemas:r", "inference:r"]
|
|
13706
|
+
allowedActions: ["records:r", "records:c", "records:u", "search:r", "schemas:r", "inference:r"],
|
|
13707
|
+
// dataScope: {} (present, empty) is the deliberate marker for tenant-wide
|
|
13708
|
+
// reach — this is a single personal-knowledge-base credential with one
|
|
13709
|
+
// holder, not a multi-owner shared resource to isolate.
|
|
13710
|
+
dataScope: {}
|
|
13264
13711
|
},
|
|
13265
13712
|
servicePrincipal: {
|
|
13266
13713
|
externalId: "second-brain",
|
|
@@ -13392,7 +13839,11 @@ var clinicalIntake = {
|
|
|
13392
13839
|
// reveal qualifier (the demo key literally cannot un-redact the PHI fields),
|
|
13393
13840
|
// and NO inference:r (we never point a RAG capability at a PHI corpus here).
|
|
13394
13841
|
accessProfile: {
|
|
13395
|
-
allowedActions: ["records:r", "records:c", "records:u", "search:r", "schemas:r"]
|
|
13842
|
+
allowedActions: ["records:r", "records:c", "records:u", "search:r", "schemas:r"],
|
|
13843
|
+
// dataScope: {} (present, empty) is the deliberate marker for tenant-wide
|
|
13844
|
+
// reach, unchanged from this demo's original intent — a real clinical
|
|
13845
|
+
// deployment should scope this (e.g. `scope:org`) before handling real PHI.
|
|
13846
|
+
dataScope: {}
|
|
13396
13847
|
},
|
|
13397
13848
|
servicePrincipal: {
|
|
13398
13849
|
externalId: "clinical-intake",
|
|
@@ -13421,7 +13872,6 @@ var clinicalIntake = {
|
|
|
13421
13872
|
var clinical_intake_default = clinicalIntake;
|
|
13422
13873
|
var BUNDLED_BLUEPRINTS = [
|
|
13423
13874
|
task_management_default,
|
|
13424
|
-
coding_agent_memory_default,
|
|
13425
13875
|
agentic_sdlc_default,
|
|
13426
13876
|
second_brain_default,
|
|
13427
13877
|
clinical_intake_default
|
|
@@ -13720,18 +14170,17 @@ async function mintBridgeToken(opts) {
|
|
|
13720
14170
|
ttlSeconds = DEFAULT_BRIDGE_TTL_SECONDS,
|
|
13721
14171
|
tenant = "live",
|
|
13722
14172
|
useScopedToken = false,
|
|
14173
|
+
contextId,
|
|
13723
14174
|
fetchImpl = fetch
|
|
13724
14175
|
} = opts;
|
|
13725
14176
|
const base = apiBaseUrl.replace(/\/$/, "");
|
|
13726
14177
|
const path4 = bridgeTokenPath(useScopedToken);
|
|
13727
|
-
|
|
13728
|
-
|
|
13729
|
-
|
|
13730
|
-
|
|
13731
|
-
|
|
13732
|
-
|
|
13733
|
-
(status) => status === 404 && !useScopedToken ? ` (the dedicated ${CLI_BOOTSTRAP_TOKEN_PATH} endpoint may not be deployed yet \u2014 retry with --use-scoped-token for the fallback path)` : ""
|
|
13734
|
-
);
|
|
14178
|
+
let url = `${base}${path4}?ttl=${encodeURIComponent(String(ttlSeconds))}&tenant=${encodeURIComponent(tenant)}`;
|
|
14179
|
+
if (contextId) url += `&context=${encodeURIComponent(contextId)}`;
|
|
14180
|
+
return fetchStToken(url, cognitoToken, fetchImpl, path4, (status) => {
|
|
14181
|
+
if (status !== 404 || useScopedToken || contextId) return "";
|
|
14182
|
+
return ` (the dedicated ${CLI_BOOTSTRAP_TOKEN_PATH} endpoint may not be deployed yet \u2014 retry with --use-scoped-token for the fallback path)`;
|
|
14183
|
+
});
|
|
13735
14184
|
}
|
|
13736
14185
|
async function mintContextToken(opts) {
|
|
13737
14186
|
const {
|
|
@@ -13995,7 +14444,12 @@ async function resolveAuthContext(opts) {
|
|
|
13995
14444
|
const baseUrl = envToBaseUrl(opts.env, opts.baseUrl ?? process.env.VECTROS_API_BASE_URL);
|
|
13996
14445
|
const cognitoToken = await resolveCognitoToken(opts.env, opts.token);
|
|
13997
14446
|
const tenant = opts.tenant ?? "live";
|
|
13998
|
-
const bridge = opts.provisioning ? await mintBridgeToken({
|
|
14447
|
+
const bridge = opts.provisioning ? await mintBridgeToken({
|
|
14448
|
+
cognitoToken,
|
|
14449
|
+
apiBaseUrl: baseUrl,
|
|
14450
|
+
tenant,
|
|
14451
|
+
...opts.context ? { contextId: opts.context } : {}
|
|
14452
|
+
}) : await mintContextToken({ cognitoToken, apiBaseUrl: baseUrl, contextId: opts.context ?? "default", tenant });
|
|
13999
14453
|
const client = new VectrosClient({ token: bridge.token, environment: baseUrl });
|
|
14000
14454
|
const identity = await resolveIdentity({ log: opts.log, apiKey: bridge.token, environment: baseUrl });
|
|
14001
14455
|
return {
|
|
@@ -14114,8 +14568,32 @@ var KNOWN_CONTROL_PLANE_RESOURCES = [
|
|
|
14114
14568
|
// control-plane, unlike per-namespace entity CRUD. (Cosmetic: any
|
|
14115
14569
|
// non-data-plane resource hard-rejects whether or not it is listed here;
|
|
14116
14570
|
// this list only sharpens the error message.)
|
|
14571
|
+
//
|
|
14572
|
+
// ⚠️ This bars `namespaces` as a SCOPE — no partner-authored role can ever
|
|
14573
|
+
// be granted it. It does NOT bar a blueprint's own top-level `namespaces[]`
|
|
14574
|
+
// FORMAT field (@vectros-ai/blueprints), which deliberately DOES write this
|
|
14575
|
+
// same registry — the same "DISCRETIONARY vs UNCONDITIONAL" distinction
|
|
14576
|
+
// {@link PARTIALLY_ADMITTED_OPS} draws for `entities`, applied here: the
|
|
14577
|
+
// write rides `provisioning:c` (non-grantable — no partner role can ever
|
|
14578
|
+
// hold it, the same bearer `keys`/`app-contexts` writes already require)
|
|
14579
|
+
// and the platform confines it to the blueprint's OWN context
|
|
14580
|
+
// unconditionally, never tenant-wide, so the reach is bounded to what the
|
|
14581
|
+
// blueprint already provisions, never the pack's say-so alone. See
|
|
14582
|
+
// `@vectros-ai/blueprints`' `namespaces[]` doc for the full design.
|
|
14117
14583
|
"namespaces"
|
|
14118
14584
|
];
|
|
14585
|
+
var PARTIALLY_ADMITTED_OPS = /* @__PURE__ */ new Map([
|
|
14586
|
+
["profiles", /* @__PURE__ */ new Set(["r", "u", "d"])]
|
|
14587
|
+
]);
|
|
14588
|
+
var SELF_QUALIFIED_OPS = /* @__PURE__ */ new Map([
|
|
14589
|
+
["profiles", /* @__PURE__ */ new Set(["c"])]
|
|
14590
|
+
]);
|
|
14591
|
+
var UNQUALIFIABLE_WITH_ANY_TAIL = /* @__PURE__ */ new Map([
|
|
14592
|
+
["profiles", /* @__PURE__ */ new Set(["r"])]
|
|
14593
|
+
]);
|
|
14594
|
+
var CAPABILITY_UNLOCKED_OPS = /* @__PURE__ */ new Map([
|
|
14595
|
+
["profiles", /* @__PURE__ */ new Map([["c", "member-lifecycle"]])]
|
|
14596
|
+
]);
|
|
14119
14597
|
var VALID_OPS = /* @__PURE__ */ new Set(["c", "r", "u", "d"]);
|
|
14120
14598
|
var DATA_PLANE_SET = new Set(DATA_PLANE_ALLOWLIST);
|
|
14121
14599
|
var CONTROL_PLANE_SET = new Set(KNOWN_CONTROL_PLANE_RESOURCES);
|
|
@@ -14132,7 +14610,60 @@ ${summary}`
|
|
|
14132
14610
|
this.rejected = rejected;
|
|
14133
14611
|
}
|
|
14134
14612
|
};
|
|
14135
|
-
|
|
14613
|
+
var GRANTED_CAPABILITIES_ALLOWLIST = ["member-lifecycle"];
|
|
14614
|
+
var GRANTED_CAPABILITIES_SET = new Set(GRANTED_CAPABILITIES_ALLOWLIST);
|
|
14615
|
+
var CAPABILITIES_HINT = GRANTED_CAPABILITIES_ALLOWLIST.join("/");
|
|
14616
|
+
var CapabilityGateError = class extends Error {
|
|
14617
|
+
rejected;
|
|
14618
|
+
constructor(rejected) {
|
|
14619
|
+
const summary = rejected.map((r) => ` \u2022 ${r.action}: ${r.reason}`).join("\n");
|
|
14620
|
+
super(
|
|
14621
|
+
`Capability gate rejected ${rejected.length} requested capability(ies); the bootstrap loader grants (${CAPABILITIES_HINT}) only and mints nothing on reject:
|
|
14622
|
+
${summary}`
|
|
14623
|
+
);
|
|
14624
|
+
this.name = "CapabilityGateError";
|
|
14625
|
+
this.rejected = rejected;
|
|
14626
|
+
}
|
|
14627
|
+
};
|
|
14628
|
+
function evaluateCapabilityToken(name) {
|
|
14629
|
+
const token = name.trim();
|
|
14630
|
+
if (token.length === 0) {
|
|
14631
|
+
return { action: name, reason: "empty capability name" };
|
|
14632
|
+
}
|
|
14633
|
+
if (!GRANTED_CAPABILITIES_SET.has(token)) {
|
|
14634
|
+
return {
|
|
14635
|
+
action: name,
|
|
14636
|
+
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`
|
|
14637
|
+
};
|
|
14638
|
+
}
|
|
14639
|
+
return null;
|
|
14640
|
+
}
|
|
14641
|
+
function evaluateCapabilityGate(capabilities) {
|
|
14642
|
+
const accepted = [];
|
|
14643
|
+
const rejected = [];
|
|
14644
|
+
for (const name of capabilities ?? []) {
|
|
14645
|
+
const rejection = evaluateCapabilityToken(name);
|
|
14646
|
+
if (rejection) {
|
|
14647
|
+
rejected.push(rejection);
|
|
14648
|
+
} else {
|
|
14649
|
+
accepted.push(name);
|
|
14650
|
+
}
|
|
14651
|
+
}
|
|
14652
|
+
return { ok: rejected.length === 0, accepted, rejected };
|
|
14653
|
+
}
|
|
14654
|
+
function assertCapabilityGate(capabilities) {
|
|
14655
|
+
const result = evaluateCapabilityGate(capabilities);
|
|
14656
|
+
if (!result.ok) {
|
|
14657
|
+
throw new CapabilityGateError(result.rejected);
|
|
14658
|
+
}
|
|
14659
|
+
}
|
|
14660
|
+
function firstInvalidOp(ops) {
|
|
14661
|
+
for (const ch of ops) {
|
|
14662
|
+
if (!VALID_OPS.has(ch)) return ch;
|
|
14663
|
+
}
|
|
14664
|
+
return void 0;
|
|
14665
|
+
}
|
|
14666
|
+
function evaluateScopeToken(action, capabilities) {
|
|
14136
14667
|
const token = action.trim();
|
|
14137
14668
|
if (token.length === 0) {
|
|
14138
14669
|
return { action, reason: "empty scope token" };
|
|
@@ -14153,6 +14684,51 @@ function evaluateScopeToken(action) {
|
|
|
14153
14684
|
};
|
|
14154
14685
|
}
|
|
14155
14686
|
if (!DATA_PLANE_SET.has(resource)) {
|
|
14687
|
+
const admittedOps = PARTIALLY_ADMITTED_OPS.get(resource);
|
|
14688
|
+
const selfOnlyOps = SELF_QUALIFIED_OPS.get(resource);
|
|
14689
|
+
if (admittedOps || selfOnlyOps) {
|
|
14690
|
+
const admittedHint = [.../* @__PURE__ */ new Set([...admittedOps ?? [], ...selfOnlyOps ?? []])].sort().join("/");
|
|
14691
|
+
if (ops === "*") {
|
|
14692
|
+
return {
|
|
14693
|
+
action,
|
|
14694
|
+
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`
|
|
14695
|
+
};
|
|
14696
|
+
}
|
|
14697
|
+
const invalidOp2 = firstInvalidOp(ops);
|
|
14698
|
+
if (invalidOp2) {
|
|
14699
|
+
return {
|
|
14700
|
+
action,
|
|
14701
|
+
reason: `invalid operation character '${invalidOp2}' (valid operations are c, r, u, d, or '*')`
|
|
14702
|
+
};
|
|
14703
|
+
}
|
|
14704
|
+
const qualifier = parts.length > 2 ? parts.slice(2).join(":") : void 0;
|
|
14705
|
+
const isSelfQualified = qualifier === "self";
|
|
14706
|
+
const unqualifiableOps = UNQUALIFIABLE_WITH_ANY_TAIL.get(resource);
|
|
14707
|
+
for (const ch of ops) {
|
|
14708
|
+
if (qualifier !== void 0 && unqualifiableOps?.has(ch)) {
|
|
14709
|
+
return {
|
|
14710
|
+
action,
|
|
14711
|
+
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).`
|
|
14712
|
+
};
|
|
14713
|
+
}
|
|
14714
|
+
if (admittedOps?.has(ch)) continue;
|
|
14715
|
+
if (selfOnlyOps?.has(ch)) {
|
|
14716
|
+
if (isSelfQualified) continue;
|
|
14717
|
+
const unlockCapability = CAPABILITY_UNLOCKED_OPS.get(resource)?.get(ch);
|
|
14718
|
+
if (unlockCapability && qualifier === void 0 && capabilities?.includes(unlockCapability)) continue;
|
|
14719
|
+
const capabilityHintApplies = unlockCapability && !capabilities?.includes(unlockCapability);
|
|
14720
|
+
return {
|
|
14721
|
+
action,
|
|
14722
|
+
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` + (capabilityHintApplies ? `, or this SAME clause also declaring 'capabilities: [${unlockCapability}]'` : unlockCapability ? ` (declaring 'capabilities: [${unlockCapability}]' does not help here \u2014 that capability only unlocks the BARE, unqualified form, never a literal-qualified one like this)` : "") + `. That operation must otherwise be done deliberately in the developer portal, never via the bootstrap loader`
|
|
14723
|
+
};
|
|
14724
|
+
}
|
|
14725
|
+
return {
|
|
14726
|
+
action,
|
|
14727
|
+
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`
|
|
14728
|
+
};
|
|
14729
|
+
}
|
|
14730
|
+
return null;
|
|
14731
|
+
}
|
|
14156
14732
|
if (CONTROL_PLANE_SET.has(resource)) {
|
|
14157
14733
|
return {
|
|
14158
14734
|
action,
|
|
@@ -14167,21 +14743,20 @@ function evaluateScopeToken(action) {
|
|
|
14167
14743
|
if (ops === "*") {
|
|
14168
14744
|
return null;
|
|
14169
14745
|
}
|
|
14170
|
-
|
|
14171
|
-
|
|
14172
|
-
|
|
14173
|
-
|
|
14174
|
-
|
|
14175
|
-
|
|
14176
|
-
}
|
|
14746
|
+
const invalidOp = firstInvalidOp(ops);
|
|
14747
|
+
if (invalidOp) {
|
|
14748
|
+
return {
|
|
14749
|
+
action,
|
|
14750
|
+
reason: `invalid operation character '${invalidOp}' (valid operations are c, r, u, d, or '*')`
|
|
14751
|
+
};
|
|
14177
14752
|
}
|
|
14178
14753
|
return null;
|
|
14179
14754
|
}
|
|
14180
|
-
function evaluateScopeGate(allowedActions) {
|
|
14755
|
+
function evaluateScopeGate(allowedActions, capabilities) {
|
|
14181
14756
|
const accepted = [];
|
|
14182
14757
|
const rejected = [];
|
|
14183
14758
|
for (const action of allowedActions) {
|
|
14184
|
-
const rejection = evaluateScopeToken(action);
|
|
14759
|
+
const rejection = evaluateScopeToken(action, capabilities);
|
|
14185
14760
|
if (rejection) {
|
|
14186
14761
|
rejected.push(rejection);
|
|
14187
14762
|
} else {
|
|
@@ -14190,12 +14765,34 @@ function evaluateScopeGate(allowedActions) {
|
|
|
14190
14765
|
}
|
|
14191
14766
|
return { ok: rejected.length === 0, accepted, rejected };
|
|
14192
14767
|
}
|
|
14193
|
-
function assertScopeGate(allowedActions) {
|
|
14194
|
-
const result = evaluateScopeGate(allowedActions);
|
|
14768
|
+
function assertScopeGate(allowedActions, capabilities) {
|
|
14769
|
+
const result = evaluateScopeGate(allowedActions, capabilities);
|
|
14195
14770
|
if (!result.ok) {
|
|
14196
14771
|
throw new ScopeGateError(result.rejected);
|
|
14197
14772
|
}
|
|
14198
14773
|
}
|
|
14774
|
+
function assertBlueprintGates(blueprint) {
|
|
14775
|
+
if (blueprint.accessProfile.allowedActions !== void 0) {
|
|
14776
|
+
assertScopeGate(blueprint.accessProfile.allowedActions, blueprint.accessProfile.capabilities);
|
|
14777
|
+
assertCapabilityGate(blueprint.accessProfile.capabilities);
|
|
14778
|
+
}
|
|
14779
|
+
for (const clauses of Object.values(blueprint.roles ?? {})) {
|
|
14780
|
+
for (const clause of clauses) {
|
|
14781
|
+
assertScopeGate(clause.allowedActions, clause.capabilities);
|
|
14782
|
+
assertCapabilityGate(clause.capabilities);
|
|
14783
|
+
}
|
|
14784
|
+
}
|
|
14785
|
+
}
|
|
14786
|
+
function effectiveAccessProfileActions(blueprint) {
|
|
14787
|
+
if (blueprint.accessProfile.allowedActions !== void 0) {
|
|
14788
|
+
return [...blueprint.accessProfile.allowedActions];
|
|
14789
|
+
}
|
|
14790
|
+
const out = [];
|
|
14791
|
+
for (const roleId of blueprint.accessProfile.roleIds ?? []) {
|
|
14792
|
+
for (const clause of blueprint.roles?.[roleId] ?? []) out.push(...clause.allowedActions);
|
|
14793
|
+
}
|
|
14794
|
+
return out;
|
|
14795
|
+
}
|
|
14199
14796
|
|
|
14200
14797
|
// src/machine-id.ts
|
|
14201
14798
|
var import_node_fs = require("fs");
|
|
@@ -14300,6 +14897,70 @@ async function resolveApplyTimeIdentities(blueprint, client, log) {
|
|
|
14300
14897
|
function actionOf(created) {
|
|
14301
14898
|
return created ? "created" : "reconciled";
|
|
14302
14899
|
}
|
|
14900
|
+
async function registerNamespaceIdempotently(client, blueprintContextId, decl) {
|
|
14901
|
+
const contextIdForRequest = decl.tenantWide ? void 0 : blueprintContextId;
|
|
14902
|
+
const body = {
|
|
14903
|
+
namespace: decl.namespace,
|
|
14904
|
+
entityBacked: decl.entityBacked,
|
|
14905
|
+
specificityRank: decl.specificityRank,
|
|
14906
|
+
membershipRecordType: decl.membershipRecordType,
|
|
14907
|
+
membershipTargetField: decl.membershipTargetField,
|
|
14908
|
+
membershipLevelField: decl.membershipLevelField,
|
|
14909
|
+
membershipLevels: decl.membershipLevels
|
|
14910
|
+
};
|
|
14911
|
+
try {
|
|
14912
|
+
await client.registerNamespace({ contextId: contextIdForRequest, body });
|
|
14913
|
+
return "created";
|
|
14914
|
+
} catch (err) {
|
|
14915
|
+
const info = extractServerError(err);
|
|
14916
|
+
if (info.statusCode !== 400) throw err;
|
|
14917
|
+
if (decl.tenantWide) throw err;
|
|
14918
|
+
let existing;
|
|
14919
|
+
try {
|
|
14920
|
+
existing = await client.getNamespace({ namespace: decl.namespace, contextId: contextIdForRequest });
|
|
14921
|
+
} catch {
|
|
14922
|
+
throw err;
|
|
14923
|
+
}
|
|
14924
|
+
if (existing.contextId !== contextIdForRequest) throw err;
|
|
14925
|
+
const mismatch = describeNamespaceMismatch(decl, existing);
|
|
14926
|
+
if (mismatch) {
|
|
14927
|
+
throw new Error(
|
|
14928
|
+
`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.`
|
|
14929
|
+
);
|
|
14930
|
+
}
|
|
14931
|
+
return "reconciled";
|
|
14932
|
+
}
|
|
14933
|
+
}
|
|
14934
|
+
function describeNamespaceMismatch(decl, existing) {
|
|
14935
|
+
const mismatches = [];
|
|
14936
|
+
const declaredEntityBacked = decl.entityBacked ?? false;
|
|
14937
|
+
const existingEntityBacked = existing.entityBacked ?? false;
|
|
14938
|
+
if (declaredEntityBacked !== existingEntityBacked) {
|
|
14939
|
+
mismatches.push(
|
|
14940
|
+
`entityBacked: declared ${JSON.stringify(declaredEntityBacked)}, registered ${JSON.stringify(existingEntityBacked)}`
|
|
14941
|
+
);
|
|
14942
|
+
}
|
|
14943
|
+
const fields = [
|
|
14944
|
+
"specificityRank",
|
|
14945
|
+
"membershipRecordType",
|
|
14946
|
+
"membershipTargetField",
|
|
14947
|
+
"membershipLevelField"
|
|
14948
|
+
];
|
|
14949
|
+
for (const field of fields) {
|
|
14950
|
+
const declared = decl[field];
|
|
14951
|
+
const existingValue = existing[field];
|
|
14952
|
+
if (declared === void 0 && (existingValue === void 0 || existingValue === null)) continue;
|
|
14953
|
+
if (declared === existingValue) continue;
|
|
14954
|
+
mismatches.push(`${field}: declared ${JSON.stringify(declared)}, registered ${JSON.stringify(existingValue)}`);
|
|
14955
|
+
}
|
|
14956
|
+
const declaredLevels = decl.membershipLevels;
|
|
14957
|
+
const existingLevels = existing.membershipLevels;
|
|
14958
|
+
const levelsDiffer = declaredLevels === void 0 ? existingLevels != null && existingLevels.length > 0 : JSON.stringify([...declaredLevels].sort()) !== JSON.stringify([...existingLevels ?? []].sort());
|
|
14959
|
+
if (levelsDiffer) {
|
|
14960
|
+
mismatches.push(`membershipLevels: declared ${JSON.stringify(declaredLevels)}, registered ${JSON.stringify(existingLevels)}`);
|
|
14961
|
+
}
|
|
14962
|
+
return mismatches.length > 0 ? mismatches.join("; ") : null;
|
|
14963
|
+
}
|
|
14303
14964
|
var LostKeyError = class extends Error {
|
|
14304
14965
|
constructor(keyName) {
|
|
14305
14966
|
super(
|
|
@@ -14310,6 +14971,20 @@ var LostKeyError = class extends Error {
|
|
|
14310
14971
|
}
|
|
14311
14972
|
keyName;
|
|
14312
14973
|
};
|
|
14974
|
+
var ExistingPrincipalNotConfirmedError = class extends Error {
|
|
14975
|
+
constructor(contextId, userId, externalId) {
|
|
14976
|
+
super(
|
|
14977
|
+
`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.`
|
|
14978
|
+
);
|
|
14979
|
+
this.contextId = contextId;
|
|
14980
|
+
this.userId = userId;
|
|
14981
|
+
this.externalId = externalId;
|
|
14982
|
+
this.name = "ExistingPrincipalNotConfirmedError";
|
|
14983
|
+
}
|
|
14984
|
+
contextId;
|
|
14985
|
+
userId;
|
|
14986
|
+
externalId;
|
|
14987
|
+
};
|
|
14313
14988
|
function mapEnumValues(enumValues) {
|
|
14314
14989
|
if (!enumValues) return void 0;
|
|
14315
14990
|
return enumValues.map((value) => ({ value }));
|
|
@@ -14384,7 +15059,8 @@ function toCapabilities(capabilities) {
|
|
|
14384
15059
|
function toRoleScopeClauses(clauses) {
|
|
14385
15060
|
return clauses.map((c) => ({
|
|
14386
15061
|
allowed_actions: c.allowedActions,
|
|
14387
|
-
data_scope: c.dataScope
|
|
15062
|
+
data_scope: c.dataScope,
|
|
15063
|
+
granted_capabilities: c.capabilities
|
|
14388
15064
|
}));
|
|
14389
15065
|
}
|
|
14390
15066
|
var UnresolvedIdentitiesError = class extends Error {
|
|
@@ -14403,13 +15079,24 @@ function assertNoUnresolvedIdentities(blueprint) {
|
|
|
14403
15079
|
);
|
|
14404
15080
|
}
|
|
14405
15081
|
}
|
|
14406
|
-
|
|
14407
|
-
|
|
14408
|
-
|
|
14409
|
-
|
|
14410
|
-
for (const clause of clauses) assertScopeGate(clause.allowedActions);
|
|
15082
|
+
var TenantWideNamespacesNotAllowedError = class extends Error {
|
|
15083
|
+
constructor(message) {
|
|
15084
|
+
super(message);
|
|
15085
|
+
this.name = "TenantWideNamespacesNotAllowedError";
|
|
14411
15086
|
}
|
|
14412
|
-
|
|
15087
|
+
};
|
|
15088
|
+
function assertTenantWideNamespacesAllowed(blueprint, allow) {
|
|
15089
|
+
if (allow) return;
|
|
15090
|
+
const names = (blueprint.namespaces ?? []).filter((ns) => ns.tenantWide === true).map((ns) => ns.namespace);
|
|
15091
|
+
if (names.length === 0) return;
|
|
15092
|
+
throw new TenantWideNamespacesNotAllowedError(
|
|
15093
|
+
`Blueprint declares namespace${names.length > 1 ? "s" : ""} ${names.map((n) => `'${n}'`).join(", ")} as tenantWide: true \u2014 visible to every context in the account, not just this blueprint's own. Refusing to request that registration without an explicit opt-in. Re-run with --allow-tenant-wide-namespaces once you've confirmed that's actually intended (only an account OWNER's credential can complete it either way \u2014 this flag doesn't change who's allowed, only whether this run tries).`
|
|
15094
|
+
);
|
|
15095
|
+
}
|
|
15096
|
+
async function provisionContext(client, input) {
|
|
15097
|
+
const { blueprint, allowTenantWideNamespaces = false, readExistingKey, confirmExistingPrincipal, log } = input;
|
|
15098
|
+
assertBlueprintGates(blueprint);
|
|
15099
|
+
assertTenantWideNamespacesAllowed(blueprint, allowTenantWideNamespaces);
|
|
14413
15100
|
const ctxResp = await client.auth.createAppContext({ body: { contextId: blueprint.contextId, name: contextNameOf(blueprint) } });
|
|
14414
15101
|
const contextAction = actionOf(ctxResp.created);
|
|
14415
15102
|
log.debug({ component: "loader", contextId: blueprint.contextId, contextAction }, "app-context ensured");
|
|
@@ -14426,10 +15113,77 @@ async function runLoader(client, input) {
|
|
|
14426
15113
|
if (!userId) {
|
|
14427
15114
|
throw new Error("Service-principal create returned no user id; cannot bind AccessProfile/key.");
|
|
14428
15115
|
}
|
|
14429
|
-
|
|
15116
|
+
const userAction = actionOf(user.created);
|
|
15117
|
+
log.debug(
|
|
15118
|
+
{ component: "loader", externalId: blueprint.servicePrincipal.externalId, userId, userAction },
|
|
15119
|
+
"service principal ensured"
|
|
15120
|
+
);
|
|
15121
|
+
const localKeyForContext = await readExistingKey(blueprint.contextId);
|
|
15122
|
+
const storedPrincipalId = localKeyForContext?.principalId;
|
|
15123
|
+
const localKeyNamesThisPrincipal = storedPrincipalId === `usr_${userId}` || storedPrincipalId === userId;
|
|
15124
|
+
if (userAction === "reconciled" && !localKeyNamesThisPrincipal) {
|
|
15125
|
+
const proceed = confirmExistingPrincipal ? await confirmExistingPrincipal({
|
|
15126
|
+
contextId: blueprint.contextId,
|
|
15127
|
+
userId,
|
|
15128
|
+
externalId: blueprint.servicePrincipal.externalId
|
|
15129
|
+
}) : true;
|
|
15130
|
+
if (!proceed) {
|
|
15131
|
+
throw new ExistingPrincipalNotConfirmedError(blueprint.contextId, userId, blueprint.servicePrincipal.externalId);
|
|
15132
|
+
}
|
|
15133
|
+
log.debug(
|
|
15134
|
+
{ component: "loader", contextId: blueprint.contextId, userId },
|
|
15135
|
+
"existing-principal collision confirmed (or unwired) \u2014 proceeding with any further write to this principal"
|
|
15136
|
+
);
|
|
15137
|
+
}
|
|
15138
|
+
const needsContextPinnedBootstrap = (blueprint.namespaces ?? []).length > 0;
|
|
15139
|
+
const ctxBootstrapClient = needsContextPinnedBootstrap ? input.remintBootstrapForContext ? await input.remintBootstrapForContext(blueprint.contextId) : client : void 0;
|
|
15140
|
+
if (ctxBootstrapClient && input.remintBootstrapForContext) {
|
|
15141
|
+
log.debug({ component: "loader", contextId: blueprint.contextId }, "re-minted context-pinned bootstrap client (namespaces)");
|
|
15142
|
+
}
|
|
15143
|
+
const namespaceActions = [];
|
|
15144
|
+
for (const ns of blueprint.namespaces ?? []) {
|
|
15145
|
+
const action = await registerNamespaceIdempotently(ctxBootstrapClient.identity, blueprint.contextId, ns);
|
|
15146
|
+
namespaceActions.push({ namespace: ns.namespace, action });
|
|
15147
|
+
log.debug({ component: "loader", namespace: ns.namespace, contextId: blueprint.contextId, action }, "namespace ensured");
|
|
15148
|
+
}
|
|
15149
|
+
return { contextAction, servicePrincipalUserId: userId, userAction, namespaceActions, ctxBootstrapClient };
|
|
15150
|
+
}
|
|
15151
|
+
async function runLoader(client, input) {
|
|
15152
|
+
const {
|
|
15153
|
+
blueprint,
|
|
15154
|
+
tenantId,
|
|
15155
|
+
machineId,
|
|
15156
|
+
rotate = false,
|
|
15157
|
+
skipSeed = false,
|
|
15158
|
+
allowTenantWideNamespaces = false,
|
|
15159
|
+
readExistingKey,
|
|
15160
|
+
persistMintedKey,
|
|
15161
|
+
confirmExistingPrincipal,
|
|
15162
|
+
log
|
|
15163
|
+
} = input;
|
|
15164
|
+
assertNoUnresolvedIdentities(blueprint);
|
|
15165
|
+
const provisioned = input.provisioned ?? await provisionContext(client, {
|
|
15166
|
+
blueprint,
|
|
15167
|
+
allowTenantWideNamespaces,
|
|
15168
|
+
remintBootstrapForContext: input.remintBootstrapForContext,
|
|
15169
|
+
readExistingKey,
|
|
15170
|
+
confirmExistingPrincipal,
|
|
15171
|
+
log
|
|
15172
|
+
});
|
|
15173
|
+
const { contextAction, servicePrincipalUserId: userId, userAction, namespaceActions } = provisioned;
|
|
15174
|
+
assertBlueprintGates(blueprint);
|
|
15175
|
+
const needsContextPinnedBootstrapForIssuers = (blueprint.issuers ?? []).length > 0;
|
|
15176
|
+
const ctxBootstrapClient = provisioned.ctxBootstrapClient ?? (needsContextPinnedBootstrapForIssuers ? input.remintBootstrapForContext ? await input.remintBootstrapForContext(blueprint.contextId) : client : void 0);
|
|
15177
|
+
if (ctxBootstrapClient && !provisioned.ctxBootstrapClient && input.remintBootstrapForContext) {
|
|
15178
|
+
log.debug({ component: "loader", contextId: blueprint.contextId }, "re-minted context-pinned bootstrap client (issuers)");
|
|
15179
|
+
}
|
|
14430
15180
|
const issuerActions = [];
|
|
14431
15181
|
for (const issuer of blueprint.issuers ?? []) {
|
|
14432
|
-
const
|
|
15182
|
+
const selfSignupPolicies = issuer.selfSignupPolicies?.map((p) => ({
|
|
15183
|
+
signup_type: p.signupType,
|
|
15184
|
+
role_id: p.roleId
|
|
15185
|
+
}));
|
|
15186
|
+
const issuerResp = await ctxBootstrapClient.auth.registerIssuer({
|
|
14433
15187
|
issuerId: issuer.issuerId,
|
|
14434
15188
|
issuer: issuer.issuer,
|
|
14435
15189
|
jwksUri: issuer.jwksUri,
|
|
@@ -14437,18 +15191,26 @@ async function runLoader(client, input) {
|
|
|
14437
15191
|
contextId: issuer.contextId,
|
|
14438
15192
|
subClaim: issuer.subClaim,
|
|
14439
15193
|
emailClaim: issuer.emailClaim,
|
|
14440
|
-
|
|
14441
|
-
|
|
14442
|
-
role_id: p.roleId
|
|
14443
|
-
}))
|
|
15194
|
+
userinfoUri: issuer.userinfoUri,
|
|
15195
|
+
selfSignupPolicies
|
|
14444
15196
|
});
|
|
14445
15197
|
const action = actionOf(issuerResp.created);
|
|
14446
15198
|
issuerActions.push({ issuerId: issuer.issuerId, action });
|
|
14447
15199
|
if (action === "reconciled") {
|
|
14448
|
-
|
|
14449
|
-
|
|
14450
|
-
|
|
14451
|
-
|
|
15200
|
+
await ctxBootstrapClient.auth.updateIssuer({
|
|
15201
|
+
issuerId: issuer.issuerId,
|
|
15202
|
+
// Trust-anchor/routing-pin fields — echoed back, never actually
|
|
15203
|
+
// changed here (the platform rejects a differing value).
|
|
15204
|
+
issuer: issuer.issuer,
|
|
15205
|
+
jwksUri: issuer.jwksUri,
|
|
15206
|
+
audience: issuer.audience,
|
|
15207
|
+
contextId: issuer.contextId,
|
|
15208
|
+
subClaim: issuer.subClaim,
|
|
15209
|
+
emailClaim: issuer.emailClaim,
|
|
15210
|
+
userinfoUri: issuer.userinfoUri,
|
|
15211
|
+
selfSignupPolicies
|
|
15212
|
+
});
|
|
15213
|
+
log.debug({ component: "loader", issuerId: issuer.issuerId }, "issuer already registered \u2014 reconciled safe fields to the declared values");
|
|
14452
15214
|
} else {
|
|
14453
15215
|
log.debug({ component: "loader", issuerId: issuer.issuerId, action }, "issuer registered");
|
|
14454
15216
|
}
|
|
@@ -14488,10 +15250,35 @@ async function runLoader(client, input) {
|
|
|
14488
15250
|
schemaActions.push({ typeName: s.typeName, action: actionOf(resp.created) });
|
|
14489
15251
|
log.debug({ component: "loader", typeName: s.typeName, schemaId: resp.id, action: actionOf(resp.created) }, "schema ensured (upsert)");
|
|
14490
15252
|
}
|
|
14491
|
-
const
|
|
15253
|
+
const roleIds = [];
|
|
15254
|
+
const roleActions = [];
|
|
15255
|
+
for (const [roleId, clauses] of Object.entries(blueprint.roles ?? {})) {
|
|
15256
|
+
const roleResp = await ctxClient.auth.createRole({
|
|
15257
|
+
contextId: blueprint.contextId,
|
|
15258
|
+
upsert: true,
|
|
15259
|
+
body: {
|
|
15260
|
+
roleId,
|
|
15261
|
+
// Blueprint roles carry no display name → default it to the roleId.
|
|
15262
|
+
name: roleId,
|
|
15263
|
+
scopes: toRoleScopeClauses(clauses),
|
|
15264
|
+
assumable: blueprint.roleAssumable?.[roleId]
|
|
15265
|
+
}
|
|
15266
|
+
});
|
|
15267
|
+
roleIds.push(roleId);
|
|
15268
|
+
roleActions.push({ roleId, action: actionOf(roleResp.created) });
|
|
15269
|
+
log.debug({ component: "loader", roleId, clauses: clauses.length, action: actionOf(roleResp.created) }, "role ensured (upsert)");
|
|
15270
|
+
}
|
|
15271
|
+
const profileBody = blueprint.accessProfile.roleIds ? {
|
|
15272
|
+
principalId: `usr_${userId}`,
|
|
15273
|
+
roleIds: blueprint.accessProfile.roleIds,
|
|
15274
|
+
identityOverrides: blueprint.accessProfile.identityOverrides
|
|
15275
|
+
} : {
|
|
14492
15276
|
principalId: `usr_${userId}`,
|
|
14493
15277
|
scopes: [
|
|
14494
15278
|
{
|
|
15279
|
+
// Non-null: this is the `else` branch of the roleIds XOR above —
|
|
15280
|
+
// `@vectros-ai/blueprints`' BlueprintAccessProfileSchema guarantees
|
|
15281
|
+
// allowedActions is set whenever roleIds is not.
|
|
14495
15282
|
allowed_actions: blueprint.accessProfile.allowedActions,
|
|
14496
15283
|
// Pass dataScope straight through to the wire. The blueprint type is
|
|
14497
15284
|
// Record<string, (string|null)[]> — a `null` element is the NULL
|
|
@@ -14499,7 +15286,10 @@ async function runLoader(client, input) {
|
|
|
14499
15286
|
// literal JSON null, which is exactly what the platform's scope
|
|
14500
15287
|
// matcher tests via `allowedValues.contains(null)`. The cast bridges
|
|
14501
15288
|
// to the SDK's loosely-typed Record<string, Record<string,unknown>>.
|
|
14502
|
-
data_scope: blueprint.accessProfile.dataScope
|
|
15289
|
+
data_scope: blueprint.accessProfile.dataScope,
|
|
15290
|
+
// Already gate-validated (assertBlueprintGates, above this function's
|
|
15291
|
+
// call site) before this body is ever built.
|
|
15292
|
+
granted_capabilities: blueprint.accessProfile.capabilities
|
|
14503
15293
|
}
|
|
14504
15294
|
],
|
|
14505
15295
|
// Optional identity overrides — scope values the service key STAMPS onto
|
|
@@ -14522,22 +15312,6 @@ async function runLoader(client, input) {
|
|
|
14522
15312
|
body: profileBody
|
|
14523
15313
|
});
|
|
14524
15314
|
log.debug({ component: "loader", principalId: `usr_${userId}`, accessProfileId: profile.id, profileAction }, "access profile ensured + reconciled to declared scopes");
|
|
14525
|
-
const roleIds = [];
|
|
14526
|
-
const roleActions = [];
|
|
14527
|
-
for (const [roleId, clauses] of Object.entries(blueprint.roles ?? {})) {
|
|
14528
|
-
const roleResp = await ctxClient.auth.createRole({
|
|
14529
|
-
contextId: blueprint.contextId,
|
|
14530
|
-
body: {
|
|
14531
|
-
roleId,
|
|
14532
|
-
// Blueprint roles carry no display name → default it to the roleId.
|
|
14533
|
-
name: roleId,
|
|
14534
|
-
scopes: toRoleScopeClauses(clauses)
|
|
14535
|
-
}
|
|
14536
|
-
});
|
|
14537
|
-
roleIds.push(roleId);
|
|
14538
|
-
roleActions.push({ roleId, action: actionOf(roleResp.created) });
|
|
14539
|
-
log.debug({ component: "loader", roleId, clauses: clauses.length, action: actionOf(roleResp.created) }, "role ensured");
|
|
14540
|
-
}
|
|
14541
15315
|
const keyName = keyNameFor(blueprint.name, machineId);
|
|
14542
15316
|
let keyAction = "created";
|
|
14543
15317
|
let revokedKeyId;
|
|
@@ -14557,7 +15331,7 @@ async function runLoader(client, input) {
|
|
|
14557
15331
|
keyId = keyResp.keyId ?? "";
|
|
14558
15332
|
rawKey = keyResp.rawKey;
|
|
14559
15333
|
if (keyId) {
|
|
14560
|
-
await persistMintedKey(blueprint.contextId, { keyId, rawKey, keyName, principalId: userId
|
|
15334
|
+
await persistMintedKey(blueprint.contextId, { keyId, rawKey, keyName, principalId: `usr_${userId}`, revokedKeyId });
|
|
14561
15335
|
}
|
|
14562
15336
|
if (keyAction !== "rotated") keyAction = "created";
|
|
14563
15337
|
} else {
|
|
@@ -14631,10 +15405,12 @@ async function runLoader(client, input) {
|
|
|
14631
15405
|
keyName,
|
|
14632
15406
|
keyAction,
|
|
14633
15407
|
contextAction,
|
|
15408
|
+
userAction,
|
|
14634
15409
|
profileAction,
|
|
14635
15410
|
schemaActions,
|
|
14636
15411
|
roleActions,
|
|
14637
15412
|
issuerActions,
|
|
15413
|
+
namespaceActions,
|
|
14638
15414
|
rawKey,
|
|
14639
15415
|
keyId,
|
|
14640
15416
|
revokedKeyId,
|
|
@@ -14646,6 +15422,11 @@ async function runLoader(client, input) {
|
|
|
14646
15422
|
seedIndexFailures
|
|
14647
15423
|
};
|
|
14648
15424
|
}
|
|
15425
|
+
function renderPlanDataScope(ds) {
|
|
15426
|
+
const entries = Object.entries(ds ?? {});
|
|
15427
|
+
if (entries.length === 0) return "(none \u2014 unconstrained)";
|
|
15428
|
+
return entries.map(([dim, values]) => `${dim}=[${values.map((v) => v === null ? "null" : v).join(", ")}]`).join(", ");
|
|
15429
|
+
}
|
|
14649
15430
|
function buildPlan(blueprint, opts = {}) {
|
|
14650
15431
|
const steps = [];
|
|
14651
15432
|
steps.push({ kind: "context", summary: `context '${blueprint.contextId}' (${contextNameOf(blueprint)})` });
|
|
@@ -14653,6 +15434,15 @@ function buildPlan(blueprint, opts = {}) {
|
|
|
14653
15434
|
kind: "principal",
|
|
14654
15435
|
summary: `principal '${blueprint.servicePrincipal.externalId}' (SERVICE)`
|
|
14655
15436
|
});
|
|
15437
|
+
for (const ns of blueprint.namespaces ?? []) {
|
|
15438
|
+
const ownerNote = ns.tenantWide ? "TENANT-WIDE \u2014 visible to every context" : `owned by '${blueprint.contextId}'`;
|
|
15439
|
+
const entityNote = ns.entityBacked ? ", entity-backed" : "";
|
|
15440
|
+
const membershipNote = ns.membershipRecordType ? `, membership: ${ns.membershipRecordType}.${ns.membershipTargetField}` + (ns.membershipLevelField ? ` (levels: ${ns.membershipLevels?.join("/")})` : "") : "";
|
|
15441
|
+
steps.push({
|
|
15442
|
+
kind: "namespace",
|
|
15443
|
+
summary: `namespace '${ns.namespace}' (${ownerNote}${entityNote}${membershipNote}) [${BLUEPRINT_FIELD_PHASES.namespaces}-token phase]`
|
|
15444
|
+
});
|
|
15445
|
+
}
|
|
14656
15446
|
for (const iss of blueprint.issuers ?? []) {
|
|
14657
15447
|
const selfSignup = iss.selfSignupPolicies?.length ? `, self-signup: ${iss.selfSignupPolicies.length} polic${iss.selfSignupPolicies.length === 1 ? "y" : "ies"}` : "";
|
|
14658
15448
|
steps.push({
|
|
@@ -14673,17 +15463,40 @@ function buildPlan(blueprint, opts = {}) {
|
|
|
14673
15463
|
summary: `schema '${s.typeName}' (${s.fields.length} field${s.fields.length === 1 ? "" : "s"}, ${indexNote})${lookupNote}${surfaceNote}${sensitiveNote}`
|
|
14674
15464
|
});
|
|
14675
15465
|
}
|
|
14676
|
-
const actions = blueprint.accessProfile.allowedActions;
|
|
14677
|
-
const scopeNote = blueprint.accessProfile.dataScope && Object.keys(blueprint.accessProfile.dataScope).length ? ` dataScope: ${Object.keys(blueprint.accessProfile.dataScope).join(", ")}` : "";
|
|
14678
|
-
steps.push({
|
|
14679
|
-
kind: "profile",
|
|
14680
|
-
summary: `profile scopes [${actions.join(", ")}] (data-plane \u2713)${scopeNote}`
|
|
14681
|
-
});
|
|
14682
15466
|
for (const [roleId, clauses] of Object.entries(blueprint.roles ?? {})) {
|
|
14683
15467
|
const actionSet = [...new Set(clauses.flatMap((c) => c.allowedActions))];
|
|
15468
|
+
const capSet = [...new Set(clauses.flatMap((c) => c.capabilities ?? []))];
|
|
15469
|
+
const roleCapabilityNote = capSet.length ? `, capabilities [${capSet.join(", ")}]` : "";
|
|
15470
|
+
const clauseLines = clauses.map((c, i) => {
|
|
15471
|
+
const capNote = c.capabilities?.length ? `, capabilities [${c.capabilities.join(", ")}]` : "";
|
|
15472
|
+
return ` clause ${i + 1}: actions [${c.allowedActions.join(", ")}], dataScope: ${renderPlanDataScope(c.dataScope)}${capNote}`;
|
|
15473
|
+
});
|
|
15474
|
+
const assumable = blueprint.roleAssumable?.[roleId];
|
|
15475
|
+
const assumableLine = assumable ? `
|
|
15476
|
+
assumable: ${renderPlanDataScope(assumable)}` : "";
|
|
14684
15477
|
steps.push({
|
|
14685
15478
|
kind: "role",
|
|
14686
|
-
summary: `role '${roleId}' (${clauses.length} clause${clauses.length === 1 ? "" : "s"}, actions [${actionSet.join(", ")}])
|
|
15479
|
+
summary: `role '${roleId}' (${clauses.length} clause${clauses.length === 1 ? "" : "s"}, actions [${actionSet.join(", ")}]${roleCapabilityNote})
|
|
15480
|
+
` + clauseLines.join("\n") + assumableLine
|
|
15481
|
+
});
|
|
15482
|
+
}
|
|
15483
|
+
const roleIdsNote = blueprint.accessProfile.roleIds;
|
|
15484
|
+
if (roleIdsNote) {
|
|
15485
|
+
steps.push({
|
|
15486
|
+
kind: "profile",
|
|
15487
|
+
summary: `profile via role(s) [${roleIdsNote.join(", ")}] (composed \u2014 each role's own clauses, concatenated in order)`
|
|
15488
|
+
});
|
|
15489
|
+
} else {
|
|
15490
|
+
const actions = blueprint.accessProfile.allowedActions ?? [];
|
|
15491
|
+
const scopeNote = blueprint.accessProfile.dataScope && Object.keys(blueprint.accessProfile.dataScope).length ? ` dataScope: ${Object.keys(blueprint.accessProfile.dataScope).join(", ")}` : "";
|
|
15492
|
+
const capabilityNote = blueprint.accessProfile.capabilities?.length ? ` capabilities: [${blueprint.accessProfile.capabilities.join(", ")}]` : "";
|
|
15493
|
+
const allDataPlane = !actions.some(
|
|
15494
|
+
(a) => KNOWN_CONTROL_PLANE_RESOURCES.includes(a.split(":", 1)[0])
|
|
15495
|
+
);
|
|
15496
|
+
const planeNote = allDataPlane ? " (data-plane \u2713)" : " (includes a control-plane exception)";
|
|
15497
|
+
steps.push({
|
|
15498
|
+
kind: "profile",
|
|
15499
|
+
summary: `profile scopes [${actions.join(", ")}]${planeNote}${scopeNote}${capabilityNote}`
|
|
14687
15500
|
});
|
|
14688
15501
|
}
|
|
14689
15502
|
steps.push({ kind: "key", summary: `key ${blueprint.name}-<machine> (mint 1 scoped ssk_*)` });
|
|
@@ -14706,10 +15519,25 @@ function renderPlan(blueprint, steps) {
|
|
|
14706
15519
|
];
|
|
14707
15520
|
if (steps.some((s) => s.kind === "issuer")) {
|
|
14708
15521
|
legendLines.push(
|
|
14709
|
-
` Exception:
|
|
14710
|
-
`
|
|
15522
|
+
` Exception: an issuer's trust-anchor fields (issuer, jwksUri, audience,`,
|
|
15523
|
+
` contextId) are immutable once registered \u2014 only subClaim/emailClaim/`,
|
|
15524
|
+
` userinfoUri/selfSignupPolicies actually reconcile on a re-apply (status is not`,
|
|
15525
|
+
` authorable in a blueprint at all).`
|
|
14711
15526
|
);
|
|
14712
15527
|
}
|
|
15528
|
+
if (steps.some((s) => s.kind === "namespace")) {
|
|
15529
|
+
legendLines.push(
|
|
15530
|
+
` Exception: namespace registration is not idempotent \u2014 a re-apply matching`,
|
|
15531
|
+
` an existing registration converges silently, but one that disagrees FAILS`,
|
|
15532
|
+
` the apply rather than reconciling it.`
|
|
15533
|
+
);
|
|
15534
|
+
if (steps.some((s) => s.kind === "namespace" && /TENANT-WIDE/.test(s.summary))) {
|
|
15535
|
+
legendLines.push(
|
|
15536
|
+
` Stricter still for a TENANT-WIDE namespace: it never converges, even on an`,
|
|
15537
|
+
` exact match \u2014 any existing registration of that name fails the apply.`
|
|
15538
|
+
);
|
|
15539
|
+
}
|
|
15540
|
+
}
|
|
14713
15541
|
const legend = legendLines.join("\n");
|
|
14714
15542
|
const lines = steps.map((s) => ` + ${s.summary}`);
|
|
14715
15543
|
return [header, legend, "", ...lines].join("\n");
|
|
@@ -14958,6 +15786,15 @@ var OsKeychainSecretStore = class {
|
|
|
14958
15786
|
* against the real Windows store on every MR — the dev-machine probe is a point
|
|
14959
15787
|
* measurement, not a regression gate. (Tracked internally; this file ships to
|
|
14960
15788
|
* public npm, so no tracker number here.)
|
|
15789
|
+
*
|
|
15790
|
+
* ⚠️ THIS METHOD'S FAIL-CLOSED GUARANTEE IS AN UPSTREAM BEHAVIOURAL CONTRACT, NOT
|
|
15791
|
+
* SOMETHING THIS CODE ENFORCES. `@napi-rs/keyring` is pinned to an EXACT version
|
|
15792
|
+
* in `package.json` for exactly this reason: if a future release returned `[]`
|
|
15793
|
+
* from `findCredentials()` on a locked store instead of throwing, the `catch`
|
|
15794
|
+
* block above would never run — `!.some(...)` on an empty array reads as "gone",
|
|
15795
|
+
* `deleteSecret` returns `true`, and `remove`/`prune` drop the index row of a
|
|
15796
|
+
* still-live credential. A version bump of this dependency is a security review
|
|
15797
|
+
* of the paragraph above against the new release, not a routine dependency bump.
|
|
14961
15798
|
*/
|
|
14962
15799
|
async deleteSecret(ref) {
|
|
14963
15800
|
try {
|
|
@@ -15891,6 +16728,26 @@ The key itself WAS created server-side and is live \u2014 its id is ${entry.keyI
|
|
|
15891
16728
|
const entry = await this.getActive();
|
|
15892
16729
|
return entry ? this.readSecret(entry) : void 0;
|
|
15893
16730
|
}
|
|
16731
|
+
/**
|
|
16732
|
+
* The ACTIVE entry's tenant (test/live), or undefined if there is no active
|
|
16733
|
+
* entry or its tenant cannot be determined — WITHOUT migrating its secret.
|
|
16734
|
+
*
|
|
16735
|
+
* Deliberately NOT built on {@link resolveActiveSecret}/{@link readSecret}:
|
|
16736
|
+
* both migrate the secret to this machine's preferred store as a side effect
|
|
16737
|
+
* of reading it, and a caller that only wants to CLASSIFY the active identity
|
|
16738
|
+
* (e.g. "is it safe to activate a different one over it?") must not silently
|
|
16739
|
+
* relocate — and take the cross-process index lock for — a credential it has
|
|
16740
|
+
* no other reason to touch. Same non-migrating shape {@link assess} already
|
|
16741
|
+
* uses per entry (`store.getSecret` directly, never `readSecret`).
|
|
16742
|
+
*/
|
|
16743
|
+
async activeTenant() {
|
|
16744
|
+
const entry = await this.getActive();
|
|
16745
|
+
if (!entry) return void 0;
|
|
16746
|
+
const store = this.storeFor(entry);
|
|
16747
|
+
if (!store) return void 0;
|
|
16748
|
+
const secret = await store.getSecret(entry.secretRef);
|
|
16749
|
+
return secret !== void 0 ? keyTenant(secret) : void 0;
|
|
16750
|
+
}
|
|
15894
16751
|
/**
|
|
15895
16752
|
* Read one entry's secret, moving it to this machine's preferred store if it is
|
|
15896
16753
|
* somewhere else.
|
|
@@ -16017,26 +16874,31 @@ var readline = __toESM(require("readline/promises"));
|
|
|
16017
16874
|
async function runKeyringList(opts, deps = {}) {
|
|
16018
16875
|
try {
|
|
16019
16876
|
const keyring = deps.keyring ?? await Keyring.open();
|
|
16020
|
-
const entries = await keyring.list();
|
|
16021
|
-
const active = await keyring.activeAlias();
|
|
16022
16877
|
if (opts.json) {
|
|
16878
|
+
const entries = await keyring.list();
|
|
16879
|
+
const active = await keyring.activeAlias();
|
|
16023
16880
|
process.stdout.write(`${formatJson({ active, entries })}
|
|
16024
16881
|
`);
|
|
16025
16882
|
return 0;
|
|
16026
16883
|
}
|
|
16027
|
-
|
|
16884
|
+
const { health } = await assessEntries(keyring);
|
|
16885
|
+
if (health.length === 0) {
|
|
16028
16886
|
process.stdout.write("\n (no keyring entries yet \u2014 run `vectros join <context>` to add one)\n");
|
|
16029
16887
|
return 0;
|
|
16030
16888
|
}
|
|
16031
16889
|
process.stdout.write(
|
|
16032
|
-
"\n" + formatTable(
|
|
16033
|
-
{ header: "", get: (
|
|
16034
|
-
{ header: "alias", get: (
|
|
16035
|
-
{ header: "context", get: (
|
|
16036
|
-
{ header: "principal", get: (
|
|
16037
|
-
{ header: "role", get: (
|
|
16038
|
-
|
|
16039
|
-
|
|
16890
|
+
"\n" + formatTable(health, [
|
|
16891
|
+
{ header: "", get: (h) => h.active ? "*" : "" },
|
|
16892
|
+
{ header: "alias", get: (h) => h.entry.alias },
|
|
16893
|
+
{ header: "context", get: (h) => h.entry.context },
|
|
16894
|
+
{ header: "principal", get: (h) => h.entry.principalId },
|
|
16895
|
+
{ header: "role", get: (h) => h.entry.role },
|
|
16896
|
+
// `tenant` (test/live) replaces the old `env` (staging/production) column
|
|
16897
|
+
// here: env is an internal-team concern an external developer's CLI never
|
|
16898
|
+
// sees, while tenant is the distinction every user routinely holds two
|
|
16899
|
+
// credentials side by side over.
|
|
16900
|
+
{ header: "tenant", get: (h) => tenantCell(h, { upper: true }) },
|
|
16901
|
+
{ header: "key id", get: (h) => h.entry.keyId }
|
|
16040
16902
|
]) + "\n"
|
|
16041
16903
|
);
|
|
16042
16904
|
return 0;
|
|
@@ -16218,6 +17080,10 @@ async function assessEntries(keyring) {
|
|
|
16218
17080
|
}))
|
|
16219
17081
|
};
|
|
16220
17082
|
}
|
|
17083
|
+
function tenantCell(h, opts) {
|
|
17084
|
+
if (h.status !== "ok") return void 0;
|
|
17085
|
+
return h.tenant ?? (opts.upper ? "UNKNOWN" : "unknown");
|
|
17086
|
+
}
|
|
16221
17087
|
function describeEntry(h) {
|
|
16222
17088
|
return `${h.entry.alias} (${h.entry.context}, ${h.entry.keyId})`;
|
|
16223
17089
|
}
|
|
@@ -16362,9 +17228,7 @@ async function runKeyringDoctor(opts, deps = {}) {
|
|
|
16362
17228
|
// `unknown` can escalate; one that sees a confident wrong answer cannot.
|
|
16363
17229
|
store: storeKindOf(h.entry) ?? "unknown",
|
|
16364
17230
|
secret: h.status,
|
|
16365
|
-
|
|
16366
|
-
// live key" from "we could not read the shape of this one".
|
|
16367
|
-
tenant: h.status === "ok" ? h.tenant ?? "unknown" : void 0
|
|
17231
|
+
tenant: tenantCell(h, { upper: false })
|
|
16368
17232
|
})),
|
|
16369
17233
|
liveKeys: liveKeys.map((h) => h.entry.alias),
|
|
16370
17234
|
findings
|
|
@@ -16379,7 +17243,7 @@ async function runKeyringDoctor(opts, deps = {}) {
|
|
|
16379
17243
|
{ header: "", get: (h) => h.active ? "*" : "" },
|
|
16380
17244
|
{ header: "alias", get: (h) => h.entry.alias },
|
|
16381
17245
|
{ header: "context", get: (h) => h.entry.context },
|
|
16382
|
-
{ header: "tenant", get: (h) => h
|
|
17246
|
+
{ header: "tenant", get: (h) => tenantCell(h, { upper: true }) },
|
|
16383
17247
|
{ header: "key id", get: (h) => h.entry.keyId },
|
|
16384
17248
|
{ header: "secret", get: (h) => h.status === "ok" ? "ok" : h.status.toUpperCase() }
|
|
16385
17249
|
]) + "\n"
|
|
@@ -16573,6 +17437,47 @@ function keyringAliasFor(alias, tenant) {
|
|
|
16573
17437
|
}
|
|
16574
17438
|
return tenant === "test" ? `${alias}.test` : alias;
|
|
16575
17439
|
}
|
|
17440
|
+
function resolveActivateDecision(opts) {
|
|
17441
|
+
if (opts.explicitActivate !== void 0) return opts.explicitActivate;
|
|
17442
|
+
if (opts.tenant === "live") return true;
|
|
17443
|
+
return opts.currentActiveTenant !== "live";
|
|
17444
|
+
}
|
|
17445
|
+
async function determineActivateDecision(keyring, opts) {
|
|
17446
|
+
if (opts.explicitActivate !== void 0 || opts.tenant === "live") {
|
|
17447
|
+
return resolveActivateDecision(opts);
|
|
17448
|
+
}
|
|
17449
|
+
let currentActiveTenant;
|
|
17450
|
+
try {
|
|
17451
|
+
if (opts.hint && opts.hint.alias === await keyring.activeAlias()) {
|
|
17452
|
+
currentActiveTenant = keyTenant(opts.hint.secret);
|
|
17453
|
+
} else {
|
|
17454
|
+
currentActiveTenant = await keyring.activeTenant();
|
|
17455
|
+
}
|
|
17456
|
+
} catch {
|
|
17457
|
+
}
|
|
17458
|
+
return resolveActivateDecision({ ...opts, currentActiveTenant });
|
|
17459
|
+
}
|
|
17460
|
+
function tenantActivateMessage(opts) {
|
|
17461
|
+
if (opts.willActivate) {
|
|
17462
|
+
if (opts.tenant !== "test") return "";
|
|
17463
|
+
return `
|
|
17464
|
+
\u26A0 '${opts.alias}' is now your ACTIVE identity \u2014 a TEST tenant credential. Anything
|
|
17465
|
+
resolving a key with no explicit --alias (including the MCP server) follows it.
|
|
17466
|
+
`;
|
|
17467
|
+
}
|
|
17468
|
+
if (opts.explicitlyDeclined) {
|
|
17469
|
+
return `
|
|
17470
|
+
\u26A0 Stored '${opts.alias}' (${opts.tenant} tenant) without activating it \u2014 you passed --no-activate.
|
|
17471
|
+
Run \`vectros switch ${opts.alias}\` when you're ready to use it.
|
|
17472
|
+
`;
|
|
17473
|
+
}
|
|
17474
|
+
return `
|
|
17475
|
+
\u26A0 Stored '${opts.alias}' (test tenant) WITHOUT activating it \u2014 your currently-active
|
|
17476
|
+
identity is a LIVE credential, and switching it out from under you silently is
|
|
17477
|
+
exactly the mistake this check exists to prevent. Run \`vectros switch ${opts.alias}\`
|
|
17478
|
+
to use the test key, or re-run with --activate to do so automatically.
|
|
17479
|
+
`;
|
|
17480
|
+
}
|
|
16576
17481
|
function nowIso() {
|
|
16577
17482
|
return (/* @__PURE__ */ new Date()).toISOString();
|
|
16578
17483
|
}
|
|
@@ -16669,10 +17574,7 @@ async function storeMintedKey(keyring, binding, secret, opts = {}) {
|
|
|
16669
17574
|
// src/cli-bootstrap.ts
|
|
16670
17575
|
function validateBlueprint(input) {
|
|
16671
17576
|
const blueprint = parseBlueprint(input);
|
|
16672
|
-
|
|
16673
|
-
for (const clauses of Object.values(blueprint.roles ?? {})) {
|
|
16674
|
-
for (const clause of clauses) assertScopeGate(clause.allowedActions);
|
|
16675
|
-
}
|
|
17577
|
+
assertBlueprintGates(blueprint);
|
|
16676
17578
|
return blueprint;
|
|
16677
17579
|
}
|
|
16678
17580
|
var BLANK_SLATE = {
|
|
@@ -16694,9 +17596,11 @@ function parseBootstrapArgs(argv) {
|
|
|
16694
17596
|
yes: false,
|
|
16695
17597
|
noMerge: false,
|
|
16696
17598
|
rotate: false,
|
|
17599
|
+
confirmExistingPrincipal: false,
|
|
16697
17600
|
useScopedToken: false,
|
|
16698
17601
|
blankSlate: false,
|
|
16699
17602
|
noSeed: false,
|
|
17603
|
+
allowTenantWideNamespaces: false,
|
|
16700
17604
|
client: "desktop",
|
|
16701
17605
|
tenant: "live",
|
|
16702
17606
|
help: false
|
|
@@ -16745,6 +17649,9 @@ function parseBootstrapArgs(argv) {
|
|
|
16745
17649
|
case "--rotate":
|
|
16746
17650
|
args.rotate = true;
|
|
16747
17651
|
break;
|
|
17652
|
+
case "--confirm-existing-principal":
|
|
17653
|
+
args.confirmExistingPrincipal = true;
|
|
17654
|
+
break;
|
|
16748
17655
|
case "--use-scoped-token":
|
|
16749
17656
|
args.useScopedToken = true;
|
|
16750
17657
|
break;
|
|
@@ -16755,6 +17662,9 @@ function parseBootstrapArgs(argv) {
|
|
|
16755
17662
|
case "--no-seed":
|
|
16756
17663
|
args.noSeed = true;
|
|
16757
17664
|
break;
|
|
17665
|
+
case "--allow-tenant-wide-namespaces":
|
|
17666
|
+
args.allowTenantWideNamespaces = true;
|
|
17667
|
+
break;
|
|
16758
17668
|
case "--tenant": {
|
|
16759
17669
|
const v = argv[++i];
|
|
16760
17670
|
if (v !== "test" && v !== "live") {
|
|
@@ -16766,6 +17676,12 @@ function parseBootstrapArgs(argv) {
|
|
|
16766
17676
|
case "--test":
|
|
16767
17677
|
args.tenant = "test";
|
|
16768
17678
|
break;
|
|
17679
|
+
case "--activate":
|
|
17680
|
+
args.activate = true;
|
|
17681
|
+
break;
|
|
17682
|
+
case "--no-activate":
|
|
17683
|
+
args.activate = false;
|
|
17684
|
+
break;
|
|
16769
17685
|
case "--help":
|
|
16770
17686
|
case "-h":
|
|
16771
17687
|
args.help = true;
|
|
@@ -16783,6 +17699,26 @@ function seedDeclined(answer) {
|
|
|
16783
17699
|
const a = answer.trim().toLowerCase();
|
|
16784
17700
|
return a === "n" || a === "no";
|
|
16785
17701
|
}
|
|
17702
|
+
function existingPrincipalConfirmed(answer) {
|
|
17703
|
+
const a = answer.trim().toLowerCase();
|
|
17704
|
+
return a === "y" || a === "yes";
|
|
17705
|
+
}
|
|
17706
|
+
function buildConfirmExistingPrincipal(args, prompt, isTty) {
|
|
17707
|
+
return async (details) => {
|
|
17708
|
+
if (!args.yes && isTty) {
|
|
17709
|
+
const ans = await prompt(
|
|
17710
|
+
`
|
|
17711
|
+
\u26A0 '${details.externalId}' already exists in your tenant (context '${details.contextId}') \u2014 this key will bind to that EXISTING identity, not a new one.
|
|
17712
|
+
Expected if you're re-applying a blueprint from a new machine, or setting up a second
|
|
17713
|
+
credential for an integration you already own. If you don't recognize this identity,
|
|
17714
|
+
do NOT proceed \u2014 this may be a name collision with something you don't control.
|
|
17715
|
+
Proceed and bind to the existing identity? [y/N]: `
|
|
17716
|
+
);
|
|
17717
|
+
return existingPrincipalConfirmed(ans);
|
|
17718
|
+
}
|
|
17719
|
+
return args.confirmExistingPrincipal;
|
|
17720
|
+
};
|
|
17721
|
+
}
|
|
16786
17722
|
function seedSummaryLine(opts) {
|
|
16787
17723
|
if (opts.noSeed) {
|
|
16788
17724
|
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";
|
|
@@ -16810,25 +17746,30 @@ function reconcileTally(items, noun) {
|
|
|
16810
17746
|
if (reconciled) seg.push(`${reconciled} reconciled`);
|
|
16811
17747
|
return `${items.length} ${noun}${items.length === 1 ? "" : "s"} (${seg.join(", ")})`;
|
|
16812
17748
|
}
|
|
16813
|
-
function
|
|
17749
|
+
function namespaceTally(items) {
|
|
16814
17750
|
if (items.length === 0) return null;
|
|
16815
17751
|
const created = items.filter((i) => i.action === "created").length;
|
|
16816
|
-
const
|
|
17752
|
+
const matched = items.length - created;
|
|
16817
17753
|
const seg = [];
|
|
16818
17754
|
if (created) seg.push(`${created} created`);
|
|
16819
|
-
if (
|
|
16820
|
-
return `${items.length}
|
|
17755
|
+
if (matched) seg.push(`${matched} already registered \u2014 confirmed matching`);
|
|
17756
|
+
return `${items.length} namespace${items.length === 1 ? "" : "s"} (${seg.join(", ")})`;
|
|
16821
17757
|
}
|
|
16822
17758
|
function applyOutcomeSummary(result) {
|
|
16823
17759
|
const parts = [
|
|
16824
17760
|
reconcileTally(result.schemaActions, "schema"),
|
|
16825
17761
|
reconcileTally(result.roleActions, "role"),
|
|
16826
|
-
|
|
17762
|
+
// #1023 — the platform's PUT /v1/auth/issuers/{issuerId} makes an issuer's
|
|
17763
|
+
// 'reconciled' mean the same "updated to match" as every other kind here.
|
|
17764
|
+
reconcileTally(result.issuerActions, "issuer"),
|
|
17765
|
+
namespaceTally(result.namespaceActions),
|
|
16827
17766
|
`profile ${result.profileAction}`
|
|
16828
17767
|
].filter((p) => p !== null);
|
|
16829
17768
|
const detail = parts.length ? ` \u2014 ${parts.join("; ")}` : "";
|
|
17769
|
+
const principalNote = result.userAction === "reconciled" ? ` \u26A0 service principal already existed in this tenant before this run (not newly created)
|
|
17770
|
+
` : "";
|
|
16830
17771
|
return ` \u2714 context '${result.contextId}' ${result.contextAction}${detail}
|
|
16831
|
-
`;
|
|
17772
|
+
${principalNote}`;
|
|
16832
17773
|
}
|
|
16833
17774
|
var BOOTSTRAP_HELP = `vectros bootstrap \u2014 provision a least-privilege MCP credential.
|
|
16834
17775
|
|
|
@@ -16836,7 +17777,7 @@ Usage:
|
|
|
16836
17777
|
vectros bootstrap [--blueprint <name> | --blank] [--no-seed]
|
|
16837
17778
|
[--set name=value ...] [--values <file>]
|
|
16838
17779
|
[--tenant test|live | --test] [--client desktop|code]
|
|
16839
|
-
[--yes] [--no-merge] [--rotate]
|
|
17780
|
+
[--yes] [--no-merge] [--rotate] [--confirm-existing-principal]
|
|
16840
17781
|
[--token <t> | env VECTROS_BOOTSTRAP_TOKEN]
|
|
16841
17782
|
[--base-url <url>] [--use-scoped-token]
|
|
16842
17783
|
|
|
@@ -16853,12 +17794,24 @@ Options:
|
|
|
16853
17794
|
sample seed records \u2014 the right choice for a production context
|
|
16854
17795
|
(a blueprint's seeds are illustrative, not real data). Unlike
|
|
16855
17796
|
--blank, the schemas are still created.
|
|
17797
|
+
--allow-tenant-wide-namespaces
|
|
17798
|
+
Required to apply a blueprint that declares a namespace with
|
|
17799
|
+
tenantWide: true \u2014 visible to every context in the account, not
|
|
17800
|
+
just this one. Without it, bootstrap refuses before provisioning
|
|
17801
|
+
anything, naming the namespace(s). Only an account OWNER's
|
|
17802
|
+
credential can actually complete the registration either way \u2014
|
|
17803
|
+
this flag doesn't change who's allowed, only whether this run
|
|
17804
|
+
tries. Never needed for a blueprint with no tenantWide namespace.
|
|
16856
17805
|
--tenant test|live Which tenant the minted key scopes to (default: live).
|
|
16857
17806
|
'test' provisions into your isolated test tenant \u2014 a
|
|
16858
17807
|
dry-run before pointing real data at the same blueprint.
|
|
16859
17808
|
The tenant rides the key (ssk_test_* vs ssk_live_*), so
|
|
16860
17809
|
the MCP/client config is otherwise identical.
|
|
16861
17810
|
--test Shorthand for --tenant test.
|
|
17811
|
+
--activate Force the freshly-stored key to become your active identity, even
|
|
17812
|
+
if that would switch away from a currently-active LIVE credential
|
|
17813
|
+
(the one case --tenant test does NOT do this automatically).
|
|
17814
|
+
--no-activate Store the key without activating it, regardless of tenant.
|
|
16862
17815
|
--base-url <url> Override the API base URL (beats VECTROS_API_BASE_URL).
|
|
16863
17816
|
--client <desktop|code> MCP host to configure (default: desktop). 'desktop' merges
|
|
16864
17817
|
claude_desktop_config.json (also works for Cursor/Cline);
|
|
@@ -16873,6 +17826,13 @@ Options:
|
|
|
16873
17826
|
print the config snippet instead of merging it into your client's
|
|
16874
17827
|
config file. Not a dry run; see 'blueprint plan' for that.
|
|
16875
17828
|
--rotate Revoke + re-mint THIS machine's key.
|
|
17829
|
+
--confirm-existing-principal
|
|
17830
|
+
Required (with --yes, or with no TTY) to mint a NEW key bound to a
|
|
17831
|
+
service principal that already existed in your tenant before this run
|
|
17832
|
+
\u2014 e.g. a deliberate second machine for an integration you already own.
|
|
17833
|
+
Refused by default so an unexpected collision halts a CI run loudly
|
|
17834
|
+
instead of silently minting a credential bound to the wrong identity.
|
|
17835
|
+
Never needed for --rotate or an ordinary same-machine re-apply.
|
|
16876
17836
|
--use-scoped-token Legacy fallback.
|
|
16877
17837
|
NOTE: it mints a token WITHOUT the provisioning
|
|
16878
17838
|
capability, so it cannot create a blueprint's
|
|
@@ -16960,6 +17920,22 @@ function buildRemintForContext(opts) {
|
|
|
16960
17920
|
});
|
|
16961
17921
|
};
|
|
16962
17922
|
}
|
|
17923
|
+
function buildRemintBootstrapForContext(opts) {
|
|
17924
|
+
const mintBridge = opts.mintBridge ?? mintBridgeToken;
|
|
17925
|
+
return async (contextId) => {
|
|
17926
|
+
const ctxToken = await mintBridge({
|
|
17927
|
+
cognitoToken: opts.cognitoToken,
|
|
17928
|
+
apiBaseUrl: opts.apiBaseUrl,
|
|
17929
|
+
contextId,
|
|
17930
|
+
tenant: opts.tenant,
|
|
17931
|
+
useScopedToken: opts.useScopedToken
|
|
17932
|
+
});
|
|
17933
|
+
return new VectrosClient({
|
|
17934
|
+
token: ctxToken.token,
|
|
17935
|
+
environment: opts.apiBaseUrl
|
|
17936
|
+
});
|
|
17937
|
+
};
|
|
17938
|
+
}
|
|
16963
17939
|
async function runBootstrapCli(argv, deps = {}) {
|
|
16964
17940
|
const log = deps.log ?? createLogger2();
|
|
16965
17941
|
let args;
|
|
@@ -16993,17 +17969,36 @@ ${BOOTSTRAP_HELP}
|
|
|
16993
17969
|
\u2716 Scope gate refused blueprint '${blueprint.name}':
|
|
16994
17970
|
`);
|
|
16995
17971
|
for (const r of err.rejected) process.stderr.write(` \u2022 ${r.action}: ${r.reason}
|
|
17972
|
+
`);
|
|
17973
|
+
process.stderr.write(" Minting nothing.\n");
|
|
17974
|
+
return 2;
|
|
17975
|
+
}
|
|
17976
|
+
if (err instanceof CapabilityGateError) {
|
|
17977
|
+
process.stderr.write(`
|
|
17978
|
+
\u2716 Capability gate refused blueprint '${blueprint.name}':
|
|
17979
|
+
`);
|
|
17980
|
+
for (const r of err.rejected) process.stderr.write(` \u2022 ${r.action}: ${r.reason}
|
|
16996
17981
|
`);
|
|
16997
17982
|
process.stderr.write(" Minting nothing.\n");
|
|
16998
17983
|
return 2;
|
|
16999
17984
|
}
|
|
17000
17985
|
throw err;
|
|
17001
17986
|
}
|
|
17002
|
-
const
|
|
17003
|
-
|
|
17987
|
+
const roleNote = blueprint.accessProfile.roleIds ? ` via role(s) [${blueprint.accessProfile.roleIds.join(", ")}]` : "";
|
|
17988
|
+
const acceptedActions = effectiveAccessProfileActions(blueprint);
|
|
17989
|
+
const allDataPlane = !acceptedActions.some(
|
|
17990
|
+
(a) => KNOWN_CONTROL_PLANE_RESOURCES.includes(a.split(":", 1)[0])
|
|
17991
|
+
);
|
|
17992
|
+
process.stdout.write(
|
|
17993
|
+
`
|
|
17004
17994
|
Blueprint: ${blueprint.name}
|
|
17005
|
-
\u2714 Scope gate: [${
|
|
17995
|
+
\u2714 Scope gate: [${acceptedActions.join(", ")}]${roleNote}` + (allDataPlane ? " \u2014 all data-plane \u2713\n" : " \u2014 includes a narrow, allowlisted control-plane exception (see above)\n")
|
|
17996
|
+
);
|
|
17997
|
+
if (blueprint.accessProfile.capabilities?.length) {
|
|
17998
|
+
const capGate = evaluateCapabilityGate(blueprint.accessProfile.capabilities);
|
|
17999
|
+
process.stdout.write(` \u2714 Capability gate: [${capGate.accepted.join(", ")}] \u2014 granted \u2713
|
|
17006
18000
|
`);
|
|
18001
|
+
}
|
|
17007
18002
|
let noSeed = args.noSeed;
|
|
17008
18003
|
let seedDeclinedInteractively = false;
|
|
17009
18004
|
if (shouldOfferSeedOptOut(args, blueprint, isTty)) {
|
|
@@ -17054,6 +18049,7 @@ ${renderPlan(blueprint, planSteps)}
|
|
|
17054
18049
|
process.stdout.write(` \u2714 Bootstrap token minted (5-min TTL, ${args.tenant} tenant)
|
|
17055
18050
|
`);
|
|
17056
18051
|
const pingIdentity = deps.resolveIdentity ?? resolveIdentity;
|
|
18052
|
+
const provision = deps.provisionContext ?? provisionContext;
|
|
17057
18053
|
const resolveIdentities = deps.resolveApplyTimeIdentities ?? resolveApplyTimeIdentities;
|
|
17058
18054
|
const loader = deps.runLoader ?? runLoader;
|
|
17059
18055
|
const client = new VectrosClient({ token: bridge.token, environment: baseUrl });
|
|
@@ -17064,11 +18060,6 @@ ${renderPlan(blueprint, planSteps)}
|
|
|
17064
18060
|
"Could not resolve tenantId from /v1/ping \u2014 the extended ping (tenant_id) must be deployed for bootstrap."
|
|
17065
18061
|
);
|
|
17066
18062
|
}
|
|
17067
|
-
const resolvedBlueprint = await resolveIdentities(
|
|
17068
|
-
blueprint,
|
|
17069
|
-
client,
|
|
17070
|
-
log
|
|
17071
|
-
);
|
|
17072
18063
|
let keyring;
|
|
17073
18064
|
let keyringOpenError;
|
|
17074
18065
|
try {
|
|
@@ -17077,6 +18068,7 @@ ${renderPlan(blueprint, planSteps)}
|
|
|
17077
18068
|
keyringOpenError = err;
|
|
17078
18069
|
}
|
|
17079
18070
|
let storedFreshInKeyring = false;
|
|
18071
|
+
const lastResolvedSecret = /* @__PURE__ */ new Map();
|
|
17080
18072
|
const readExistingKey = async (contextId) => {
|
|
17081
18073
|
if (!keyring) return void 0;
|
|
17082
18074
|
const alias = keyringAliasFor(contextId, args.tenant);
|
|
@@ -17084,7 +18076,8 @@ ${renderPlan(blueprint, planSteps)}
|
|
|
17084
18076
|
const existing = await keyring.get(alias);
|
|
17085
18077
|
if (!existing?.keyId) return void 0;
|
|
17086
18078
|
const rawKey = await keyring.resolveSecret(alias);
|
|
17087
|
-
|
|
18079
|
+
lastResolvedSecret.set(alias, rawKey);
|
|
18080
|
+
return rawKey ? { keyId: existing.keyId, rawKey, principalId: existing.principalId } : void 0;
|
|
17088
18081
|
} catch {
|
|
17089
18082
|
return void 0;
|
|
17090
18083
|
}
|
|
@@ -17103,6 +18096,11 @@ ${renderPlan(blueprint, planSteps)}
|
|
|
17103
18096
|
const alias = keyringAliasFor(contextId, args.tenant);
|
|
17104
18097
|
try {
|
|
17105
18098
|
warnReplacingAlias(await keyring.get(alias), alias, record.revokedKeyId);
|
|
18099
|
+
const willActivate = await determineActivateDecision(keyring, {
|
|
18100
|
+
tenant: args.tenant,
|
|
18101
|
+
explicitActivate: args.activate,
|
|
18102
|
+
hint: lastResolvedSecret.has(alias) ? { alias, secret: lastResolvedSecret.get(alias) } : void 0
|
|
18103
|
+
});
|
|
17106
18104
|
await storeMintedKey(
|
|
17107
18105
|
keyring,
|
|
17108
18106
|
{
|
|
@@ -17117,11 +18115,14 @@ ${renderPlan(blueprint, planSteps)}
|
|
|
17117
18115
|
env: deriveEnvironment(baseUrl) ?? args.env
|
|
17118
18116
|
},
|
|
17119
18117
|
record.rawKey,
|
|
17120
|
-
{ activate:
|
|
18118
|
+
{ activate: willActivate, revokedKeyId: record.revokedKeyId }
|
|
17121
18119
|
);
|
|
17122
18120
|
if (record.revokedKeyId) {
|
|
17123
18121
|
await warnOtherAliasesNaming(keyring, record.revokedKeyId, alias);
|
|
17124
18122
|
}
|
|
18123
|
+
process.stdout.write(
|
|
18124
|
+
tenantActivateMessage({ alias, tenant: args.tenant, willActivate, explicitlyDeclined: args.activate === false })
|
|
18125
|
+
);
|
|
17125
18126
|
storedFreshInKeyring = true;
|
|
17126
18127
|
} catch (err) {
|
|
17127
18128
|
process.stderr.write(
|
|
@@ -17133,6 +18134,27 @@ ${renderPlan(blueprint, planSteps)}
|
|
|
17133
18134
|
);
|
|
17134
18135
|
}
|
|
17135
18136
|
};
|
|
18137
|
+
const remintBootstrapForContext = buildRemintBootstrapForContext({
|
|
18138
|
+
cognitoToken,
|
|
18139
|
+
apiBaseUrl: baseUrl,
|
|
18140
|
+
tenant: args.tenant,
|
|
18141
|
+
useScopedToken: args.useScopedToken,
|
|
18142
|
+
mintBridge
|
|
18143
|
+
});
|
|
18144
|
+
const confirmExistingPrincipal = buildConfirmExistingPrincipal(args, promptCtx.prompt, isTty);
|
|
18145
|
+
const provisioned = await provision(client, {
|
|
18146
|
+
blueprint,
|
|
18147
|
+
allowTenantWideNamespaces: args.allowTenantWideNamespaces,
|
|
18148
|
+
remintBootstrapForContext,
|
|
18149
|
+
readExistingKey,
|
|
18150
|
+
confirmExistingPrincipal,
|
|
18151
|
+
log
|
|
18152
|
+
});
|
|
18153
|
+
const resolvedBlueprint = await resolveIdentities(
|
|
18154
|
+
blueprint,
|
|
18155
|
+
provisioned.ctxBootstrapClient ?? client,
|
|
18156
|
+
log
|
|
18157
|
+
);
|
|
17136
18158
|
const machineId = await resolveMachineId();
|
|
17137
18159
|
const result = await loader(client, {
|
|
17138
18160
|
blueprint: resolvedBlueprint,
|
|
@@ -17140,14 +18162,18 @@ ${renderPlan(blueprint, planSteps)}
|
|
|
17140
18162
|
machineId,
|
|
17141
18163
|
rotate: args.rotate,
|
|
17142
18164
|
skipSeed: noSeed,
|
|
18165
|
+
allowTenantWideNamespaces: args.allowTenantWideNamespaces,
|
|
17143
18166
|
readExistingKey,
|
|
17144
18167
|
persistMintedKey,
|
|
18168
|
+
provisioned,
|
|
18169
|
+
remintBootstrapForContext,
|
|
17145
18170
|
remintForContext: buildRemintForContext({
|
|
17146
18171
|
cognitoToken,
|
|
17147
18172
|
apiBaseUrl: baseUrl,
|
|
17148
18173
|
tenant: args.tenant,
|
|
17149
18174
|
mintContext
|
|
17150
18175
|
}),
|
|
18176
|
+
confirmExistingPrincipal,
|
|
17151
18177
|
log
|
|
17152
18178
|
});
|
|
17153
18179
|
process.stdout.write(
|
|
@@ -17225,10 +18251,31 @@ ${codeAddHint}`);
|
|
|
17225
18251
|
`);
|
|
17226
18252
|
return 3;
|
|
17227
18253
|
}
|
|
18254
|
+
if (err instanceof ExistingPrincipalNotConfirmedError) {
|
|
18255
|
+
process.stderr.write(
|
|
18256
|
+
`
|
|
18257
|
+
\u2716 ${err.message}
|
|
18258
|
+
|
|
18259
|
+
If this is expected (a re-apply, or a deliberate second machine for an integration you already own),
|
|
18260
|
+
re-run with --confirm-existing-principal, or confirm interactively.
|
|
18261
|
+
If you don't recognize this identity, investigate the blueprint's servicePrincipal.externalId before re-running.
|
|
18262
|
+
`
|
|
18263
|
+
);
|
|
18264
|
+
return 3;
|
|
18265
|
+
}
|
|
17228
18266
|
if (err instanceof BlueprintIdentityError || err instanceof UnresolvedIdentitiesError) {
|
|
18267
|
+
process.stderr.write(
|
|
18268
|
+
`
|
|
18269
|
+
\u2716 ${err.message}
|
|
18270
|
+
The app-context, service principal, and any declared namespaces were already provisioned. No schemas, profile, roles, key, or seed records were.
|
|
18271
|
+
`
|
|
18272
|
+
);
|
|
18273
|
+
return 2;
|
|
18274
|
+
}
|
|
18275
|
+
if (err instanceof TenantWideNamespacesNotAllowedError) {
|
|
17229
18276
|
process.stderr.write(`
|
|
17230
18277
|
\u2716 ${err.message}
|
|
17231
|
-
|
|
18278
|
+
Nothing was provisioned.
|
|
17232
18279
|
`);
|
|
17233
18280
|
return 2;
|
|
17234
18281
|
}
|
|
@@ -17303,11 +18350,14 @@ function assertIdentityOverrideValues(overrides, flag) {
|
|
|
17303
18350
|
}
|
|
17304
18351
|
}
|
|
17305
18352
|
function placeholderScopeNamespace(value) {
|
|
17306
|
-
const
|
|
17307
|
-
|
|
18353
|
+
const self2 = /^\$\{\{\s*(?:under\.)?self\.scope\.([^\s}]+)\s*\}\}$/.exec(value);
|
|
18354
|
+
if (self2) return self2[1];
|
|
18355
|
+
const member = MEMBER_SCOPE_PLACEHOLDER_RE2.exec(value);
|
|
18356
|
+
return member ? member[1] : null;
|
|
17308
18357
|
}
|
|
17309
18358
|
var SELF_PLACEHOLDER_RE = /^\$\{\{\s*self\.(?:userId|partnerUserId|scope\.[a-z][a-z0-9_-]{1,31})\s*\}\}$/;
|
|
17310
|
-
var
|
|
18359
|
+
var UNDER_SELF_PLACEHOLDER_RE2 = /^\$\{\{\s*under\.self\.(?:userId|partnerUserId|scope\.[a-z][a-z0-9_-]{1,31})\s*\}\}$/;
|
|
18360
|
+
var MEMBER_SCOPE_PLACEHOLDER_RE2 = /^\$\{\{\s*member\.scope\.([a-z][a-z0-9_-]{1,31})(?::[a-z][a-z0-9_-]{1,31})?\s*\}\}$/;
|
|
17311
18361
|
|
|
17312
18362
|
// src/lint.ts
|
|
17313
18363
|
var ENUM_COMPATIBLE_TYPES = /* @__PURE__ */ new Set(["enum", "string", "string[]"]);
|
|
@@ -17445,6 +18495,10 @@ function lintBlueprint(bp) {
|
|
|
17445
18495
|
}
|
|
17446
18496
|
}
|
|
17447
18497
|
}
|
|
18498
|
+
if (bp.accessProfile.allowedActions !== void 0) {
|
|
18499
|
+
const reached = ownerScopedReachWithoutDataScope(bp.accessProfile.allowedActions, ds);
|
|
18500
|
+
if (reached.length > 0) findings.push(unconstrainedReachFinding("accessProfile", reached));
|
|
18501
|
+
}
|
|
17448
18502
|
for (const roleId of Object.keys(bp.roles ?? {})) {
|
|
17449
18503
|
if (!ROLE_ID_RE.test(roleId)) {
|
|
17450
18504
|
findings.push({
|
|
@@ -17457,6 +18511,10 @@ function lintBlueprint(bp) {
|
|
|
17457
18511
|
for (const [roleId, clauses] of Object.entries(bp.roles ?? {})) {
|
|
17458
18512
|
clauses.forEach((clause, i) => {
|
|
17459
18513
|
const cds = clause.dataScope;
|
|
18514
|
+
const reached = ownerScopedReachWithoutDataScope(clause.allowedActions, cds);
|
|
18515
|
+
if (reached.length > 0) {
|
|
18516
|
+
findings.push(unconstrainedReachFinding(`role '${roleId}' clause ${i}`, reached));
|
|
18517
|
+
}
|
|
17460
18518
|
if (!cds) return;
|
|
17461
18519
|
for (const [key, values] of Object.entries(cds)) {
|
|
17462
18520
|
for (const value of values) {
|
|
@@ -17488,7 +18546,12 @@ function lintBlueprint(bp) {
|
|
|
17488
18546
|
}
|
|
17489
18547
|
var ROLE_ID_RE = /^[a-z][a-z0-9-]{2,30}$/;
|
|
17490
18548
|
var PLACEHOLDER_SHAPED_RE = /^\s*\$\{\{.*\}\}\s*$/s;
|
|
17491
|
-
var RECOGNIZED_PLACEHOLDER_RES = [
|
|
18549
|
+
var RECOGNIZED_PLACEHOLDER_RES = [
|
|
18550
|
+
/^\$\{\{\s*any\s*\}\}$/,
|
|
18551
|
+
SELF_PLACEHOLDER_RE,
|
|
18552
|
+
UNDER_SELF_PLACEHOLDER_RE2,
|
|
18553
|
+
MEMBER_SCOPE_PLACEHOLDER_RE2
|
|
18554
|
+
];
|
|
17492
18555
|
var BLUEPRINT_TOKEN_RE = /^\s*\$\{\{\s*(identities|inputs|vectros|ns)\./;
|
|
17493
18556
|
function placeholderFinding(where, key, value) {
|
|
17494
18557
|
if (typeof value !== "string" || !PLACEHOLDER_SHAPED_RE.test(value)) return null;
|
|
@@ -17506,7 +18569,26 @@ function placeholderFinding(where, key, value) {
|
|
|
17506
18569
|
return {
|
|
17507
18570
|
severity: "error",
|
|
17508
18571
|
rule: "datascope-unknown-placeholder",
|
|
17509
|
-
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.`
|
|
18572
|
+
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> }}, \${{ member.scope.<namespace> }}, \${{ member.scope.<namespace>:<level> }}. Spelling is exact and lowercase; use a plain value for a literal owner id.`
|
|
18573
|
+
};
|
|
18574
|
+
}
|
|
18575
|
+
var OWNER_SCOPED_RESOURCES = /* @__PURE__ */ new Set(["records", "documents", "entities"]);
|
|
18576
|
+
var UNCONSTRAINED_REACH_OPS = /* @__PURE__ */ new Set(["r", "u", "d"]);
|
|
18577
|
+
function ownerScopedReachWithoutDataScope(allowedActions, dataScope) {
|
|
18578
|
+
const reached = /* @__PURE__ */ new Set();
|
|
18579
|
+
for (const action of allowedActions) {
|
|
18580
|
+
const [resource, ops] = action.split(":", 2);
|
|
18581
|
+
if (!resource || !ops || !OWNER_SCOPED_RESOURCES.has(resource)) continue;
|
|
18582
|
+
if (ops === "*" || [...ops].some((op) => UNCONSTRAINED_REACH_OPS.has(op))) reached.add(resource);
|
|
18583
|
+
}
|
|
18584
|
+
if (dataScope !== void 0) return [];
|
|
18585
|
+
return [...reached].sort();
|
|
18586
|
+
}
|
|
18587
|
+
function unconstrainedReachFinding(where, resources) {
|
|
18588
|
+
return {
|
|
18589
|
+
severity: "warning",
|
|
18590
|
+
rule: "datascope-absent-unconstrained-reach",
|
|
18591
|
+
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.`
|
|
17510
18592
|
};
|
|
17511
18593
|
}
|
|
17512
18594
|
function lintPasses(findings) {
|
|
@@ -17547,8 +18629,12 @@ schemas:
|
|
|
17547
18629
|
lookupFields: [title] # extra O(1) exact-match lookup indexes (max 10)
|
|
17548
18630
|
|
|
17549
18631
|
accessProfile:
|
|
17550
|
-
# SCOPE GATE: data-plane only (records/schemas/search/documents/folders/inference/entities)
|
|
17551
|
-
#
|
|
18632
|
+
# SCOPE GATE: data-plane only (records/schemas/search/documents/folders/inference/entities),
|
|
18633
|
+
# PLUS profiles:r/u/d (bounded by the platform to redistributing access you already have,
|
|
18634
|
+
# never granting more) and profiles:c:self (create a profile for yourself only \u2014 the
|
|
18635
|
+
# platform resolves "self" from the caller, not from anything you declare here). A bare
|
|
18636
|
+
# profiles:c, or profiles:c:usr_<id> naming another principal, is HARD-REJECTED \u2014 by
|
|
18637
|
+
# design, no override. Same for keys/users/admin/namespaces/*.
|
|
17552
18638
|
# Ops: c|r|u|d, combinable (records:cru). Prefer the narrowest set (least privilege).
|
|
17553
18639
|
allowedActions: [records:r, records:c, records:u, search:r, schemas:r]
|
|
17554
18640
|
# dataScope binds the key to specific owners. Include null to ALSO see
|
|
@@ -17556,6 +18642,17 @@ accessProfile:
|
|
|
17556
18642
|
# owners' records, not shared/seed data. Keys are userId (the principal) and
|
|
17557
18643
|
# namespaced scopes (scope:org, scope:client, ...):
|
|
17558
18644
|
# dataScope: { "scope:org": [<org-uuid>, null] }
|
|
18645
|
+
# Left empty ({}) below \u2014 the explicit marker for "deliberately tenant-wide,
|
|
18646
|
+
# every record visible" ('blueprint validate' warns on a MISSING dataScope
|
|
18647
|
+
# key, but not on this one). This is a SCAFFOLD PLACEHOLDER, not a
|
|
18648
|
+
# recommendation: it also grants records:c/u tenant-wide, so before this
|
|
18649
|
+
# profile touches real data, replace it with a real scope (the example
|
|
18650
|
+
# above) unless every owner genuinely sharing every record is what you want.
|
|
18651
|
+
dataScope: {}
|
|
18652
|
+
# capabilities is a SEPARATE gated dimension from allowedActions \u2014 named platform
|
|
18653
|
+
# effects a resource:ops verb can't express. Only 'member-lifecycle' is admitted
|
|
18654
|
+
# today; every other name is HARD-REJECTED, same posture as the scope gate above.
|
|
18655
|
+
# capabilities: [member-lifecycle]
|
|
17559
18656
|
|
|
17560
18657
|
servicePrincipal:
|
|
17561
18658
|
externalId: ${name}-sp # stable; the principal the profile + key bind to
|
|
@@ -17675,15 +18772,19 @@ async function resolveAndValidate(arg, verb, supplied) {
|
|
|
17675
18772
|
return null;
|
|
17676
18773
|
}
|
|
17677
18774
|
try {
|
|
17678
|
-
|
|
17679
|
-
for (const clauses of Object.values(blueprint.roles ?? {})) {
|
|
17680
|
-
for (const clause of clauses) assertScopeGate(clause.allowedActions);
|
|
17681
|
-
}
|
|
18775
|
+
assertBlueprintGates(blueprint);
|
|
17682
18776
|
} catch (err) {
|
|
17683
18777
|
if (err instanceof ScopeGateError) {
|
|
17684
18778
|
process.stderr.write(` \u2716 Scope gate rejected '${blueprint.name}':
|
|
17685
18779
|
`);
|
|
17686
18780
|
for (const r of err.rejected) process.stderr.write(` \u2022 ${r.action}: ${r.reason}
|
|
18781
|
+
`);
|
|
18782
|
+
return null;
|
|
18783
|
+
}
|
|
18784
|
+
if (err instanceof CapabilityGateError) {
|
|
18785
|
+
process.stderr.write(` \u2716 Capability gate rejected '${blueprint.name}':
|
|
18786
|
+
`);
|
|
18787
|
+
for (const r of err.rejected) process.stderr.write(` \u2022 ${r.action}: ${r.reason}
|
|
17687
18788
|
`);
|
|
17688
18789
|
return null;
|
|
17689
18790
|
}
|
|
@@ -17714,10 +18815,11 @@ async function runValidate(args) {
|
|
|
17714
18815
|
}
|
|
17715
18816
|
const blueprint = await resolveAndValidate(args.file, "validate", supplied);
|
|
17716
18817
|
if (!blueprint) return 2;
|
|
17717
|
-
const
|
|
18818
|
+
const capGate = evaluateCapabilityGate(blueprint.accessProfile.capabilities);
|
|
17718
18819
|
const warnCount = lintBlueprint(blueprint).length;
|
|
18820
|
+
const roleNote = blueprint.accessProfile.roleIds ? ` via role(s) [${blueprint.accessProfile.roleIds.join(", ")}]` : "";
|
|
17719
18821
|
process.stdout.write(
|
|
17720
|
-
` \u2714 '${blueprint.name}' is valid \u2014 structural \u2713, scope gate \u2713 [${gate.accepted.join(", ")}]
|
|
18822
|
+
` \u2714 '${blueprint.name}' is valid \u2014 structural \u2713, scope gate \u2713 [${effectiveAccessProfileActions(blueprint).join(", ")}]${roleNote}` + (capGate.accepted.length ? `, capability gate \u2713 [${capGate.accepted.join(", ")}]` : "") + `, lint \u2713` + (warnCount ? ` (${warnCount} warning${warnCount === 1 ? "" : "s"})` : "") + "\n"
|
|
17721
18823
|
);
|
|
17722
18824
|
return 0;
|
|
17723
18825
|
}
|
|
@@ -18048,7 +19150,14 @@ async function resolveKeyDestination(opts, deps) {
|
|
|
18048
19150
|
`);
|
|
18049
19151
|
return 2;
|
|
18050
19152
|
}
|
|
18051
|
-
|
|
19153
|
+
let alias;
|
|
19154
|
+
try {
|
|
19155
|
+
alias = keyringAliasFor(opts.alias ?? opts.context, opts.tenant ?? "live");
|
|
19156
|
+
} catch (err) {
|
|
19157
|
+
process.stderr.write(` \u2716 ${formatError(err)}
|
|
19158
|
+
`);
|
|
19159
|
+
return 2;
|
|
19160
|
+
}
|
|
18052
19161
|
if (opts.activate === false && await keyring.activeAlias() === alias) {
|
|
18053
19162
|
process.stderr.write(
|
|
18054
19163
|
` \u2716 --no-activate cannot be used on '${alias}': it is your ACTIVE identity, and storing
|
|
@@ -18069,6 +19178,8 @@ async function storeAndReport(dest, key, auth, opts) {
|
|
|
18069
19178
|
return 1;
|
|
18070
19179
|
}
|
|
18071
19180
|
const principalId = opts.principalId;
|
|
19181
|
+
const tenant = keyTenant(key.rawKey) ?? "test";
|
|
19182
|
+
const willActivate = await determineActivateDecision(dest.keyring, { tenant, explicitActivate: opts.activate });
|
|
18072
19183
|
let entry;
|
|
18073
19184
|
try {
|
|
18074
19185
|
warnReplacingAlias(await dest.keyring.get(dest.alias), dest.alias, opts.revokedKeyId);
|
|
@@ -18083,7 +19194,7 @@ async function storeAndReport(dest, key, auth, opts) {
|
|
|
18083
19194
|
env: auth.env
|
|
18084
19195
|
},
|
|
18085
19196
|
key.rawKey,
|
|
18086
|
-
{ activate:
|
|
19197
|
+
{ activate: willActivate, revokedKeyId: opts.revokedKeyId }
|
|
18087
19198
|
));
|
|
18088
19199
|
} catch (err) {
|
|
18089
19200
|
process.stderr.write(
|
|
@@ -18095,7 +19206,6 @@ async function storeAndReport(dest, key, auth, opts) {
|
|
|
18095
19206
|
);
|
|
18096
19207
|
return 1;
|
|
18097
19208
|
}
|
|
18098
|
-
const active = opts.activate !== false;
|
|
18099
19209
|
process.stdout.write(
|
|
18100
19210
|
`
|
|
18101
19211
|
\u2714 Stored the new key in your ${describeEntryStoreNoun(entry)} \u2014 it was not printed.
|
|
@@ -18105,9 +19215,19 @@ async function storeAndReport(dest, key, auth, opts) {
|
|
|
18105
19215
|
["principal", entry.principalId || void 0],
|
|
18106
19216
|
["key id", entry.keyId],
|
|
18107
19217
|
["env", entry.env],
|
|
18108
|
-
["active",
|
|
18109
|
-
]) + "\n\n" +
|
|
18110
|
-
|
|
19218
|
+
["active", willActivate ? "yes" : "no"]
|
|
19219
|
+
]) + "\n\n" + // The plain "now active" note fires only on the activated path. When it
|
|
19220
|
+
// did NOT activate, tenantActivateMessage below is the ONLY explanation —
|
|
19221
|
+
// it already says to run `switch`, AND (unlike a bare "Not active") says
|
|
19222
|
+
// WHY: an explicit --no-activate vs. this call's tenant-safety default.
|
|
19223
|
+
// Saying both here would print two different "run switch" instructions
|
|
19224
|
+
// for one outcome.
|
|
19225
|
+
(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({
|
|
19226
|
+
alias: entry.alias,
|
|
19227
|
+
tenant,
|
|
19228
|
+
willActivate,
|
|
19229
|
+
explicitlyDeclined: opts.activate === false
|
|
19230
|
+
})
|
|
18111
19231
|
);
|
|
18112
19232
|
return 0;
|
|
18113
19233
|
}
|
|
@@ -18570,6 +19690,52 @@ async function runIdentityDelete(opts, deps = {}) {
|
|
|
18570
19690
|
}
|
|
18571
19691
|
});
|
|
18572
19692
|
}
|
|
19693
|
+
function renderInvite(v) {
|
|
19694
|
+
return formatKeyValues([
|
|
19695
|
+
["userId", v.userId],
|
|
19696
|
+
["inviteExpiresAt", v.inviteExpiresAt],
|
|
19697
|
+
["emailSent", v.emailSent === void 0 ? void 0 : String(v.emailSent)],
|
|
19698
|
+
["inviteToken", v.inviteToken],
|
|
19699
|
+
["acceptLink", v.acceptLink]
|
|
19700
|
+
]);
|
|
19701
|
+
}
|
|
19702
|
+
async function runIdentityInvite(opts, deps = {}) {
|
|
19703
|
+
const sendEmail = !opts.noSendEmail;
|
|
19704
|
+
if (sendEmail && !opts.acceptUrl) {
|
|
19705
|
+
process.stderr.write(
|
|
19706
|
+
" \u2716 --accept-url is required unless --no-send-email is given (the platform delivers the invitation email itself and needs somewhere to point it).\n"
|
|
19707
|
+
);
|
|
19708
|
+
return 2;
|
|
19709
|
+
}
|
|
19710
|
+
return withAuth(opts, deps, async (auth) => {
|
|
19711
|
+
const client = auth.client;
|
|
19712
|
+
try {
|
|
19713
|
+
const invite = await client.auth.createInvite({
|
|
19714
|
+
email: opts.email,
|
|
19715
|
+
contextId: opts.context,
|
|
19716
|
+
accessProfile: { roleId: opts.role },
|
|
19717
|
+
firstName: opts.firstName,
|
|
19718
|
+
acceptUrl: opts.acceptUrl,
|
|
19719
|
+
sendEmail
|
|
19720
|
+
});
|
|
19721
|
+
if (opts.json) {
|
|
19722
|
+
process.stdout.write(`${formatJson(invite)}
|
|
19723
|
+
`);
|
|
19724
|
+
return 0;
|
|
19725
|
+
}
|
|
19726
|
+
process.stdout.write(
|
|
19727
|
+
`
|
|
19728
|
+
\u2714 Invited '${opts.email}' to '${opts.context}' as '${opts.role}'
|
|
19729
|
+
${renderInvite(invite)}
|
|
19730
|
+
`
|
|
19731
|
+
);
|
|
19732
|
+
return 0;
|
|
19733
|
+
} catch (err) {
|
|
19734
|
+
process.stderr.write(formatCliError(err, { action: "identity invite failed" }));
|
|
19735
|
+
return 1;
|
|
19736
|
+
}
|
|
19737
|
+
});
|
|
19738
|
+
}
|
|
18573
19739
|
|
|
18574
19740
|
// src/commands/access.ts
|
|
18575
19741
|
function assertPrincipalShape(principal) {
|
|
@@ -18588,20 +19754,24 @@ async function resolvePrincipalArg(raw, auth) {
|
|
|
18588
19754
|
}
|
|
18589
19755
|
return self2.principalId;
|
|
18590
19756
|
}
|
|
19757
|
+
function roleSummary(p) {
|
|
19758
|
+
if (p.roleIds?.length) return p.roleIds.join(", ");
|
|
19759
|
+
return p.roleId;
|
|
19760
|
+
}
|
|
18591
19761
|
function renderProfile(p) {
|
|
18592
19762
|
return formatKeyValues([
|
|
18593
19763
|
["context", p.contextId],
|
|
18594
19764
|
["principal", p.principalId],
|
|
18595
|
-
["role", p
|
|
19765
|
+
["role", roleSummary(p)],
|
|
18596
19766
|
["scopes", p.scopes?.length ? `${p.scopes.length} clause(s)` : void 0],
|
|
18597
19767
|
["status", p.status]
|
|
18598
19768
|
]);
|
|
18599
19769
|
}
|
|
18600
19770
|
async function runAccessGrant(opts, deps = {}) {
|
|
18601
|
-
const hasRole = Boolean(opts.role);
|
|
19771
|
+
const hasRole = Boolean(opts.role?.length);
|
|
18602
19772
|
const hasActions = Boolean(opts.actions);
|
|
18603
19773
|
if (hasRole === hasActions) {
|
|
18604
|
-
process.stderr.write(" \u2716 Provide exactly one of --role <roleId> or --actions <csv>.\n");
|
|
19774
|
+
process.stderr.write(" \u2716 Provide exactly one of --role <roleId> (repeatable) or --actions <csv>.\n");
|
|
18605
19775
|
return 2;
|
|
18606
19776
|
}
|
|
18607
19777
|
if (hasActions && splitCsv(opts.actions).length === 0) {
|
|
@@ -18641,7 +19811,11 @@ async function runAccessGrant(opts, deps = {}) {
|
|
|
18641
19811
|
upsert: true,
|
|
18642
19812
|
body: {
|
|
18643
19813
|
principalId: principal,
|
|
18644
|
-
|
|
19814
|
+
// Always send the plural `roleIds`, even for a single
|
|
19815
|
+
// `--role`: it is a strict superset of the deprecated singular
|
|
19816
|
+
// `roleId` (equivalent to `roleIds: ["<value>"]`), and the platform
|
|
19817
|
+
// reads it uniformly regardless of how many roles compose.
|
|
19818
|
+
roleIds: opts.role,
|
|
18645
19819
|
scopes,
|
|
18646
19820
|
identityOverrides
|
|
18647
19821
|
}
|
|
@@ -18653,7 +19827,7 @@ async function runAccessGrant(opts, deps = {}) {
|
|
|
18653
19827
|
}
|
|
18654
19828
|
process.stdout.write(
|
|
18655
19829
|
`
|
|
18656
|
-
\u2714 Granted ${principal} in '${opts.context}'` + (hasRole ? ` via role
|
|
19830
|
+
\u2714 Granted ${principal} in '${opts.context}'` + (hasRole ? ` via role(s) [${opts.role.join(", ")}]` : ` with ${splitCsv(opts.actions).length} action(s)`) + `
|
|
18657
19831
|
${renderProfile(profile)}
|
|
18658
19832
|
`
|
|
18659
19833
|
);
|
|
@@ -18722,7 +19896,7 @@ async function runAccessList(opts, deps = {}) {
|
|
|
18722
19896
|
"\n" + formatTable(rows, [
|
|
18723
19897
|
{ header: "context", get: (r) => r.contextId },
|
|
18724
19898
|
{ header: "principal", get: (r) => r.principalId },
|
|
18725
|
-
{ header: "role", get: (r) => r
|
|
19899
|
+
{ header: "role", get: (r) => roleSummary(r) ?? (r.scopes?.length ? "(inline)" : void 0) },
|
|
18726
19900
|
{ header: "status", get: (r) => r.status }
|
|
18727
19901
|
]) + "\n"
|
|
18728
19902
|
);
|
|
@@ -18770,6 +19944,7 @@ ${renderProfile(p)}
|
|
|
18770
19944
|
var SELF = /* @__PURE__ */ Symbol("self");
|
|
18771
19945
|
var ANY = /* @__PURE__ */ Symbol("any");
|
|
18772
19946
|
var UNDER_SELF = /* @__PURE__ */ Symbol("under.self");
|
|
19947
|
+
var MEMBER = /* @__PURE__ */ Symbol("member.scope");
|
|
18773
19948
|
function canonicalDim(key) {
|
|
18774
19949
|
if (key === "userId" || key === "partnerUserId") return "user";
|
|
18775
19950
|
return key;
|
|
@@ -18799,7 +19974,8 @@ function normalizeClauses(raw) {
|
|
|
18799
19974
|
if (val === null) return null;
|
|
18800
19975
|
const s = String(val);
|
|
18801
19976
|
if (ANY_TOKEN_RE.test(s)) return ANY;
|
|
18802
|
-
if (
|
|
19977
|
+
if (UNDER_SELF_PLACEHOLDER_RE2.test(s)) return UNDER_SELF;
|
|
19978
|
+
if (MEMBER_SCOPE_PLACEHOLDER_RE2.test(s)) return MEMBER;
|
|
18803
19979
|
return SELF_PLACEHOLDER_RE.test(s) ? SELF : s;
|
|
18804
19980
|
});
|
|
18805
19981
|
if (!isRecognizedOwnershipKey(k)) unrecognizedDims.push(k);
|
|
@@ -18854,7 +20030,7 @@ function buildArchetypes(clauses) {
|
|
|
18854
20030
|
const set = scopeDims.get(dim) ?? /* @__PURE__ */ new Set();
|
|
18855
20031
|
set.add(SELF);
|
|
18856
20032
|
for (const v of values) {
|
|
18857
|
-
if (v === null || v === ANY || v === UNDER_SELF) continue;
|
|
20033
|
+
if (v === null || v === ANY || v === UNDER_SELF || v === MEMBER) continue;
|
|
18858
20034
|
set.add(v);
|
|
18859
20035
|
}
|
|
18860
20036
|
scopeDims.set(dim, set);
|
|
@@ -18881,6 +20057,7 @@ function listAdmits(allowed, recordValue) {
|
|
|
18881
20057
|
if (a === SELF) return recordValue === SELF;
|
|
18882
20058
|
if (a === ANY) return recordValue !== void 0;
|
|
18883
20059
|
if (a === UNDER_SELF) return false;
|
|
20060
|
+
if (a === MEMBER) return false;
|
|
18884
20061
|
return recordValue === a;
|
|
18885
20062
|
});
|
|
18886
20063
|
}
|
|
@@ -18906,7 +20083,7 @@ function undecidableDims(clauses) {
|
|
|
18906
20083
|
const dims = /* @__PURE__ */ new Set();
|
|
18907
20084
|
for (const c of clauses) {
|
|
18908
20085
|
for (const [dim, values] of Object.entries(c.dataScope)) {
|
|
18909
|
-
if (values.includes(UNDER_SELF)) dims.add(dim);
|
|
20086
|
+
if (values.includes(UNDER_SELF) || values.includes(MEMBER)) dims.add(dim);
|
|
18910
20087
|
}
|
|
18911
20088
|
}
|
|
18912
20089
|
return [...dims];
|
|
@@ -18939,6 +20116,7 @@ function renderDataScope(ds) {
|
|
|
18939
20116
|
if (v === SELF) return "self";
|
|
18940
20117
|
if (v === ANY) return "any";
|
|
18941
20118
|
if (v === UNDER_SELF) return "under-self";
|
|
20119
|
+
if (v === MEMBER) return "member";
|
|
18942
20120
|
if (v === null) return "null";
|
|
18943
20121
|
return v;
|
|
18944
20122
|
});
|
|
@@ -18970,14 +20148,25 @@ async function runAccessExplain(opts, deps = {}) {
|
|
|
18970
20148
|
const profile = await client.auth.getAccessProfile({ contextId: opts.context, principalId: principal });
|
|
18971
20149
|
let rawClauses = profile.scopes ?? [];
|
|
18972
20150
|
let via = "inline scopes";
|
|
18973
|
-
|
|
18974
|
-
|
|
18975
|
-
|
|
18976
|
-
|
|
20151
|
+
const roleIds = profile.roleIds ?? (profile.roleId ? [profile.roleId] : []);
|
|
20152
|
+
if (roleIds.length > 0) {
|
|
20153
|
+
const roleClauses = [];
|
|
20154
|
+
for (const roleId of roleIds) {
|
|
20155
|
+
const role = await client.auth.getRole({ contextId: opts.context, roleId });
|
|
20156
|
+
roleClauses.push(...role.scopes ?? []);
|
|
20157
|
+
}
|
|
20158
|
+
rawClauses = roleClauses;
|
|
20159
|
+
via = roleIds.length === 1 ? `role '${roleIds[0]}'` : `role(s) [${roleIds.join(", ")}]`;
|
|
18977
20160
|
}
|
|
18978
20161
|
const clauses = normalizeClauses(rawClauses);
|
|
18979
20162
|
const rows = explainClauses(clauses);
|
|
18980
20163
|
const undecidable = undecidableDims(clauses);
|
|
20164
|
+
const hasUnderSelfUndecidable = clauses.some(
|
|
20165
|
+
(c) => Object.values(c.dataScope).some((values) => values.includes(UNDER_SELF))
|
|
20166
|
+
);
|
|
20167
|
+
const hasMemberUndecidable = clauses.some(
|
|
20168
|
+
(c) => Object.values(c.dataScope).some((values) => values.includes(MEMBER))
|
|
20169
|
+
);
|
|
18981
20170
|
if (opts.json) {
|
|
18982
20171
|
process.stdout.write(
|
|
18983
20172
|
`${formatJson({
|
|
@@ -19034,10 +20223,7 @@ async function runAccessExplain(opts, deps = {}) {
|
|
|
19034
20223
|
]) + (undecidable.length > 0 ? `
|
|
19035
20224
|
|
|
19036
20225
|
\u26A0 UNDECIDED offline: ${undecidable.join(", ")}
|
|
19037
|
-
A clause matching on
|
|
19038
|
-
so rows granted through it are shown as NO above. Read those cells as "cannot tell here",
|
|
19039
|
-
NOT as a denial \u2014 the matrix under-reports this clause.
|
|
19040
|
-
` : "") + '\n\n \u26A0 READ ONLY: "granted?"/"actions" model whether a row can be READ, not whether a c/u/d\n verb shown there can actually WRITE that row\'s exact shape \u2014 the platform requires a clause\n to name a dimension to place a value into it, while reads are never narrowed by a dimension a\n clause omits. A create/update verb next to a row is advisory: it names which records exist to\n act on, not that a specific write will succeed.\n\n\n NOTE: this matrix models the OWNERSHIP dimensions only \u2014 "granted?" says a row\'s ownership\n shape matches a clause, and "record types" (above) says which `records:` types that grant\n actually reaches; "all" reaches every type, "\u2014" means no `records:` action grants this row at\n all. Search/RAG visibility is a separate, still-unmodeled narrowing: it is bounded per row by\n the SAME clause\'s typed read grants \u2014 a row shown with `search:r` is searchable only for the\n content types that clause can read.\n Preview only \u2014 computed client-side from the documented matching rules; the platform decides at request time.\n'
|
|
20226
|
+
` + (hasUnderSelfUndecidable ? " A clause matching on `${{ under.self.* }}` needs the server's parent graph, which this\n preview does not have, so rows granted through it are shown as NO above.\n" : "") + (hasMemberUndecidable ? " A clause matching on `${{ member.scope.<ns>[:<level>] }}` needs a live per-request read of\n this account's membership records, which this preview does not have, so rows granted\n through it are shown as NO above.\n" : "") + ' Read those cells as "cannot tell here", NOT as a denial \u2014 the matrix under-reports\n whichever clause(s) above are responsible.\n' : "") + '\n\n \u26A0 READ ONLY: "granted?"/"actions" model whether a row can be READ, not whether a c/u/d\n verb shown there can actually WRITE that row\'s exact shape \u2014 the platform requires a clause\n to name a dimension to place a value into it, while reads are never narrowed by a dimension a\n clause omits. A create/update verb next to a row is advisory: it names which records exist to\n act on, not that a specific write will succeed.\n\n\n NOTE: this matrix models the OWNERSHIP dimensions only \u2014 "granted?" says a row\'s ownership\n shape matches a clause, and "record types" (above) says which `records:` types that grant\n actually reaches; "all" reaches every type, "\u2014" means no `records:` action grants this row at\n all. Search/RAG visibility is a separate, still-unmodeled narrowing: it is bounded per row by\n the SAME clause\'s typed read grants \u2014 a row shown with `search:r` is searchable only for the\n content types that clause can read.\n Preview only \u2014 computed client-side from the documented matching rules; the platform decides at request time.\n'
|
|
19041
20227
|
);
|
|
19042
20228
|
return 0;
|
|
19043
20229
|
} catch (err) {
|
|
@@ -19055,7 +20241,8 @@ function renderRole(r) {
|
|
|
19055
20241
|
["name", r.name],
|
|
19056
20242
|
["context", r.contextId],
|
|
19057
20243
|
["description", r.description],
|
|
19058
|
-
["scopes", r.scopes?.length ? `${r.scopes.length} clause(s)` : void 0]
|
|
20244
|
+
["scopes", r.scopes?.length ? `${r.scopes.length} clause(s)` : void 0],
|
|
20245
|
+
["assumable", r.assumable && Object.keys(r.assumable).length ? formatJson(r.assumable) : void 0]
|
|
19059
20246
|
]);
|
|
19060
20247
|
}
|
|
19061
20248
|
async function runRoleCreate(opts, deps = {}) {
|
|
@@ -19326,6 +20513,75 @@ ${renderContext(ctx)}
|
|
|
19326
20513
|
});
|
|
19327
20514
|
}
|
|
19328
20515
|
|
|
20516
|
+
// src/commands/issuers.ts
|
|
20517
|
+
function renderSelfSignup(policies) {
|
|
20518
|
+
if (!policies || policies.length === 0) return "\u2014";
|
|
20519
|
+
return policies.map((p) => `${p.signup_type} \u2192 ${p.role_id}`).join(", ");
|
|
20520
|
+
}
|
|
20521
|
+
function renderIssuer(i) {
|
|
20522
|
+
return formatKeyValues([
|
|
20523
|
+
["issuerId", i.issuerId],
|
|
20524
|
+
["issuer", i.issuer],
|
|
20525
|
+
["jwksUri", i.jwksUri],
|
|
20526
|
+
["audience", i.audience],
|
|
20527
|
+
["contextId", i.contextId],
|
|
20528
|
+
["subClaim", i.subClaim],
|
|
20529
|
+
["emailClaim", i.emailClaim],
|
|
20530
|
+
["userinfoUri", i.userinfoUri],
|
|
20531
|
+
["status", i.status],
|
|
20532
|
+
["createdAt", i.createdAt],
|
|
20533
|
+
["selfSignup", renderSelfSignup(i.selfSignupPolicies)]
|
|
20534
|
+
]);
|
|
20535
|
+
}
|
|
20536
|
+
async function runIssuersList(opts, deps = {}) {
|
|
20537
|
+
return withAuth({ ...opts, provisioning: true, context: opts.context }, deps, async (auth) => {
|
|
20538
|
+
const client = auth.client;
|
|
20539
|
+
try {
|
|
20540
|
+
const rows = await drainPages(
|
|
20541
|
+
(startFrom) => client.auth.listIssuers({ limit: PAGE_LIMIT, ...startFrom ? { startFrom } : {} })
|
|
20542
|
+
);
|
|
20543
|
+
if (opts.json) {
|
|
20544
|
+
process.stdout.write(`${formatJson(rows)}
|
|
20545
|
+
`);
|
|
20546
|
+
return 0;
|
|
20547
|
+
}
|
|
20548
|
+
process.stdout.write(
|
|
20549
|
+
"\n" + formatTable(rows, [
|
|
20550
|
+
{ header: "issuerId", get: (i) => i.issuerId },
|
|
20551
|
+
{ header: "issuer", get: (i) => i.issuer },
|
|
20552
|
+
{ header: "audience", get: (i) => i.audience },
|
|
20553
|
+
{ header: "status", get: (i) => i.status },
|
|
20554
|
+
{ header: "self-signup", get: (i) => renderSelfSignup(i.selfSignupPolicies) }
|
|
20555
|
+
]) + "\n"
|
|
20556
|
+
);
|
|
20557
|
+
return 0;
|
|
20558
|
+
} catch (err) {
|
|
20559
|
+
process.stderr.write(formatCliError(err, { action: "issuers list failed" }));
|
|
20560
|
+
return 1;
|
|
20561
|
+
}
|
|
20562
|
+
});
|
|
20563
|
+
}
|
|
20564
|
+
async function runIssuersGet(opts, deps = {}) {
|
|
20565
|
+
return withAuth({ ...opts, provisioning: true, context: opts.context }, deps, async (auth) => {
|
|
20566
|
+
const client = auth.client;
|
|
20567
|
+
try {
|
|
20568
|
+
const issuer = await client.auth.getIssuer({ issuerId: opts.issuerId });
|
|
20569
|
+
if (opts.json) {
|
|
20570
|
+
process.stdout.write(`${formatJson(issuer)}
|
|
20571
|
+
`);
|
|
20572
|
+
return 0;
|
|
20573
|
+
}
|
|
20574
|
+
process.stdout.write(`
|
|
20575
|
+
${renderIssuer(issuer)}
|
|
20576
|
+
`);
|
|
20577
|
+
return 0;
|
|
20578
|
+
} catch (err) {
|
|
20579
|
+
process.stderr.write(formatCliError(err, { action: "issuers get failed" }));
|
|
20580
|
+
return 1;
|
|
20581
|
+
}
|
|
20582
|
+
});
|
|
20583
|
+
}
|
|
20584
|
+
|
|
19329
20585
|
// src/commands/login.ts
|
|
19330
20586
|
var import_node_http = require("http");
|
|
19331
20587
|
var import_open = __toESM(require("open"));
|
|
@@ -19547,7 +20803,7 @@ async function runAssertions(client, bootstrapClient, deps, blueprint, tenantId,
|
|
|
19547
20803
|
const assertions = [];
|
|
19548
20804
|
try {
|
|
19549
20805
|
const ping = await deps.pingWithKey(result.rawKey);
|
|
19550
|
-
const wantActions = blueprint
|
|
20806
|
+
const wantActions = effectiveAccessProfileActions(blueprint);
|
|
19551
20807
|
const got = ping.allowedActions ?? [];
|
|
19552
20808
|
const missing = wantActions.filter((a) => !got.includes(a));
|
|
19553
20809
|
const ok = ping.principalType === "scoped_key" && ping.tenantId === tenantId && missing.length === 0;
|
|
@@ -19641,6 +20897,19 @@ async function runAssertions(client, bootstrapClient, deps, blueprint, tenantId,
|
|
|
19641
20897
|
assertions.push({ name: `issuer:${issuerId}`, ok: false, detail: `getIssuer(${issuerId}) failed: ${errMsg(e)}` });
|
|
19642
20898
|
}
|
|
19643
20899
|
}
|
|
20900
|
+
for (const { namespace } of result.namespaceActions) {
|
|
20901
|
+
try {
|
|
20902
|
+
const ns = await bootstrapClient.identity.getNamespace({ namespace, contextId: blueprint.contextId });
|
|
20903
|
+
const ok = ns.namespace === namespace && ns.contextId === blueprint.contextId;
|
|
20904
|
+
assertions.push({
|
|
20905
|
+
name: `namespace:${namespace}`,
|
|
20906
|
+
ok,
|
|
20907
|
+
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}'`
|
|
20908
|
+
});
|
|
20909
|
+
} catch (e) {
|
|
20910
|
+
assertions.push({ name: `namespace:${namespace}`, ok: false, detail: `getNamespace(${namespace}) failed: ${errMsg(e)}` });
|
|
20911
|
+
}
|
|
20912
|
+
}
|
|
19644
20913
|
return assertions;
|
|
19645
20914
|
}
|
|
19646
20915
|
async function runTeardown(client, bootstrapClient, blueprint, machineId, snapshot, result) {
|
|
@@ -19753,19 +21022,36 @@ async function runTeardown(client, bootstrapClient, blueprint, machineId, snapsh
|
|
|
19753
21022
|
steps.push({ step: `delete issuer ${issuerId}`, ok: false, error: errMsg(e) });
|
|
19754
21023
|
}
|
|
19755
21024
|
}
|
|
21025
|
+
for (const { namespace } of result.namespaceActions) {
|
|
21026
|
+
steps.push({
|
|
21027
|
+
step: `delete namespace ${namespace}`,
|
|
21028
|
+
ok: true,
|
|
21029
|
+
skipped: true,
|
|
21030
|
+
error: "namespace deletion requires a root API key (sk_*) \u2014 blueprint-test never holds one; the registration is left in place"
|
|
21031
|
+
});
|
|
21032
|
+
}
|
|
19756
21033
|
return steps;
|
|
19757
21034
|
}
|
|
19758
21035
|
async function runBlueprintHarness(client, input, deps) {
|
|
19759
|
-
const { tenantId, machineId, keep = false, log, remintForContext } = input;
|
|
21036
|
+
const { tenantId, machineId, keep = false, log, remintForContext, remintBootstrapForContext } = input;
|
|
19760
21037
|
const readExistingKey = async () => void 0;
|
|
19761
21038
|
const persistMintedKey = async () => {
|
|
19762
21039
|
};
|
|
21040
|
+
const provisioned = await provisionContext(client, {
|
|
21041
|
+
blueprint: input.blueprint,
|
|
21042
|
+
remintBootstrapForContext,
|
|
21043
|
+
readExistingKey,
|
|
21044
|
+
log
|
|
21045
|
+
});
|
|
19763
21046
|
const blueprint = await resolveApplyTimeIdentities(
|
|
19764
21047
|
input.blueprint,
|
|
19765
|
-
client,
|
|
21048
|
+
provisioned.ctxBootstrapClient ?? client,
|
|
19766
21049
|
log
|
|
19767
21050
|
);
|
|
19768
21051
|
const remint = remintForContext ?? (async () => client);
|
|
21052
|
+
const remintBootstrap = remintBootstrapForContext ?? (async () => client);
|
|
21053
|
+
const hasIssuers = (blueprint.issuers ?? []).length > 0;
|
|
21054
|
+
const hasNamespaces = provisioned.namespaceActions.length > 0;
|
|
19769
21055
|
let contextSnapshot;
|
|
19770
21056
|
if (remintForContext) {
|
|
19771
21057
|
try {
|
|
@@ -19781,7 +21067,18 @@ async function runBlueprintHarness(client, input, deps) {
|
|
|
19781
21067
|
} else {
|
|
19782
21068
|
contextSnapshot = await captureSnapshot(client, blueprint);
|
|
19783
21069
|
}
|
|
19784
|
-
|
|
21070
|
+
let issuerSnapshot;
|
|
21071
|
+
if (!hasIssuers) {
|
|
21072
|
+
issuerSnapshot = { existingIssuerIds: /* @__PURE__ */ new Set(), failed: false };
|
|
21073
|
+
} else if (remintBootstrapForContext) {
|
|
21074
|
+
try {
|
|
21075
|
+
issuerSnapshot = await captureIssuerSnapshot(await remintBootstrapForContext(blueprint.contextId));
|
|
21076
|
+
} catch (e) {
|
|
21077
|
+
issuerSnapshot = { existingIssuerIds: /* @__PURE__ */ new Set(), failed: !isNotFound(e) };
|
|
21078
|
+
}
|
|
21079
|
+
} else {
|
|
21080
|
+
issuerSnapshot = await captureIssuerSnapshot(client);
|
|
21081
|
+
}
|
|
19785
21082
|
const snapshot = {
|
|
19786
21083
|
...contextSnapshot,
|
|
19787
21084
|
existingIssuerIds: issuerSnapshot.existingIssuerIds,
|
|
@@ -19791,9 +21088,26 @@ async function runBlueprintHarness(client, input, deps) {
|
|
|
19791
21088
|
let assertions = [];
|
|
19792
21089
|
let applyError;
|
|
19793
21090
|
try {
|
|
19794
|
-
result = await runLoader(client, {
|
|
21091
|
+
result = await runLoader(client, {
|
|
21092
|
+
blueprint,
|
|
21093
|
+
tenantId,
|
|
21094
|
+
machineId,
|
|
21095
|
+
readExistingKey,
|
|
21096
|
+
persistMintedKey,
|
|
21097
|
+
log,
|
|
21098
|
+
provisioned,
|
|
21099
|
+
remintForContext,
|
|
21100
|
+
remintBootstrapForContext
|
|
21101
|
+
});
|
|
19795
21102
|
log.debug({ component: "harness", contextId: result.contextId, keyName: result.keyName }, "applied");
|
|
19796
|
-
assertions = await runAssertions(
|
|
21103
|
+
assertions = await runAssertions(
|
|
21104
|
+
await remint(blueprint.contextId),
|
|
21105
|
+
hasIssuers || hasNamespaces ? await remintBootstrap(blueprint.contextId) : client,
|
|
21106
|
+
deps,
|
|
21107
|
+
blueprint,
|
|
21108
|
+
tenantId,
|
|
21109
|
+
result
|
|
21110
|
+
);
|
|
19797
21111
|
} catch (e) {
|
|
19798
21112
|
applyError = errMsg(e);
|
|
19799
21113
|
log.warn({ component: "harness", err: applyError }, "apply phase failed");
|
|
@@ -19805,7 +21119,14 @@ async function runBlueprintHarness(client, input, deps) {
|
|
|
19805
21119
|
tdClient = await remint(blueprint.contextId);
|
|
19806
21120
|
} catch {
|
|
19807
21121
|
}
|
|
19808
|
-
|
|
21122
|
+
let tdBootstrapClient = client;
|
|
21123
|
+
if (hasIssuers) {
|
|
21124
|
+
try {
|
|
21125
|
+
tdBootstrapClient = await remintBootstrap(blueprint.contextId);
|
|
21126
|
+
} catch {
|
|
21127
|
+
}
|
|
21128
|
+
}
|
|
21129
|
+
teardown = await runTeardown(tdClient, tdBootstrapClient, blueprint, machineId, snapshot, result);
|
|
19809
21130
|
}
|
|
19810
21131
|
const passed = applyError === void 0 && assertions.length > 0 && assertions.every((a) => a.ok);
|
|
19811
21132
|
return {
|
|
@@ -19879,6 +21200,13 @@ async function runBlueprintTest(fileOrName, opts, deps = {}) {
|
|
|
19879
21200
|
process.stderr.write(` \u2716 Scope gate refused blueprint:
|
|
19880
21201
|
`);
|
|
19881
21202
|
for (const r of err.rejected) process.stderr.write(` \u2022 ${r.action}: ${r.reason}
|
|
21203
|
+
`);
|
|
21204
|
+
return 2;
|
|
21205
|
+
}
|
|
21206
|
+
if (err instanceof CapabilityGateError) {
|
|
21207
|
+
process.stderr.write(` \u2716 Capability gate refused blueprint:
|
|
21208
|
+
`);
|
|
21209
|
+
for (const r of err.rejected) process.stderr.write(` \u2022 ${r.action}: ${r.reason}
|
|
19882
21210
|
`);
|
|
19883
21211
|
return 2;
|
|
19884
21212
|
}
|
|
@@ -19914,9 +21242,22 @@ async function runBlueprintTest(fileOrName, opts, deps = {}) {
|
|
|
19914
21242
|
environment: auth.baseUrl
|
|
19915
21243
|
});
|
|
19916
21244
|
};
|
|
21245
|
+
const remintBootstrapForContext = async (contextId) => {
|
|
21246
|
+
const ctxToken = await mintBridgeToken({
|
|
21247
|
+
cognitoToken: auth.cognitoToken,
|
|
21248
|
+
apiBaseUrl: auth.baseUrl,
|
|
21249
|
+
contextId,
|
|
21250
|
+
tenant: auth.tenant,
|
|
21251
|
+
useScopedToken: opts.useScopedToken
|
|
21252
|
+
});
|
|
21253
|
+
return new VectrosClient({
|
|
21254
|
+
token: ctxToken.token,
|
|
21255
|
+
environment: auth.baseUrl
|
|
21256
|
+
});
|
|
21257
|
+
};
|
|
19917
21258
|
const report = await runBlueprintHarness(
|
|
19918
21259
|
client,
|
|
19919
|
-
{ blueprint, tenantId, machineId, keep: opts.keep, log, remintForContext },
|
|
21260
|
+
{ blueprint, tenantId, machineId, keep: opts.keep, log, remintForContext, remintBootstrapForContext },
|
|
19920
21261
|
{ pingWithKey }
|
|
19921
21262
|
);
|
|
19922
21263
|
if (opts.json) {
|
|
@@ -19973,12 +21314,13 @@ async function issueOrRotateKey(client, tenantId, context, userId, keyName) {
|
|
|
19973
21314
|
return { key: rotated, revokedKeyId: existing?.keyId };
|
|
19974
21315
|
}
|
|
19975
21316
|
async function runJoin(opts, deps = {}) {
|
|
19976
|
-
const hasRole = Boolean(opts.role);
|
|
21317
|
+
const hasRole = Boolean(opts.role?.length);
|
|
19977
21318
|
const hasActions = Boolean(opts.actions);
|
|
19978
21319
|
if (hasRole === hasActions) {
|
|
19979
|
-
process.stderr.write(" \u2716 Provide exactly one of --role <roleId> or --actions <csv>.\n");
|
|
21320
|
+
process.stderr.write(" \u2716 Provide exactly one of --role <roleId> (repeatable) or --actions <csv>.\n");
|
|
19980
21321
|
return 2;
|
|
19981
21322
|
}
|
|
21323
|
+
const roleDisplay = opts.role?.join(", ");
|
|
19982
21324
|
if (hasActions && splitCsv(opts.actions).length === 0) {
|
|
19983
21325
|
process.stderr.write(" \u2716 --actions needs at least one action (an empty/blank list grants nothing).\n");
|
|
19984
21326
|
return 2;
|
|
@@ -19997,9 +21339,10 @@ async function runJoin(opts, deps = {}) {
|
|
|
19997
21339
|
return 2;
|
|
19998
21340
|
}
|
|
19999
21341
|
const context = opts.contextArg;
|
|
21342
|
+
const tenant = opts.tenant ?? "live";
|
|
20000
21343
|
let alias;
|
|
20001
21344
|
try {
|
|
20002
|
-
alias = keyringAliasFor(opts.alias ?? context,
|
|
21345
|
+
alias = keyringAliasFor(opts.alias ?? context, tenant);
|
|
20003
21346
|
} catch (err) {
|
|
20004
21347
|
process.stderr.write(` \u2716 ${formatError(err)}
|
|
20005
21348
|
`);
|
|
@@ -20025,7 +21368,10 @@ async function runJoin(opts, deps = {}) {
|
|
|
20025
21368
|
upsert: true,
|
|
20026
21369
|
body: {
|
|
20027
21370
|
principalId,
|
|
20028
|
-
|
|
21371
|
+
// Always send the plural `roleIds`, even for a single
|
|
21372
|
+
// `--role`: a strict superset of the deprecated singular `roleId`,
|
|
21373
|
+
// read uniformly by the platform regardless of how many compose.
|
|
21374
|
+
roleIds: opts.role,
|
|
20029
21375
|
scopes,
|
|
20030
21376
|
identityOverrides
|
|
20031
21377
|
}
|
|
@@ -20034,8 +21380,23 @@ async function runJoin(opts, deps = {}) {
|
|
|
20034
21380
|
const heldSecret = existingEntry ? await keyring.resolveSecret(alias) : void 0;
|
|
20035
21381
|
const canReuse = existingEntry && heldSecret && existingEntry.context === context && existingEntry.principalId === principalId && existingEntry.env === auth.env;
|
|
20036
21382
|
if (canReuse && existingEntry) {
|
|
20037
|
-
await keyring
|
|
20038
|
-
|
|
21383
|
+
const willActivate2 = await determineActivateDecision(keyring, {
|
|
21384
|
+
tenant,
|
|
21385
|
+
explicitActivate: opts.activate,
|
|
21386
|
+
hint: { alias, secret: heldSecret }
|
|
21387
|
+
});
|
|
21388
|
+
if (willActivate2) await keyring.setActive(alias);
|
|
21389
|
+
emitJoined({
|
|
21390
|
+
alias,
|
|
21391
|
+
context,
|
|
21392
|
+
principalId,
|
|
21393
|
+
role: roleDisplay,
|
|
21394
|
+
keyId: existingEntry.keyId,
|
|
21395
|
+
reused: true,
|
|
21396
|
+
tenant,
|
|
21397
|
+
willActivate: willActivate2,
|
|
21398
|
+
explicitlyDeclined: opts.activate === false
|
|
21399
|
+
});
|
|
20039
21400
|
return 0;
|
|
20040
21401
|
}
|
|
20041
21402
|
warnReplacingAlias(existingEntry, alias);
|
|
@@ -20047,13 +21408,24 @@ async function runJoin(opts, deps = {}) {
|
|
|
20047
21408
|
);
|
|
20048
21409
|
}
|
|
20049
21410
|
if (revokedKeyId) await warnOtherAliasesNaming(keyring, revokedKeyId, alias);
|
|
21411
|
+
const willActivate = await determineActivateDecision(keyring, { tenant, explicitActivate: opts.activate });
|
|
20050
21412
|
const { entry } = await storeMintedKey(
|
|
20051
21413
|
keyring,
|
|
20052
|
-
{ alias, context, principalId, role:
|
|
21414
|
+
{ alias, context, principalId, role: roleDisplay, keyId: key.keyId, env: auth.env },
|
|
20053
21415
|
key.rawKey,
|
|
20054
|
-
{ revokedKeyId }
|
|
21416
|
+
{ activate: willActivate, revokedKeyId }
|
|
20055
21417
|
);
|
|
20056
|
-
emitJoined(
|
|
21418
|
+
emitJoined({
|
|
21419
|
+
alias,
|
|
21420
|
+
context,
|
|
21421
|
+
principalId,
|
|
21422
|
+
role: roleDisplay,
|
|
21423
|
+
keyId: entry.keyId,
|
|
21424
|
+
reused: false,
|
|
21425
|
+
tenant,
|
|
21426
|
+
willActivate,
|
|
21427
|
+
explicitlyDeclined: opts.activate === false
|
|
21428
|
+
});
|
|
20057
21429
|
return 0;
|
|
20058
21430
|
} catch (err) {
|
|
20059
21431
|
process.stderr.write(formatCliError(err, { action: "join failed" }));
|
|
@@ -20061,27 +21433,34 @@ async function runJoin(opts, deps = {}) {
|
|
|
20061
21433
|
}
|
|
20062
21434
|
});
|
|
20063
21435
|
}
|
|
20064
|
-
function emitJoined(
|
|
20065
|
-
|
|
20066
|
-
|
|
20067
|
-
|
|
20068
|
-
|
|
20069
|
-
|
|
20070
|
-
|
|
20071
|
-
|
|
20072
|
-
|
|
20073
|
-
|
|
21436
|
+
function emitJoined(opts) {
|
|
21437
|
+
const header = `
|
|
21438
|
+
\u2714 Joined '${opts.context}' as ${opts.principalId}` + (opts.reused ? " (existing key reused)" : "") + "\n" + formatKeyValues([
|
|
21439
|
+
["alias", opts.alias],
|
|
21440
|
+
["context", opts.context],
|
|
21441
|
+
["principal", opts.principalId],
|
|
21442
|
+
["role", opts.role],
|
|
21443
|
+
["key id", opts.keyId]
|
|
21444
|
+
]);
|
|
21445
|
+
const activationNote = opts.willActivate ? `
|
|
20074
21446
|
|
|
20075
|
-
Now active. Use \`vectros switch ${alias}\` to return to it, \`vectros keyring show --format env\` to export the key for the MCP server.
|
|
20076
|
-
`
|
|
21447
|
+
Now active. Use \`vectros switch ${opts.alias}\` to return to it, \`vectros keyring show --format env\` to export the key for the MCP server.
|
|
21448
|
+
` : "";
|
|
21449
|
+
process.stdout.write(
|
|
21450
|
+
header + activationNote + tenantActivateMessage({
|
|
21451
|
+
alias: opts.alias,
|
|
21452
|
+
tenant: opts.tenant,
|
|
21453
|
+
willActivate: opts.willActivate,
|
|
21454
|
+
explicitlyDeclined: opts.explicitlyDeclined
|
|
21455
|
+
})
|
|
20077
21456
|
);
|
|
20078
21457
|
}
|
|
20079
21458
|
|
|
20080
21459
|
// src/build-info.ts
|
|
20081
21460
|
var BUILD_INFO = {
|
|
20082
|
-
cli: true ? "0.
|
|
20083
|
-
sdk: true ? "0.
|
|
20084
|
-
blueprints: true ? "0.
|
|
21461
|
+
cli: true ? "0.19.0" : "dev",
|
|
21462
|
+
sdk: true ? "0.41.0" : "dev",
|
|
21463
|
+
blueprints: true ? "0.16.0" : "dev"
|
|
20085
21464
|
};
|
|
20086
21465
|
function formatBuildInfo() {
|
|
20087
21466
|
return `vectros ${BUILD_INFO.cli} (sdk ${BUILD_INFO.sdk}, blueprints ${BUILD_INFO.blueprints})`;
|
|
@@ -20105,6 +21484,14 @@ function addCommonAuthOptions(cmd) {
|
|
|
20105
21484
|
"emit machine-readable JSON instead of the human-readable view"
|
|
20106
21485
|
);
|
|
20107
21486
|
}
|
|
21487
|
+
function parseTenantFlag(raw) {
|
|
21488
|
+
if (raw !== void 0 && raw !== "test" && raw !== "live") {
|
|
21489
|
+
process.stderr.write(` \u2716 --tenant must be 'test' or 'live' (got '${raw}')
|
|
21490
|
+
`);
|
|
21491
|
+
process.exit(2);
|
|
21492
|
+
}
|
|
21493
|
+
return raw;
|
|
21494
|
+
}
|
|
20108
21495
|
function toCommonOptions(flags) {
|
|
20109
21496
|
return {
|
|
20110
21497
|
env: flags.env,
|
|
@@ -20153,12 +21540,16 @@ function buildProgram(deps = {}) {
|
|
|
20153
21540
|
registerAccessCommands(program, deps);
|
|
20154
21541
|
registerRoleCommands(program, deps);
|
|
20155
21542
|
registerContextCommands(program, deps);
|
|
21543
|
+
registerIssuersCommands(program, deps);
|
|
20156
21544
|
registerJoinCommand(program, deps);
|
|
20157
21545
|
registerKeyringCommands(program);
|
|
20158
21546
|
return program;
|
|
20159
21547
|
}
|
|
20160
21548
|
function registerJoinCommand(program, deps) {
|
|
20161
|
-
const join3 = program.command("join").description("Enroll a principal in a context (grant + key) and keep the key in the keyring.").argument("<context>", "the app context to join").option("--role <roleId>", "reference a Role to grant (XOR --actions)").option("--actions <csv>", "inline single-clause actions to grant, comma-separated (XOR --role)").option("--as <principal>", "who to enroll: 'me' (default) or an explicit 'usr_<id>'", "me").option("--scope <ns:value>", "identity override: a scope the member acts under (repeatable, e.g. org:<id>)", collect, []).option("--alias <name>", "keyring entry name (defaults to the context id)").option("--tenant <tenant>", "target tenant: 'test' or 'live' (default live)")
|
|
21549
|
+
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 (repeatable \u2014 2+ compose additively; XOR --actions)", collect, []).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(
|
|
21550
|
+
"--activate",
|
|
21551
|
+
"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)"
|
|
21552
|
+
).option("--no-activate", "store the key without activating it, regardless of tenant");
|
|
20162
21553
|
addAuthOptions(join3).action(
|
|
20163
21554
|
async (contextArg, flags) => {
|
|
20164
21555
|
if (flags.tenant !== void 0 && flags.tenant !== "test" && flags.tenant !== "live") {
|
|
@@ -20176,7 +21567,8 @@ function registerJoinCommand(program, deps) {
|
|
|
20176
21567
|
as: flags.as,
|
|
20177
21568
|
scope: flags.scope,
|
|
20178
21569
|
alias: flags.alias,
|
|
20179
|
-
tenant: flags.tenant
|
|
21570
|
+
tenant: flags.tenant,
|
|
21571
|
+
activate: flags.activate
|
|
20180
21572
|
},
|
|
20181
21573
|
deps
|
|
20182
21574
|
)
|
|
@@ -20288,6 +21680,27 @@ function registerContextCommands(program, deps) {
|
|
|
20288
21680
|
}
|
|
20289
21681
|
);
|
|
20290
21682
|
}
|
|
21683
|
+
function registerIssuersCommands(program, deps) {
|
|
21684
|
+
const issuers = program.command("issuers").description("Inspect registered third-party IdP issuers (token-exchange trust anchors).");
|
|
21685
|
+
const list = issuers.command("list").description("List issuers registered in one app context.").requiredOption("--context <c>", "app context the issuer(s) are registered under").option("--tenant <tenant>", "tenant to read: 'test' or 'live' (default live)");
|
|
21686
|
+
addCommonAuthOptions(list).action(
|
|
21687
|
+
async (flags) => {
|
|
21688
|
+
const tenant = parseTenantFlag(flags.tenant);
|
|
21689
|
+
process.exit(
|
|
21690
|
+
await runIssuersList({ ...toCommonOptions(flags), context: flags.context, tenant }, deps)
|
|
21691
|
+
);
|
|
21692
|
+
}
|
|
21693
|
+
);
|
|
21694
|
+
const get = issuers.command("get").description("Show one registered issuer.").argument("<issuerId>").requiredOption("--context <c>", "app context the issuer is registered under").option("--tenant <tenant>", "tenant to read: 'test' or 'live' (default live)");
|
|
21695
|
+
addCommonAuthOptions(get).action(
|
|
21696
|
+
async (issuerId, flags) => {
|
|
21697
|
+
const tenant = parseTenantFlag(flags.tenant);
|
|
21698
|
+
process.exit(
|
|
21699
|
+
await runIssuersGet({ ...toCommonOptions(flags), context: flags.context, issuerId, tenant }, deps)
|
|
21700
|
+
);
|
|
21701
|
+
}
|
|
21702
|
+
);
|
|
21703
|
+
}
|
|
20291
21704
|
function registerRoleCommands(program, deps) {
|
|
20292
21705
|
const role = program.command("role").description("Manage context-scoped roles (reusable scope rules; bind via `access grant --role`).");
|
|
20293
21706
|
const create = role.command("create").description("Create (idempotent by roleId) a single-clause role from --actions.").requiredOption("--context <c>", "app context").requiredOption("--role-id <id>", "role id (3-31 chars; lowercase letter + lowercase/digit/dash)").requiredOption("--name <name>", "human-readable name").requiredOption("--actions <csv>", "allowed actions, comma-separated (e.g. records:cru,search:r)").option(
|
|
@@ -20324,7 +21737,7 @@ function registerRoleCommands(program, deps) {
|
|
|
20324
21737
|
}
|
|
20325
21738
|
function registerAccessCommands(program, deps) {
|
|
20326
21739
|
const access2 = program.command("access").description("Manage access bindings: principal \u2194 context \u2194 role/scopes.");
|
|
20327
|
-
const grant = access2.command("grant").description("Bind a principal to a context via a role or inline actions.").requiredOption("--principal <p>", "principal: 'usr_<userId>', 'key_<keyId>', or 'me' (your own user)").requiredOption("--context <c>", "app context").option("--role <roleId>", "reference a Role (XOR --actions)").option("--actions <csv>", "inline single-clause actions, comma-separated (XOR --role)").option("--scope <ns:value>", "identity override: a scope the principal acts under (repeatable, e.g. org:<id>)", collect, []).option("--identity-overrides <json>", "general identity overrides as a JSON object (merged over --scope)");
|
|
21740
|
+
const grant = access2.command("grant").description("Bind a principal to a context via a role or inline actions.").requiredOption("--principal <p>", "principal: 'usr_<userId>', 'key_<keyId>', or 'me' (your own user)").requiredOption("--context <c>", "app context").option("--role <roleId>", "reference a Role (repeatable \u2014 2+ compose additively; XOR --actions)", collect, []).option("--actions <csv>", "inline single-clause actions, comma-separated (XOR --role)").option("--scope <ns:value>", "identity override: a scope the principal acts under (repeatable, e.g. org:<id>)", collect, []).option("--identity-overrides <json>", "general identity overrides as a JSON object (merged over --scope)");
|
|
20328
21741
|
addCommonAuthOptions(grant).action(
|
|
20329
21742
|
async (flags) => {
|
|
20330
21743
|
process.exit(
|
|
@@ -20405,6 +21818,25 @@ function registerIdentityCommands(program, deps) {
|
|
|
20405
21818
|
addAuthOptions(del).action(async (flags) => {
|
|
20406
21819
|
process.exit(await runIdentityDelete({ ...toCommonOptions(flags), type: flags.type, id: flags.id }, deps));
|
|
20407
21820
|
});
|
|
21821
|
+
const invite = identity.command("invite").description("Onboard a person into a context by email (POST /v1/users/invite) \u2014 no root key required.").requiredOption("--context <contextId>", "the app context the invitee will operate in").requiredOption("--role <roleId>", "an existing Role in that context; the invitee inherits its scopes").requiredOption("--email <email>", "the invitee's email address").option("--first-name <name>", "personalizes the invitation email greeting").option("--accept-url <url>", "your accept-invite landing page (required unless --no-send-email)").option("--no-send-email", "skip platform delivery; the response carries the raw inviteToken/acceptLink instead");
|
|
21822
|
+
addCommonAuthOptions(invite).action(
|
|
21823
|
+
async (flags) => {
|
|
21824
|
+
process.exit(
|
|
21825
|
+
await runIdentityInvite(
|
|
21826
|
+
{
|
|
21827
|
+
...toCommonOptions(flags),
|
|
21828
|
+
context: flags.context,
|
|
21829
|
+
role: flags.role,
|
|
21830
|
+
email: flags.email,
|
|
21831
|
+
firstName: flags.firstName,
|
|
21832
|
+
acceptUrl: flags.acceptUrl,
|
|
21833
|
+
noSendEmail: flags.sendEmail === false
|
|
21834
|
+
},
|
|
21835
|
+
deps
|
|
21836
|
+
)
|
|
21837
|
+
);
|
|
21838
|
+
}
|
|
21839
|
+
);
|
|
20408
21840
|
}
|
|
20409
21841
|
function addKeyDestinationOptions(cmd) {
|
|
20410
21842
|
return cmd.addOption(new import_commander.Option("--format <fmt>", "how to emit the secret").choices(KEY_FORMATS).default("human")).option(
|
|
@@ -20423,9 +21855,10 @@ function toKeyDestination(flags) {
|
|
|
20423
21855
|
function registerKeyCommands(program, deps) {
|
|
20424
21856
|
const key = program.command("key").description("Manage scoped API keys (ssk_*) \u2014 issue, list, revoke, rotate.");
|
|
20425
21857
|
const issue = addKeyDestinationOptions(
|
|
20426
|
-
key.command("issue").description("Mint an ssk_* for a principal that already has a profile in the context.").requiredOption("--principal <p>", "principal (usr_<id> or <id>) with an AccessProfile in the context").requiredOption("--context <c>", "app context the key operates in").option("--name <name>", "key name (idempotency tuple component)", DEFAULT_KEY_NAME).option("--label <label>", "optional principal label (surfaced in /v1/ping)")
|
|
21858
|
+
key.command("issue").description("Mint an ssk_* for a principal that already has a profile in the context.").requiredOption("--principal <p>", "principal (usr_<id> or <id>) with an AccessProfile in the context").requiredOption("--context <c>", "app context the key operates in").option("--name <name>", "key name (idempotency tuple component)", DEFAULT_KEY_NAME).option("--label <label>", "optional principal label (surfaced in /v1/ping)").option("--tenant <tenant>", "tenant the key lives in: 'test' or 'live' (default live)")
|
|
20427
21859
|
);
|
|
20428
21860
|
addAuthOptions(issue).action(async (flags) => {
|
|
21861
|
+
const tenant = parseTenantFlag(flags.tenant);
|
|
20429
21862
|
process.exit(
|
|
20430
21863
|
await runKeyIssue({
|
|
20431
21864
|
...toCommonOptions(flags),
|
|
@@ -20433,6 +21866,7 @@ function registerKeyCommands(program, deps) {
|
|
|
20433
21866
|
context: flags.context,
|
|
20434
21867
|
name: flags.name,
|
|
20435
21868
|
label: flags.label,
|
|
21869
|
+
tenant,
|
|
20436
21870
|
...toKeyDestination(flags)
|
|
20437
21871
|
}, deps)
|
|
20438
21872
|
);
|
|
@@ -20440,37 +21874,50 @@ function registerKeyCommands(program, deps) {
|
|
|
20440
21874
|
const list = key.command("list").description("List active scoped keys in one context (default, or --context <id>); --all-contexts for every context.").option("--principal <p>", "only keys for this principal (usr_<id> or <id>)").option("--context <c>", "only keys in this context (default: the default context)").option(
|
|
20441
21875
|
"--all-contexts",
|
|
20442
21876
|
"list every key across ALL your contexts (owner-only) \u2014 the tenant-wide view for the key limit"
|
|
20443
|
-
);
|
|
21877
|
+
).option("--tenant <tenant>", "tenant to list: 'test' or 'live' (default live; not combinable with --all-contexts)");
|
|
20444
21878
|
addCommonAuthOptions(list).action(
|
|
20445
21879
|
async (flags) => {
|
|
21880
|
+
if (flags.allContexts && flags.tenant !== void 0) {
|
|
21881
|
+
process.stderr.write(" \u2716 --tenant is not combinable with --all-contexts (which already spans both tenants).\n");
|
|
21882
|
+
process.exit(2);
|
|
21883
|
+
}
|
|
21884
|
+
const tenant = parseTenantFlag(flags.tenant);
|
|
20446
21885
|
process.exit(
|
|
20447
21886
|
await runKeyList(
|
|
20448
21887
|
{
|
|
20449
21888
|
...toCommonOptions(flags),
|
|
20450
21889
|
principal: flags.principal,
|
|
20451
21890
|
context: flags.context,
|
|
20452
|
-
allContexts: Boolean(flags.allContexts)
|
|
21891
|
+
allContexts: Boolean(flags.allContexts),
|
|
21892
|
+
tenant
|
|
20453
21893
|
},
|
|
20454
21894
|
deps
|
|
20455
21895
|
)
|
|
20456
21896
|
);
|
|
20457
21897
|
}
|
|
20458
21898
|
);
|
|
20459
|
-
const get = key.command("get").description("Show one key\u2019s metadata (no secret).").argument("<keyId>").option("--context <c>", "context the key is bound to (default: the default context)");
|
|
20460
|
-
addCommonAuthOptions(get).action(
|
|
20461
|
-
|
|
20462
|
-
|
|
20463
|
-
|
|
20464
|
-
|
|
20465
|
-
|
|
20466
|
-
|
|
21899
|
+
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)");
|
|
21900
|
+
addCommonAuthOptions(get).action(
|
|
21901
|
+
async (keyId, flags) => {
|
|
21902
|
+
const tenant = parseTenantFlag(flags.tenant);
|
|
21903
|
+
process.exit(await runKeyGet(keyId, { ...toCommonOptions(flags), context: flags.context, tenant }, deps));
|
|
21904
|
+
}
|
|
21905
|
+
);
|
|
21906
|
+
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)");
|
|
21907
|
+
addAuthOptions(revoke).action(
|
|
21908
|
+
async (keyId, flags) => {
|
|
21909
|
+
const tenant = parseTenantFlag(flags.tenant);
|
|
21910
|
+
process.exit(await runKeyRevoke(keyId, { ...toCommonOptions(flags), context: flags.context, tenant }, deps));
|
|
21911
|
+
}
|
|
21912
|
+
);
|
|
20467
21913
|
const rotate = addKeyDestinationOptions(
|
|
20468
21914
|
key.command("rotate").description("Revoke the matching active key and mint a fresh secret.").requiredOption("--principal <p>", "principal (usr_<id> or <id>)").requiredOption("--context <c>", "app context").option("--name <name>", "key name to rotate", DEFAULT_KEY_NAME).option(
|
|
20469
21915
|
"--force-new",
|
|
20470
21916
|
"mint a new key even though other active keys already exist for this principal/context under a different name (bypasses the default-name-mismatch guard)"
|
|
20471
|
-
)
|
|
21917
|
+
).option("--tenant <tenant>", "tenant the key lives in: 'test' or 'live' (default live)")
|
|
20472
21918
|
);
|
|
20473
21919
|
addAuthOptions(rotate).action(async (flags) => {
|
|
21920
|
+
const tenant = parseTenantFlag(flags.tenant);
|
|
20474
21921
|
process.exit(
|
|
20475
21922
|
await runKeyRotate({
|
|
20476
21923
|
...toCommonOptions(flags),
|
|
@@ -20478,6 +21925,7 @@ function registerKeyCommands(program, deps) {
|
|
|
20478
21925
|
context: flags.context,
|
|
20479
21926
|
name: flags.name,
|
|
20480
21927
|
forceNew: flags.forceNew,
|
|
21928
|
+
tenant,
|
|
20481
21929
|
...toKeyDestination(flags)
|
|
20482
21930
|
}, deps)
|
|
20483
21931
|
);
|