@vectros-ai/cli 0.16.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 +56 -11
- package/CHANGELOG.md +191 -0
- package/README.md +66 -7
- package/THIRD-PARTY-NOTICES.md +4 -4
- package/dist/cli.js +895 -161
- package/dist/cli.js.map +1 -1
- package/dist/cli.mjs +895 -161
- package/dist/cli.mjs.map +1 -1
- package/package.json +3 -3
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.
|
|
@@ -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. 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.
|
|
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.
|
|
@@ -5656,6 +5656,73 @@ var AuthClient = class {
|
|
|
5656
5656
|
}
|
|
5657
5657
|
return handleNonStatusCodeError(_response.error, _response.rawResponse, "GET", "/v1/auth/issuers/{issuerId}");
|
|
5658
5658
|
}
|
|
5659
|
+
/**
|
|
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
|
+
}
|
|
5659
5726
|
/**
|
|
5660
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
|
*
|
|
@@ -5726,7 +5793,7 @@ var AuthClient = class {
|
|
|
5726
5793
|
);
|
|
5727
5794
|
}
|
|
5728
5795
|
/**
|
|
5729
|
-
* Returns the issuers registered in your tenant. Requires a root API key or the bootstrap's provisioning capability. A credential confined to one app context sees only the issuers registered in that context; a root API key sees every context. Returns a `{data, nextCursor}` envelope.
|
|
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.
|
|
5730
5797
|
*
|
|
5731
5798
|
* @param {Vectros.ListIssuersRequest} request
|
|
5732
5799
|
* @param {AuthClient.RequestOptions} requestOptions - Request-specific configuration.
|
|
@@ -5783,7 +5850,7 @@ var AuthClient = class {
|
|
|
5783
5850
|
return handleNonStatusCodeError(_response.error, _response.rawResponse, "GET", "/v1/auth/issuers");
|
|
5784
5851
|
}
|
|
5785
5852
|
/**
|
|
5786
|
-
* Registers a trusted third-party IdP issuer that BYO-IdP token exchange (`POST /v1/auth/token/exchange`) may accept a `subject_token` from. Requires a root API key or the CLI bootstrap's provisioning capability — never an ordinary partner-grantable scope. Idempotent by `issuerId` within your tenant; the `(issuer, audience)` pair must not already be registered by a different issuerId/tenant. An app context may have at most one active issuer — deregister the existing one first if you need to replace it. One issuer MAY serve several contexts today, each via its own registration row with a distinct `audience`.
|
|
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`.
|
|
5787
5854
|
*
|
|
5788
5855
|
* @param {Vectros.IssuerRequest} request
|
|
5789
5856
|
* @param {AuthClient.RequestOptions} requestOptions - Request-specific configuration.
|
|
@@ -5968,6 +6035,7 @@ var AuthClient = class {
|
|
|
5968
6035
|
*
|
|
5969
6036
|
* @throws {@link Vectros.BadRequestError}
|
|
5970
6037
|
* @throws {@link Vectros.ForbiddenError}
|
|
6038
|
+
* @throws {@link Vectros.NotFoundError}
|
|
5971
6039
|
* @throws {@link Vectros.TooManyRequestsError}
|
|
5972
6040
|
*
|
|
5973
6041
|
* @example
|
|
@@ -6021,6 +6089,8 @@ var AuthClient = class {
|
|
|
6021
6089
|
throw new BadRequestError(_response.error.body, _response.rawResponse);
|
|
6022
6090
|
case 403:
|
|
6023
6091
|
throw new ForbiddenError(_response.error.body, _response.rawResponse);
|
|
6092
|
+
case 404:
|
|
6093
|
+
throw new NotFoundError(_response.error.body, _response.rawResponse);
|
|
6024
6094
|
case 429:
|
|
6025
6095
|
throw new TooManyRequestsError(_response.error.body, _response.rawResponse);
|
|
6026
6096
|
default:
|
|
@@ -6034,7 +6104,7 @@ var AuthClient = class {
|
|
|
6034
6104
|
return handleNonStatusCodeError(_response.error, _response.rawResponse, "POST", "/v1/auth/token");
|
|
6035
6105
|
}
|
|
6036
6106
|
/**
|
|
6037
|
-
* 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.
|
|
6038
6108
|
*
|
|
6039
6109
|
* @param {Vectros.CreateInviteRequest} request
|
|
6040
6110
|
* @param {AuthClient.RequestOptions} requestOptions - Request-specific configuration.
|
|
@@ -6172,6 +6242,83 @@ var AuthClient = class {
|
|
|
6172
6242
|
}
|
|
6173
6243
|
return handleNonStatusCodeError(_response.error, _response.rawResponse, "POST", "/v1/users/invite/resend");
|
|
6174
6244
|
}
|
|
6245
|
+
/**
|
|
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
|
+
}
|
|
6175
6322
|
/**
|
|
6176
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.
|
|
6177
6324
|
*
|
|
@@ -6249,9 +6396,9 @@ var ComplianceClient = class {
|
|
|
6249
6396
|
* @param {Vectros.ErasureRequest} request
|
|
6250
6397
|
* @param {ComplianceClient.RequestOptions} requestOptions - Request-specific configuration.
|
|
6251
6398
|
*
|
|
6399
|
+
* @throws {@link Vectros.BadRequestError}
|
|
6252
6400
|
* @throws {@link Vectros.ForbiddenError}
|
|
6253
6401
|
* @throws {@link Vectros.TooManyRequestsError}
|
|
6254
|
-
* @throws {@link Vectros.NotImplementedError}
|
|
6255
6402
|
*
|
|
6256
6403
|
* @example
|
|
6257
6404
|
* await client.compliance.createErasureRequest({
|
|
@@ -6290,12 +6437,12 @@ var ComplianceClient = class {
|
|
|
6290
6437
|
}
|
|
6291
6438
|
if (_response.error.reason === "status-code") {
|
|
6292
6439
|
switch (_response.error.statusCode) {
|
|
6440
|
+
case 400:
|
|
6441
|
+
throw new BadRequestError(_response.error.body, _response.rawResponse);
|
|
6293
6442
|
case 403:
|
|
6294
6443
|
throw new ForbiddenError(_response.error.body, _response.rawResponse);
|
|
6295
6444
|
case 429:
|
|
6296
6445
|
throw new TooManyRequestsError(_response.error.body, _response.rawResponse);
|
|
6297
|
-
case 501:
|
|
6298
|
-
throw new NotImplementedError(_response.error.body, _response.rawResponse);
|
|
6299
6446
|
default:
|
|
6300
6447
|
throw new VectrosError({
|
|
6301
6448
|
statusCode: _response.error.statusCode,
|
|
@@ -6314,7 +6461,6 @@ var ComplianceClient = class {
|
|
|
6314
6461
|
*
|
|
6315
6462
|
* @throws {@link Vectros.ForbiddenError}
|
|
6316
6463
|
* @throws {@link Vectros.NotFoundError}
|
|
6317
|
-
* @throws {@link Vectros.NotImplementedError}
|
|
6318
6464
|
*
|
|
6319
6465
|
* @example
|
|
6320
6466
|
* await client.compliance.getErasureRequest({
|
|
@@ -6355,8 +6501,6 @@ var ComplianceClient = class {
|
|
|
6355
6501
|
throw new ForbiddenError(_response.error.body, _response.rawResponse);
|
|
6356
6502
|
case 404:
|
|
6357
6503
|
throw new NotFoundError(_response.error.body, _response.rawResponse);
|
|
6358
|
-
case 501:
|
|
6359
|
-
throw new NotImplementedError(_response.error.body, _response.rawResponse);
|
|
6360
6504
|
default:
|
|
6361
6505
|
throw new VectrosError({
|
|
6362
6506
|
statusCode: _response.error.statusCode,
|
|
@@ -8196,7 +8340,7 @@ var IdentityClient = class {
|
|
|
8196
8340
|
);
|
|
8197
8341
|
}
|
|
8198
8342
|
/**
|
|
8199
|
-
* Retrieves a single scope-namespace registration by name.
|
|
8343
|
+
* Retrieves a single scope-namespace registration by name.
|
|
8200
8344
|
*
|
|
8201
8345
|
* @param {Vectros.GetNamespaceRequest} request
|
|
8202
8346
|
* @param {IdentityClient.RequestOptions} requestOptions - Request-specific configuration.
|
|
@@ -8389,7 +8533,7 @@ var IdentityClient = class {
|
|
|
8389
8533
|
return handleNonStatusCodeError(_response.error, _response.rawResponse, "DELETE", "/v1/namespaces/{namespace}");
|
|
8390
8534
|
}
|
|
8391
8535
|
/**
|
|
8392
|
-
* Returns the scope namespaces in your account
|
|
8536
|
+
* Returns the scope namespaces registered in your account. Returns a `{data, nextCursor}` envelope.
|
|
8393
8537
|
*
|
|
8394
8538
|
* @param {Vectros.ListNamespacesRequest} request
|
|
8395
8539
|
* @param {IdentityClient.RequestOptions} requestOptions - Request-specific configuration.
|
|
@@ -8440,7 +8584,7 @@ var IdentityClient = class {
|
|
|
8440
8584
|
return handleNonStatusCodeError(_response.error, _response.rawResponse, "GET", "/v1/namespaces");
|
|
8441
8585
|
}
|
|
8442
8586
|
/**
|
|
8443
|
-
* Registers a new scope namespace and declares whether its values resolve to identity entities (`entityBacked`). Also requires `specificityRank`, an explicit, account-unique position in the specificity order used to break recordType schema-resolution ties. Requires a root API key or the CLI bootstrap's provisioning
|
|
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.
|
|
8444
8588
|
*
|
|
8445
8589
|
* @param {Vectros.RegisterNamespaceRequest} request
|
|
8446
8590
|
* @param {IdentityClient.RequestOptions} requestOptions - Request-specific configuration.
|
|
@@ -8510,7 +8654,7 @@ var IdentityClient = class {
|
|
|
8510
8654
|
return handleNonStatusCodeError(_response.error, _response.rawResponse, "POST", "/v1/namespaces");
|
|
8511
8655
|
}
|
|
8512
8656
|
/**
|
|
8513
|
-
* 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.
|
|
8514
8658
|
*
|
|
8515
8659
|
* @param {Vectros.ListUsersRequest} request
|
|
8516
8660
|
* @param {IdentityClient.RequestOptions} requestOptions - Request-specific configuration.
|
|
@@ -8575,7 +8719,7 @@ var IdentityClient = class {
|
|
|
8575
8719
|
return handleNonStatusCodeError(_response.error, _response.rawResponse, "GET", "/v1/users");
|
|
8576
8720
|
}
|
|
8577
8721
|
/**
|
|
8578
|
-
* 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`.
|
|
8579
8723
|
*
|
|
8580
8724
|
* @param {Vectros.CreateUserRequest} request
|
|
8581
8725
|
* @param {IdentityClient.RequestOptions} requestOptions - Request-specific configuration.
|
|
@@ -8699,7 +8843,7 @@ var IdentityClient = class {
|
|
|
8699
8843
|
return handleNonStatusCodeError(_response.error, _response.rawResponse, "GET", "/v1/users/{id}");
|
|
8700
8844
|
}
|
|
8701
8845
|
/**
|
|
8702
|
-
* 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.
|
|
8703
8847
|
*
|
|
8704
8848
|
* @param {Vectros.UpdateUserRequest} request
|
|
8705
8849
|
* @param {IdentityClient.RequestOptions} requestOptions - Request-specific configuration.
|
|
@@ -8766,7 +8910,7 @@ var IdentityClient = class {
|
|
|
8766
8910
|
return handleNonStatusCodeError(_response.error, _response.rawResponse, "PUT", "/v1/users/{id}");
|
|
8767
8911
|
}
|
|
8768
8912
|
/**
|
|
8769
|
-
* Permanently deletes a user identity. This cannot be undone. If the user is a pending invitation, the associated access profile created for that invitation is also removed. Requires the `users:d` scope. Deleting your account's last OWNER is refused (409) — an account must always retain at least one owner.
|
|
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.
|
|
8770
8914
|
*
|
|
8771
8915
|
* @param {Vectros.DeleteUserRequest} request
|
|
8772
8916
|
* @param {IdentityClient.RequestOptions} requestOptions - Request-specific configuration.
|
|
@@ -8827,7 +8971,7 @@ var IdentityClient = class {
|
|
|
8827
8971
|
return handleNonStatusCodeError(_response.error, _response.rawResponse, "DELETE", "/v1/users/{id}");
|
|
8828
8972
|
}
|
|
8829
8973
|
/**
|
|
8830
|
-
* 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.
|
|
8831
8975
|
*
|
|
8832
8976
|
* @param {Vectros.IdentityLookupRequest} request
|
|
8833
8977
|
* @param {IdentityClient.RequestOptions} requestOptions - Request-specific configuration.
|
|
@@ -9270,14 +9414,14 @@ var RecordsClient = class {
|
|
|
9270
9414
|
this._options = normalizeClientOptionsWithAuth(options);
|
|
9271
9415
|
}
|
|
9272
9416
|
/**
|
|
9273
|
-
*
|
|
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).
|
|
9274
9418
|
*
|
|
9275
9419
|
* @param {Vectros.BatchGetRequest} request
|
|
9276
9420
|
* @param {RecordsClient.RequestOptions} requestOptions - Request-specific configuration.
|
|
9277
9421
|
*
|
|
9422
|
+
* @throws {@link Vectros.BadRequestError}
|
|
9278
9423
|
* @throws {@link Vectros.ForbiddenError}
|
|
9279
9424
|
* @throws {@link Vectros.TooManyRequestsError}
|
|
9280
|
-
* @throws {@link Vectros.NotImplementedError}
|
|
9281
9425
|
*
|
|
9282
9426
|
* @example
|
|
9283
9427
|
* await client.records.batchGetRecords()
|
|
@@ -9314,12 +9458,12 @@ var RecordsClient = class {
|
|
|
9314
9458
|
}
|
|
9315
9459
|
if (_response.error.reason === "status-code") {
|
|
9316
9460
|
switch (_response.error.statusCode) {
|
|
9461
|
+
case 400:
|
|
9462
|
+
throw new BadRequestError(_response.error.body, _response.rawResponse);
|
|
9317
9463
|
case 403:
|
|
9318
9464
|
throw new ForbiddenError(_response.error.body, _response.rawResponse);
|
|
9319
9465
|
case 429:
|
|
9320
9466
|
throw new TooManyRequestsError(_response.error.body, _response.rawResponse);
|
|
9321
|
-
case 501:
|
|
9322
|
-
throw new NotImplementedError(_response.error.body, _response.rawResponse);
|
|
9323
9467
|
default:
|
|
9324
9468
|
throw new VectrosError({
|
|
9325
9469
|
statusCode: _response.error.statusCode,
|
|
@@ -10133,7 +10277,7 @@ var SchemasClient = class {
|
|
|
10133
10277
|
this._options = normalizeClientOptionsWithAuth(options);
|
|
10134
10278
|
}
|
|
10135
10279
|
/**
|
|
10136
|
-
* 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.
|
|
10137
10281
|
*
|
|
10138
10282
|
* @param {Vectros.ListSchemasRequest} request
|
|
10139
10283
|
* @param {SchemasClient.RequestOptions} requestOptions - Request-specific configuration.
|
|
@@ -10421,6 +10565,7 @@ var SchemasClient = class {
|
|
|
10421
10565
|
* @param {Vectros.DeleteSchemaRequest} request
|
|
10422
10566
|
* @param {SchemasClient.RequestOptions} requestOptions - Request-specific configuration.
|
|
10423
10567
|
*
|
|
10568
|
+
* @throws {@link Vectros.ForbiddenError}
|
|
10424
10569
|
* @throws {@link Vectros.NotFoundError}
|
|
10425
10570
|
* @throws {@link Vectros.ConflictError}
|
|
10426
10571
|
* @throws {@link Vectros.TooManyRequestsError}
|
|
@@ -10460,6 +10605,8 @@ var SchemasClient = class {
|
|
|
10460
10605
|
}
|
|
10461
10606
|
if (_response.error.reason === "status-code") {
|
|
10462
10607
|
switch (_response.error.statusCode) {
|
|
10608
|
+
case 403:
|
|
10609
|
+
throw new ForbiddenError(_response.error.body, _response.rawResponse);
|
|
10463
10610
|
case 404:
|
|
10464
10611
|
throw new NotFoundError(_response.error.body, _response.rawResponse);
|
|
10465
10612
|
case 409:
|
|
@@ -10928,7 +11075,26 @@ var GrantedCapabilitiesSchema = import_zod.z.array(import_zod.z.string().min(1))
|
|
|
10928
11075
|
var BlueprintAccessProfileSchema = import_zod.z.object({
|
|
10929
11076
|
// Validated structurally here; the SCOPE GATE (in @vectros-ai/cli) is
|
|
10930
11077
|
// what enforces the data-plane-only security boundary.
|
|
10931
|
-
|
|
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(),
|
|
10932
11098
|
// Optional ownership binding: { userId: [...], "scope:org": [...], "scope:client": [...] }.
|
|
10933
11099
|
// A `null` element in a value list is the documented NULL SENTINEL — it
|
|
10934
11100
|
// grants access to TENANT-LEVEL (owner-less) records IN ADDITION to the
|
|
@@ -10976,13 +11142,54 @@ var BlueprintAccessProfileSchema = import_zod.z.object({
|
|
|
10976
11142
|
// a person is bound to. The gate should weigh the two separately rather
|
|
10977
11143
|
// than reuse one policy for both.
|
|
10978
11144
|
capabilities: GrantedCapabilitiesSchema
|
|
10979
|
-
}).strict()
|
|
11145
|
+
}).strict().superRefine((ap, ctx) => {
|
|
11146
|
+
const hasInline = ap.allowedActions !== void 0;
|
|
11147
|
+
const hasRoleIds = ap.roleIds !== void 0;
|
|
11148
|
+
if (hasInline === hasRoleIds) {
|
|
11149
|
+
ctx.addIssue({
|
|
11150
|
+
code: import_zod.z.ZodIssueCode.custom,
|
|
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)"
|
|
11153
|
+
});
|
|
11154
|
+
return;
|
|
11155
|
+
}
|
|
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
|
+
});
|
|
10980
11173
|
var BlueprintRoleClauseSchema = import_zod.z.object({
|
|
10981
11174
|
allowedActions: import_zod.z.array(import_zod.z.string().min(1)).min(1),
|
|
10982
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(),
|
|
10983
11177
|
capabilities: GrantedCapabilitiesSchema
|
|
10984
|
-
}).strict()
|
|
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
|
+
);
|
|
10985
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);
|
|
10986
11193
|
var IDENTITY_NAMESPACE_RE = /^[a-z][a-z0-9_-]{1,31}$/;
|
|
10987
11194
|
var FORBIDDEN_IDENTITY_NAMESPACES = /* @__PURE__ */ new Set(["record", "document", "entity", "user"]);
|
|
10988
11195
|
var IdentityKindSchema = import_zod.z.string().min(1).superRefine((val, ctx) => {
|
|
@@ -11046,6 +11253,14 @@ var BlueprintIssuerSchema = import_zod.z.object({
|
|
|
11046
11253
|
subClaim: import_zod.z.string().min(1).optional(),
|
|
11047
11254
|
/** The claim carrying the subject's email (first-login invite matching). Platform defaults to 'email' when omitted. */
|
|
11048
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(),
|
|
11049
11264
|
/**
|
|
11050
11265
|
* Opt-in self-service signup: a list of {signupType, roleId} pairs. When a first-time exchange
|
|
11051
11266
|
* caller presents no invite token but names a signupType matching one of these (or omits it and
|
|
@@ -11075,6 +11290,24 @@ var BlueprintNamespaceSchema = import_zod.z.object({
|
|
|
11075
11290
|
* free-form strings validated by grammar only.
|
|
11076
11291
|
*/
|
|
11077
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(),
|
|
11078
11311
|
/**
|
|
11079
11312
|
* This namespace's position in your account's specificity order (used to
|
|
11080
11313
|
* break a tie when a caller holds two scope dimensions at once). Required
|
|
@@ -11258,7 +11491,7 @@ function lintSelfTokenPlacement(value, ctx) {
|
|
|
11258
11491
|
ctx.addIssue({
|
|
11259
11492
|
code: import_zod.z.ZodIssueCode.custom,
|
|
11260
11493
|
path: path4,
|
|
11261
|
-
message: "'${{ self.* }}' / '${{ member.* }}' is a runtime placeholder \u2014 it is only valid inside a roles[].dataScope value"
|
|
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"
|
|
11262
11495
|
});
|
|
11263
11496
|
}
|
|
11264
11497
|
return;
|
|
@@ -11269,7 +11502,10 @@ function lintSelfTokenPlacement(value, ctx) {
|
|
|
11269
11502
|
}
|
|
11270
11503
|
if (node && typeof node === "object") {
|
|
11271
11504
|
for (const [k, v] of Object.entries(node)) {
|
|
11272
|
-
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;
|
|
11273
11509
|
walk(v, [...path4, k], entering);
|
|
11274
11510
|
}
|
|
11275
11511
|
}
|
|
@@ -11440,6 +11676,140 @@ function lintDataScopeKeys(value, ctx) {
|
|
|
11440
11676
|
}
|
|
11441
11677
|
});
|
|
11442
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;
|
|
11443
11813
|
}
|
|
11444
11814
|
function lintSeedSurfaces(value, ctx) {
|
|
11445
11815
|
const bp = value;
|
|
@@ -11510,6 +11880,24 @@ var BlueprintSchema = import_zod.z.object({
|
|
|
11510
11880
|
seed: import_zod.z.array(BlueprintSeedRecordSchema).optional(),
|
|
11511
11881
|
/** Optional multi-clause roles, bound to principals via `access grant --role`. */
|
|
11512
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(),
|
|
11513
11901
|
/** Optional principals ensured-exist at apply; referenced via ${{ identities.* }}. */
|
|
11514
11902
|
identities: IdentitiesDeclSchema.optional(),
|
|
11515
11903
|
/**
|
|
@@ -11534,6 +11922,9 @@ var BlueprintSchema = import_zod.z.object({
|
|
|
11534
11922
|
lintIdentityOverrides(bp, ctx);
|
|
11535
11923
|
lintNamespacedScopeArrays(bp, ctx);
|
|
11536
11924
|
lintDataScopeKeys(bp, ctx);
|
|
11925
|
+
lintDataScopeRefs(bp, ctx);
|
|
11926
|
+
lintRoleAssumable(bp, ctx);
|
|
11927
|
+
lintAccessProfileRoleIds(bp, ctx);
|
|
11537
11928
|
lintSeedSurfaces(bp, ctx);
|
|
11538
11929
|
lintIdentitySurfaces(bp, ctx);
|
|
11539
11930
|
lintIssuerContext(bp, ctx);
|
|
@@ -11573,7 +11964,7 @@ function parseBlueprint(input) {
|
|
|
11573
11964
|
throw new BlueprintValidationError(`Malformed blueprint:
|
|
11574
11965
|
${renderIssues(issues)}`, issues);
|
|
11575
11966
|
}
|
|
11576
|
-
return result.data;
|
|
11967
|
+
return expandDataScopeRefs(result.data);
|
|
11577
11968
|
}
|
|
11578
11969
|
function contextNameOf(blueprint) {
|
|
11579
11970
|
return blueprint.contextName ?? `MCP \u2014 ${blueprint.name}`;
|
|
@@ -11693,6 +12084,7 @@ var WHOLE_TOKEN_RE = /^\$\{\{\s*([A-Za-z_]\w*)\.([A-Za-z_]\w*)\s*\}\}$/;
|
|
|
11693
12084
|
var TOKEN_SCAN_RE = /\$\$\{\{|\$\{\{([\s\S]*?)\}\}|\$\{\{/g;
|
|
11694
12085
|
var INNER_REF_RE = /^([A-Za-z_]\w*)\.([A-Za-z_]\w*)$/;
|
|
11695
12086
|
var DEFERRED_NAMESPACES = /* @__PURE__ */ new Set(["self", "identities", "under", "member"]);
|
|
12087
|
+
var DEFERRED_BARE_TOKENS = /* @__PURE__ */ new Set(["any"]);
|
|
11696
12088
|
var DEFERRED_NS_ALTERNATION = [...DEFERRED_NAMESPACES].join("|");
|
|
11697
12089
|
var DEFERRED_INNER_RE = new RegExp(
|
|
11698
12090
|
`^(?:${DEFERRED_NS_ALTERNATION})(?:\\.[A-Za-z_]\\w*)+(?::[A-Za-z_]\\w*)?$`
|
|
@@ -11754,7 +12146,7 @@ function interpolate(str, ctx, path4) {
|
|
|
11754
12146
|
});
|
|
11755
12147
|
} else {
|
|
11756
12148
|
const inner = (m[1] ?? "").trim();
|
|
11757
|
-
if (DEFERRED_INNER_RE.test(inner)) {
|
|
12149
|
+
if (DEFERRED_INNER_RE.test(inner) || DEFERRED_BARE_TOKENS.has(inner)) {
|
|
11758
12150
|
out += m[0];
|
|
11759
12151
|
last = TOKEN_SCAN_RE.lastIndex;
|
|
11760
12152
|
continue;
|
|
@@ -14052,7 +14444,12 @@ async function resolveAuthContext(opts) {
|
|
|
14052
14444
|
const baseUrl = envToBaseUrl(opts.env, opts.baseUrl ?? process.env.VECTROS_API_BASE_URL);
|
|
14053
14445
|
const cognitoToken = await resolveCognitoToken(opts.env, opts.token);
|
|
14054
14446
|
const tenant = opts.tenant ?? "live";
|
|
14055
|
-
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 });
|
|
14056
14453
|
const client = new VectrosClient({ token: bridge.token, environment: baseUrl });
|
|
14057
14454
|
const identity = await resolveIdentity({ log: opts.log, apiKey: bridge.token, environment: baseUrl });
|
|
14058
14455
|
return {
|
|
@@ -14194,6 +14591,9 @@ var SELF_QUALIFIED_OPS = /* @__PURE__ */ new Map([
|
|
|
14194
14591
|
var UNQUALIFIABLE_WITH_ANY_TAIL = /* @__PURE__ */ new Map([
|
|
14195
14592
|
["profiles", /* @__PURE__ */ new Set(["r"])]
|
|
14196
14593
|
]);
|
|
14594
|
+
var CAPABILITY_UNLOCKED_OPS = /* @__PURE__ */ new Map([
|
|
14595
|
+
["profiles", /* @__PURE__ */ new Map([["c", "member-lifecycle"]])]
|
|
14596
|
+
]);
|
|
14197
14597
|
var VALID_OPS = /* @__PURE__ */ new Set(["c", "r", "u", "d"]);
|
|
14198
14598
|
var DATA_PLANE_SET = new Set(DATA_PLANE_ALLOWLIST);
|
|
14199
14599
|
var CONTROL_PLANE_SET = new Set(KNOWN_CONTROL_PLANE_RESOURCES);
|
|
@@ -14263,7 +14663,7 @@ function firstInvalidOp(ops) {
|
|
|
14263
14663
|
}
|
|
14264
14664
|
return void 0;
|
|
14265
14665
|
}
|
|
14266
|
-
function evaluateScopeToken(action) {
|
|
14666
|
+
function evaluateScopeToken(action, capabilities) {
|
|
14267
14667
|
const token = action.trim();
|
|
14268
14668
|
if (token.length === 0) {
|
|
14269
14669
|
return { action, reason: "empty scope token" };
|
|
@@ -14314,9 +14714,12 @@ function evaluateScopeToken(action) {
|
|
|
14314
14714
|
if (admittedOps?.has(ch)) continue;
|
|
14315
14715
|
if (selfOnlyOps?.has(ch)) {
|
|
14316
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);
|
|
14317
14720
|
return {
|
|
14318
14721
|
action,
|
|
14319
|
-
reason: `requests control-plane scope '${resource}:${ch}' without the required ':self' qualifier. A bare or arbitrarily-qualified '${resource}:${ch}' is outside the narrow allowlist (${admittedHint}) admitted for this resource \u2014 only '${resource}:${ch}:self' is
|
|
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`
|
|
14320
14723
|
};
|
|
14321
14724
|
}
|
|
14322
14725
|
return {
|
|
@@ -14349,11 +14752,11 @@ function evaluateScopeToken(action) {
|
|
|
14349
14752
|
}
|
|
14350
14753
|
return null;
|
|
14351
14754
|
}
|
|
14352
|
-
function evaluateScopeGate(allowedActions) {
|
|
14755
|
+
function evaluateScopeGate(allowedActions, capabilities) {
|
|
14353
14756
|
const accepted = [];
|
|
14354
14757
|
const rejected = [];
|
|
14355
14758
|
for (const action of allowedActions) {
|
|
14356
|
-
const rejection = evaluateScopeToken(action);
|
|
14759
|
+
const rejection = evaluateScopeToken(action, capabilities);
|
|
14357
14760
|
if (rejection) {
|
|
14358
14761
|
rejected.push(rejection);
|
|
14359
14762
|
} else {
|
|
@@ -14362,22 +14765,34 @@ function evaluateScopeGate(allowedActions) {
|
|
|
14362
14765
|
}
|
|
14363
14766
|
return { ok: rejected.length === 0, accepted, rejected };
|
|
14364
14767
|
}
|
|
14365
|
-
function assertScopeGate(allowedActions) {
|
|
14366
|
-
const result = evaluateScopeGate(allowedActions);
|
|
14768
|
+
function assertScopeGate(allowedActions, capabilities) {
|
|
14769
|
+
const result = evaluateScopeGate(allowedActions, capabilities);
|
|
14367
14770
|
if (!result.ok) {
|
|
14368
14771
|
throw new ScopeGateError(result.rejected);
|
|
14369
14772
|
}
|
|
14370
14773
|
}
|
|
14371
14774
|
function assertBlueprintGates(blueprint) {
|
|
14372
|
-
|
|
14373
|
-
|
|
14775
|
+
if (blueprint.accessProfile.allowedActions !== void 0) {
|
|
14776
|
+
assertScopeGate(blueprint.accessProfile.allowedActions, blueprint.accessProfile.capabilities);
|
|
14777
|
+
assertCapabilityGate(blueprint.accessProfile.capabilities);
|
|
14778
|
+
}
|
|
14374
14779
|
for (const clauses of Object.values(blueprint.roles ?? {})) {
|
|
14375
14780
|
for (const clause of clauses) {
|
|
14376
|
-
assertScopeGate(clause.allowedActions);
|
|
14781
|
+
assertScopeGate(clause.allowedActions, clause.capabilities);
|
|
14377
14782
|
assertCapabilityGate(clause.capabilities);
|
|
14378
14783
|
}
|
|
14379
14784
|
}
|
|
14380
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
|
+
}
|
|
14381
14796
|
|
|
14382
14797
|
// src/machine-id.ts
|
|
14383
14798
|
var import_node_fs = require("fs");
|
|
@@ -14482,7 +14897,8 @@ async function resolveApplyTimeIdentities(blueprint, client, log) {
|
|
|
14482
14897
|
function actionOf(created) {
|
|
14483
14898
|
return created ? "created" : "reconciled";
|
|
14484
14899
|
}
|
|
14485
|
-
async function registerNamespaceIdempotently(client,
|
|
14900
|
+
async function registerNamespaceIdempotently(client, blueprintContextId, decl) {
|
|
14901
|
+
const contextIdForRequest = decl.tenantWide ? void 0 : blueprintContextId;
|
|
14486
14902
|
const body = {
|
|
14487
14903
|
namespace: decl.namespace,
|
|
14488
14904
|
entityBacked: decl.entityBacked,
|
|
@@ -14498,6 +14914,7 @@ async function registerNamespaceIdempotently(client, contextIdForRequest, decl)
|
|
|
14498
14914
|
} catch (err) {
|
|
14499
14915
|
const info = extractServerError(err);
|
|
14500
14916
|
if (info.statusCode !== 400) throw err;
|
|
14917
|
+
if (decl.tenantWide) throw err;
|
|
14501
14918
|
let existing;
|
|
14502
14919
|
try {
|
|
14503
14920
|
existing = await client.getNamespace({ namespace: decl.namespace, contextId: contextIdForRequest });
|
|
@@ -14662,20 +15079,24 @@ function assertNoUnresolvedIdentities(blueprint) {
|
|
|
14662
15079
|
);
|
|
14663
15080
|
}
|
|
14664
15081
|
}
|
|
14665
|
-
|
|
14666
|
-
|
|
14667
|
-
|
|
14668
|
-
|
|
14669
|
-
|
|
14670
|
-
|
|
14671
|
-
|
|
14672
|
-
|
|
14673
|
-
|
|
14674
|
-
|
|
14675
|
-
|
|
14676
|
-
|
|
15082
|
+
var TenantWideNamespacesNotAllowedError = class extends Error {
|
|
15083
|
+
constructor(message) {
|
|
15084
|
+
super(message);
|
|
15085
|
+
this.name = "TenantWideNamespacesNotAllowedError";
|
|
15086
|
+
}
|
|
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;
|
|
14677
15098
|
assertBlueprintGates(blueprint);
|
|
14678
|
-
|
|
15099
|
+
assertTenantWideNamespacesAllowed(blueprint, allowTenantWideNamespaces);
|
|
14679
15100
|
const ctxResp = await client.auth.createAppContext({ body: { contextId: blueprint.contextId, name: contextNameOf(blueprint) } });
|
|
14680
15101
|
const contextAction = actionOf(ctxResp.created);
|
|
14681
15102
|
log.debug({ component: "loader", contextId: blueprint.contextId, contextAction }, "app-context ensured");
|
|
@@ -14714,10 +15135,10 @@ async function runLoader(client, input) {
|
|
|
14714
15135
|
"existing-principal collision confirmed (or unwired) \u2014 proceeding with any further write to this principal"
|
|
14715
15136
|
);
|
|
14716
15137
|
}
|
|
14717
|
-
const needsContextPinnedBootstrap = (blueprint.
|
|
15138
|
+
const needsContextPinnedBootstrap = (blueprint.namespaces ?? []).length > 0;
|
|
14718
15139
|
const ctxBootstrapClient = needsContextPinnedBootstrap ? input.remintBootstrapForContext ? await input.remintBootstrapForContext(blueprint.contextId) : client : void 0;
|
|
14719
15140
|
if (ctxBootstrapClient && input.remintBootstrapForContext) {
|
|
14720
|
-
log.debug({ component: "loader", contextId: blueprint.contextId }, "re-minted context-pinned bootstrap client");
|
|
15141
|
+
log.debug({ component: "loader", contextId: blueprint.contextId }, "re-minted context-pinned bootstrap client (namespaces)");
|
|
14721
15142
|
}
|
|
14722
15143
|
const namespaceActions = [];
|
|
14723
15144
|
for (const ns of blueprint.namespaces ?? []) {
|
|
@@ -14725,8 +15146,43 @@ async function runLoader(client, input) {
|
|
|
14725
15146
|
namespaceActions.push({ namespace: ns.namespace, action });
|
|
14726
15147
|
log.debug({ component: "loader", namespace: ns.namespace, contextId: blueprint.contextId, action }, "namespace ensured");
|
|
14727
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
|
+
}
|
|
14728
15180
|
const issuerActions = [];
|
|
14729
15181
|
for (const issuer of blueprint.issuers ?? []) {
|
|
15182
|
+
const selfSignupPolicies = issuer.selfSignupPolicies?.map((p) => ({
|
|
15183
|
+
signup_type: p.signupType,
|
|
15184
|
+
role_id: p.roleId
|
|
15185
|
+
}));
|
|
14730
15186
|
const issuerResp = await ctxBootstrapClient.auth.registerIssuer({
|
|
14731
15187
|
issuerId: issuer.issuerId,
|
|
14732
15188
|
issuer: issuer.issuer,
|
|
@@ -14735,18 +15191,26 @@ async function runLoader(client, input) {
|
|
|
14735
15191
|
contextId: issuer.contextId,
|
|
14736
15192
|
subClaim: issuer.subClaim,
|
|
14737
15193
|
emailClaim: issuer.emailClaim,
|
|
14738
|
-
|
|
14739
|
-
|
|
14740
|
-
role_id: p.roleId
|
|
14741
|
-
}))
|
|
15194
|
+
userinfoUri: issuer.userinfoUri,
|
|
15195
|
+
selfSignupPolicies
|
|
14742
15196
|
});
|
|
14743
15197
|
const action = actionOf(issuerResp.created);
|
|
14744
15198
|
issuerActions.push({ issuerId: issuer.issuerId, action });
|
|
14745
15199
|
if (action === "reconciled") {
|
|
14746
|
-
|
|
14747
|
-
|
|
14748
|
-
|
|
14749
|
-
|
|
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");
|
|
14750
15214
|
} else {
|
|
14751
15215
|
log.debug({ component: "loader", issuerId: issuer.issuerId, action }, "issuer registered");
|
|
14752
15216
|
}
|
|
@@ -14786,10 +15250,35 @@ async function runLoader(client, input) {
|
|
|
14786
15250
|
schemaActions.push({ typeName: s.typeName, action: actionOf(resp.created) });
|
|
14787
15251
|
log.debug({ component: "loader", typeName: s.typeName, schemaId: resp.id, action: actionOf(resp.created) }, "schema ensured (upsert)");
|
|
14788
15252
|
}
|
|
14789
|
-
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
|
+
} : {
|
|
14790
15276
|
principalId: `usr_${userId}`,
|
|
14791
15277
|
scopes: [
|
|
14792
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.
|
|
14793
15282
|
allowed_actions: blueprint.accessProfile.allowedActions,
|
|
14794
15283
|
// Pass dataScope straight through to the wire. The blueprint type is
|
|
14795
15284
|
// Record<string, (string|null)[]> — a `null` element is the NULL
|
|
@@ -14823,22 +15312,6 @@ async function runLoader(client, input) {
|
|
|
14823
15312
|
body: profileBody
|
|
14824
15313
|
});
|
|
14825
15314
|
log.debug({ component: "loader", principalId: `usr_${userId}`, accessProfileId: profile.id, profileAction }, "access profile ensured + reconciled to declared scopes");
|
|
14826
|
-
const roleIds = [];
|
|
14827
|
-
const roleActions = [];
|
|
14828
|
-
for (const [roleId, clauses] of Object.entries(blueprint.roles ?? {})) {
|
|
14829
|
-
const roleResp = await ctxClient.auth.createRole({
|
|
14830
|
-
contextId: blueprint.contextId,
|
|
14831
|
-
body: {
|
|
14832
|
-
roleId,
|
|
14833
|
-
// Blueprint roles carry no display name → default it to the roleId.
|
|
14834
|
-
name: roleId,
|
|
14835
|
-
scopes: toRoleScopeClauses(clauses)
|
|
14836
|
-
}
|
|
14837
|
-
});
|
|
14838
|
-
roleIds.push(roleId);
|
|
14839
|
-
roleActions.push({ roleId, action: actionOf(roleResp.created) });
|
|
14840
|
-
log.debug({ component: "loader", roleId, clauses: clauses.length, action: actionOf(roleResp.created) }, "role ensured");
|
|
14841
|
-
}
|
|
14842
15315
|
const keyName = keyNameFor(blueprint.name, machineId);
|
|
14843
15316
|
let keyAction = "created";
|
|
14844
15317
|
let revokedKeyId;
|
|
@@ -14949,6 +15422,11 @@ async function runLoader(client, input) {
|
|
|
14949
15422
|
seedIndexFailures
|
|
14950
15423
|
};
|
|
14951
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
|
+
}
|
|
14952
15430
|
function buildPlan(blueprint, opts = {}) {
|
|
14953
15431
|
const steps = [];
|
|
14954
15432
|
steps.push({ kind: "context", summary: `context '${blueprint.contextId}' (${contextNameOf(blueprint)})` });
|
|
@@ -14957,7 +15435,7 @@ function buildPlan(blueprint, opts = {}) {
|
|
|
14957
15435
|
summary: `principal '${blueprint.servicePrincipal.externalId}' (SERVICE)`
|
|
14958
15436
|
});
|
|
14959
15437
|
for (const ns of blueprint.namespaces ?? []) {
|
|
14960
|
-
const ownerNote = `owned by '${blueprint.contextId}'`;
|
|
15438
|
+
const ownerNote = ns.tenantWide ? "TENANT-WIDE \u2014 visible to every context" : `owned by '${blueprint.contextId}'`;
|
|
14961
15439
|
const entityNote = ns.entityBacked ? ", entity-backed" : "";
|
|
14962
15440
|
const membershipNote = ns.membershipRecordType ? `, membership: ${ns.membershipRecordType}.${ns.membershipTargetField}` + (ns.membershipLevelField ? ` (levels: ${ns.membershipLevels?.join("/")})` : "") : "";
|
|
14963
15441
|
steps.push({
|
|
@@ -14985,20 +15463,40 @@ function buildPlan(blueprint, opts = {}) {
|
|
|
14985
15463
|
summary: `schema '${s.typeName}' (${s.fields.length} field${s.fields.length === 1 ? "" : "s"}, ${indexNote})${lookupNote}${surfaceNote}${sensitiveNote}`
|
|
14986
15464
|
});
|
|
14987
15465
|
}
|
|
14988
|
-
const actions = blueprint.accessProfile.allowedActions;
|
|
14989
|
-
const scopeNote = blueprint.accessProfile.dataScope && Object.keys(blueprint.accessProfile.dataScope).length ? ` dataScope: ${Object.keys(blueprint.accessProfile.dataScope).join(", ")}` : "";
|
|
14990
|
-
const capabilityNote = blueprint.accessProfile.capabilities?.length ? ` capabilities: [${blueprint.accessProfile.capabilities.join(", ")}]` : "";
|
|
14991
|
-
steps.push({
|
|
14992
|
-
kind: "profile",
|
|
14993
|
-
summary: `profile scopes [${actions.join(", ")}] (data-plane \u2713)${scopeNote}${capabilityNote}`
|
|
14994
|
-
});
|
|
14995
15466
|
for (const [roleId, clauses] of Object.entries(blueprint.roles ?? {})) {
|
|
14996
15467
|
const actionSet = [...new Set(clauses.flatMap((c) => c.allowedActions))];
|
|
14997
15468
|
const capSet = [...new Set(clauses.flatMap((c) => c.capabilities ?? []))];
|
|
14998
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)}` : "";
|
|
14999
15477
|
steps.push({
|
|
15000
15478
|
kind: "role",
|
|
15001
|
-
summary: `role '${roleId}' (${clauses.length} clause${clauses.length === 1 ? "" : "s"}, actions [${actionSet.join(", ")}]${roleCapabilityNote})
|
|
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}`
|
|
15002
15500
|
});
|
|
15003
15501
|
}
|
|
15004
15502
|
steps.push({ kind: "key", summary: `key ${blueprint.name}-<machine> (mint 1 scoped ssk_*)` });
|
|
@@ -15021,8 +15519,10 @@ function renderPlan(blueprint, steps) {
|
|
|
15021
15519
|
];
|
|
15022
15520
|
if (steps.some((s) => s.kind === "issuer")) {
|
|
15023
15521
|
legendLines.push(
|
|
15024
|
-
` Exception:
|
|
15025
|
-
`
|
|
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).`
|
|
15026
15526
|
);
|
|
15027
15527
|
}
|
|
15028
15528
|
if (steps.some((s) => s.kind === "namespace")) {
|
|
@@ -15031,6 +15531,12 @@ function renderPlan(blueprint, steps) {
|
|
|
15031
15531
|
` an existing registration converges silently, but one that disagrees FAILS`,
|
|
15032
15532
|
` the apply rather than reconciling it.`
|
|
15033
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
|
+
}
|
|
15034
15540
|
}
|
|
15035
15541
|
const legend = legendLines.join("\n");
|
|
15036
15542
|
const lines = steps.map((s) => ` + ${s.summary}`);
|
|
@@ -17094,6 +17600,7 @@ function parseBootstrapArgs(argv) {
|
|
|
17094
17600
|
useScopedToken: false,
|
|
17095
17601
|
blankSlate: false,
|
|
17096
17602
|
noSeed: false,
|
|
17603
|
+
allowTenantWideNamespaces: false,
|
|
17097
17604
|
client: "desktop",
|
|
17098
17605
|
tenant: "live",
|
|
17099
17606
|
help: false
|
|
@@ -17155,6 +17662,9 @@ function parseBootstrapArgs(argv) {
|
|
|
17155
17662
|
case "--no-seed":
|
|
17156
17663
|
args.noSeed = true;
|
|
17157
17664
|
break;
|
|
17665
|
+
case "--allow-tenant-wide-namespaces":
|
|
17666
|
+
args.allowTenantWideNamespaces = true;
|
|
17667
|
+
break;
|
|
17158
17668
|
case "--tenant": {
|
|
17159
17669
|
const v = argv[++i];
|
|
17160
17670
|
if (v !== "test" && v !== "live") {
|
|
@@ -17236,15 +17746,6 @@ function reconcileTally(items, noun) {
|
|
|
17236
17746
|
if (reconciled) seg.push(`${reconciled} reconciled`);
|
|
17237
17747
|
return `${items.length} ${noun}${items.length === 1 ? "" : "s"} (${seg.join(", ")})`;
|
|
17238
17748
|
}
|
|
17239
|
-
function issuerTally(items) {
|
|
17240
|
-
if (items.length === 0) return null;
|
|
17241
|
-
const created = items.filter((i) => i.action === "created").length;
|
|
17242
|
-
const unchanged = items.length - created;
|
|
17243
|
-
const seg = [];
|
|
17244
|
-
if (created) seg.push(`${created} created`);
|
|
17245
|
-
if (unchanged) seg.push(`${unchanged} already registered \u2014 unchanged (issuers cannot be updated once registered)`);
|
|
17246
|
-
return `${items.length} issuer${items.length === 1 ? "" : "s"} (${seg.join(", ")})`;
|
|
17247
|
-
}
|
|
17248
17749
|
function namespaceTally(items) {
|
|
17249
17750
|
if (items.length === 0) return null;
|
|
17250
17751
|
const created = items.filter((i) => i.action === "created").length;
|
|
@@ -17258,7 +17759,9 @@ function applyOutcomeSummary(result) {
|
|
|
17258
17759
|
const parts = [
|
|
17259
17760
|
reconcileTally(result.schemaActions, "schema"),
|
|
17260
17761
|
reconcileTally(result.roleActions, "role"),
|
|
17261
|
-
|
|
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"),
|
|
17262
17765
|
namespaceTally(result.namespaceActions),
|
|
17263
17766
|
`profile ${result.profileAction}`
|
|
17264
17767
|
].filter((p) => p !== null);
|
|
@@ -17291,6 +17794,14 @@ Options:
|
|
|
17291
17794
|
sample seed records \u2014 the right choice for a production context
|
|
17292
17795
|
(a blueprint's seeds are illustrative, not real data). Unlike
|
|
17293
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.
|
|
17294
17805
|
--tenant test|live Which tenant the minted key scopes to (default: live).
|
|
17295
17806
|
'test' provisions into your isolated test tenant \u2014 a
|
|
17296
17807
|
dry-run before pointing real data at the same blueprint.
|
|
@@ -17473,11 +17984,16 @@ ${BOOTSTRAP_HELP}
|
|
|
17473
17984
|
}
|
|
17474
17985
|
throw err;
|
|
17475
17986
|
}
|
|
17476
|
-
const
|
|
17477
|
-
|
|
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
|
+
`
|
|
17478
17994
|
Blueprint: ${blueprint.name}
|
|
17479
|
-
\u2714 Scope gate: [${
|
|
17480
|
-
|
|
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
|
+
);
|
|
17481
17997
|
if (blueprint.accessProfile.capabilities?.length) {
|
|
17482
17998
|
const capGate = evaluateCapabilityGate(blueprint.accessProfile.capabilities);
|
|
17483
17999
|
process.stdout.write(` \u2714 Capability gate: [${capGate.accepted.join(", ")}] \u2014 granted \u2713
|
|
@@ -17533,6 +18049,7 @@ ${renderPlan(blueprint, planSteps)}
|
|
|
17533
18049
|
process.stdout.write(` \u2714 Bootstrap token minted (5-min TTL, ${args.tenant} tenant)
|
|
17534
18050
|
`);
|
|
17535
18051
|
const pingIdentity = deps.resolveIdentity ?? resolveIdentity;
|
|
18052
|
+
const provision = deps.provisionContext ?? provisionContext;
|
|
17536
18053
|
const resolveIdentities = deps.resolveApplyTimeIdentities ?? resolveApplyTimeIdentities;
|
|
17537
18054
|
const loader = deps.runLoader ?? runLoader;
|
|
17538
18055
|
const client = new VectrosClient({ token: bridge.token, environment: baseUrl });
|
|
@@ -17543,11 +18060,6 @@ ${renderPlan(blueprint, planSteps)}
|
|
|
17543
18060
|
"Could not resolve tenantId from /v1/ping \u2014 the extended ping (tenant_id) must be deployed for bootstrap."
|
|
17544
18061
|
);
|
|
17545
18062
|
}
|
|
17546
|
-
const resolvedBlueprint = await resolveIdentities(
|
|
17547
|
-
blueprint,
|
|
17548
|
-
client,
|
|
17549
|
-
log
|
|
17550
|
-
);
|
|
17551
18063
|
let keyring;
|
|
17552
18064
|
let keyringOpenError;
|
|
17553
18065
|
try {
|
|
@@ -17622,6 +18134,27 @@ ${renderPlan(blueprint, planSteps)}
|
|
|
17622
18134
|
);
|
|
17623
18135
|
}
|
|
17624
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
|
+
);
|
|
17625
18158
|
const machineId = await resolveMachineId();
|
|
17626
18159
|
const result = await loader(client, {
|
|
17627
18160
|
blueprint: resolvedBlueprint,
|
|
@@ -17629,22 +18162,18 @@ ${renderPlan(blueprint, planSteps)}
|
|
|
17629
18162
|
machineId,
|
|
17630
18163
|
rotate: args.rotate,
|
|
17631
18164
|
skipSeed: noSeed,
|
|
18165
|
+
allowTenantWideNamespaces: args.allowTenantWideNamespaces,
|
|
17632
18166
|
readExistingKey,
|
|
17633
18167
|
persistMintedKey,
|
|
17634
|
-
|
|
17635
|
-
|
|
17636
|
-
apiBaseUrl: baseUrl,
|
|
17637
|
-
tenant: args.tenant,
|
|
17638
|
-
useScopedToken: args.useScopedToken,
|
|
17639
|
-
mintBridge
|
|
17640
|
-
}),
|
|
18168
|
+
provisioned,
|
|
18169
|
+
remintBootstrapForContext,
|
|
17641
18170
|
remintForContext: buildRemintForContext({
|
|
17642
18171
|
cognitoToken,
|
|
17643
18172
|
apiBaseUrl: baseUrl,
|
|
17644
18173
|
tenant: args.tenant,
|
|
17645
18174
|
mintContext
|
|
17646
18175
|
}),
|
|
17647
|
-
confirmExistingPrincipal
|
|
18176
|
+
confirmExistingPrincipal,
|
|
17648
18177
|
log
|
|
17649
18178
|
});
|
|
17650
18179
|
process.stdout.write(
|
|
@@ -17735,9 +18264,18 @@ ${codeAddHint}`);
|
|
|
17735
18264
|
return 3;
|
|
17736
18265
|
}
|
|
17737
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) {
|
|
17738
18276
|
process.stderr.write(`
|
|
17739
18277
|
\u2716 ${err.message}
|
|
17740
|
-
|
|
18278
|
+
Nothing was provisioned.
|
|
17741
18279
|
`);
|
|
17742
18280
|
return 2;
|
|
17743
18281
|
}
|
|
@@ -17812,11 +18350,14 @@ function assertIdentityOverrideValues(overrides, flag) {
|
|
|
17812
18350
|
}
|
|
17813
18351
|
}
|
|
17814
18352
|
function placeholderScopeNamespace(value) {
|
|
17815
|
-
const
|
|
17816
|
-
|
|
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;
|
|
17817
18357
|
}
|
|
17818
18358
|
var SELF_PLACEHOLDER_RE = /^\$\{\{\s*self\.(?:userId|partnerUserId|scope\.[a-z][a-z0-9_-]{1,31})\s*\}\}$/;
|
|
17819
|
-
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*\}\}$/;
|
|
17820
18361
|
|
|
17821
18362
|
// src/lint.ts
|
|
17822
18363
|
var ENUM_COMPATIBLE_TYPES = /* @__PURE__ */ new Set(["enum", "string", "string[]"]);
|
|
@@ -17954,7 +18495,7 @@ function lintBlueprint(bp) {
|
|
|
17954
18495
|
}
|
|
17955
18496
|
}
|
|
17956
18497
|
}
|
|
17957
|
-
{
|
|
18498
|
+
if (bp.accessProfile.allowedActions !== void 0) {
|
|
17958
18499
|
const reached = ownerScopedReachWithoutDataScope(bp.accessProfile.allowedActions, ds);
|
|
17959
18500
|
if (reached.length > 0) findings.push(unconstrainedReachFinding("accessProfile", reached));
|
|
17960
18501
|
}
|
|
@@ -18005,7 +18546,12 @@ function lintBlueprint(bp) {
|
|
|
18005
18546
|
}
|
|
18006
18547
|
var ROLE_ID_RE = /^[a-z][a-z0-9-]{2,30}$/;
|
|
18007
18548
|
var PLACEHOLDER_SHAPED_RE = /^\s*\$\{\{.*\}\}\s*$/s;
|
|
18008
|
-
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
|
+
];
|
|
18009
18555
|
var BLUEPRINT_TOKEN_RE = /^\s*\$\{\{\s*(identities|inputs|vectros|ns)\./;
|
|
18010
18556
|
function placeholderFinding(where, key, value) {
|
|
18011
18557
|
if (typeof value !== "string" || !PLACEHOLDER_SHAPED_RE.test(value)) return null;
|
|
@@ -18023,7 +18569,7 @@ function placeholderFinding(where, key, value) {
|
|
|
18023
18569
|
return {
|
|
18024
18570
|
severity: "error",
|
|
18025
18571
|
rule: "datascope-unknown-placeholder",
|
|
18026
|
-
message: `${where}: dataScope.${key} value '${value}' is not a placeholder the platform recognizes \u2014 it will be rejected at apply. Valid: \${{ any }}, \${{ self.userId }}, \${{ self.scope.<namespace> }}, \${{ under.self.userId }}, \${{ under.self.scope.<namespace> }}. Spelling is exact and lowercase; use a plain value for a literal owner id.`
|
|
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.`
|
|
18027
18573
|
};
|
|
18028
18574
|
}
|
|
18029
18575
|
var OWNER_SCOPED_RESOURCES = /* @__PURE__ */ new Set(["records", "documents", "entities"]);
|
|
@@ -18269,11 +18815,11 @@ async function runValidate(args) {
|
|
|
18269
18815
|
}
|
|
18270
18816
|
const blueprint = await resolveAndValidate(args.file, "validate", supplied);
|
|
18271
18817
|
if (!blueprint) return 2;
|
|
18272
|
-
const gate = evaluateScopeGate(blueprint.accessProfile.allowedActions);
|
|
18273
18818
|
const capGate = evaluateCapabilityGate(blueprint.accessProfile.capabilities);
|
|
18274
18819
|
const warnCount = lintBlueprint(blueprint).length;
|
|
18820
|
+
const roleNote = blueprint.accessProfile.roleIds ? ` via role(s) [${blueprint.accessProfile.roleIds.join(", ")}]` : "";
|
|
18275
18821
|
process.stdout.write(
|
|
18276
|
-
` \u2714 '${blueprint.name}' is valid \u2014 structural \u2713, scope gate \u2713 [${
|
|
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"
|
|
18277
18823
|
);
|
|
18278
18824
|
return 0;
|
|
18279
18825
|
}
|
|
@@ -19144,6 +19690,52 @@ async function runIdentityDelete(opts, deps = {}) {
|
|
|
19144
19690
|
}
|
|
19145
19691
|
});
|
|
19146
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
|
+
}
|
|
19147
19739
|
|
|
19148
19740
|
// src/commands/access.ts
|
|
19149
19741
|
function assertPrincipalShape(principal) {
|
|
@@ -19162,20 +19754,24 @@ async function resolvePrincipalArg(raw, auth) {
|
|
|
19162
19754
|
}
|
|
19163
19755
|
return self2.principalId;
|
|
19164
19756
|
}
|
|
19757
|
+
function roleSummary(p) {
|
|
19758
|
+
if (p.roleIds?.length) return p.roleIds.join(", ");
|
|
19759
|
+
return p.roleId;
|
|
19760
|
+
}
|
|
19165
19761
|
function renderProfile(p) {
|
|
19166
19762
|
return formatKeyValues([
|
|
19167
19763
|
["context", p.contextId],
|
|
19168
19764
|
["principal", p.principalId],
|
|
19169
|
-
["role", p
|
|
19765
|
+
["role", roleSummary(p)],
|
|
19170
19766
|
["scopes", p.scopes?.length ? `${p.scopes.length} clause(s)` : void 0],
|
|
19171
19767
|
["status", p.status]
|
|
19172
19768
|
]);
|
|
19173
19769
|
}
|
|
19174
19770
|
async function runAccessGrant(opts, deps = {}) {
|
|
19175
|
-
const hasRole = Boolean(opts.role);
|
|
19771
|
+
const hasRole = Boolean(opts.role?.length);
|
|
19176
19772
|
const hasActions = Boolean(opts.actions);
|
|
19177
19773
|
if (hasRole === hasActions) {
|
|
19178
|
-
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");
|
|
19179
19775
|
return 2;
|
|
19180
19776
|
}
|
|
19181
19777
|
if (hasActions && splitCsv(opts.actions).length === 0) {
|
|
@@ -19215,7 +19811,11 @@ async function runAccessGrant(opts, deps = {}) {
|
|
|
19215
19811
|
upsert: true,
|
|
19216
19812
|
body: {
|
|
19217
19813
|
principalId: principal,
|
|
19218
|
-
|
|
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,
|
|
19219
19819
|
scopes,
|
|
19220
19820
|
identityOverrides
|
|
19221
19821
|
}
|
|
@@ -19227,7 +19827,7 @@ async function runAccessGrant(opts, deps = {}) {
|
|
|
19227
19827
|
}
|
|
19228
19828
|
process.stdout.write(
|
|
19229
19829
|
`
|
|
19230
|
-
\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)`) + `
|
|
19231
19831
|
${renderProfile(profile)}
|
|
19232
19832
|
`
|
|
19233
19833
|
);
|
|
@@ -19296,7 +19896,7 @@ async function runAccessList(opts, deps = {}) {
|
|
|
19296
19896
|
"\n" + formatTable(rows, [
|
|
19297
19897
|
{ header: "context", get: (r) => r.contextId },
|
|
19298
19898
|
{ header: "principal", get: (r) => r.principalId },
|
|
19299
|
-
{ header: "role", get: (r) => r
|
|
19899
|
+
{ header: "role", get: (r) => roleSummary(r) ?? (r.scopes?.length ? "(inline)" : void 0) },
|
|
19300
19900
|
{ header: "status", get: (r) => r.status }
|
|
19301
19901
|
]) + "\n"
|
|
19302
19902
|
);
|
|
@@ -19344,6 +19944,7 @@ ${renderProfile(p)}
|
|
|
19344
19944
|
var SELF = /* @__PURE__ */ Symbol("self");
|
|
19345
19945
|
var ANY = /* @__PURE__ */ Symbol("any");
|
|
19346
19946
|
var UNDER_SELF = /* @__PURE__ */ Symbol("under.self");
|
|
19947
|
+
var MEMBER = /* @__PURE__ */ Symbol("member.scope");
|
|
19347
19948
|
function canonicalDim(key) {
|
|
19348
19949
|
if (key === "userId" || key === "partnerUserId") return "user";
|
|
19349
19950
|
return key;
|
|
@@ -19373,7 +19974,8 @@ function normalizeClauses(raw) {
|
|
|
19373
19974
|
if (val === null) return null;
|
|
19374
19975
|
const s = String(val);
|
|
19375
19976
|
if (ANY_TOKEN_RE.test(s)) return ANY;
|
|
19376
|
-
if (
|
|
19977
|
+
if (UNDER_SELF_PLACEHOLDER_RE2.test(s)) return UNDER_SELF;
|
|
19978
|
+
if (MEMBER_SCOPE_PLACEHOLDER_RE2.test(s)) return MEMBER;
|
|
19377
19979
|
return SELF_PLACEHOLDER_RE.test(s) ? SELF : s;
|
|
19378
19980
|
});
|
|
19379
19981
|
if (!isRecognizedOwnershipKey(k)) unrecognizedDims.push(k);
|
|
@@ -19428,7 +20030,7 @@ function buildArchetypes(clauses) {
|
|
|
19428
20030
|
const set = scopeDims.get(dim) ?? /* @__PURE__ */ new Set();
|
|
19429
20031
|
set.add(SELF);
|
|
19430
20032
|
for (const v of values) {
|
|
19431
|
-
if (v === null || v === ANY || v === UNDER_SELF) continue;
|
|
20033
|
+
if (v === null || v === ANY || v === UNDER_SELF || v === MEMBER) continue;
|
|
19432
20034
|
set.add(v);
|
|
19433
20035
|
}
|
|
19434
20036
|
scopeDims.set(dim, set);
|
|
@@ -19455,6 +20057,7 @@ function listAdmits(allowed, recordValue) {
|
|
|
19455
20057
|
if (a === SELF) return recordValue === SELF;
|
|
19456
20058
|
if (a === ANY) return recordValue !== void 0;
|
|
19457
20059
|
if (a === UNDER_SELF) return false;
|
|
20060
|
+
if (a === MEMBER) return false;
|
|
19458
20061
|
return recordValue === a;
|
|
19459
20062
|
});
|
|
19460
20063
|
}
|
|
@@ -19480,7 +20083,7 @@ function undecidableDims(clauses) {
|
|
|
19480
20083
|
const dims = /* @__PURE__ */ new Set();
|
|
19481
20084
|
for (const c of clauses) {
|
|
19482
20085
|
for (const [dim, values] of Object.entries(c.dataScope)) {
|
|
19483
|
-
if (values.includes(UNDER_SELF)) dims.add(dim);
|
|
20086
|
+
if (values.includes(UNDER_SELF) || values.includes(MEMBER)) dims.add(dim);
|
|
19484
20087
|
}
|
|
19485
20088
|
}
|
|
19486
20089
|
return [...dims];
|
|
@@ -19513,6 +20116,7 @@ function renderDataScope(ds) {
|
|
|
19513
20116
|
if (v === SELF) return "self";
|
|
19514
20117
|
if (v === ANY) return "any";
|
|
19515
20118
|
if (v === UNDER_SELF) return "under-self";
|
|
20119
|
+
if (v === MEMBER) return "member";
|
|
19516
20120
|
if (v === null) return "null";
|
|
19517
20121
|
return v;
|
|
19518
20122
|
});
|
|
@@ -19544,14 +20148,25 @@ async function runAccessExplain(opts, deps = {}) {
|
|
|
19544
20148
|
const profile = await client.auth.getAccessProfile({ contextId: opts.context, principalId: principal });
|
|
19545
20149
|
let rawClauses = profile.scopes ?? [];
|
|
19546
20150
|
let via = "inline scopes";
|
|
19547
|
-
|
|
19548
|
-
|
|
19549
|
-
|
|
19550
|
-
|
|
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(", ")}]`;
|
|
19551
20160
|
}
|
|
19552
20161
|
const clauses = normalizeClauses(rawClauses);
|
|
19553
20162
|
const rows = explainClauses(clauses);
|
|
19554
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
|
+
);
|
|
19555
20170
|
if (opts.json) {
|
|
19556
20171
|
process.stdout.write(
|
|
19557
20172
|
`${formatJson({
|
|
@@ -19608,10 +20223,7 @@ async function runAccessExplain(opts, deps = {}) {
|
|
|
19608
20223
|
]) + (undecidable.length > 0 ? `
|
|
19609
20224
|
|
|
19610
20225
|
\u26A0 UNDECIDED offline: ${undecidable.join(", ")}
|
|
19611
|
-
A clause matching on
|
|
19612
|
-
so rows granted through it are shown as NO above. Read those cells as "cannot tell here",
|
|
19613
|
-
NOT as a denial \u2014 the matrix under-reports this clause.
|
|
19614
|
-
` : "") + '\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'
|
|
19615
20227
|
);
|
|
19616
20228
|
return 0;
|
|
19617
20229
|
} catch (err) {
|
|
@@ -19629,7 +20241,8 @@ function renderRole(r) {
|
|
|
19629
20241
|
["name", r.name],
|
|
19630
20242
|
["context", r.contextId],
|
|
19631
20243
|
["description", r.description],
|
|
19632
|
-
["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]
|
|
19633
20246
|
]);
|
|
19634
20247
|
}
|
|
19635
20248
|
async function runRoleCreate(opts, deps = {}) {
|
|
@@ -19900,6 +20513,75 @@ ${renderContext(ctx)}
|
|
|
19900
20513
|
});
|
|
19901
20514
|
}
|
|
19902
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
|
+
|
|
19903
20585
|
// src/commands/login.ts
|
|
19904
20586
|
var import_node_http = require("http");
|
|
19905
20587
|
var import_open = __toESM(require("open"));
|
|
@@ -20121,7 +20803,7 @@ async function runAssertions(client, bootstrapClient, deps, blueprint, tenantId,
|
|
|
20121
20803
|
const assertions = [];
|
|
20122
20804
|
try {
|
|
20123
20805
|
const ping = await deps.pingWithKey(result.rawKey);
|
|
20124
|
-
const wantActions = blueprint
|
|
20806
|
+
const wantActions = effectiveAccessProfileActions(blueprint);
|
|
20125
20807
|
const got = ping.allowedActions ?? [];
|
|
20126
20808
|
const missing = wantActions.filter((a) => !got.includes(a));
|
|
20127
20809
|
const ok = ping.principalType === "scoped_key" && ping.tenantId === tenantId && missing.length === 0;
|
|
@@ -20355,15 +21037,21 @@ async function runBlueprintHarness(client, input, deps) {
|
|
|
20355
21037
|
const readExistingKey = async () => void 0;
|
|
20356
21038
|
const persistMintedKey = async () => {
|
|
20357
21039
|
};
|
|
21040
|
+
const provisioned = await provisionContext(client, {
|
|
21041
|
+
blueprint: input.blueprint,
|
|
21042
|
+
remintBootstrapForContext,
|
|
21043
|
+
readExistingKey,
|
|
21044
|
+
log
|
|
21045
|
+
});
|
|
20358
21046
|
const blueprint = await resolveApplyTimeIdentities(
|
|
20359
21047
|
input.blueprint,
|
|
20360
|
-
client,
|
|
21048
|
+
provisioned.ctxBootstrapClient ?? client,
|
|
20361
21049
|
log
|
|
20362
21050
|
);
|
|
20363
21051
|
const remint = remintForContext ?? (async () => client);
|
|
20364
21052
|
const remintBootstrap = remintBootstrapForContext ?? (async () => client);
|
|
20365
21053
|
const hasIssuers = (blueprint.issuers ?? []).length > 0;
|
|
20366
|
-
const hasNamespaces =
|
|
21054
|
+
const hasNamespaces = provisioned.namespaceActions.length > 0;
|
|
20367
21055
|
let contextSnapshot;
|
|
20368
21056
|
if (remintForContext) {
|
|
20369
21057
|
try {
|
|
@@ -20407,6 +21095,7 @@ async function runBlueprintHarness(client, input, deps) {
|
|
|
20407
21095
|
readExistingKey,
|
|
20408
21096
|
persistMintedKey,
|
|
20409
21097
|
log,
|
|
21098
|
+
provisioned,
|
|
20410
21099
|
remintForContext,
|
|
20411
21100
|
remintBootstrapForContext
|
|
20412
21101
|
});
|
|
@@ -20625,12 +21314,13 @@ async function issueOrRotateKey(client, tenantId, context, userId, keyName) {
|
|
|
20625
21314
|
return { key: rotated, revokedKeyId: existing?.keyId };
|
|
20626
21315
|
}
|
|
20627
21316
|
async function runJoin(opts, deps = {}) {
|
|
20628
|
-
const hasRole = Boolean(opts.role);
|
|
21317
|
+
const hasRole = Boolean(opts.role?.length);
|
|
20629
21318
|
const hasActions = Boolean(opts.actions);
|
|
20630
21319
|
if (hasRole === hasActions) {
|
|
20631
|
-
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");
|
|
20632
21321
|
return 2;
|
|
20633
21322
|
}
|
|
21323
|
+
const roleDisplay = opts.role?.join(", ");
|
|
20634
21324
|
if (hasActions && splitCsv(opts.actions).length === 0) {
|
|
20635
21325
|
process.stderr.write(" \u2716 --actions needs at least one action (an empty/blank list grants nothing).\n");
|
|
20636
21326
|
return 2;
|
|
@@ -20678,7 +21368,10 @@ async function runJoin(opts, deps = {}) {
|
|
|
20678
21368
|
upsert: true,
|
|
20679
21369
|
body: {
|
|
20680
21370
|
principalId,
|
|
20681
|
-
|
|
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,
|
|
20682
21375
|
scopes,
|
|
20683
21376
|
identityOverrides
|
|
20684
21377
|
}
|
|
@@ -20697,7 +21390,7 @@ async function runJoin(opts, deps = {}) {
|
|
|
20697
21390
|
alias,
|
|
20698
21391
|
context,
|
|
20699
21392
|
principalId,
|
|
20700
|
-
role:
|
|
21393
|
+
role: roleDisplay,
|
|
20701
21394
|
keyId: existingEntry.keyId,
|
|
20702
21395
|
reused: true,
|
|
20703
21396
|
tenant,
|
|
@@ -20718,7 +21411,7 @@ async function runJoin(opts, deps = {}) {
|
|
|
20718
21411
|
const willActivate = await determineActivateDecision(keyring, { tenant, explicitActivate: opts.activate });
|
|
20719
21412
|
const { entry } = await storeMintedKey(
|
|
20720
21413
|
keyring,
|
|
20721
|
-
{ alias, context, principalId, role:
|
|
21414
|
+
{ alias, context, principalId, role: roleDisplay, keyId: key.keyId, env: auth.env },
|
|
20722
21415
|
key.rawKey,
|
|
20723
21416
|
{ activate: willActivate, revokedKeyId }
|
|
20724
21417
|
);
|
|
@@ -20726,7 +21419,7 @@ async function runJoin(opts, deps = {}) {
|
|
|
20726
21419
|
alias,
|
|
20727
21420
|
context,
|
|
20728
21421
|
principalId,
|
|
20729
|
-
role:
|
|
21422
|
+
role: roleDisplay,
|
|
20730
21423
|
keyId: entry.keyId,
|
|
20731
21424
|
reused: false,
|
|
20732
21425
|
tenant,
|
|
@@ -20765,9 +21458,9 @@ function emitJoined(opts) {
|
|
|
20765
21458
|
|
|
20766
21459
|
// src/build-info.ts
|
|
20767
21460
|
var BUILD_INFO = {
|
|
20768
|
-
cli: true ? "0.
|
|
20769
|
-
sdk: true ? "0.
|
|
20770
|
-
blueprints: true ? "0.
|
|
21461
|
+
cli: true ? "0.19.0" : "dev",
|
|
21462
|
+
sdk: true ? "0.41.0" : "dev",
|
|
21463
|
+
blueprints: true ? "0.16.0" : "dev"
|
|
20771
21464
|
};
|
|
20772
21465
|
function formatBuildInfo() {
|
|
20773
21466
|
return `vectros ${BUILD_INFO.cli} (sdk ${BUILD_INFO.sdk}, blueprints ${BUILD_INFO.blueprints})`;
|
|
@@ -20847,12 +21540,13 @@ function buildProgram(deps = {}) {
|
|
|
20847
21540
|
registerAccessCommands(program, deps);
|
|
20848
21541
|
registerRoleCommands(program, deps);
|
|
20849
21542
|
registerContextCommands(program, deps);
|
|
21543
|
+
registerIssuersCommands(program, deps);
|
|
20850
21544
|
registerJoinCommand(program, deps);
|
|
20851
21545
|
registerKeyringCommands(program);
|
|
20852
21546
|
return program;
|
|
20853
21547
|
}
|
|
20854
21548
|
function registerJoinCommand(program, deps) {
|
|
20855
|
-
const join3 = program.command("join").description("Enroll a principal in a context (grant + key) and keep the key in the keyring.").argument("<context>", "the app context to join").option("--role <roleId>", "reference a Role to grant (XOR --actions)").option("--actions <csv>", "inline single-clause actions to grant, comma-separated (XOR --role)").option("--as <principal>", "who to enroll: 'me' (default) or an explicit 'usr_<id>'", "me").option("--scope <ns:value>", "identity override: a scope the member acts under (repeatable, e.g. org:<id>)", collect, []).option("--alias <name>", "keyring entry name (defaults to the context id)").option("--tenant <tenant>", "target tenant: 'test' or 'live' (default live)").option(
|
|
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(
|
|
20856
21550
|
"--activate",
|
|
20857
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)"
|
|
20858
21552
|
).option("--no-activate", "store the key without activating it, regardless of tenant");
|
|
@@ -20986,6 +21680,27 @@ function registerContextCommands(program, deps) {
|
|
|
20986
21680
|
}
|
|
20987
21681
|
);
|
|
20988
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
|
+
}
|
|
20989
21704
|
function registerRoleCommands(program, deps) {
|
|
20990
21705
|
const role = program.command("role").description("Manage context-scoped roles (reusable scope rules; bind via `access grant --role`).");
|
|
20991
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(
|
|
@@ -21022,7 +21737,7 @@ function registerRoleCommands(program, deps) {
|
|
|
21022
21737
|
}
|
|
21023
21738
|
function registerAccessCommands(program, deps) {
|
|
21024
21739
|
const access2 = program.command("access").description("Manage access bindings: principal \u2194 context \u2194 role/scopes.");
|
|
21025
|
-
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)");
|
|
21026
21741
|
addCommonAuthOptions(grant).action(
|
|
21027
21742
|
async (flags) => {
|
|
21028
21743
|
process.exit(
|
|
@@ -21103,6 +21818,25 @@ function registerIdentityCommands(program, deps) {
|
|
|
21103
21818
|
addAuthOptions(del).action(async (flags) => {
|
|
21104
21819
|
process.exit(await runIdentityDelete({ ...toCommonOptions(flags), type: flags.type, id: flags.id }, deps));
|
|
21105
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
|
+
);
|
|
21106
21840
|
}
|
|
21107
21841
|
function addKeyDestinationOptions(cmd) {
|
|
21108
21842
|
return cmd.addOption(new import_commander.Option("--format <fmt>", "how to emit the secret").choices(KEY_FORMATS).default("human")).option(
|