@vectros-ai/cli 0.21.0 → 0.23.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 +77 -23
- package/CHANGELOG.md +310 -1
- package/README.md +105 -16
- package/THIRD-PARTY-NOTICES.md +2 -3
- package/dist/cli.js +2530 -684
- package/dist/cli.js.map +1 -1
- package/dist/cli.mjs +2531 -685
- package/dist/cli.mjs.map +1 -1
- package/package.json +3 -3
package/dist/cli.mjs
CHANGED
|
@@ -523,7 +523,7 @@ var require_graceful_fs = __commonJS({
|
|
|
523
523
|
function patch(fs5) {
|
|
524
524
|
polyfills(fs5);
|
|
525
525
|
fs5.gracefulify = patch;
|
|
526
|
-
fs5.createReadStream =
|
|
526
|
+
fs5.createReadStream = createReadStream2;
|
|
527
527
|
fs5.createWriteStream = createWriteStream;
|
|
528
528
|
var fs$readFile = fs5.readFile;
|
|
529
529
|
fs5.readFile = readFile4;
|
|
@@ -733,7 +733,7 @@ var require_graceful_fs = __commonJS({
|
|
|
733
733
|
}
|
|
734
734
|
});
|
|
735
735
|
}
|
|
736
|
-
function
|
|
736
|
+
function createReadStream2(path4, options) {
|
|
737
737
|
return new fs5.ReadStream(path4, options);
|
|
738
738
|
}
|
|
739
739
|
function createWriteStream(path4, options) {
|
|
@@ -5018,6 +5018,7 @@ var AuthClient = class {
|
|
|
5018
5018
|
*
|
|
5019
5019
|
* @throws {@link Vectros.ForbiddenError}
|
|
5020
5020
|
* @throws {@link Vectros.NotFoundError}
|
|
5021
|
+
* @throws {@link Vectros.ConflictError}
|
|
5021
5022
|
* @throws {@link Vectros.TooManyRequestsError}
|
|
5022
5023
|
*
|
|
5023
5024
|
* @example
|
|
@@ -5060,6 +5061,8 @@ var AuthClient = class {
|
|
|
5060
5061
|
throw new ForbiddenError(_response.error.body, _response.rawResponse);
|
|
5061
5062
|
case 404:
|
|
5062
5063
|
throw new NotFoundError(_response.error.body, _response.rawResponse);
|
|
5064
|
+
case 409:
|
|
5065
|
+
throw new ConflictError(_response.error.body, _response.rawResponse);
|
|
5063
5066
|
case 429:
|
|
5064
5067
|
throw new TooManyRequestsError(_response.error.body, _response.rawResponse);
|
|
5065
5068
|
default:
|
|
@@ -5136,7 +5139,7 @@ var AuthClient = class {
|
|
|
5136
5139
|
return handleNonStatusCodeError(_response.error, _response.rawResponse, "GET", "/v1/app-contexts/{contextId}");
|
|
5137
5140
|
}
|
|
5138
5141
|
/**
|
|
5139
|
-
* Updates
|
|
5142
|
+
* Updates an app context. This is a partial update: any field you omit (or send as null) keeps its existing value. The `contextId` is immutable and is taken from the URL path, so any `contextId` in the request body is ignored. Patchable fields: `name`, `description`, `companyName`; the per-principal metering trio (`meteringAxis`, `principalBurstLimit`, `principalUsageCap` — only takes effect for a partner with the corresponding account-level feature enabled); `readAccessLogDefault` (the PHI read-access-logging context default); and `identityProjectionClaims` (requires the platform provisioning capability on top of the ordinary scope below — see its own field description). Requires the `app-contexts:u` scope.
|
|
5140
5143
|
*
|
|
5141
5144
|
* @param {Vectros.UpdateAppContextRequest} request
|
|
5142
5145
|
* @param {AuthClient.RequestOptions} requestOptions - Request-specific configuration.
|
|
@@ -5204,7 +5207,7 @@ var AuthClient = class {
|
|
|
5204
5207
|
return handleNonStatusCodeError(_response.error, _response.rawResponse, "PUT", "/v1/app-contexts/{contextId}");
|
|
5205
5208
|
}
|
|
5206
5209
|
/**
|
|
5207
|
-
* Permanently deletes an app context and everything in it — every record, document, folder, schema, role,
|
|
5210
|
+
* Permanently deletes an app context and everything in it — every record, document, folder, schema, role, access profile, and trusted-issuer registration belonging to the context. This is irreversible. The deletion runs asynchronously: the call returns 202 immediately and the context's data drains in the background. Poll the context's `status` field to observe when the teardown completes (`purging` while draining, then `deleted`). To guard against accidental deletion, you must echo the contextId back in the `confirm` query parameter (`?confirm={contextId}`). The reserved `default` and `vectros-admin` contexts cannot be deleted. This operation requires a root API key (one beginning with `sk_`): no scoped credential, not even one with full wildcard (`*`) scope, can trigger this teardown.
|
|
5208
5211
|
*
|
|
5209
5212
|
* @param {Vectros.DeleteAppContextRequest} request
|
|
5210
5213
|
* @param {AuthClient.RequestOptions} requestOptions - Request-specific configuration.
|
|
@@ -5739,7 +5742,7 @@ var AuthClient = class {
|
|
|
5739
5742
|
return handleNonStatusCodeError(_response.error, _response.rawResponse, "GET", "/v1/auth/issuers/{issuerId}");
|
|
5740
5743
|
}
|
|
5741
5744
|
/**
|
|
5742
|
-
* 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.
|
|
5745
|
+
* Updates the mutable fields of a registered issuer: `subClaim`, `emailClaim`, `userinfoUri`, `capturedClaims`, `restrictedToDomain` (a new non-blank value must already be a VERIFIED domain for your account; an empty string clears it back to domain-less; see `IssuerUpdateRequest.restrictedToDomain` for the full semantics), `status` (`active`/`suspended` — a registration awaiting verification (`pending_verification`) accepts no status change until it is verified; 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. **`subClaim` is identity-determining, not merely cosmetic**: it names which verified JWT claim is read as the federated user's identifier, so changing it on an issuer that already has bound users would silently re-identify (or, under self-signup, orphan) every one of them — changing it is therefore refused once any user has bound through this issuer, the same guard `DELETE` already applies, and is only free before the first real login. 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.
|
|
5743
5746
|
*
|
|
5744
5747
|
* @param {Vectros.IssuerUpdateRequest} request
|
|
5745
5748
|
* @param {AuthClient.RequestOptions} requestOptions - Request-specific configuration.
|
|
@@ -5806,7 +5809,7 @@ var AuthClient = class {
|
|
|
5806
5809
|
return handleNonStatusCodeError(_response.error, _response.rawResponse, "PUT", "/v1/auth/issuers/{issuerId}");
|
|
5807
5810
|
}
|
|
5808
5811
|
/**
|
|
5809
|
-
* 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.
|
|
5812
|
+
* 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. This is unconditional on the affected users' `status`: suspending them first does not lift the refusal. **A bound registration cannot be replaced, self-service, within its own app context**: registering a replacement issuer under a different issuerId in the SAME context also fails, because the context's one-active-issuer claim is released only when THIS registration is deleted. Two real options: suspend this issuer (stops new exchanges immediately) and register a replacement under a DIFFERENT app context — note that targets a different context, so this context's existing users/roles/access profiles are not carried over; or contact your platform operator, who can force-release this registration so a replacement may be registered in the SAME context under a new issuerId. **The operator path is not a lighter-weight alternative to the first — it PERMANENTLY RETIRES this issuerId and leaves every bound user unable to ever exchange through it again; it does not preserve continuity for them any better than registering under a different context does.** An issuer that has never been used for an exchange (no bound users yet) can always be deregistered.
|
|
5810
5813
|
*
|
|
5811
5814
|
* @param {Vectros.DeleteIssuerRequest} request
|
|
5812
5815
|
* @param {AuthClient.RequestOptions} requestOptions - Request-specific configuration.
|
|
@@ -5932,7 +5935,7 @@ var AuthClient = class {
|
|
|
5932
5935
|
return handleNonStatusCodeError(_response.error, _response.rawResponse, "GET", "/v1/auth/issuers");
|
|
5933
5936
|
}
|
|
5934
5937
|
/**
|
|
5935
|
-
* 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.
|
|
5938
|
+
* 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. A registration without `restrictedToDomain` is created `pending_verification` with a one-time challenge (`verificationClaim`, `verificationNonce`, `verificationExpiresAt`) and accepts no token until you prove you control the issuer with `POST /v1/auth/issuers/{issuerId}/verify`; it claims no `(issuer, audience)` pair until then, so registering never reveals whether another tenant holds one. A registration scoped to an already-verified domain with `restrictedToDomain` is `active` at once. Idempotent by `issuerId` within your 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`. Optionally name `restrictedToDomain` to scope this registration's (issuer, audience) uniqueness to a specific VERIFIED domain rather than the bare pair — see that field's own description. A domain-scoped registration proves control of the DOMAIN, not of the issuer itself: it routes only tokens carrying that domain's `hd` claim.
|
|
5936
5939
|
*
|
|
5937
5940
|
* @param {Vectros.IssuerRequest} request
|
|
5938
5941
|
* @param {AuthClient.RequestOptions} requestOptions - Request-specific configuration.
|
|
@@ -5998,6 +6001,82 @@ var AuthClient = class {
|
|
|
5998
6001
|
}
|
|
5999
6002
|
return handleNonStatusCodeError(_response.error, _response.rawResponse, "POST", "/v1/auth/issuers");
|
|
6000
6003
|
}
|
|
6004
|
+
/**
|
|
6005
|
+
* Proves you administer the IdP application a `pending_verification` registration names, activating it. A registration created without `restrictedToDomain` accepts no tokens until this succeeds. Configure your IdP, with an admin-controlled rule (an Auth0 Action, an Okta inline hook, an Entra claims-mapping policy, a Keycloak protocol mapper), to add the registration's `verificationClaim` to the tokens it issues, set to its `verificationNonce`; sign in once; and send that token here. The `jwksUri` you registered must be exactly the `jwks_uri` your issuer's OpenID Connect discovery document (`<issuer>/.well-known/openid-configuration`) publishes, and the token must verify against that key set — an issuer that publishes no discovery document cannot be verified this way. The token is checked and discarded; it is never stored. The `(issuer, audience)` pair is claimed at this moment, so a pair another registration already holds is refused with 400. A challenge expires 7 days after registration; an expired registration cannot be verified — delete it and register again. 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 verify an issuer registered in that context; naming one registered in another context returns 404, identically to a nonexistent issuerId.
|
|
6006
|
+
*
|
|
6007
|
+
* @param {Vectros.IssuerVerifyRequest} request
|
|
6008
|
+
* @param {AuthClient.RequestOptions} requestOptions - Request-specific configuration.
|
|
6009
|
+
*
|
|
6010
|
+
* @throws {@link Vectros.BadRequestError}
|
|
6011
|
+
* @throws {@link Vectros.ForbiddenError}
|
|
6012
|
+
* @throws {@link Vectros.NotFoundError}
|
|
6013
|
+
* @throws {@link Vectros.ConflictError}
|
|
6014
|
+
* @throws {@link Vectros.TooManyRequestsError}
|
|
6015
|
+
*
|
|
6016
|
+
* @example
|
|
6017
|
+
* await client.auth.verifyIssuer({
|
|
6018
|
+
* issuerId: "auth0-prod",
|
|
6019
|
+
* token: "token"
|
|
6020
|
+
* })
|
|
6021
|
+
*/
|
|
6022
|
+
verifyIssuer(request, requestOptions) {
|
|
6023
|
+
return HttpResponsePromise.fromPromise(this.__verifyIssuer(request, requestOptions));
|
|
6024
|
+
}
|
|
6025
|
+
async __verifyIssuer(request, requestOptions) {
|
|
6026
|
+
const { issuerId, ..._body } = request;
|
|
6027
|
+
const _authRequest = await this._options.authProvider.getAuthRequest();
|
|
6028
|
+
const _headers = mergeHeaders(
|
|
6029
|
+
_authRequest.headers,
|
|
6030
|
+
this._options?.headers,
|
|
6031
|
+
requestOptions?.headers
|
|
6032
|
+
);
|
|
6033
|
+
const _response = await fetcher({
|
|
6034
|
+
url: url_exports.join(
|
|
6035
|
+
await Supplier.get(this._options.baseUrl) ?? await Supplier.get(this._options.environment),
|
|
6036
|
+
`v1/auth/issuers/${url_exports.encodePathParam(issuerId)}/verify`
|
|
6037
|
+
),
|
|
6038
|
+
method: "POST",
|
|
6039
|
+
headers: _headers,
|
|
6040
|
+
contentType: "application/json",
|
|
6041
|
+
queryString: url_exports.queryBuilder().mergeAdditional(requestOptions?.queryParams).build(),
|
|
6042
|
+
requestType: "json",
|
|
6043
|
+
body: _body,
|
|
6044
|
+
timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1e3,
|
|
6045
|
+
maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries,
|
|
6046
|
+
abortSignal: requestOptions?.abortSignal,
|
|
6047
|
+
fetchFn: this._options?.fetch,
|
|
6048
|
+
logging: this._options.logging
|
|
6049
|
+
});
|
|
6050
|
+
if (_response.ok) {
|
|
6051
|
+
return { data: _response.body, rawResponse: _response.rawResponse };
|
|
6052
|
+
}
|
|
6053
|
+
if (_response.error.reason === "status-code") {
|
|
6054
|
+
switch (_response.error.statusCode) {
|
|
6055
|
+
case 400:
|
|
6056
|
+
throw new BadRequestError(_response.error.body, _response.rawResponse);
|
|
6057
|
+
case 403:
|
|
6058
|
+
throw new ForbiddenError(_response.error.body, _response.rawResponse);
|
|
6059
|
+
case 404:
|
|
6060
|
+
throw new NotFoundError(_response.error.body, _response.rawResponse);
|
|
6061
|
+
case 409:
|
|
6062
|
+
throw new ConflictError(_response.error.body, _response.rawResponse);
|
|
6063
|
+
case 429:
|
|
6064
|
+
throw new TooManyRequestsError(_response.error.body, _response.rawResponse);
|
|
6065
|
+
default:
|
|
6066
|
+
throw new VectrosError({
|
|
6067
|
+
statusCode: _response.error.statusCode,
|
|
6068
|
+
body: _response.error.body,
|
|
6069
|
+
rawResponse: _response.rawResponse
|
|
6070
|
+
});
|
|
6071
|
+
}
|
|
6072
|
+
}
|
|
6073
|
+
return handleNonStatusCodeError(
|
|
6074
|
+
_response.error,
|
|
6075
|
+
_response.rawResponse,
|
|
6076
|
+
"POST",
|
|
6077
|
+
"/v1/auth/issuers/{issuerId}/verify"
|
|
6078
|
+
);
|
|
6079
|
+
}
|
|
6001
6080
|
/**
|
|
6002
6081
|
* Returns the identity bound to your credential — your account, principal type, key id, and scope details — so you can confirm who you are authenticated as and that the credential is valid. MCP clients use this to render "signed in as ..." in a chat UI without a separate identity endpoint.
|
|
6003
6082
|
*
|
|
@@ -7430,7 +7509,7 @@ var DocumentsClient = class {
|
|
|
7430
7509
|
return handleNonStatusCodeError(_response.error, _response.rawResponse, "GET", "/v1/documents/{id}/versions");
|
|
7431
7510
|
}
|
|
7432
7511
|
/**
|
|
7433
|
-
* Starts a file-based document by returning a short-lived presigned S3 PUT URL. Upload the file bytes directly to `uploadUrl`; the document is then automatically queued for text extraction and asynchronous indexing. Supplying an `externalId` makes this idempotent — re-initiating an upload with the same `externalId` re-issues a fresh presigned URL
|
|
7512
|
+
* Starts a file-based document by returning a short-lived presigned S3 PUT URL. Upload the file bytes directly to `uploadUrl`; the document is then automatically queued for text extraction and asynchronous indexing. Supplying an `externalId` makes this idempotent — re-initiating an upload with the same `externalId` re-issues a fresh presigned URL for the SAME existing document, targeting a newly-staged object rather than the document's current one — your PUT lands there first, and the document only adopts it once the upload is validated (so a re-upload inherently replaces the file body) rather than creating a duplicate. The response's `created` field (and the HTTP status — 201 when a new document was minted, 200 when an existing one was matched) tells the two apart. With `?upsert=true`, the submitted `payload`/`title` are also applied to the matched document (file-body divergence cannot be diffed at upload-init — the bytes have not arrived yet — so the re-upload itself replaces the body). Creating a NEW document requires the `documents:c` scope. Re-uploading over an EXISTING document overwrites (and re-indexes) its body, so it is an update: it requires the `documents:u` scope (as does `?upsert=true` for the metadata). **Your PUT to `uploadUrl` MUST include the header named in the response's `requiredHeaderName`, set to `requiredHeaderValue` exactly.** It is a signed part of the presigned request — the PUT never reaches this service either way, so neither failure uses this API's JSON error shape. Omitting it, or sending a different value, invalidates the request's signature and S3 rejects it with a 403 `SignatureDoesNotMatch`. Sending it correctly makes the URL single-use: `requiredHeaderName` is always `If-None-Match` and `requiredHeaderValue` is always `*` — a create, a recovery upload for a document whose earlier file is missing, and a re-upload ALL target a freshly-minted, never-written key, so the same S3 conditional-write precondition applies uniformly (S3's own documented conditional-write behavior): a replay after the object already exists (from your own completed PUT, or anyone else's) fails with `412 Precondition Failed`; a replay after the object was deleted and never re-created finds an absent key again and can succeed — single-use is a property of the object's existence, not a one-time token. **Either precondition can also fail with `409 Conflict` if your PUT genuinely races a concurrent delete on the same object.** Whichever of `412` or `409` you receive, the remedy is the same: call this endpoint again for a fresh URL rather than retrying the same PUT — a fresh call always targets a new key, so it is never subject to whatever raced the previous one.
|
|
7434
7513
|
*
|
|
7435
7514
|
* @param {Vectros.FileUploadRequest} request
|
|
7436
7515
|
* @param {DocumentsClient.RequestOptions} requestOptions - Request-specific configuration.
|
|
@@ -9196,7 +9275,7 @@ var IdentityClient = class {
|
|
|
9196
9275
|
return handleNonStatusCodeError(_response.error, _response.rawResponse, "GET", "/v1/users/exists-by-email");
|
|
9197
9276
|
}
|
|
9198
9277
|
/**
|
|
9199
|
-
* Returns the audit trail of changes to a user, most recent first. Identity history is always recorded
|
|
9278
|
+
* Returns the audit trail of changes to a user, most recent first, including after the user has been deleted. Identity history is always recorded. Post-delete, this is readable by an account-level (root) API key only — a context-confined credential 404s after the delete, the same response it would get for a nonexistent id. Sensitive field values are redacted in every historical version. Returns a page in the `{data, nextCursor}` envelope. Requires the `users:r` scope.
|
|
9200
9279
|
*
|
|
9201
9280
|
* @param {Vectros.GetUserVersionsRequest} request
|
|
9202
9281
|
* @param {IdentityClient.RequestOptions} requestOptions - Request-specific configuration.
|
|
@@ -10266,7 +10345,7 @@ var RecordsClient = class {
|
|
|
10266
10345
|
return handleNonStatusCodeError(_response.error, _response.rawResponse, "POST", "/v1/records/lookup");
|
|
10267
10346
|
}
|
|
10268
10347
|
/**
|
|
10269
|
-
* Returns the tombstone left behind when a record was hard-deleted, confirming the deletion and recording when it happened. Look it up using the deleted record's original ID. Requires the `records:r:<type>` scope.
|
|
10348
|
+
* Returns the tombstone left behind when a record was hard-deleted, confirming the deletion and recording when it happened. Look it up using the deleted record's original ID. Requires the `records:r:<type>` scope, granted without a `data_scope` restriction: the owner of a deleted record cannot be checked, so a credential confined to an ownership compartment (e.g. `data_scope: {scope:org: [org_A]}`) cannot read tombstones, including for records it owned.
|
|
10270
10349
|
*
|
|
10271
10350
|
* @param {Vectros.GetRecordTombstoneRequest} request
|
|
10272
10351
|
* @param {RecordsClient.RequestOptions} requestOptions - Request-specific configuration.
|
|
@@ -10673,7 +10752,7 @@ var SchemasClient = class {
|
|
|
10673
10752
|
return handleNonStatusCodeError(_response.error, _response.rawResponse, "PUT", "/v1/schemas/{id}");
|
|
10674
10753
|
}
|
|
10675
10754
|
/**
|
|
10676
|
-
* Permanently deletes a record schema. The request is refused with 409 if records of this type still exist — delete those records first, since every record must reference a live schema. A lineage base (a schema other schemas declare `basedOn`) also cannot be deleted while any such variant still exists — delete the variant schema(s) first. It is likewise refused while any trigger rule fires off this schema — delete those trigger rules first. Requires the `schemas:d` scope.
|
|
10755
|
+
* Permanently deletes a record schema. The request is refused with 409 if records of this type still exist — delete those records first, since every record must reference a live schema. The same applies to documents bound to this schema (for a schema declaring the `document` surface) — delete those documents first. A lineage base (a schema other schemas declare `basedOn`) also cannot be deleted while any such variant still exists — delete the variant schema(s) first. It is likewise refused while any trigger rule fires off this schema — delete those trigger rules first. Requires the `schemas:d` scope.
|
|
10677
10756
|
*
|
|
10678
10757
|
* @param {Vectros.DeleteSchemaRequest} request
|
|
10679
10758
|
* @param {SchemasClient.RequestOptions} requestOptions - Request-specific configuration.
|
|
@@ -10808,7 +10887,7 @@ var ScriptsClient = class {
|
|
|
10808
10887
|
*
|
|
10809
10888
|
* **Budget.** Seconds of useful work: the script has 15 s of wall clock (never more than 16) and a bounded number of statements and host calls; a runaway script is stopped early with `RESOURCE_LIMIT_EXCEEDED`. The transaction is bounded by the number of underlying storage rows it writes — 100 rows after merging — rather than by the number of entities: creating a folder costs 3 rows; creating a plain text document or a record costs 1 row plus one per range-indexed lookup field plus one per `reference` field. So "create a folder and file N documents" holds up to 97 plain documents, 48 with one range field or one reference, 32 with both; if a script exceeds the limit the error says so (`WRITE_BUFFER_CAP_EXCEEDED`, with `total` and `limit`) and nothing is committed. This request takes the write-path limits (burst, credit ceiling, principal quota) once, before the script runs — there is no free-read allowance here, so an account over its credit ceiling is refused even for a read-only script. Reads the script performs are metered as reads, writes as writes, and execution time beyond what those operations include is charged as script execution time (see `GET /v1/usage`).
|
|
10810
10889
|
*
|
|
10811
|
-
* **Reads inside the script.** A `get` sees the script's own staged writes. A list, query or lookup never shows a row the script has deleted, shows a row it re-saved with the updated content, and does not
|
|
10890
|
+
* **Reads inside the script.** A `get` sees the script's own staged writes. A list, query or lookup never shows a row the script has deleted, shows a row it re-saved with the updated content, and does not show a row it created — you hold that row's id from `create()`.
|
|
10812
10891
|
*
|
|
10813
10892
|
* **Retries.** Send an optional `Idempotency-Key` header (1–128 characters of letters, digits, `.`, `_`, `:` or `-`) to make a retry safe: a request repeated under the same key within 24 hours receives the same response as the first attempt without executing again — when that first attempt SUCCEEDED (a duplicate still in flight is refused with `409 IDEMPOTENCY_IN_PROGRESS`; the same key on a different request is refused with `422 IDEMPOTENCY_KEY_REUSED`). A first attempt that FAILED with any other status is not remembered: the same key is free to try again, so a retry after a `400` or a `429` executes. If an execution's outcome cannot be determined — the script did not finish inside the budget while its writes may have committed — the response is `500 EXECUTION_OUTCOME_UNKNOWN` carrying `execution.id`, and a keyed retry receives that same answer: inspect what the script would have written, then use a NEW key for a new attempt. When a key is present the serialised `result` is capped at 16 KB instead of 256 KB. Without a key, a lost response cannot be distinguished from a failed one; re-read before retrying.
|
|
10814
10893
|
*
|
|
@@ -10903,11 +10982,14 @@ var ScriptsClient = class {
|
|
|
10903
10982
|
return handleNonStatusCodeError(_response.error, _response.rawResponse, "POST", "/v1/scripts/execute");
|
|
10904
10983
|
}
|
|
10905
10984
|
/**
|
|
10906
|
-
* Returns a paginated list of your scripts. Pass `name` to list every version of one script name (oldest first); omit it for a flat list across every name in your account (newest first). Results are returned as a `{data, nextCursor}` envelope — pass `nextCursor` as `startFrom` to fetch the next page. Requires the `scripts:r` scope.
|
|
10985
|
+
* Returns a paginated list of your scripts. Pass `name` to list every version of one script name (oldest first); omit it for a flat list across every name in your account (newest first). Results are returned as a `{data, nextCursor}` envelope — pass `nextCursor` as `startFrom` to fetch the next page. Requires the `scripts:r` scope. **Each row's `source` is omitted by default** (`sourceOmitted: true`) — scripts are immutable per version, so a name's version history can otherwise carry every version's complete source in one response. Pass `?includeSource=true` to get it back inline, or fetch one version's full source with a by-id GET. **`?name=<n>&latest=true`** answers "what is the current version of this name" in one bounded call — a single `ScriptResponse` object (not a page), instead of draining every page of the name's version history to find the newest yourself.
|
|
10907
10986
|
*
|
|
10908
10987
|
* @param {Vectros.ListScriptsRequest} request
|
|
10909
10988
|
* @param {ScriptsClient.RequestOptions} requestOptions - Request-specific configuration.
|
|
10910
10989
|
*
|
|
10990
|
+
* @throws {@link Vectros.BadRequestError}
|
|
10991
|
+
* @throws {@link Vectros.NotFoundError}
|
|
10992
|
+
*
|
|
10911
10993
|
* @example
|
|
10912
10994
|
* await client.scripts.listScripts({
|
|
10913
10995
|
* name: "notify-assignee",
|
|
@@ -10918,11 +11000,13 @@ var ScriptsClient = class {
|
|
|
10918
11000
|
return HttpResponsePromise.fromPromise(this.__listScripts(request, requestOptions));
|
|
10919
11001
|
}
|
|
10920
11002
|
async __listScripts(request = {}, requestOptions) {
|
|
10921
|
-
const { name, startFrom, limit } = request;
|
|
11003
|
+
const { name, startFrom, limit, includeSource, latest } = request;
|
|
10922
11004
|
const _queryParams = {
|
|
10923
11005
|
name,
|
|
10924
11006
|
startFrom,
|
|
10925
|
-
limit
|
|
11007
|
+
limit,
|
|
11008
|
+
includeSource,
|
|
11009
|
+
latest
|
|
10926
11010
|
};
|
|
10927
11011
|
const _authRequest = await this._options.authProvider.getAuthRequest();
|
|
10928
11012
|
const _headers = mergeHeaders(
|
|
@@ -10948,16 +11032,23 @@ var ScriptsClient = class {
|
|
|
10948
11032
|
return { data: _response.body, rawResponse: _response.rawResponse };
|
|
10949
11033
|
}
|
|
10950
11034
|
if (_response.error.reason === "status-code") {
|
|
10951
|
-
|
|
10952
|
-
|
|
10953
|
-
|
|
10954
|
-
|
|
10955
|
-
|
|
11035
|
+
switch (_response.error.statusCode) {
|
|
11036
|
+
case 400:
|
|
11037
|
+
throw new BadRequestError(_response.error.body, _response.rawResponse);
|
|
11038
|
+
case 404:
|
|
11039
|
+
throw new NotFoundError(_response.error.body, _response.rawResponse);
|
|
11040
|
+
default:
|
|
11041
|
+
throw new VectrosError({
|
|
11042
|
+
statusCode: _response.error.statusCode,
|
|
11043
|
+
body: _response.error.body,
|
|
11044
|
+
rawResponse: _response.rawResponse
|
|
11045
|
+
});
|
|
11046
|
+
}
|
|
10956
11047
|
}
|
|
10957
11048
|
return handleNonStatusCodeError(_response.error, _response.rawResponse, "GET", "/v1/scripts");
|
|
10958
11049
|
}
|
|
10959
11050
|
/**
|
|
10960
|
-
* Stores a new version of a script object. Every POST creates a genuinely new version — this is NOT idempotent-by-name: pushing `name` again always creates a new row with `scriptVersion` auto-incremented (the first version pushed for a name is 1). `source` is stored as-is: nothing parses or validates it at push time. A version runs when a trigger rule references it (`POST /v1/triggers`) or when `POST /v1/scripts/execute` names it. Requires the `scripts:c` scope.
|
|
11051
|
+
* Stores a new version of a script object. Every POST creates a genuinely new version — this is NOT idempotent-by-name: pushing `name` again always creates a new row with `scriptVersion` auto-incremented (the first version pushed for a name is 1). `source` is stored as-is: nothing parses or validates it at push time. A version runs when a trigger rule references it (`POST /v1/triggers`) or when `POST /v1/scripts/execute` names it. `provisionedBy` MAY BE SET ON ABSENT, NEVER CHANGED, evaluated against the CURRENT LATEST version of this `name` at push time: if the latest version has no marker recorded, this push may set any value; if the latest version already has one, an omitted value inherits it forward and a DIFFERENT value is refused — a live comparison against whichever version is newest at push time, not a value fixed for the name's whole history. Pushing onto an EXISTING name with no `provisionedBy` recorded on its current latest version additionally requires that you were the caller who pushed that latest version — a name carrying a `provisionedBy` marker is exempt from this check (proving you can supply or inherit the matching marker is itself the ownership proof there). Requires the `scripts:c` scope.
|
|
10961
11052
|
*
|
|
10962
11053
|
* @param {Vectros.ScriptRequest} request
|
|
10963
11054
|
* @param {ScriptsClient.RequestOptions} requestOptions - Request-specific configuration.
|
|
@@ -11144,12 +11235,13 @@ var ScriptsClient = class {
|
|
|
11144
11235
|
return handleNonStatusCodeError(_response.error, _response.rawResponse, "PUT", "/v1/scripts/{id}");
|
|
11145
11236
|
}
|
|
11146
11237
|
/**
|
|
11147
|
-
* Permanently deletes one script version by ID. Deleting one version does not affect any other version of the same name. Requires the `scripts:d` scope.
|
|
11238
|
+
* Permanently deletes one script version by ID. Deleting one version does not affect any other version of the same name. Requires the `scripts:d` scope. Refused with 409 if a live trigger rule still references this version — either directly (a pinned `scriptRef`), or via `"latest"` when this IS the newest version of its name. Delete or re-point those rules first.
|
|
11148
11239
|
*
|
|
11149
11240
|
* @param {Vectros.DeleteScriptRequest} request
|
|
11150
11241
|
* @param {ScriptsClient.RequestOptions} requestOptions - Request-specific configuration.
|
|
11151
11242
|
*
|
|
11152
11243
|
* @throws {@link Vectros.NotFoundError}
|
|
11244
|
+
* @throws {@link Vectros.ConflictError}
|
|
11153
11245
|
* @throws {@link Vectros.TooManyRequestsError}
|
|
11154
11246
|
*
|
|
11155
11247
|
* @example
|
|
@@ -11189,6 +11281,8 @@ var ScriptsClient = class {
|
|
|
11189
11281
|
switch (_response.error.statusCode) {
|
|
11190
11282
|
case 404:
|
|
11191
11283
|
throw new NotFoundError(_response.error.body, _response.rawResponse);
|
|
11284
|
+
case 409:
|
|
11285
|
+
throw new ConflictError(_response.error.body, _response.rawResponse);
|
|
11192
11286
|
case 429:
|
|
11193
11287
|
throw new TooManyRequestsError(_response.error.body, _response.rawResponse);
|
|
11194
11288
|
default:
|
|
@@ -11734,6 +11828,25 @@ var logging;
|
|
|
11734
11828
|
import { z } from "zod";
|
|
11735
11829
|
import { z as z2 } from "zod";
|
|
11736
11830
|
var CONTEXT_ID_RE = /^[a-z][a-z0-9-]{2,30}$/;
|
|
11831
|
+
var FORGERY_CHARS_RE = /[\x00-\x1f\x7f-\x9f\u00ad\u061c\u200b-\u200f\u2028\u2029\u202a-\u202e\u2060\u2066-\u2069\ufeff\u{e0000}-\u{e007f}]/u;
|
|
11832
|
+
function safeDisplayString(base) {
|
|
11833
|
+
return base.refine((v) => !FORGERY_CHARS_RE.test(v), {
|
|
11834
|
+
message: "must not contain control characters, invisible/zero-width characters, line/paragraph separators, or bidi marks/overrides \u2014 these can forge, hide, or reorder text in a rendered plan preview"
|
|
11835
|
+
});
|
|
11836
|
+
}
|
|
11837
|
+
function safeUrlString(base) {
|
|
11838
|
+
return base.refine(
|
|
11839
|
+
(v) => {
|
|
11840
|
+
if (!/^[\x20-\x7e]*$/.test(v)) return false;
|
|
11841
|
+
const authorityMatch = /^https:\/\/([^/?#]*)/i.exec(v);
|
|
11842
|
+
if (!authorityMatch) return false;
|
|
11843
|
+
return /^[A-Za-z0-9.-]+(:\d+)?$/.test(authorityMatch[1]);
|
|
11844
|
+
},
|
|
11845
|
+
{
|
|
11846
|
+
message: "must be a printable-ASCII https:// URL whose authority is only letters, digits, dot, dash, and an optional port \u2014 no userinfo ('@'), percent-encoding, or non-ASCII host (including a look-alike/homoglyph domain) is accepted; supply an internationalized domain in its punycode (xn--\u2026) form"
|
|
11847
|
+
}
|
|
11848
|
+
);
|
|
11849
|
+
}
|
|
11737
11850
|
var MAX_SCOPE_DIMENSIONS = 2;
|
|
11738
11851
|
var ValidationRulesSchema = z.object({
|
|
11739
11852
|
required: z.boolean().optional(),
|
|
@@ -11763,7 +11876,7 @@ var RenderHintsSchema = z.object({
|
|
|
11763
11876
|
displayField: z.boolean().optional()
|
|
11764
11877
|
}).strict();
|
|
11765
11878
|
var BlueprintFieldDefSchema = z.object({
|
|
11766
|
-
fieldId: z.string().min(1),
|
|
11879
|
+
fieldId: safeDisplayString(z.string().min(1)),
|
|
11767
11880
|
fieldType: z.string().min(1),
|
|
11768
11881
|
required: z.boolean().optional(),
|
|
11769
11882
|
searchable: z.boolean().optional(),
|
|
@@ -11854,13 +11967,13 @@ var BlueprintFieldDefSchema = z.object({
|
|
|
11854
11967
|
}
|
|
11855
11968
|
});
|
|
11856
11969
|
var BlueprintLookupFieldSchema = z.union([
|
|
11857
|
-
z.string().min(1),
|
|
11970
|
+
safeDisplayString(z.string().min(1)),
|
|
11858
11971
|
z.object({
|
|
11859
11972
|
// A lookup field declares either ONE field (fieldName) or 2-3 fields
|
|
11860
11973
|
// matched together (fieldNames) — never both, never neither. `.strict()`
|
|
11861
11974
|
// below only rejects unknown KEYS; it does not enforce this combination,
|
|
11862
11975
|
// so the `.superRefine()` after it carries the actual rule.
|
|
11863
|
-
fieldName: z.string().min(1).optional(),
|
|
11976
|
+
fieldName: safeDisplayString(z.string().min(1)).optional(),
|
|
11864
11977
|
// Composite (conjunctive) lookup: match on all of these fields at once —
|
|
11865
11978
|
// "every record where `status` is `open` AND `area` is `billing`", exact,
|
|
11866
11979
|
// in a stable order. Order is significant and locked at create: a query
|
|
@@ -11869,7 +11982,7 @@ var BlueprintLookupFieldSchema = z.union([
|
|
|
11869
11982
|
// lookup for that. Record-only: the schema's `allowedSurfaces` must be
|
|
11870
11983
|
// exactly `['record']` (checked at the schema level below) — the
|
|
11871
11984
|
// platform's composite index has no document/user/entity reader yet.
|
|
11872
|
-
fieldNames: z.array(z.string().min(1)).min(2).max(3).optional(),
|
|
11985
|
+
fieldNames: z.array(safeDisplayString(z.string().min(1))).min(2).max(3).optional(),
|
|
11873
11986
|
// Not available on a composite (`fieldNames`) lookup — checked below.
|
|
11874
11987
|
unique: z.boolean().optional(),
|
|
11875
11988
|
// Opt this field into ordered range + prefix lookups (from/to/prefix) on
|
|
@@ -11919,7 +12032,14 @@ var BlueprintCapabilitiesSchema = z.object({
|
|
|
11919
12032
|
triggersEnabled: z.boolean().optional()
|
|
11920
12033
|
}).strict();
|
|
11921
12034
|
var BlueprintSchemaSchema = z.object({
|
|
11922
|
-
|
|
12035
|
+
// safeDisplayString: buildPlan's schema step renders this raw into a single summary
|
|
12036
|
+
// line (`schema '${s.typeName}' (...)`) — a gap found after the rest of this file's
|
|
12037
|
+
// forgery-character pass had already shipped, the same "unconstrained free-text field
|
|
12038
|
+
// reaches a rendered plan line" shape as issuer/allowedActions/etc. `displayName`
|
|
12039
|
+
// here is NOT wrapped: unlike
|
|
12040
|
+
// servicePrincipal's own displayName, this one is never interpolated into a rendered
|
|
12041
|
+
// summary anywhere in loader.ts today (only forwarded to the schema-upsert payload).
|
|
12042
|
+
typeName: safeDisplayString(z.string().min(1)),
|
|
11923
12043
|
displayName: z.string().min(1),
|
|
11924
12044
|
description: z.string().optional(),
|
|
11925
12045
|
// Default search-index mode for instances of this type. HYBRID, SEMANTIC and
|
|
@@ -12056,7 +12176,7 @@ var BlueprintAccessProfileSchema = z.object({
|
|
|
12056
12176
|
// `.superRefine` on this schema enforces the XOR, mirroring the platform's
|
|
12057
12177
|
// own access-profile scopes/role-composition XOR check). Exactly one of
|
|
12058
12178
|
// the two authors this profile's grant.
|
|
12059
|
-
allowedActions: z.array(z.string().min(1)).min(1).optional(),
|
|
12179
|
+
allowedActions: z.array(safeDisplayString(z.string().min(1))).min(1).optional(),
|
|
12060
12180
|
// Additive role composition: this profile's effective grant is
|
|
12061
12181
|
// each named role's own clauses, concatenated in the order listed, instead of
|
|
12062
12182
|
// an inline clause authored here. Every id must name a role this SAME
|
|
@@ -12085,7 +12205,14 @@ var BlueprintAccessProfileSchema = z.object({
|
|
|
12085
12205
|
// NOT see tenant-level/seed records). Every KEY is validated below
|
|
12086
12206
|
// (`lintDataScopeKeys`) — `userId` or a grammar-valid `scope:<ns>`, no
|
|
12087
12207
|
// other shape.
|
|
12088
|
-
|
|
12208
|
+
// safeDisplayString on the value: not currently rendered by VALUE anywhere in buildPlan
|
|
12209
|
+
// (only `Object.keys(...)` of a profile's dataScope is shown today, unlike a role clause's
|
|
12210
|
+
// dataScope, which `renderPlanDataScope` prints values for — the reason that sibling field
|
|
12211
|
+
// WAS wrapped). Wrapped anyway for consistency with every other dataScope-shaped field in
|
|
12212
|
+
// this file, and so a future preview enhancement that starts showing these values (a very
|
|
12213
|
+
// plausible one, given the existing renderPlanDataScope precedent) doesn't silently reopen
|
|
12214
|
+
// this exact class on a field that looks, byte-for-byte, like every other one already closed.
|
|
12215
|
+
dataScope: z.record(z.array(z.union([safeDisplayString(z.string().min(1)), z.null()]))).optional(),
|
|
12089
12216
|
// Optional identity overrides for the service principal's profile — scope
|
|
12090
12217
|
// values its key STAMPS onto everything it writes (a credential can only
|
|
12091
12218
|
// stamp ownership its identity carries). Keys are the namespaced `scope:<ns>`
|
|
@@ -12161,8 +12288,8 @@ var BlueprintAccessProfileSchema = z.object({
|
|
|
12161
12288
|
}
|
|
12162
12289
|
});
|
|
12163
12290
|
var BlueprintRoleClauseSchema = z.object({
|
|
12164
|
-
allowedActions: z.array(z.string().min(1)).min(1),
|
|
12165
|
-
dataScope: z.record(z.array(z.union([z.string().min(1), z.null()]))).optional(),
|
|
12291
|
+
allowedActions: z.array(safeDisplayString(z.string().min(1))).min(1),
|
|
12292
|
+
dataScope: z.record(z.array(z.union([safeDisplayString(z.string().min(1)), z.null()]))).optional(),
|
|
12166
12293
|
dataScopeRef: z.string().min(1).optional(),
|
|
12167
12294
|
capabilities: GrantedCapabilitiesSchema,
|
|
12168
12295
|
assignableRoles: AssignableRolesSchema
|
|
@@ -12176,10 +12303,10 @@ var BlueprintRoleClauseSchema = z.object({
|
|
|
12176
12303
|
}
|
|
12177
12304
|
});
|
|
12178
12305
|
var BlueprintFragmentsSchema = z.record(
|
|
12179
|
-
z.record(z.array(z.union([z.string().min(1), z.null()])))
|
|
12306
|
+
z.record(z.array(z.union([safeDisplayString(z.string().min(1)), z.null()])))
|
|
12180
12307
|
);
|
|
12181
|
-
var BlueprintRolesSchema = z.record(z.array(BlueprintRoleClauseSchema).min(1));
|
|
12182
|
-
var BlueprintRoleAssumableSchema = z.record(z.array(z.string().min(1)).min(1));
|
|
12308
|
+
var BlueprintRolesSchema = z.record(safeDisplayString(z.string().min(1)), z.array(BlueprintRoleClauseSchema).min(1));
|
|
12309
|
+
var BlueprintRoleAssumableSchema = z.record(z.array(safeDisplayString(z.string().min(1))).min(1));
|
|
12183
12310
|
var BlueprintRoleAssumableMapSchema = z.record(BlueprintRoleAssumableSchema);
|
|
12184
12311
|
var BlueprintTriggerFiringSourceSchema = z.object({
|
|
12185
12312
|
schemaName: z.string().min(1),
|
|
@@ -12187,7 +12314,12 @@ var BlueprintTriggerFiringSourceSchema = z.object({
|
|
|
12187
12314
|
}).strict();
|
|
12188
12315
|
var SCRIPT_VERSION_INTEGER_RE = /^[1-9][0-9]*$/;
|
|
12189
12316
|
var BlueprintTriggerScriptRefSchema = z.object({
|
|
12190
|
-
|
|
12317
|
+
// safeDisplayString: rendered raw into buildPlan's trigger step (`${scriptRef.name}@...`).
|
|
12318
|
+
// When naming a script THIS blueprint ships, `name` is transitively safe already (it must
|
|
12319
|
+
// equal a `scripts` map key, itself grammar-pinned by SCRIPT_NAME_RE) — but a scriptRef
|
|
12320
|
+
// naming a script this blueprint does NOT ship is, by design, "entirely unrestricted" (see
|
|
12321
|
+
// this file's own doc on that case), so the wrap is load-bearing for exactly that path.
|
|
12322
|
+
name: safeDisplayString(z.string().min(1)),
|
|
12191
12323
|
version: z.string().min(1)
|
|
12192
12324
|
}).strict().superRefine((ref, ctx) => {
|
|
12193
12325
|
if (ref.version !== "latest" && !SCRIPT_VERSION_INTEGER_RE.test(ref.version)) {
|
|
@@ -12207,9 +12339,9 @@ var BlueprintTriggerManifestSchema = z.array(
|
|
|
12207
12339
|
var BlueprintTriggerEntrySchema = z.object({
|
|
12208
12340
|
firingSource: BlueprintTriggerFiringSourceSchema,
|
|
12209
12341
|
scriptRef: BlueprintTriggerScriptRefSchema,
|
|
12210
|
-
allowedActions: z.array(z.string().min(1)).min(1).optional(),
|
|
12342
|
+
allowedActions: z.array(safeDisplayString(z.string().min(1))).min(1).optional(),
|
|
12211
12343
|
roleIds: z.array(z.string().min(1)).min(1).optional(),
|
|
12212
|
-
dataScope: z.record(z.array(z.union([z.string().min(1), z.null()]))).optional(),
|
|
12344
|
+
dataScope: z.record(z.array(z.union([safeDisplayString(z.string().min(1)), z.null()]))).optional(),
|
|
12213
12345
|
capabilities: GrantedCapabilitiesSchema,
|
|
12214
12346
|
manifest: BlueprintTriggerManifestSchema,
|
|
12215
12347
|
/**
|
|
@@ -12233,7 +12365,7 @@ var BlueprintTriggerEntrySchema = z.object({
|
|
|
12233
12365
|
* together are capped at 224 KB serialised — a firing whose projected input exceeds that is not
|
|
12234
12366
|
* run, and is recorded as an `INPUT_TOO_LARGE` failure naming the size.
|
|
12235
12367
|
*/
|
|
12236
|
-
fields: z.array(z.string().min(1))
|
|
12368
|
+
fields: z.array(safeDisplayString(z.string().min(1)))
|
|
12237
12369
|
}).strict().superRefine((entry, ctx) => {
|
|
12238
12370
|
const hasInline = entry.allowedActions !== void 0;
|
|
12239
12371
|
const hasRoleIds = entry.roleIds !== void 0;
|
|
@@ -12262,7 +12394,7 @@ var BlueprintTriggerEntrySchema = z.object({
|
|
|
12262
12394
|
}
|
|
12263
12395
|
}
|
|
12264
12396
|
});
|
|
12265
|
-
var BlueprintTriggersSchema = z.record(BlueprintTriggerEntrySchema);
|
|
12397
|
+
var BlueprintTriggersSchema = z.record(safeDisplayString(z.string().min(1)), BlueprintTriggerEntrySchema);
|
|
12266
12398
|
var SCRIPT_NAME_RE = /^[A-Za-z0-9_-]{1,64}$/;
|
|
12267
12399
|
var SCRIPT_SOURCE_MAX_BYTES = 3e5;
|
|
12268
12400
|
var SCRIPT_INPUT_CONTRACT_MAX_BYTES = 5e4;
|
|
@@ -12308,8 +12440,15 @@ var IdentityKindSchema = z.string().min(1).superRefine((val, ctx) => {
|
|
|
12308
12440
|
});
|
|
12309
12441
|
var IdentityDeclSchema = z.object({
|
|
12310
12442
|
kind: IdentityKindSchema,
|
|
12311
|
-
|
|
12312
|
-
|
|
12443
|
+
// safeDisplayString: `@vectros-ai/cli`'s `runBootstrapCli` and `blueprint plan` both print
|
|
12444
|
+
// this RAW to stdout ("~ <name> (<kind>) — external id '<externalId>'"), immediately before
|
|
12445
|
+
// the "Apply this plan? [y/N]" confirmation gate itself — the highest-severity gap found in
|
|
12446
|
+
// this pass, since it sits at the confirmation prompt's own disclosure text, not merely
|
|
12447
|
+
// somewhere upstream of it. `displayName` is wrapped too though not currently
|
|
12448
|
+
// rendered in any preview (defense-in-depth, matching this file's practice for its adjacent
|
|
12449
|
+
// sibling fields) — it does reach a platform API payload as an entity's `name`.
|
|
12450
|
+
externalId: safeDisplayString(z.string().min(1)),
|
|
12451
|
+
displayName: safeDisplayString(z.string().min(1)).optional(),
|
|
12313
12452
|
metadata: z.record(z.unknown()).optional()
|
|
12314
12453
|
}).strict();
|
|
12315
12454
|
var IDENTITY_NAME_RE = /^[A-Za-z_]\w*$/;
|
|
@@ -12320,10 +12459,14 @@ var IdentitiesDeclSchema = z.record(
|
|
|
12320
12459
|
IdentityDeclSchema
|
|
12321
12460
|
);
|
|
12322
12461
|
var BlueprintServicePrincipalSchema = z.object({
|
|
12323
|
-
externalId: z.string().min(1),
|
|
12324
|
-
displayName: z.string().min(1)
|
|
12462
|
+
externalId: safeDisplayString(z.string().min(1)),
|
|
12463
|
+
displayName: safeDisplayString(z.string().min(1))
|
|
12325
12464
|
}).strict();
|
|
12326
12465
|
var ISSUER_ID_RE = CONTEXT_ID_RE;
|
|
12466
|
+
var RESTRICTED_DOMAIN_RE = /^(?=.{3,253}$)[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)+$/;
|
|
12467
|
+
function isRestrictedToDomainName(value) {
|
|
12468
|
+
return RESTRICTED_DOMAIN_RE.test(value);
|
|
12469
|
+
}
|
|
12327
12470
|
var MAX_CAPTURED_CLAIMS = 20;
|
|
12328
12471
|
var MAX_CAPTURED_CLAIM_NAME_LENGTH = 64;
|
|
12329
12472
|
function assertNoDuplicateClaimNames(field) {
|
|
@@ -12347,10 +12490,17 @@ var BlueprintIssuerSchema = z.object({
|
|
|
12347
12490
|
issuerId: z.string().regex(ISSUER_ID_RE, {
|
|
12348
12491
|
message: "issuerId must be 3-31 chars, start with a lowercase letter, then lowercase letters/digits/dashes (e.g. 'auth0-prod')"
|
|
12349
12492
|
}),
|
|
12350
|
-
/**
|
|
12351
|
-
|
|
12352
|
-
|
|
12353
|
-
|
|
12493
|
+
/**
|
|
12494
|
+
* The IdP's `iss` claim value, exactly as it appears in tokens it issues. `safeUrlString`,
|
|
12495
|
+
* not `safeDisplayString`: this is the trust-anchor field the plan preview exists to
|
|
12496
|
+
* disclose, so it gets the stronger positive check (see that function's own doc) rather
|
|
12497
|
+
* than the blocklist every OTHER free-text field uses — a homoglyph host here is exactly
|
|
12498
|
+
* what an operator approving a plan must be able to trust reading. Virtually every real
|
|
12499
|
+
* OIDC issuer is URL-shaped in practice, even though the spec does not strictly require it.
|
|
12500
|
+
*/
|
|
12501
|
+
issuer: safeUrlString(z.string().min(1)),
|
|
12502
|
+
/** The IdP's remote JWKS endpoint, used to verify presented tokens' signatures. `safeUrlString` — see `issuer`'s own comment just above. */
|
|
12503
|
+
jwksUri: safeUrlString(z.string().min(1)),
|
|
12354
12504
|
/**
|
|
12355
12505
|
* The `aud` claim value a presented subject_token must carry. Must be globally
|
|
12356
12506
|
* unique in combination with `issuer` — use a distinct audience per
|
|
@@ -12378,7 +12528,29 @@ var BlueprintIssuerSchema = z.object({
|
|
|
12378
12528
|
* back to calling this endpoint (with the presented token as the bearer credential) and reads
|
|
12379
12529
|
* `emailClaim` from its JSON response instead. Omit to leave the fallback disabled.
|
|
12380
12530
|
*/
|
|
12381
|
-
|
|
12531
|
+
// `safeUrlString` — see `issuer`'s own comment above for why these three trust-anchor
|
|
12532
|
+
// fields get the stronger positive check instead of `safeDisplayString`'s blocklist.
|
|
12533
|
+
userinfoUri: safeUrlString(z.string().min(1)).optional(),
|
|
12534
|
+
/**
|
|
12535
|
+
* Opt-in domain scope: a domain ALREADY VERIFIED for your account (verified once, by the
|
|
12536
|
+
* account owner, in the developer portal). The registration's `(issuer, audience)` uniqueness
|
|
12537
|
+
* is then scoped to that domain, so an unrelated registration of the same pair never blocks
|
|
12538
|
+
* it, and a presented token matches only when its `hd` claim equals the domain (Google
|
|
12539
|
+
* Workspace emits `hd` natively; other identity providers need a claims-mapping rule that
|
|
12540
|
+
* emits a claim named exactly `hd`). A token with no matching `hd` claim is not matched at all,
|
|
12541
|
+
* so scoping an issuer that already has users signing in without one locks those users out.
|
|
12542
|
+
*
|
|
12543
|
+
* Written the way the platform stores it: a lowercase, ASCII DNS name (`acme.com`), never a URL.
|
|
12544
|
+
* An internationalized domain must be given in its punycode (`xn--…`) form, the same rule
|
|
12545
|
+
* `issuer` follows, so what an operator reads in the plan preview is the domain that is
|
|
12546
|
+
* actually scoped. An unverified domain fails the apply with the platform's `400`.
|
|
12547
|
+
*
|
|
12548
|
+
* Omit to register a domain-less issuer. Omitting it on a re-apply leaves an existing scope
|
|
12549
|
+
* unchanged; removing a scope is `vectros issuers update --clear-restricted-to-domain`.
|
|
12550
|
+
*/
|
|
12551
|
+
restrictedToDomain: z.string().regex(RESTRICTED_DOMAIN_RE, {
|
|
12552
|
+
message: "restrictedToDomain must be a lowercase DNS name such as 'acme.com' \u2014 letters, digits and dashes in dot-separated labels, at least two labels, no scheme, path, port or uppercase; write an internationalized domain in its punycode (xn--\u2026) form"
|
|
12553
|
+
}).optional(),
|
|
12382
12554
|
/**
|
|
12383
12555
|
* Opt-in self-service signup: a list of {signupType, roleId} pairs. When a first-time exchange
|
|
12384
12556
|
* caller presents no invite token but names a signupType matching one of these (or omits it and
|
|
@@ -12418,9 +12590,18 @@ var BlueprintIssuerSchema = z.object({
|
|
|
12418
12590
|
* separate opt-ins, and capturing a claim does not by itself expose it on a profile.
|
|
12419
12591
|
*/
|
|
12420
12592
|
capturedClaims: z.array(
|
|
12421
|
-
|
|
12422
|
-
|
|
12423
|
-
|
|
12593
|
+
// `safeDisplayString` (a `.refine`) then the blank-name `.refine` — both ordered AFTER
|
|
12594
|
+
// `.max`, since `.refine` returns a ZodEffects, which has no `.max` (`.refine` itself
|
|
12595
|
+
// chains fine onto a ZodEffects — it's on the ZodType base, not ZodString-specific).
|
|
12596
|
+
// `.min(1)` would be a LENGTH check and would admit a whitespace-only name while the
|
|
12597
|
+
// message claims "non-blank"; the platform rejects on isBlank(). Note the value is
|
|
12598
|
+
// REJECTED, not trimmed — it reaches the wire exactly as authored, matching how the
|
|
12599
|
+
// platform stores it.
|
|
12600
|
+
safeDisplayString(
|
|
12601
|
+
z.string().max(MAX_CAPTURED_CLAIM_NAME_LENGTH, {
|
|
12602
|
+
message: `a capturedClaims entry must be at most ${MAX_CAPTURED_CLAIM_NAME_LENGTH} characters`
|
|
12603
|
+
})
|
|
12604
|
+
).refine((name) => name.trim().length > 0, {
|
|
12424
12605
|
message: "a capturedClaims entry must be a non-blank OIDC claim name"
|
|
12425
12606
|
})
|
|
12426
12607
|
).max(MAX_CAPTURED_CLAIMS, {
|
|
@@ -12591,14 +12772,24 @@ function lintNamespaces(value, ctx) {
|
|
|
12591
12772
|
});
|
|
12592
12773
|
}
|
|
12593
12774
|
var SeedCommonShape = {
|
|
12594
|
-
/**
|
|
12595
|
-
|
|
12775
|
+
/**
|
|
12776
|
+
* The schema typeName this seed instantiates (should match a declared schema).
|
|
12777
|
+
* `safeDisplayString`, matching `BlueprintSchemaSchema.typeName`: NOT currently
|
|
12778
|
+
* rendered into any plan-preview line (the seed step only counts records/documents),
|
|
12779
|
+
* but it is the same field concept and costs nothing to close now rather than leave
|
|
12780
|
+
* as a silent gap the next preview enhancement reopens.
|
|
12781
|
+
*/
|
|
12782
|
+
typeName: safeDisplayString(z.string().min(1)),
|
|
12596
12783
|
/**
|
|
12597
12784
|
* Stable, caller-supplied id — the loader's idempotency key AND the value other
|
|
12598
12785
|
* seeds resolve a `reference` against (across surfaces: a record seed may
|
|
12599
12786
|
* reference a document seed by its externalId, and vice versa).
|
|
12787
|
+
* `safeDisplayString`: not currently rendered in a plan-preview line (same as this schema's
|
|
12788
|
+
* `typeName` just above until this pass), but the same "costs nothing to close now" standard
|
|
12789
|
+
* applies — a short caller-supplied id, not free-form prose, so no YAML block-scalar
|
|
12790
|
+
* collision risk like `description`'s.
|
|
12600
12791
|
*/
|
|
12601
|
-
externalId: z.string().min(1),
|
|
12792
|
+
externalId: safeDisplayString(z.string().min(1)),
|
|
12602
12793
|
/**
|
|
12603
12794
|
* Optional scope ownership for the seeded item, as `namespace:value` entries
|
|
12604
12795
|
* (≤2, e.g. `['org:${{ identities.team }}']`; tokens substitute at apply
|
|
@@ -13257,15 +13448,31 @@ var BLUEPRINT_FIELD_PHASES = Object.freeze({
|
|
|
13257
13448
|
});
|
|
13258
13449
|
var BlueprintSchema = z.object({
|
|
13259
13450
|
/** Stable blueprint id (the `--blueprint <name>` selector + idempotency key). */
|
|
13260
|
-
name: z.string().min(1),
|
|
13261
|
-
version: z.string().min(1),
|
|
13451
|
+
name: safeDisplayString(z.string().min(1)),
|
|
13452
|
+
version: safeDisplayString(z.string().min(1)),
|
|
13453
|
+
// NOT safeDisplayString — deliberately, after that wrap broke a real, multi-line-authored
|
|
13454
|
+
// blueprint's `description` in review. `description` is never interpolated into any
|
|
13455
|
+
// rendered plan-preview line (`buildPlan`'s header prints
|
|
13456
|
+
// `name`/`version`/`contextId`, never `description` — confirmed by grep, not assumed), so
|
|
13457
|
+
// wrapping it protected a sink that does not exist. It also actively broke real content:
|
|
13458
|
+
// this is exactly the field authors reach for a YAML folded (`>`) or literal (`|`) block
|
|
13459
|
+
// scalar to write, and clip chomping (the YAML default) always leaves exactly one trailing
|
|
13460
|
+
// `\n` — which `safeDisplayString` rejects as a C0 control, indistinguishable from a
|
|
13461
|
+
// forgery attempt. See `safeDisplayString`'s own doc for why this is a property of the
|
|
13462
|
+
// wrapper, not a one-off carve-out for this field alone.
|
|
13262
13463
|
description: z.string().min(1),
|
|
13263
13464
|
/** The app-context the profile + scoped key bind to (e.g. "mcp"). */
|
|
13264
13465
|
contextId: z.string().regex(CONTEXT_ID_RE, {
|
|
13265
13466
|
message: "contextId must be 3-31 chars, start with a lowercase letter, then lowercase letters/digits/dashes (e.g. 'mcp')"
|
|
13266
13467
|
}),
|
|
13267
|
-
/**
|
|
13268
|
-
|
|
13468
|
+
/**
|
|
13469
|
+
* Human-readable app-context name; defaults to `MCP — <name>` (see {@link contextNameOf})
|
|
13470
|
+
* when absent. `safeDisplayString`: `contextNameOf` renders this raw into the FIRST line of
|
|
13471
|
+
* every `buildPlan` summary (the `context '...' (...)` step) — a gap found after the rest
|
|
13472
|
+
* of this file's forgery-character pass had already shipped; same shape as
|
|
13473
|
+
* `name`/`version`/`description` right above it.
|
|
13474
|
+
*/
|
|
13475
|
+
contextName: safeDisplayString(z.string().min(1)).optional(),
|
|
13269
13476
|
/**
|
|
13270
13477
|
* The deploying organization's own display name — distinct from {@link contextName}.
|
|
13271
13478
|
* `contextName` is the blueprint author's fixed identity for the app (the same for every
|
|
@@ -13273,8 +13480,13 @@ var BlueprintSchema = z.object({
|
|
|
13273
13480
|
* deployer-supplied `${{ inputs.x }}` value rather than hardcoded. Used for branding on
|
|
13274
13481
|
* platform-sent correspondence (e.g. invite emails) alongside `contextName` — not a
|
|
13275
13482
|
* replacement for it. See {@link companyNameOf}.
|
|
13483
|
+
*
|
|
13484
|
+
* `safeDisplayString`: not interpolated into any CLI plan-preview line today (confirmed —
|
|
13485
|
+
* `loader.ts` never reads `companyName`), but it IS forwarded to the platform for the
|
|
13486
|
+
* invite-email correspondence described above, a second class of human-read surface this
|
|
13487
|
+
* value reaches. Wrapped for the same reason as every sibling scalar identity field here.
|
|
13276
13488
|
*/
|
|
13277
|
-
companyName: z.string().min(1).optional(),
|
|
13489
|
+
companyName: safeDisplayString(z.string().min(1)).optional(),
|
|
13278
13490
|
schemas: z.array(BlueprintSchemaSchema).default([]),
|
|
13279
13491
|
accessProfile: BlueprintAccessProfileSchema,
|
|
13280
13492
|
servicePrincipal: BlueprintServicePrincipalSchema,
|
|
@@ -13351,9 +13563,16 @@ var BlueprintSchema = z.object({
|
|
|
13351
13563
|
* context's name and needs a broader scope — see `@vectros-ai/cli`'s own notes.)
|
|
13352
13564
|
*/
|
|
13353
13565
|
identityProjectionClaims: z.array(
|
|
13354
|
-
|
|
13355
|
-
|
|
13356
|
-
|
|
13566
|
+
// safeDisplayString: rendered raw into buildPlan's context step (`identity projection:
|
|
13567
|
+
// [...]`) — same rendered-plan-preview shape as `capturedClaims` (its structural pair,
|
|
13568
|
+
// and fixed alongside it originally), missed here in the first pass; same ordering rule
|
|
13569
|
+
// applies (safeDisplayString's own `.refine` before the blank-name `.refine` below,
|
|
13570
|
+
// both after `.max` since `.refine` returns a ZodEffects with no `.max`).
|
|
13571
|
+
safeDisplayString(
|
|
13572
|
+
z.string().max(MAX_CAPTURED_CLAIM_NAME_LENGTH, {
|
|
13573
|
+
message: `an identityProjectionClaims entry must be at most ${MAX_CAPTURED_CLAIM_NAME_LENGTH} characters`
|
|
13574
|
+
})
|
|
13575
|
+
).refine((name) => name.trim().length > 0, {
|
|
13357
13576
|
message: "an identityProjectionClaims entry must be a non-blank claim name"
|
|
13358
13577
|
})
|
|
13359
13578
|
).max(MAX_CAPTURED_CLAIMS, {
|
|
@@ -15505,6 +15724,357 @@ function renderActionableError(info) {
|
|
|
15505
15724
|
return null;
|
|
15506
15725
|
}
|
|
15507
15726
|
|
|
15727
|
+
// src/redact.ts
|
|
15728
|
+
var JOSE_SHAPED = /eyJ[A-Za-z0-9_-]{6,}(?:\.[A-Za-z0-9_-]*){0,4}/g;
|
|
15729
|
+
var LONG_SEGMENTS = /[A-Za-z0-9_-]{20,}(?:\.[A-Za-z0-9_-]{16,}){1,4}/g;
|
|
15730
|
+
var BEARER = /\bBearer\s+[A-Za-z0-9._~+/=-]{8,}/gi;
|
|
15731
|
+
var VECTROS_SECRET_ENV = /(?:ssk|sk|st)_(?:live|test)_[A-Za-z0-9._-]+/gi;
|
|
15732
|
+
var VECTROS_SECRET_BARE = /(?<![A-Za-z0-9])(?:ssk|sk|st)_[A-Za-z0-9._-]{8,}/gi;
|
|
15733
|
+
function redactCredentials(text) {
|
|
15734
|
+
return text.replace(BEARER, "Bearer [redacted]").replace(JOSE_SHAPED, "[redacted]").replace(LONG_SEGMENTS, "[redacted]").replace(VECTROS_SECRET_ENV, "[redacted]").replace(VECTROS_SECRET_BARE, "[redacted]");
|
|
15735
|
+
}
|
|
15736
|
+
function looksLikeCredential(value) {
|
|
15737
|
+
const trimmed = value.trim();
|
|
15738
|
+
if (trimmed.length > 200) return true;
|
|
15739
|
+
if (/eyJ[A-Za-z0-9_-]{8,}/.test(trimmed)) return true;
|
|
15740
|
+
const bare = trimmed.replace(/^["'`]+|["'`,;]+$/g, "").replace(/^bearer\s+/i, "");
|
|
15741
|
+
const segments = bare.split(".");
|
|
15742
|
+
return segments.length >= 3 && segments.filter((s) => s.length >= 16).length >= 2;
|
|
15743
|
+
}
|
|
15744
|
+
function safeBaseName(path4) {
|
|
15745
|
+
const base = path4.split(/[\\/]/).filter(Boolean).pop();
|
|
15746
|
+
return base !== void 0 && base.length <= 40 && /^[A-Za-z0-9._ ()-]+$/.test(base) ? base : void 0;
|
|
15747
|
+
}
|
|
15748
|
+
|
|
15749
|
+
// src/provision-options.ts
|
|
15750
|
+
var BLUEPRINT_NAMES_TOKEN = "{{BLUEPRINT_NAMES}}";
|
|
15751
|
+
var PROVISION_OPTIONS = [
|
|
15752
|
+
{
|
|
15753
|
+
flag: "--blueprint",
|
|
15754
|
+
value: "<name|file>",
|
|
15755
|
+
modes: ["bootstrap"],
|
|
15756
|
+
help: [
|
|
15757
|
+
"A bundled blueprint ({{BLUEPRINT_NAMES}}),",
|
|
15758
|
+
'OR a path to your own blueprint YAML/JSON (./my.yaml) \u2014 the "copy &',
|
|
15759
|
+
'own" path. File blueprints get the identical scope gate (no override).'
|
|
15760
|
+
]
|
|
15761
|
+
},
|
|
15762
|
+
{
|
|
15763
|
+
flag: "--set",
|
|
15764
|
+
value: "name=value",
|
|
15765
|
+
modes: ["bootstrap", "apply"],
|
|
15766
|
+
help: [
|
|
15767
|
+
"Set an install-time input value (repeatable) for a file blueprint's",
|
|
15768
|
+
"'inputs:' block. Beats --values + the declared default. (Same flag as",
|
|
15769
|
+
"'vectros blueprint validate/plan'.)"
|
|
15770
|
+
]
|
|
15771
|
+
},
|
|
15772
|
+
{
|
|
15773
|
+
flag: "--values",
|
|
15774
|
+
value: "<file>",
|
|
15775
|
+
modes: ["bootstrap", "apply"],
|
|
15776
|
+
help: [
|
|
15777
|
+
"A YAML/JSON map of input values. (File blueprints only.)"
|
|
15778
|
+
]
|
|
15779
|
+
},
|
|
15780
|
+
{
|
|
15781
|
+
flag: "--blank",
|
|
15782
|
+
modes: ["bootstrap"],
|
|
15783
|
+
help: [
|
|
15784
|
+
"Credential only (read-only data-plane profile, no schemas)."
|
|
15785
|
+
]
|
|
15786
|
+
},
|
|
15787
|
+
{
|
|
15788
|
+
flag: "--no-seed",
|
|
15789
|
+
modes: ["bootstrap"],
|
|
15790
|
+
help: [
|
|
15791
|
+
"Provision the blueprint's schemas, profile and key but SKIP its",
|
|
15792
|
+
"sample seed records \u2014 the right choice for a production context",
|
|
15793
|
+
"(a blueprint's seeds are illustrative, not real data). Unlike",
|
|
15794
|
+
"--blank, the schemas are still created."
|
|
15795
|
+
]
|
|
15796
|
+
},
|
|
15797
|
+
{
|
|
15798
|
+
// The opposite default to bootstrap's: a re-apply from CI must not write sample data into a tenant by omission.
|
|
15799
|
+
flag: "--seed",
|
|
15800
|
+
modes: ["apply"],
|
|
15801
|
+
help: [
|
|
15802
|
+
"Also write the blueprint's sample seed records. Off by default, in BOTH tenants: apply",
|
|
15803
|
+
"never writes sample data unless you ask (a blueprint's seeds are illustrative, not real",
|
|
15804
|
+
"data). Seeds are written by external id, so a re-run with --seed is idempotent.",
|
|
15805
|
+
"(bootstrap is the other way round: it seeds unless --no-seed. apply refuses --no-seed.)"
|
|
15806
|
+
]
|
|
15807
|
+
},
|
|
15808
|
+
{
|
|
15809
|
+
flag: "--allow-tenant-wide-namespaces",
|
|
15810
|
+
modes: ["bootstrap", "apply"],
|
|
15811
|
+
help: {
|
|
15812
|
+
bootstrap: [
|
|
15813
|
+
"Required to apply a blueprint that declares a namespace with",
|
|
15814
|
+
"tenantWide: true \u2014 visible to every context in the account, not",
|
|
15815
|
+
"just this one. Without it, bootstrap refuses before provisioning",
|
|
15816
|
+
"anything, naming the namespace(s). Only an account OWNER's",
|
|
15817
|
+
"credential can actually complete the registration either way \u2014",
|
|
15818
|
+
"this flag doesn't change who's allowed, only whether this run",
|
|
15819
|
+
"tries. Never needed for a blueprint with no tenantWide namespace."
|
|
15820
|
+
],
|
|
15821
|
+
apply: [
|
|
15822
|
+
"Required to apply a blueprint that declares a namespace with",
|
|
15823
|
+
"tenantWide: true \u2014 visible to every context in the account, not",
|
|
15824
|
+
"just this one. Without it, apply refuses before provisioning",
|
|
15825
|
+
"anything, naming the namespace(s). Only an account OWNER's",
|
|
15826
|
+
"credential can actually complete the registration either way \u2014",
|
|
15827
|
+
"this flag doesn't change who's allowed, only whether this run",
|
|
15828
|
+
"tries. Never needed for a blueprint with no tenantWide namespace."
|
|
15829
|
+
]
|
|
15830
|
+
}
|
|
15831
|
+
},
|
|
15832
|
+
{
|
|
15833
|
+
flag: "--allow-script-adoption",
|
|
15834
|
+
stacked: true,
|
|
15835
|
+
modes: ["bootstrap", "apply"],
|
|
15836
|
+
help: {
|
|
15837
|
+
bootstrap: [
|
|
15838
|
+
"Required to push a script version onto a name that already exists in",
|
|
15839
|
+
"this context but carries no blueprint provenance marker (hand-authored,",
|
|
15840
|
+
"or pushed before this marker existed) \u2014 without it, bootstrap refuses",
|
|
15841
|
+
"before minting anything for that name, naming it. Never needed for a",
|
|
15842
|
+
"brand-new script name, or for re-applying a blueprint that already owns",
|
|
15843
|
+
"the name it's pushing to. Does NOT help if the name is already owned by",
|
|
15844
|
+
"a DIFFERENT blueprint \u2014 that is refused unconditionally, no flag past it."
|
|
15845
|
+
],
|
|
15846
|
+
apply: [
|
|
15847
|
+
"Required to push a script version onto a name that already exists in",
|
|
15848
|
+
"this context but carries no blueprint provenance marker (hand-authored,",
|
|
15849
|
+
"or pushed before this marker existed) \u2014 without it, apply refuses",
|
|
15850
|
+
"before changing anything for that name, naming it. Never needed for a",
|
|
15851
|
+
"brand-new script name, or for re-applying a blueprint that already owns",
|
|
15852
|
+
"the name it's pushing to. Does NOT help if the name is already owned by",
|
|
15853
|
+
"a DIFFERENT blueprint \u2014 that is refused unconditionally, no flag past it."
|
|
15854
|
+
]
|
|
15855
|
+
}
|
|
15856
|
+
},
|
|
15857
|
+
{
|
|
15858
|
+
flag: "--tenant",
|
|
15859
|
+
value: "test|live",
|
|
15860
|
+
modes: ["bootstrap", "apply"],
|
|
15861
|
+
help: {
|
|
15862
|
+
bootstrap: [
|
|
15863
|
+
"Which tenant the minted key scopes to (default: live).",
|
|
15864
|
+
"'test' provisions into your isolated test tenant \u2014 a",
|
|
15865
|
+
"dry-run before pointing real data at the same blueprint.",
|
|
15866
|
+
"The tenant rides the key (ssk_test_* vs ssk_live_*), so",
|
|
15867
|
+
"the MCP/client config is otherwise identical."
|
|
15868
|
+
],
|
|
15869
|
+
apply: [
|
|
15870
|
+
"Which tenant to apply into. REQUIRED: there is no default, so a run",
|
|
15871
|
+
"never lands on 'live' by omission. 'test' is your isolated test tenant",
|
|
15872
|
+
"\u2014 apply there first."
|
|
15873
|
+
]
|
|
15874
|
+
}
|
|
15875
|
+
},
|
|
15876
|
+
{
|
|
15877
|
+
flag: "--test",
|
|
15878
|
+
modes: ["bootstrap"],
|
|
15879
|
+
help: [
|
|
15880
|
+
"Shorthand for --tenant test."
|
|
15881
|
+
]
|
|
15882
|
+
},
|
|
15883
|
+
{
|
|
15884
|
+
flag: "--activate",
|
|
15885
|
+
modes: ["bootstrap"],
|
|
15886
|
+
help: [
|
|
15887
|
+
"Force the freshly-stored key to become your active identity, even",
|
|
15888
|
+
"if that would switch away from a currently-active LIVE credential",
|
|
15889
|
+
"(the one case --tenant test does NOT do this automatically)."
|
|
15890
|
+
]
|
|
15891
|
+
},
|
|
15892
|
+
{
|
|
15893
|
+
flag: "--no-activate",
|
|
15894
|
+
modes: ["bootstrap"],
|
|
15895
|
+
help: [
|
|
15896
|
+
"Store the key without activating it, regardless of tenant."
|
|
15897
|
+
]
|
|
15898
|
+
},
|
|
15899
|
+
{
|
|
15900
|
+
flag: "--base-url",
|
|
15901
|
+
value: "<url>",
|
|
15902
|
+
modes: ["bootstrap", "apply"],
|
|
15903
|
+
help: [
|
|
15904
|
+
"Override the API base URL (beats VECTROS_API_BASE_URL)."
|
|
15905
|
+
]
|
|
15906
|
+
},
|
|
15907
|
+
{
|
|
15908
|
+
flag: "--client",
|
|
15909
|
+
value: "<desktop|code>",
|
|
15910
|
+
modes: ["bootstrap"],
|
|
15911
|
+
help: [
|
|
15912
|
+
"MCP host to configure (default: desktop). 'desktop' merges",
|
|
15913
|
+
"claude_desktop_config.json (also works for Cursor/Cline);",
|
|
15914
|
+
"'code' targets Claude Code's project .mcp.json and prints",
|
|
15915
|
+
"the equivalent 'claude mcp add' command."
|
|
15916
|
+
]
|
|
15917
|
+
},
|
|
15918
|
+
{
|
|
15919
|
+
flag: "--token",
|
|
15920
|
+
value: "<t>",
|
|
15921
|
+
modes: ["bootstrap", "apply"],
|
|
15922
|
+
help: {
|
|
15923
|
+
bootstrap: [
|
|
15924
|
+
"Cognito bridge token; or set VECTROS_BOOTSTRAP_TOKEN.",
|
|
15925
|
+
"Falls back to your stored `vectros login` session",
|
|
15926
|
+
"(same env) when neither is set \u2014 login once, then",
|
|
15927
|
+
"bootstrap unattended."
|
|
15928
|
+
],
|
|
15929
|
+
apply: [
|
|
15930
|
+
"Cognito bridge token; or set VECTROS_BOOTSTRAP_TOKEN.",
|
|
15931
|
+
"Falls back to your stored `vectros login` session",
|
|
15932
|
+
"(same env) when neither is set \u2014 login once, then",
|
|
15933
|
+
"apply unattended."
|
|
15934
|
+
]
|
|
15935
|
+
}
|
|
15936
|
+
},
|
|
15937
|
+
{
|
|
15938
|
+
flag: "--yes",
|
|
15939
|
+
aliases: ["-y"],
|
|
15940
|
+
modes: ["bootstrap", "apply"],
|
|
15941
|
+
help: {
|
|
15942
|
+
bootstrap: [
|
|
15943
|
+
"Skip confirmations (agent / CI path)."
|
|
15944
|
+
],
|
|
15945
|
+
apply: [
|
|
15946
|
+
"Skip the 'Apply this plan?' confirmation. REQUIRED when there is no terminal",
|
|
15947
|
+
"(CI, an agent): apply changes a tenant, so it never runs unattended without this."
|
|
15948
|
+
]
|
|
15949
|
+
}
|
|
15950
|
+
},
|
|
15951
|
+
{
|
|
15952
|
+
flag: "--no-merge",
|
|
15953
|
+
modes: ["bootstrap"],
|
|
15954
|
+
help: [
|
|
15955
|
+
"Still mints/reconciles \u2014 this only changes where the result goes:",
|
|
15956
|
+
"print the config snippet instead of merging it into your client's",
|
|
15957
|
+
"config file. Not a dry run; see 'blueprint plan' for that. Does NOT",
|
|
15958
|
+
"skip the 'Apply this plan?' confirmation \u2014 pass --yes for that."
|
|
15959
|
+
]
|
|
15960
|
+
},
|
|
15961
|
+
{
|
|
15962
|
+
flag: "--rotate",
|
|
15963
|
+
modes: ["bootstrap"],
|
|
15964
|
+
help: [
|
|
15965
|
+
"Revoke + re-mint THIS machine's key."
|
|
15966
|
+
]
|
|
15967
|
+
},
|
|
15968
|
+
{
|
|
15969
|
+
flag: "--require-verified-issuers",
|
|
15970
|
+
modes: ["bootstrap", "apply"],
|
|
15971
|
+
help: {
|
|
15972
|
+
bootstrap: [
|
|
15973
|
+
"Exit 4 (the apply itself still completes) when an issuer this blueprint registers",
|
|
15974
|
+
"is still pending verification. Without it a finished apply exits 0 and prints the",
|
|
15975
|
+
"challenge and the command that finishes verification: an issuer registered without",
|
|
15976
|
+
"restrictedToDomain accepts no token until you prove control of the identity",
|
|
15977
|
+
"provider, which needs a real login that no bootstrap can perform."
|
|
15978
|
+
],
|
|
15979
|
+
apply: [
|
|
15980
|
+
"Exit 4 (the apply itself still completes) when an issuer this blueprint registers",
|
|
15981
|
+
"is still pending verification. Without it a finished apply exits 0 and prints the",
|
|
15982
|
+
"challenge and the command that finishes verification: an issuer registered without",
|
|
15983
|
+
"restrictedToDomain accepts no token until you prove control of the identity",
|
|
15984
|
+
"provider, which needs a real login that no apply can perform."
|
|
15985
|
+
]
|
|
15986
|
+
}
|
|
15987
|
+
},
|
|
15988
|
+
{
|
|
15989
|
+
flag: "--confirm-existing-principal",
|
|
15990
|
+
modes: ["bootstrap", "apply"],
|
|
15991
|
+
help: {
|
|
15992
|
+
bootstrap: [
|
|
15993
|
+
"Required (with --yes, or with no TTY) to mint a NEW key bound to a",
|
|
15994
|
+
"service principal that already existed in your tenant before this run",
|
|
15995
|
+
"\u2014 e.g. a deliberate second machine for an integration you already own.",
|
|
15996
|
+
"Refused by default so an unexpected collision halts a CI run loudly",
|
|
15997
|
+
"instead of silently minting a credential bound to the wrong identity.",
|
|
15998
|
+
"Never needed for --rotate or an ordinary same-machine re-apply."
|
|
15999
|
+
],
|
|
16000
|
+
apply: [
|
|
16001
|
+
"Required (with --yes, or with no TTY) when the blueprint's service principal",
|
|
16002
|
+
"already exists in your tenant. Apply never reads a local key record, so that is",
|
|
16003
|
+
"every apply once the principal exists, on any machine, including one that ran",
|
|
16004
|
+
"bootstrap. It lets apply rewrite that principal's access profile (the",
|
|
16005
|
+
"blueprint's own service principal only). In a pipeline this flag is STANDING",
|
|
16006
|
+
"consent: it also waves through a blueprint whose servicePrincipal.externalId",
|
|
16007
|
+
"collides with an unrelated identity, so keep the blueprint's source reviewed.",
|
|
16008
|
+
"Every run that proceeds prints which principal it reused and why."
|
|
16009
|
+
]
|
|
16010
|
+
}
|
|
16011
|
+
},
|
|
16012
|
+
{
|
|
16013
|
+
flag: "--prune",
|
|
16014
|
+
modes: ["apply"],
|
|
16015
|
+
help: [
|
|
16016
|
+
"Also remove the triggers this blueprint provisioned earlier and its file no longer",
|
|
16017
|
+
"declares. OFF by default: without it apply never deletes anything. A trigger is",
|
|
16018
|
+
"matched by a STORED MARKER \u2014 its provisionedBy equals the blueprint's name, and its",
|
|
16019
|
+
"principal's external id is the one this blueprint would derive \u2014 not a verified",
|
|
16020
|
+
"identity. The name comes from the blueprint FILE, after any input substitution (so",
|
|
16021
|
+
"--set and --values can change it), and the scope gate bounds what a blueprint may",
|
|
16022
|
+
"grant, not what it may delete. A matched row that cannot be removed (no id or",
|
|
16023
|
+
"principal, a failed read or delete) makes apply exit 1. Rows with no marker are left",
|
|
16024
|
+
"alone and reported; another blueprint's rows are left alone. Needs --yes when there",
|
|
16025
|
+
"is no terminal."
|
|
16026
|
+
]
|
|
16027
|
+
},
|
|
16028
|
+
{
|
|
16029
|
+
flag: "--use-scoped-token",
|
|
16030
|
+
modes: ["bootstrap"],
|
|
16031
|
+
help: [
|
|
16032
|
+
"Legacy fallback.",
|
|
16033
|
+
"NOTE: it mints a token WITHOUT the provisioning",
|
|
16034
|
+
"capability, so it cannot create a blueprint's",
|
|
16035
|
+
"app-context \u2014 use only with the 'default' context",
|
|
16036
|
+
"(the dedicated endpoint is required otherwise)."
|
|
16037
|
+
]
|
|
16038
|
+
},
|
|
16039
|
+
{
|
|
16040
|
+
flag: "--help",
|
|
16041
|
+
aliases: ["-h"],
|
|
16042
|
+
modes: ["bootstrap", "apply"],
|
|
16043
|
+
help: [
|
|
16044
|
+
"Show this help."
|
|
16045
|
+
]
|
|
16046
|
+
},
|
|
16047
|
+
{ flag: "--env", value: "<staging|production>", modes: ["bootstrap", "apply"] },
|
|
16048
|
+
{ flag: "--blank-slate", modes: ["bootstrap"] }
|
|
16049
|
+
];
|
|
16050
|
+
function optionsFor(mode) {
|
|
16051
|
+
return PROVISION_OPTIONS.filter((o) => o.modes.includes(mode));
|
|
16052
|
+
}
|
|
16053
|
+
function spellingsFor(mode) {
|
|
16054
|
+
return optionsFor(mode).flatMap((o) => [o.flag, ...o.aliases ?? []]);
|
|
16055
|
+
}
|
|
16056
|
+
function helpLines(option, mode) {
|
|
16057
|
+
if (!option.modes.includes(mode) || option.help === void 0) return void 0;
|
|
16058
|
+
return Array.isArray(option.help) ? option.help : option.help[mode];
|
|
16059
|
+
}
|
|
16060
|
+
var LABEL_COLUMN = 25;
|
|
16061
|
+
var TEXT_INDENT = " ".repeat(LABEL_COLUMN + 2);
|
|
16062
|
+
function renderOptionHelp(mode, blueprintNames) {
|
|
16063
|
+
const blocks = [];
|
|
16064
|
+
for (const option of optionsFor(mode)) {
|
|
16065
|
+
const lines = helpLines(option, mode);
|
|
16066
|
+
if (!lines) continue;
|
|
16067
|
+
const label = [option.flag + (option.value ? ` ${option.value}` : ""), ...option.aliases ?? []].join(", ");
|
|
16068
|
+
const text = lines.map((l) => l.split(BLUEPRINT_NAMES_TOKEN).join(blueprintNames.join(", ")));
|
|
16069
|
+
if (label.length <= LABEL_COLUMN - 2 && !option.stacked) {
|
|
16070
|
+
blocks.push([` ${label.padEnd(LABEL_COLUMN)}${text[0]}`, ...text.slice(1).map((l) => TEXT_INDENT + l)].join("\n"));
|
|
16071
|
+
} else {
|
|
16072
|
+
blocks.push([` ${label}`, ...text.map((l) => TEXT_INDENT + l)].join("\n"));
|
|
16073
|
+
}
|
|
16074
|
+
}
|
|
16075
|
+
return blocks.join("\n");
|
|
16076
|
+
}
|
|
16077
|
+
|
|
15508
16078
|
// src/base-url.ts
|
|
15509
16079
|
var ALLOWED_HOST_APEX = "vectros.ai";
|
|
15510
16080
|
var ALLOWED_HOST_SUFFIX = ".vectros.ai";
|
|
@@ -15568,6 +16138,9 @@ function validateBaseUrl(rawUrl, opts = {}) {
|
|
|
15568
16138
|
if (host === ALLOWED_HOST_APEX || host.endsWith(ALLOWED_HOST_SUFFIX)) {
|
|
15569
16139
|
return rawUrl;
|
|
15570
16140
|
}
|
|
16141
|
+
if ((opts.additionalAllowedHosts ?? []).some((h) => host === h.toLowerCase())) {
|
|
16142
|
+
return rawUrl;
|
|
16143
|
+
}
|
|
15571
16144
|
throw new InvalidBaseUrlError(
|
|
15572
16145
|
`Refusing base URL host "${url.host}" \u2014 not an official Vectros host (expected vectros.ai or a *.vectros.ai subdomain). Set ${INSECURE_BASE_URL_ENV}=1 to override (e.g. for a trusted local proxy); never point the CLI at an untrusted host while authenticated.`
|
|
15573
16146
|
);
|
|
@@ -15747,14 +16320,24 @@ import { mkdir, readFile, writeFile, rm } from "fs/promises";
|
|
|
15747
16320
|
import { homedir } from "os";
|
|
15748
16321
|
import { join as join2 } from "path";
|
|
15749
16322
|
|
|
16323
|
+
// src/terminal-text.ts
|
|
16324
|
+
var STRIPPED = /[\p{Cc}\p{Cf}\p{Zl}\p{Zp}]/gu;
|
|
16325
|
+
function stripControlCharacters(value, maxLength = 200) {
|
|
16326
|
+
const characters = Array.from(value.replace(STRIPPED, ""));
|
|
16327
|
+
return characters.length > maxLength ? `${characters.slice(0, maxLength).join("")}...` : characters.join("");
|
|
16328
|
+
}
|
|
16329
|
+
|
|
15750
16330
|
// src/pkce.ts
|
|
15751
|
-
import { createHash, randomBytes } from "crypto";
|
|
16331
|
+
import { createHash, randomBytes, timingSafeEqual } from "crypto";
|
|
15752
16332
|
|
|
15753
16333
|
// src/cognito-config.ts
|
|
15754
16334
|
var PER_ENV = {
|
|
15755
16335
|
staging: { domainPrefix: "vectros-developer-staging", region: "us-east-1" },
|
|
15756
16336
|
production: { domainPrefix: "vectros-developer", region: "us-east-1" }
|
|
15757
16337
|
};
|
|
16338
|
+
var COGNITO_ALLOWED_HOSTS = Object.values(PER_ENV).map(
|
|
16339
|
+
(p) => `${p.domainPrefix}.auth.${p.region}.amazoncognito.com`
|
|
16340
|
+
);
|
|
15758
16341
|
var BAKED_CLIENT_IDS = {
|
|
15759
16342
|
staging: "61hjvm25kkjb1eei8t2d3jkcl8",
|
|
15760
16343
|
// validated against deployed staging hosted UI (2026-06-05)
|
|
@@ -15769,13 +16352,30 @@ var MissingCognitoClientIdError = class extends Error {
|
|
|
15769
16352
|
this.name = "MissingCognitoClientIdError";
|
|
15770
16353
|
}
|
|
15771
16354
|
};
|
|
16355
|
+
var REGION_SHAPE = /^[a-z]{2}(?:-[a-z]+)+-\d+$/;
|
|
16356
|
+
var InvalidCognitoRegionError = class extends Error {
|
|
16357
|
+
constructor(region) {
|
|
16358
|
+
super(
|
|
16359
|
+
`Invalid VECTROS_COGNITO_REGION ${JSON.stringify(region)} \u2014 expected an AWS region shape (e.g. "us-east-1").`
|
|
16360
|
+
);
|
|
16361
|
+
this.name = "InvalidCognitoRegionError";
|
|
16362
|
+
}
|
|
16363
|
+
};
|
|
15772
16364
|
function resolveCognitoConfig(env) {
|
|
15773
16365
|
const per = PER_ENV[env];
|
|
15774
|
-
const
|
|
15775
|
-
const
|
|
16366
|
+
const domainOverride = process.env.VECTROS_COGNITO_DOMAIN;
|
|
16367
|
+
const regionOverride = process.env.VECTROS_COGNITO_REGION;
|
|
16368
|
+
if (domainOverride === void 0 && regionOverride !== void 0 && !REGION_SHAPE.test(regionOverride)) {
|
|
16369
|
+
throw new InvalidCognitoRegionError(regionOverride);
|
|
16370
|
+
}
|
|
16371
|
+
const region = regionOverride ?? per.region;
|
|
16372
|
+
const rawDomain = domainOverride ?? `https://${per.domainPrefix}.auth.${region}.amazoncognito.com`;
|
|
16373
|
+
const domain = validateBaseUrl(rawDomain, {
|
|
16374
|
+
additionalAllowedHosts: COGNITO_ALLOWED_HOSTS
|
|
16375
|
+
}).replace(/\/$/, "");
|
|
15776
16376
|
const clientId = process.env.VECTROS_COGNITO_CLIENT_ID ?? BAKED_CLIENT_IDS[env];
|
|
15777
16377
|
if (!clientId) throw new MissingCognitoClientIdError(env);
|
|
15778
|
-
return { domain
|
|
16378
|
+
return { domain, clientId, region };
|
|
15779
16379
|
}
|
|
15780
16380
|
var LOOPBACK_PORTS = [8976, 8977, 8978];
|
|
15781
16381
|
var OAUTH_SCOPES = ["openid", "email", "profile"];
|
|
@@ -15835,6 +16435,24 @@ function parseCallback(requestUrl) {
|
|
|
15835
16435
|
errorDescription: url.searchParams.get("error_description") ?? void 0
|
|
15836
16436
|
};
|
|
15837
16437
|
}
|
|
16438
|
+
var CALLBACK_PATH = "/callback";
|
|
16439
|
+
function stateMatches(received, expected) {
|
|
16440
|
+
if (received === void 0) return false;
|
|
16441
|
+
const a = Buffer.from(received);
|
|
16442
|
+
const b = Buffer.from(expected);
|
|
16443
|
+
return a.length === b.length && timingSafeEqual(a, b);
|
|
16444
|
+
}
|
|
16445
|
+
function classifyCallback(method, requestUrl, expectedState) {
|
|
16446
|
+
if (method !== "GET") return { kind: "not-a-callback" };
|
|
16447
|
+
const target = requestUrl ?? "/";
|
|
16448
|
+
const queryStart = target.indexOf("?");
|
|
16449
|
+
const path4 = queryStart === -1 ? target : target.slice(0, queryStart);
|
|
16450
|
+
if (path4 !== CALLBACK_PATH) return { kind: "not-a-callback" };
|
|
16451
|
+
const params = parseCallback(target);
|
|
16452
|
+
if (!params.code && !params.error) return { kind: "not-a-callback" };
|
|
16453
|
+
if (!stateMatches(params.state, expectedState)) return { kind: "wrong-state" };
|
|
16454
|
+
return { kind: "accepted", params };
|
|
16455
|
+
}
|
|
15838
16456
|
|
|
15839
16457
|
// src/auth-store.ts
|
|
15840
16458
|
var DIR_NAME = ".vectros";
|
|
@@ -15880,7 +16498,7 @@ async function postToken(config, body, fetchImpl) {
|
|
|
15880
16498
|
});
|
|
15881
16499
|
const text = await res.text().catch(() => "");
|
|
15882
16500
|
if (!res.ok) {
|
|
15883
|
-
throw new Error(`token endpoint HTTP ${res.status}${text ? ` \u2014 ${text}` : ""}`);
|
|
16501
|
+
throw new Error(`token endpoint HTTP ${res.status}${text ? ` \u2014 ${stripControlCharacters(text, 500)}` : ""}`);
|
|
15884
16502
|
}
|
|
15885
16503
|
try {
|
|
15886
16504
|
return JSON.parse(text);
|
|
@@ -16573,7 +17191,7 @@ async function withAuth(opts, deps, body) {
|
|
|
16573
17191
|
log
|
|
16574
17192
|
});
|
|
16575
17193
|
} catch (err) {
|
|
16576
|
-
if (err instanceof MissingCredentialError) {
|
|
17194
|
+
if (err instanceof MissingCredentialError || err instanceof InvalidBaseUrlError || err instanceof InvalidCognitoRegionError) {
|
|
16577
17195
|
process.stderr.write(` \u2716 ${err.message}
|
|
16578
17196
|
`);
|
|
16579
17197
|
return 2;
|
|
@@ -16649,6 +17267,12 @@ async function drainPages(fetch2) {
|
|
|
16649
17267
|
return out;
|
|
16650
17268
|
}
|
|
16651
17269
|
|
|
17270
|
+
// src/shell-quote.ts
|
|
17271
|
+
function shellQuote(value) {
|
|
17272
|
+
if (/^[A-Za-z0-9_@%+=:,./-]+$/.test(value)) return value;
|
|
17273
|
+
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
17274
|
+
}
|
|
17275
|
+
|
|
16652
17276
|
// src/loader.ts
|
|
16653
17277
|
function actionOf(created) {
|
|
16654
17278
|
return created ? "created" : "reconciled";
|
|
@@ -16660,15 +17284,42 @@ var SCRIPT_CONFLICT_RETRY_DELAY_MS = 750;
|
|
|
16660
17284
|
function isVersionConflict(err) {
|
|
16661
17285
|
return typeof err === "object" && err !== null && err.statusCode === 409;
|
|
16662
17286
|
}
|
|
16663
|
-
|
|
16664
|
-
|
|
16665
|
-
|
|
17287
|
+
function isForbidden(err) {
|
|
17288
|
+
return typeof err === "object" && err !== null && err.statusCode === 403;
|
|
17289
|
+
}
|
|
17290
|
+
function withAdoptionContext(err, name) {
|
|
17291
|
+
if (!(err instanceof Error) || !isForbidden(err)) return err;
|
|
17292
|
+
return new Error(
|
|
17293
|
+
`${err.message}
|
|
17294
|
+
|
|
17295
|
+
(This push was adopting '${name}' into this blueprint's management, via --allow-script-adoption \u2014 this CLI's own ownership check cleared it, but the platform refused the push itself. The platform requires EITHER the credential pushing this scoped-token bootstrap to hold 'scripts:c', OR \u2014 if '${name}' already has an unmarked version \u2014 that this same apply is running as the SAME caller identity that originally pushed it. An account OWNER always resolves to the same identity and can adopt any unmarked name; a Dev Portal SUB-USER can only adopt a name they themselves hand-pushed. If neither applies here, ask the original pusher to run this apply, push the account owner's own login first, or push it directly via 'vectros scripts push'.)`
|
|
16666
17296
|
);
|
|
16667
|
-
|
|
16668
|
-
|
|
16669
|
-
|
|
16670
|
-
|
|
16671
|
-
|
|
17297
|
+
}
|
|
17298
|
+
function isScriptNameNotFound(err) {
|
|
17299
|
+
return typeof err === "object" && err !== null && err.statusCode === 404;
|
|
17300
|
+
}
|
|
17301
|
+
function isScriptPageEnvelope(result) {
|
|
17302
|
+
return "data" in result;
|
|
17303
|
+
}
|
|
17304
|
+
async function readLatestScriptVersion(ctxClient, name) {
|
|
17305
|
+
let result;
|
|
17306
|
+
try {
|
|
17307
|
+
result = await ctxClient.scripts.listScripts({ name, latest: true, includeSource: true });
|
|
17308
|
+
} catch (err) {
|
|
17309
|
+
if (isScriptNameNotFound(err)) return void 0;
|
|
17310
|
+
throw err;
|
|
17311
|
+
}
|
|
17312
|
+
if (result == null) {
|
|
17313
|
+
throw new Error(
|
|
17314
|
+
`Script '${name}': a bounded '?latest=true' read returned an empty response body instead of a version object. This is a platform/SDK contract change this code does not understand \u2014 refusing to guess whether the name is unclaimed or a version was silently omitted.`
|
|
17315
|
+
);
|
|
17316
|
+
}
|
|
17317
|
+
if (isScriptPageEnvelope(result)) {
|
|
17318
|
+
throw new Error(
|
|
17319
|
+
`Script '${name}': a bounded '?latest=true' read unexpectedly returned a page envelope (a \`data\` array) instead of a single version. This is a platform/SDK contract change this code does not understand \u2014 refusing to guess which row is actually current.`
|
|
17320
|
+
);
|
|
17321
|
+
}
|
|
17322
|
+
return result;
|
|
16672
17323
|
}
|
|
16673
17324
|
function requireScriptId(row, name) {
|
|
16674
17325
|
if (!row.id) {
|
|
@@ -16679,8 +17330,34 @@ function requireScriptId(row, name) {
|
|
|
16679
17330
|
function scriptMatches(stored, declared) {
|
|
16680
17331
|
return stored.source === declared.source && (stored.declaredInputContract ?? void 0) === declared.declaredInputContract;
|
|
16681
17332
|
}
|
|
17333
|
+
var ScriptOwnershipConflictError = class extends Error {
|
|
17334
|
+
constructor(name, owner, blueprintName) {
|
|
17335
|
+
super(
|
|
17336
|
+
`Script '${name}' is already provisioned by a different blueprint ('${owner}', this blueprint is '${blueprintName}') \u2014 refusing to push over it. No script version is pushed (anything already provisioned earlier in this same apply is unaffected). If you intended to replace that blueprint's script, do it under ITS name; if two blueprints legitimately need distinct code, give this one a different script name.`
|
|
17337
|
+
);
|
|
17338
|
+
this.name = "ScriptOwnershipConflictError";
|
|
17339
|
+
}
|
|
17340
|
+
};
|
|
17341
|
+
var ScriptAdoptionNotAllowedError = class extends Error {
|
|
17342
|
+
constructor(name) {
|
|
17343
|
+
super(
|
|
17344
|
+
`Script '${name}' already exists in this context and carries no blueprint provenance marker \u2014 it is either hand-authored or was pushed before this blueprint managed it. Refusing to push a new version onto it without an explicit opt-in: if a pre-existing trigger rule references '${name}' via 'version: latest', that rule's own operator-authored grant would run this blueprint's code from its very next firing \u2014 the blueprint supplying the code, someone else's rule supplying the authority. No script version is pushed (anything already provisioned earlier in this same apply is unaffected). Re-run with --allow-script-adoption once you've confirmed this blueprint should take over '${name}' going forward.`
|
|
17345
|
+
);
|
|
17346
|
+
this.name = "ScriptAdoptionNotAllowedError";
|
|
17347
|
+
}
|
|
17348
|
+
};
|
|
17349
|
+
function assertScriptPushOwnable(name, latest, blueprintName, allowScriptAdoption) {
|
|
17350
|
+
if (latest === void 0) return { adopting: false };
|
|
17351
|
+
const owner = latest.provisionedBy?.trim() || void 0;
|
|
17352
|
+
if (owner === void 0) {
|
|
17353
|
+
if (!allowScriptAdoption) throw new ScriptAdoptionNotAllowedError(name);
|
|
17354
|
+
return { adopting: true };
|
|
17355
|
+
}
|
|
17356
|
+
if (owner !== blueprintName) throw new ScriptOwnershipConflictError(name, owner, blueprintName);
|
|
17357
|
+
return { adopting: false };
|
|
17358
|
+
}
|
|
16682
17359
|
async function ensureScripts(args) {
|
|
16683
|
-
const { ctxClient, scripts, log } = args;
|
|
17360
|
+
const { ctxClient, scripts, blueprintName, allowScriptAdoption, log } = args;
|
|
16684
17361
|
const actions = [];
|
|
16685
17362
|
for (const [name, entry] of Object.entries(scripts)) {
|
|
16686
17363
|
let latest = await readLatestScriptVersion(ctxClient, name);
|
|
@@ -16691,18 +17368,32 @@ async function ensureScripts(args) {
|
|
|
16691
17368
|
id: requireScriptId(latest, name),
|
|
16692
17369
|
scriptVersion: latest.scriptVersion ?? 0
|
|
16693
17370
|
});
|
|
16694
|
-
|
|
16695
|
-
|
|
16696
|
-
|
|
16697
|
-
|
|
17371
|
+
const foreignOwner = latest.provisionedBy?.trim() || void 0;
|
|
17372
|
+
if (foreignOwner !== void 0 && foreignOwner !== blueprintName) {
|
|
17373
|
+
log.warn(
|
|
17374
|
+
{ component: "loader", scriptName: name, provisionedBy: foreignOwner },
|
|
17375
|
+
`script '${name}' already matches the declared source, but is provisioned by a DIFFERENT blueprint ('${foreignOwner}') \u2014 if this blueprint also declares a 'version: latest' trigger on this name, a future push by that other blueprint will change what that trigger runs`
|
|
17376
|
+
);
|
|
17377
|
+
} else {
|
|
17378
|
+
log.debug(
|
|
17379
|
+
{ component: "loader", scriptName: name, scriptVersion: latest.scriptVersion },
|
|
17380
|
+
"script already matches the declared source \u2014 no version pushed"
|
|
17381
|
+
);
|
|
17382
|
+
}
|
|
16698
17383
|
continue;
|
|
16699
17384
|
}
|
|
16700
|
-
|
|
17385
|
+
let { adopting } = assertScriptPushOwnable(name, latest, blueprintName, allowScriptAdoption);
|
|
17386
|
+
const body = {
|
|
17387
|
+
name,
|
|
17388
|
+
source: entry.source,
|
|
17389
|
+
declaredInputContract: entry.declaredInputContract,
|
|
17390
|
+
provisionedBy: blueprintName
|
|
17391
|
+
};
|
|
16701
17392
|
let pushed;
|
|
16702
17393
|
try {
|
|
16703
17394
|
pushed = await ctxClient.scripts.createScript(body);
|
|
16704
17395
|
} catch (err) {
|
|
16705
|
-
if (!isVersionConflict(err)) throw err;
|
|
17396
|
+
if (!isVersionConflict(err)) throw adopting ? withAdoptionContext(err, name) : err;
|
|
16706
17397
|
log.debug(
|
|
16707
17398
|
{ component: "loader", scriptName: name },
|
|
16708
17399
|
"concurrent push detected (409) \u2014 pausing, then re-reading the current version"
|
|
@@ -16728,10 +17419,11 @@ async function ensureScripts(args) {
|
|
|
16728
17419
|
);
|
|
16729
17420
|
continue;
|
|
16730
17421
|
}
|
|
17422
|
+
({ adopting } = assertScriptPushOwnable(name, latest, blueprintName, allowScriptAdoption));
|
|
16731
17423
|
try {
|
|
16732
17424
|
pushed = await ctxClient.scripts.createScript(body);
|
|
16733
17425
|
} catch (retryErr) {
|
|
16734
|
-
if (!isVersionConflict(retryErr)) throw retryErr;
|
|
17426
|
+
if (!isVersionConflict(retryErr)) throw adopting ? withAdoptionContext(retryErr, name) : retryErr;
|
|
16735
17427
|
throw new Error(
|
|
16736
17428
|
`Script '${name}': still conflicting (409) after a re-read and retry. Another apply is pushing this script name concurrently \u2014 re-run once it has finished.`
|
|
16737
17429
|
);
|
|
@@ -16743,13 +17435,13 @@ async function ensureScripts(args) {
|
|
|
16743
17435
|
const firstVersion = latest === void 0;
|
|
16744
17436
|
actions.push({
|
|
16745
17437
|
name,
|
|
16746
|
-
action: firstVersion ? "created" : "superseded",
|
|
17438
|
+
action: firstVersion ? "created" : adopting ? "adopted" : "superseded",
|
|
16747
17439
|
id: pushed.id,
|
|
16748
17440
|
scriptVersion: pushed.scriptVersion ?? 0
|
|
16749
17441
|
});
|
|
16750
17442
|
log.debug(
|
|
16751
17443
|
{ component: "loader", scriptName: name, scriptVersion: pushed.scriptVersion },
|
|
16752
|
-
firstVersion ? "script pushed \u2014 first version for this name in this context" : "script source changed \u2014 new version pushed over an existing name"
|
|
17444
|
+
firstVersion ? "script pushed \u2014 first version for this name in this context" : adopting ? "script pushed \u2014 adopted a previously-unmarked name into this blueprint" : "script source changed \u2014 new version pushed over an existing name"
|
|
16753
17445
|
);
|
|
16754
17446
|
}
|
|
16755
17447
|
return actions;
|
|
@@ -16768,14 +17460,16 @@ async function pruneUndeclaredTriggers(args) {
|
|
|
16768
17460
|
{ component: "loader", err: extractServerError(err) },
|
|
16769
17461
|
"could not list trigger rules \u2014 skipping the trigger reconcile entirely; a trigger removed from this blueprint may still be live"
|
|
16770
17462
|
);
|
|
16771
|
-
return { pruned, skipped: [{ reason: "the context's trigger rules could not be listed" }] };
|
|
17463
|
+
return { pruned, skipped: [{ reason: "the context's trigger rules could not be listed", failed: true }] };
|
|
16772
17464
|
}
|
|
16773
17465
|
for (const row of live) {
|
|
16774
17466
|
if (!row.name || !row.id || !row.principalId) {
|
|
16775
17467
|
if (!declaredNames.has(row.name ?? "")) {
|
|
17468
|
+
const ours = row.provisionedBy === blueprintName;
|
|
16776
17469
|
skipped.push({
|
|
16777
17470
|
name: row.name,
|
|
16778
|
-
reason: "the trigger rule carries no id/name/principal, so it cannot be safely de-provisioned"
|
|
17471
|
+
reason: ours ? "it carries this blueprint's marker but has no id, name or principal to remove it by" : "the trigger rule carries no id/name/principal, so it cannot be safely de-provisioned",
|
|
17472
|
+
failed: ours
|
|
16779
17473
|
});
|
|
16780
17474
|
}
|
|
16781
17475
|
continue;
|
|
@@ -16784,7 +17478,8 @@ async function pruneUndeclaredTriggers(args) {
|
|
|
16784
17478
|
if (!row.provisionedBy) {
|
|
16785
17479
|
skipped.push({
|
|
16786
17480
|
name: row.name,
|
|
16787
|
-
reason: "it carries no blueprint provenance marker, so it cannot be attributed to this blueprint"
|
|
17481
|
+
reason: "it carries no blueprint provenance marker, so it cannot be attributed to this blueprint",
|
|
17482
|
+
failed: false
|
|
16788
17483
|
});
|
|
16789
17484
|
continue;
|
|
16790
17485
|
}
|
|
@@ -16805,7 +17500,8 @@ async function pruneUndeclaredTriggers(args) {
|
|
|
16805
17500
|
);
|
|
16806
17501
|
skipped.push({
|
|
16807
17502
|
name: row.name,
|
|
16808
|
-
reason: "its service principal could not be read, so ownership could not be fully established"
|
|
17503
|
+
reason: "its service principal could not be read, so ownership could not be fully established",
|
|
17504
|
+
failed: true
|
|
16809
17505
|
});
|
|
16810
17506
|
continue;
|
|
16811
17507
|
}
|
|
@@ -16816,7 +17512,8 @@ async function pruneUndeclaredTriggers(args) {
|
|
|
16816
17512
|
);
|
|
16817
17513
|
skipped.push({
|
|
16818
17514
|
name: row.name,
|
|
16819
|
-
reason: "it names a service principal this blueprint did not mint for a trigger of that name, so its provenance is inconsistent"
|
|
17515
|
+
reason: "it names a service principal this blueprint did not mint for a trigger of that name, so its provenance is inconsistent",
|
|
17516
|
+
failed: false
|
|
16820
17517
|
});
|
|
16821
17518
|
continue;
|
|
16822
17519
|
}
|
|
@@ -16829,7 +17526,7 @@ async function pruneUndeclaredTriggers(args) {
|
|
|
16829
17526
|
{ component: "loader", triggerName: row.name, principalId: row.principalId, err: extractServerError(err) },
|
|
16830
17527
|
"could not delete a removed trigger's access profile \u2014 leaving the trigger rule in place so the next apply retries"
|
|
16831
17528
|
);
|
|
16832
|
-
skipped.push({ name: row.name, reason: "its access profile could not be deleted, so its grant may still be live" });
|
|
17529
|
+
skipped.push({ name: row.name, reason: "its access profile could not be deleted, so its grant may still be live", failed: true });
|
|
16833
17530
|
continue;
|
|
16834
17531
|
}
|
|
16835
17532
|
profileDeleted = false;
|
|
@@ -16841,7 +17538,7 @@ async function pruneUndeclaredTriggers(args) {
|
|
|
16841
17538
|
{ component: "loader", triggerName: row.name, err: extractServerError(err) },
|
|
16842
17539
|
"a removed trigger's access profile was deleted but the rule itself was not \u2014 it can no longer execute, and the next apply will retry the rule"
|
|
16843
17540
|
);
|
|
16844
|
-
skipped.push({ name: row.name, reason: "its grant was removed but the rule row itself could not be deleted" });
|
|
17541
|
+
skipped.push({ name: row.name, reason: "its grant was removed but the rule row itself could not be deleted", failed: true });
|
|
16845
17542
|
continue;
|
|
16846
17543
|
}
|
|
16847
17544
|
pruned.push({ name: row.name, principalId: row.principalId, profileDeleted });
|
|
@@ -16920,9 +17617,10 @@ function describeNamespaceMismatch(decl, existing) {
|
|
|
16920
17617
|
return mismatches.length > 0 ? mismatches.join("; ") : null;
|
|
16921
17618
|
}
|
|
16922
17619
|
var LostKeyError = class extends Error {
|
|
16923
|
-
constructor(keyName) {
|
|
17620
|
+
constructor(keyName, rotateAttempted = false, revokedKeyId) {
|
|
17621
|
+
const revokedDisplay = revokedKeyId?.map((id) => `'${id}'`).join(", ");
|
|
16924
17622
|
super(
|
|
16925
|
-
`Scoped key '${keyName}' already exists for this machine but its local copy (~/.vectros) is missing. The raw key is shown only once and cannot be re-read. Re-run with --rotate to revoke + re-mint this machine's key.`
|
|
17623
|
+
`Scoped key '${keyName}' already exists for this machine but its local copy (~/.vectros) is missing. The raw key is shown only once and cannot be re-read. ` + (revokedKeyId && revokedKeyId.length > 0 ? `--rotate just revoked ${revokedKeyId.length === 1 ? "key" : "keys"} ${revokedDisplay}, but this call still sees the scoped key as already existing \u2014 most likely a brief propagation delay right after the revoke, not a failed rotate. Wait a few seconds and re-run, or run \`vectros key list --context <contextId>\` to confirm ${revokedKeyId.length === 1 ? "it is" : "they are"} actually gone before retrying.` : rotateAttempted ? `--rotate ran and found no matching active key to revoke for this context/principal \u2014 either it was already revoked elsewhere or something else holds that name. Run \`vectros key list --context <contextId>\` to find the key actually blocking this, then \`vectros key revoke <id>\` it directly before re-running.` : `Re-run with --rotate to revoke + re-mint this machine's key.`)
|
|
16926
17624
|
);
|
|
16927
17625
|
this.keyName = keyName;
|
|
16928
17626
|
this.name = "LostKeyError";
|
|
@@ -16930,18 +17628,20 @@ var LostKeyError = class extends Error {
|
|
|
16930
17628
|
keyName;
|
|
16931
17629
|
};
|
|
16932
17630
|
var ExistingPrincipalNotConfirmedError = class extends Error {
|
|
16933
|
-
constructor(contextId, userId, externalId) {
|
|
17631
|
+
constructor(contextId, userId, externalId, mintsKey = true) {
|
|
16934
17632
|
super(
|
|
16935
|
-
`About to bind a NEW scoped key to a principal that already existed in this tenant before this run (externalId '${externalId}', userId '${userId}', context '${contextId}'). This is either an intentional multi-machine setup for an integration you already own, or this blueprint's servicePrincipal.externalId collided with an unrelated existing identity \u2014 confirm before a new credential is minted bound to it.`
|
|
17633
|
+
mintsKey ? `About to bind a NEW scoped key to a principal that already existed in this tenant before this run (externalId '${externalId}', userId '${userId}', context '${contextId}'). This is either an intentional multi-machine setup for an integration you already own, or this blueprint's servicePrincipal.externalId collided with an unrelated existing identity \u2014 confirm before a new credential is minted bound to it.` : `About to write the grants of a principal that already existed in this tenant before this run (externalId '${externalId}', userId '${userId}', context '${contextId}'). This is either a re-apply of a blueprint you already manage, or this blueprint's servicePrincipal.externalId collided with an unrelated existing identity \u2014 confirm before that identity's access profile is changed.`
|
|
16936
17634
|
);
|
|
16937
17635
|
this.contextId = contextId;
|
|
16938
17636
|
this.userId = userId;
|
|
16939
17637
|
this.externalId = externalId;
|
|
17638
|
+
this.mintsKey = mintsKey;
|
|
16940
17639
|
this.name = "ExistingPrincipalNotConfirmedError";
|
|
16941
17640
|
}
|
|
16942
17641
|
contextId;
|
|
16943
17642
|
userId;
|
|
16944
17643
|
externalId;
|
|
17644
|
+
mintsKey;
|
|
16945
17645
|
};
|
|
16946
17646
|
function mapEnumValues(enumValues) {
|
|
16947
17647
|
if (!enumValues) return void 0;
|
|
@@ -17044,6 +17744,30 @@ function assertNoUnresolvedIdentities(blueprint) {
|
|
|
17044
17744
|
);
|
|
17045
17745
|
}
|
|
17046
17746
|
}
|
|
17747
|
+
var IssuerScopeNotAppliedError = class extends Error {
|
|
17748
|
+
constructor(issuerId, contextId, declared, returned, phase) {
|
|
17749
|
+
super(
|
|
17750
|
+
`issuer '${issuerId}' was ${phase} ${returned ? `restricted to '${returned}'` : "with no domain scope"}, but the blueprint declares restrictedToDomain '${declared}': the platform did not apply it, so the issuer accepts more tokens than declared. Once the platform applies the field, run: vectros issuers update ${shellQuote(issuerId)} --context ${shellQuote(contextId)} --restricted-to-domain ${shellQuote(declared)}`
|
|
17751
|
+
);
|
|
17752
|
+
this.issuerId = issuerId;
|
|
17753
|
+
this.contextId = contextId;
|
|
17754
|
+
this.declared = declared;
|
|
17755
|
+
this.returned = returned;
|
|
17756
|
+
this.phase = phase;
|
|
17757
|
+
this.name = "IssuerScopeNotAppliedError";
|
|
17758
|
+
}
|
|
17759
|
+
issuerId;
|
|
17760
|
+
contextId;
|
|
17761
|
+
declared;
|
|
17762
|
+
returned;
|
|
17763
|
+
phase;
|
|
17764
|
+
};
|
|
17765
|
+
function assertIssuerScopeApplied(issuer, returned, phase) {
|
|
17766
|
+
const declared = issuer.restrictedToDomain;
|
|
17767
|
+
if (declared === void 0) return;
|
|
17768
|
+
if ((returned ?? "").toLowerCase() === declared.toLowerCase()) return;
|
|
17769
|
+
throw new IssuerScopeNotAppliedError(issuer.issuerId, issuer.contextId, declared, returned, phase);
|
|
17770
|
+
}
|
|
17047
17771
|
var TenantWideNamespacesNotAllowedError = class extends Error {
|
|
17048
17772
|
constructor(message) {
|
|
17049
17773
|
super(message);
|
|
@@ -17099,7 +17823,12 @@ async function provisionContext(client, input) {
|
|
|
17099
17823
|
externalId: blueprint.servicePrincipal.externalId
|
|
17100
17824
|
}) : true;
|
|
17101
17825
|
if (!proceed) {
|
|
17102
|
-
throw new ExistingPrincipalNotConfirmedError(
|
|
17826
|
+
throw new ExistingPrincipalNotConfirmedError(
|
|
17827
|
+
blueprint.contextId,
|
|
17828
|
+
userId,
|
|
17829
|
+
blueprint.servicePrincipal.externalId,
|
|
17830
|
+
!input.skipKey
|
|
17831
|
+
);
|
|
17103
17832
|
}
|
|
17104
17833
|
log.debug(
|
|
17105
17834
|
{ component: "loader", contextId: blueprint.contextId, userId },
|
|
@@ -17117,7 +17846,14 @@ async function provisionContext(client, input) {
|
|
|
17117
17846
|
namespaceActions.push({ namespace: ns.namespace, action });
|
|
17118
17847
|
log.debug({ component: "loader", namespace: ns.namespace, contextId: blueprint.contextId, action }, "namespace ensured");
|
|
17119
17848
|
}
|
|
17120
|
-
return {
|
|
17849
|
+
return {
|
|
17850
|
+
contextAction,
|
|
17851
|
+
servicePrincipalUserId: userId,
|
|
17852
|
+
...user.createdAt === void 0 ? {} : { servicePrincipalCreatedAt: user.createdAt },
|
|
17853
|
+
userAction,
|
|
17854
|
+
namespaceActions,
|
|
17855
|
+
ctxBootstrapClient
|
|
17856
|
+
};
|
|
17121
17857
|
}
|
|
17122
17858
|
async function runLoader(client, input) {
|
|
17123
17859
|
const {
|
|
@@ -17125,13 +17861,18 @@ async function runLoader(client, input) {
|
|
|
17125
17861
|
tenantId,
|
|
17126
17862
|
machineId,
|
|
17127
17863
|
rotate = false,
|
|
17864
|
+
skipKey = false,
|
|
17128
17865
|
skipSeed = false,
|
|
17129
17866
|
seedIndexPoll,
|
|
17130
17867
|
provisioned,
|
|
17131
17868
|
readExistingKey,
|
|
17132
17869
|
persistMintedKey,
|
|
17870
|
+
allowScriptAdoption = false,
|
|
17133
17871
|
log
|
|
17134
17872
|
} = input;
|
|
17873
|
+
if (skipKey && rotate) {
|
|
17874
|
+
throw new Error("rotate and skipKey cannot be combined: rotate replaces this machine's key, and skipKey mints none.");
|
|
17875
|
+
}
|
|
17135
17876
|
assertNoUnresolvedIdentities(blueprint);
|
|
17136
17877
|
const { contextAction, servicePrincipalUserId: userId, userAction, namespaceActions } = provisioned;
|
|
17137
17878
|
assertBlueprintGates(blueprint);
|
|
@@ -17155,13 +17896,15 @@ async function runLoader(client, input) {
|
|
|
17155
17896
|
subClaim: issuer.subClaim,
|
|
17156
17897
|
emailClaim: issuer.emailClaim,
|
|
17157
17898
|
userinfoUri: issuer.userinfoUri,
|
|
17899
|
+
restrictedToDomain: issuer.restrictedToDomain,
|
|
17158
17900
|
selfSignupPolicies,
|
|
17159
17901
|
capturedClaims: issuer.capturedClaims
|
|
17160
17902
|
});
|
|
17161
17903
|
const action = actionOf(issuerResp.created);
|
|
17162
|
-
|
|
17904
|
+
let observed = issuerResp;
|
|
17905
|
+
if (action === "created") assertIssuerScopeApplied(issuer, issuerResp.restrictedToDomain, "registered");
|
|
17163
17906
|
if (action === "reconciled") {
|
|
17164
|
-
await ctxBootstrapClient.auth.updateIssuer({
|
|
17907
|
+
const updated = await ctxBootstrapClient.auth.updateIssuer({
|
|
17165
17908
|
issuerId: issuer.issuerId,
|
|
17166
17909
|
// Trust-anchor/routing-pin fields — echoed back, never actually
|
|
17167
17910
|
// changed here (the platform rejects a differing value).
|
|
@@ -17172,15 +17915,37 @@ async function runLoader(client, input) {
|
|
|
17172
17915
|
subClaim: issuer.subClaim,
|
|
17173
17916
|
emailClaim: issuer.emailClaim,
|
|
17174
17917
|
userinfoUri: issuer.userinfoUri,
|
|
17918
|
+
// Undeclared stays `undefined` (leave the stored scope unchanged) — never '', which the
|
|
17919
|
+
// platform reads as "remove the scope". That "never ''" invariant is enforced by the blueprint
|
|
17920
|
+
// schema, not here: the schema's grammar has no empty spelling, and this CLI is only ever run
|
|
17921
|
+
// as a binary, so a blueprint always passes through `parseBlueprint` before it reaches this loop.
|
|
17922
|
+
restrictedToDomain: issuer.restrictedToDomain,
|
|
17175
17923
|
selfSignupPolicies,
|
|
17176
17924
|
capturedClaims: issuer.capturedClaims
|
|
17177
17925
|
});
|
|
17926
|
+
assertIssuerScopeApplied(issuer, updated.restrictedToDomain, "reconciled");
|
|
17927
|
+
if (updated.status !== void 0) {
|
|
17928
|
+
observed = {
|
|
17929
|
+
status: updated.status,
|
|
17930
|
+
verificationClaim: updated.verificationClaim,
|
|
17931
|
+
verificationNonce: updated.verificationNonce,
|
|
17932
|
+
verificationExpiresAt: updated.verificationExpiresAt
|
|
17933
|
+
};
|
|
17934
|
+
}
|
|
17178
17935
|
log.debug({ component: "loader", issuerId: issuer.issuerId }, "issuer already registered \u2014 reconciled safe fields to the declared values");
|
|
17179
17936
|
} else {
|
|
17180
17937
|
log.debug({ component: "loader", issuerId: issuer.issuerId, action }, "issuer registered");
|
|
17181
17938
|
}
|
|
17182
|
-
|
|
17183
|
-
|
|
17939
|
+
issuerActions.push({
|
|
17940
|
+
issuerId: issuer.issuerId,
|
|
17941
|
+
action,
|
|
17942
|
+
...observed.status !== void 0 ? { status: observed.status } : {},
|
|
17943
|
+
...observed.verificationClaim !== void 0 ? { verificationClaim: observed.verificationClaim } : {},
|
|
17944
|
+
...observed.verificationNonce !== void 0 ? { verificationNonce: observed.verificationNonce } : {},
|
|
17945
|
+
...observed.verificationExpiresAt !== void 0 ? { verificationExpiresAt: observed.verificationExpiresAt } : {}
|
|
17946
|
+
});
|
|
17947
|
+
}
|
|
17948
|
+
const ctxClient = input.remintForContext ? await input.remintForContext(blueprint.contextId) : client;
|
|
17184
17949
|
log.debug({ component: "loader", contextId: blueprint.contextId }, "re-minted per-context client");
|
|
17185
17950
|
const schemaIds = {};
|
|
17186
17951
|
const schemaActions = [];
|
|
@@ -17280,7 +18045,13 @@ async function runLoader(client, input) {
|
|
|
17280
18045
|
body: profileBody
|
|
17281
18046
|
});
|
|
17282
18047
|
log.debug({ component: "loader", principalId: `usr_${userId}`, accessProfileId: profile.id, profileAction }, "access profile ensured + reconciled to declared scopes");
|
|
17283
|
-
const scriptActions = blueprint.scripts ? await ensureScripts({
|
|
18048
|
+
const scriptActions = blueprint.scripts ? await ensureScripts({
|
|
18049
|
+
ctxClient,
|
|
18050
|
+
scripts: blueprint.scripts,
|
|
18051
|
+
blueprintName: blueprint.name,
|
|
18052
|
+
allowScriptAdoption,
|
|
18053
|
+
log
|
|
18054
|
+
}) : [];
|
|
17284
18055
|
const triggerActions = [];
|
|
17285
18056
|
for (const [triggerName, entry] of Object.entries(blueprint.triggers ?? {})) {
|
|
17286
18057
|
const schemaId = schemaIds[entry.firingSource.schemaName];
|
|
@@ -17373,7 +18144,9 @@ async function runLoader(client, input) {
|
|
|
17373
18144
|
{ component: "loader", contextId: blueprint.contextId },
|
|
17374
18145
|
"trigger reconcile-by-absence skipped \u2014 no per-context client is wired, so a removed trigger may still be live"
|
|
17375
18146
|
);
|
|
17376
|
-
triggerPruneSkipped = [
|
|
18147
|
+
triggerPruneSkipped = [
|
|
18148
|
+
{ reason: "no per-context client was wired, so triggers could not be reconciled in the blueprint's own context", failed: true }
|
|
18149
|
+
];
|
|
17377
18150
|
} else {
|
|
17378
18151
|
({ pruned: triggerPrunes, skipped: triggerPruneSkipped } = await pruneUndeclaredTriggers({
|
|
17379
18152
|
ctxClient,
|
|
@@ -17383,38 +18156,76 @@ async function runLoader(client, input) {
|
|
|
17383
18156
|
log
|
|
17384
18157
|
}));
|
|
17385
18158
|
}
|
|
17386
|
-
const keyName = keyNameFor(blueprint.name, machineId);
|
|
17387
|
-
let keyAction = "created";
|
|
18159
|
+
const keyName = skipKey ? "" : keyNameFor(blueprint.name, machineId);
|
|
18160
|
+
let keyAction = skipKey ? "skipped" : "created";
|
|
17388
18161
|
let revokedKeyId;
|
|
17389
|
-
|
|
17390
|
-
|
|
17391
|
-
|
|
17392
|
-
|
|
17393
|
-
|
|
17394
|
-
|
|
17395
|
-
|
|
17396
|
-
|
|
17397
|
-
|
|
17398
|
-
|
|
17399
|
-
|
|
17400
|
-
|
|
17401
|
-
|
|
17402
|
-
|
|
17403
|
-
|
|
17404
|
-
|
|
17405
|
-
|
|
17406
|
-
|
|
17407
|
-
|
|
17408
|
-
|
|
17409
|
-
|
|
17410
|
-
|
|
17411
|
-
|
|
18162
|
+
let rawKey = "";
|
|
18163
|
+
let keyId = "";
|
|
18164
|
+
if (!skipKey) {
|
|
18165
|
+
if (rotate) {
|
|
18166
|
+
const existing = await readExistingKey(blueprint.contextId);
|
|
18167
|
+
if (existing?.keyId) {
|
|
18168
|
+
await ctxClient.auth.revokeScopedKey({ keyId: existing.keyId });
|
|
18169
|
+
log.debug({ component: "loader", keyId: existing.keyId }, "revoked prior key for rotate");
|
|
18170
|
+
revokedKeyId = [existing.keyId];
|
|
18171
|
+
} else {
|
|
18172
|
+
let activeKeys;
|
|
18173
|
+
try {
|
|
18174
|
+
activeKeys = await drainPages(() => ctxClient.auth.listScopedKeys());
|
|
18175
|
+
} catch (err) {
|
|
18176
|
+
throw new Error(
|
|
18177
|
+
`--rotate could not check for a server-side key to revoke, and no local record of one exists on this machine: ${err instanceof Error ? err.message : String(err)}. Run \`vectros key list --context ${blueprint.contextId}\` and \`vectros key revoke <id>\` manually to find and remove the key blocking this, then re-run without --rotate.`,
|
|
18178
|
+
{ cause: err }
|
|
18179
|
+
);
|
|
18180
|
+
}
|
|
18181
|
+
const matches = activeKeys.filter(
|
|
18182
|
+
(k) => k.contextId === blueprint.contextId && k.userId === userId && k.keyName === keyName && k.status === "active" && k.keyId
|
|
18183
|
+
);
|
|
18184
|
+
if (matches.length > 1) {
|
|
18185
|
+
log.warn(
|
|
18186
|
+
{ component: "loader", keyName, contextId: blueprint.contextId, count: matches.length },
|
|
18187
|
+
"rotate: more than one active key matched this context/user/name tuple server-side \u2014 revoking all of them"
|
|
18188
|
+
);
|
|
18189
|
+
}
|
|
18190
|
+
if (matches.length > 0) {
|
|
18191
|
+
const revokedIds = [];
|
|
18192
|
+
for (const match of matches) {
|
|
18193
|
+
await ctxClient.auth.revokeScopedKey({ keyId: match.keyId });
|
|
18194
|
+
revokedIds.push(match.keyId);
|
|
18195
|
+
}
|
|
18196
|
+
log.debug(
|
|
18197
|
+
{ component: "loader", keyIds: revokedIds },
|
|
18198
|
+
"revoked prior key(s) for rotate \u2014 resolved server-side, no local record existed"
|
|
18199
|
+
);
|
|
18200
|
+
revokedKeyId = revokedIds;
|
|
18201
|
+
} else {
|
|
18202
|
+
log.warn(
|
|
18203
|
+
{ component: "loader", keyName, contextId: blueprint.contextId },
|
|
18204
|
+
"rotate: no local record and no matching active key found server-side \u2014 nothing to revoke"
|
|
18205
|
+
);
|
|
18206
|
+
}
|
|
18207
|
+
}
|
|
18208
|
+
if (revokedKeyId) keyAction = "rotated";
|
|
18209
|
+
}
|
|
18210
|
+
const keyResp = await ctxClient.auth.createScopedKey({ keyName, tenantId, contextId: blueprint.contextId, userId });
|
|
18211
|
+
if (keyResp.rawKey) {
|
|
18212
|
+
keyId = keyResp.keyId ?? "";
|
|
18213
|
+
rawKey = keyResp.rawKey;
|
|
18214
|
+
if (keyId) {
|
|
18215
|
+
await persistMintedKey(blueprint.contextId, { keyId, rawKey, keyName, principalId: `usr_${userId}`, revokedKeyId });
|
|
18216
|
+
}
|
|
18217
|
+
if (keyAction !== "rotated") keyAction = "created";
|
|
18218
|
+
} else {
|
|
18219
|
+
const existing = await readExistingKey(blueprint.contextId);
|
|
18220
|
+
if (!existing) {
|
|
18221
|
+
throw new LostKeyError(keyName, rotate, revokedKeyId);
|
|
18222
|
+
}
|
|
18223
|
+
rawKey = existing.rawKey;
|
|
18224
|
+
keyId = existing.keyId;
|
|
18225
|
+
keyAction = "reused";
|
|
17412
18226
|
}
|
|
17413
|
-
|
|
17414
|
-
keyId = existing.keyId;
|
|
17415
|
-
keyAction = "reused";
|
|
18227
|
+
log.debug({ component: "loader", keyName, keyAction }, "scoped key resolved");
|
|
17416
18228
|
}
|
|
17417
|
-
log.debug({ component: "loader", keyName, keyAction }, "scoped key resolved");
|
|
17418
18229
|
let seededCount = 0;
|
|
17419
18230
|
const seededRecordIds = [];
|
|
17420
18231
|
const seededDocumentIds = [];
|
|
@@ -17521,6 +18332,9 @@ async function runLoader(client, input) {
|
|
|
17521
18332
|
seedIndexFailures
|
|
17522
18333
|
};
|
|
17523
18334
|
}
|
|
18335
|
+
function sanitizeForPlanDisplay(value, maxLen = 200) {
|
|
18336
|
+
return stripControlCharacters(value, maxLen);
|
|
18337
|
+
}
|
|
17524
18338
|
function renderPlanDataScope(ds) {
|
|
17525
18339
|
const entries = Object.entries(ds ?? {});
|
|
17526
18340
|
if (entries.length === 0) return "(none \u2014 unconstrained)";
|
|
@@ -17547,11 +18361,15 @@ function buildPlan(blueprint, opts = {}) {
|
|
|
17547
18361
|
});
|
|
17548
18362
|
}
|
|
17549
18363
|
for (const iss of blueprint.issuers ?? []) {
|
|
17550
|
-
const
|
|
17551
|
-
const
|
|
18364
|
+
const issuerUrl = sanitizeForPlanDisplay(iss.issuer);
|
|
18365
|
+
const jwksNote = `, jwksUri: ${sanitizeForPlanDisplay(iss.jwksUri)}`;
|
|
18366
|
+
const userinfoNote = iss.userinfoUri ? `, userinfoUri: ${sanitizeForPlanDisplay(iss.userinfoUri)}` : "";
|
|
18367
|
+
const domainNote = iss.restrictedToDomain ? `, restricted to domain: ${sanitizeForPlanDisplay(iss.restrictedToDomain)}` : "";
|
|
18368
|
+
const selfSignup = iss.selfSignupPolicies?.length ? `, self-signup: [${iss.selfSignupPolicies.map((p) => `${p.signupType}\u2192${p.roleId}`).join(", ")}]` : "";
|
|
18369
|
+
const captured = iss.capturedClaims?.length ? `, captured claims: [${iss.capturedClaims.map((c) => sanitizeForPlanDisplay(c)).join(", ")}]` : "";
|
|
17552
18370
|
steps.push({
|
|
17553
18371
|
kind: "issuer",
|
|
17554
|
-
summary: `issuer '${iss.issuerId}' (${
|
|
18372
|
+
summary: `issuer '${iss.issuerId}' (${issuerUrl} \u2192 context '${iss.contextId}'${jwksNote}${userinfoNote}${domainNote}${selfSignup}${captured}) [${BLUEPRINT_FIELD_PHASES.issuers}-token phase]`
|
|
17555
18373
|
});
|
|
17556
18374
|
}
|
|
17557
18375
|
for (const s of blueprint.schemas) {
|
|
@@ -17641,7 +18459,9 @@ function buildPlan(blueprint, opts = {}) {
|
|
|
17641
18459
|
});
|
|
17642
18460
|
}
|
|
17643
18461
|
}
|
|
17644
|
-
|
|
18462
|
+
if (!opts.noKey) {
|
|
18463
|
+
steps.push({ kind: "key", summary: `key ${blueprint.name}-<machine> (mint 1 scoped ssk_*)` });
|
|
18464
|
+
}
|
|
17645
18465
|
const seeds = blueprint.seed ?? [];
|
|
17646
18466
|
if (seeds.length > 0 && !opts.noSeed) {
|
|
17647
18467
|
const docs = seeds.filter((s) => s.surface === "document").length;
|
|
@@ -17653,36 +18473,57 @@ function buildPlan(blueprint, opts = {}) {
|
|
|
17653
18473
|
}
|
|
17654
18474
|
return steps;
|
|
17655
18475
|
}
|
|
17656
|
-
function renderPlan(blueprint, steps) {
|
|
18476
|
+
function renderPlan(blueprint, steps, opts = {}) {
|
|
17657
18477
|
const header = ` Plan for blueprint '${blueprint.name}' (v${blueprint.version}) \u2192 context '${blueprint.contextId}':`;
|
|
17658
18478
|
const legendLines = [
|
|
17659
18479
|
` Apply is a reconcile \u2014 each item is created if absent, else updated in place to`,
|
|
17660
18480
|
` match. Nothing is recreated, and nothing is dropped except as noted below.`
|
|
17661
18481
|
];
|
|
17662
|
-
|
|
17663
|
-
|
|
17664
|
-
|
|
17665
|
-
|
|
17666
|
-
|
|
17667
|
-
|
|
17668
|
-
|
|
17669
|
-
|
|
18482
|
+
if (opts.prune === false) {
|
|
18483
|
+
legendLines.push(
|
|
18484
|
+
` Nothing is deleted: this apply does not prune. A trigger this blueprint provisioned`,
|
|
18485
|
+
` earlier and its file no longer declares is LEFT IN PLACE; pass --prune to remove it.`,
|
|
18486
|
+
` (Access profiles and roles are still replaced in place: their scopes become the declared set.)`
|
|
18487
|
+
);
|
|
18488
|
+
} else {
|
|
18489
|
+
legendLines.push(
|
|
18490
|
+
opts.prune === true ? ` Exception (--prune): triggers are also reconciled by ABSENCE \u2014 a trigger this blueprint` : ` Exception: triggers are also reconciled by ABSENCE \u2014 a trigger this blueprint`,
|
|
18491
|
+
` previously provisioned and no longer declares is DELETED (the rule and its access`,
|
|
18492
|
+
` profile; its service principal is kept). This applies even when this plan shows no`,
|
|
18493
|
+
` trigger steps: removing the last one is exactly what leaves none to show. Triggers`,
|
|
18494
|
+
` created outside this blueprint are never touched. Each deletion is named in the`,
|
|
18495
|
+
` apply output. bootstrap does this on every apply; 'vectros blueprint apply' only`,
|
|
18496
|
+
` when --prune is passed.`,
|
|
18497
|
+
` "This blueprint's" is decided by a STORED MARKER, not a verified identity: the row's`,
|
|
18498
|
+
` provisionedBy equals this blueprint's name (read from the blueprint file) and its`,
|
|
18499
|
+
` principal's external id is the one this blueprint would derive for that trigger.`
|
|
18500
|
+
);
|
|
18501
|
+
}
|
|
17670
18502
|
legendLines.push(
|
|
17671
18503
|
` Counter-exception: a script is never updated in place \u2014 a changed source ADDS a new`,
|
|
17672
18504
|
` immutable version and every earlier one persists, which is also how a 'latest' rule`,
|
|
17673
18505
|
` picks the change up. And scripts are NOT reconciled by absence: a script this blueprint`,
|
|
17674
18506
|
` previously shipped and no longer declares is LEFT IN PLACE, including every stored`,
|
|
17675
|
-
` version.
|
|
17676
|
-
`
|
|
17677
|
-
`
|
|
18507
|
+
` version. Each version does carry this blueprint's own provenance marker, and a push`,
|
|
18508
|
+
` ONTO an already-marked-or-existing name is gated on it (see 'script' steps above) \u2014 but`,
|
|
18509
|
+
` that gate has no bearing on DELETION: there is no second, principal-shaped factor to`,
|
|
18510
|
+
` pair the marker with the way trigger rows have one, so a wrong delete stays unrecoverable`,
|
|
18511
|
+
` and would break any trigger still referencing it. Remove one deliberately with`,
|
|
17678
18512
|
` 'vectros scripts delete', after 'vectros scripts list' shows you what is there.`
|
|
17679
18513
|
);
|
|
18514
|
+
if (blueprint.issuers?.some((i) => i.restrictedToDomain === void 0)) {
|
|
18515
|
+
legendLines.push(
|
|
18516
|
+
` Note: a newly registered issuer without restrictedToDomain starts pending verification on platform`,
|
|
18517
|
+
` 0.45.0 and later: it accepts no token until you prove you control the identity provider.`,
|
|
18518
|
+
` the apply prints the challenge and the command that finishes it.`
|
|
18519
|
+
);
|
|
18520
|
+
}
|
|
17680
18521
|
if (steps.some((s) => s.kind === "issuer")) {
|
|
17681
18522
|
legendLines.push(
|
|
17682
18523
|
` Exception: an issuer's trust-anchor fields (issuer, jwksUri, audience,`,
|
|
17683
18524
|
` contextId) are immutable once registered \u2014 only subClaim/emailClaim/`,
|
|
17684
|
-
` userinfoUri/selfSignupPolicies actually reconcile
|
|
17685
|
-
` authorable in a blueprint at all).`
|
|
18525
|
+
` userinfoUri/restrictedToDomain/selfSignupPolicies/capturedClaims actually reconcile`,
|
|
18526
|
+
` on a re-apply (status is not authorable in a blueprint at all).`
|
|
17686
18527
|
);
|
|
17687
18528
|
}
|
|
17688
18529
|
if (steps.some((s) => s.kind === "namespace")) {
|
|
@@ -17703,6 +18544,104 @@ function renderPlan(blueprint, steps) {
|
|
|
17703
18544
|
return [header, legend, "", ...lines].join("\n");
|
|
17704
18545
|
}
|
|
17705
18546
|
|
|
18547
|
+
// src/issuer-challenge.ts
|
|
18548
|
+
var PENDING_VERIFICATION = "pending_verification";
|
|
18549
|
+
function isPendingVerification(view) {
|
|
18550
|
+
return view.status === PENDING_VERIFICATION;
|
|
18551
|
+
}
|
|
18552
|
+
function challengeExpired(view, nowMs) {
|
|
18553
|
+
if (!view.verificationExpiresAt) return false;
|
|
18554
|
+
const at = Date.parse(view.verificationExpiresAt);
|
|
18555
|
+
return Number.isFinite(at) && at <= nowMs;
|
|
18556
|
+
}
|
|
18557
|
+
function humanizeRemaining(ms) {
|
|
18558
|
+
const minutes = Math.floor(ms / 6e4);
|
|
18559
|
+
if (minutes < 1) return "under a minute";
|
|
18560
|
+
const days = Math.floor(minutes / 1440);
|
|
18561
|
+
const hours = Math.floor(minutes % 1440 / 60);
|
|
18562
|
+
const mins = minutes % 60;
|
|
18563
|
+
if (days > 0) return `${days} day${days === 1 ? "" : "s"} ${hours} h`;
|
|
18564
|
+
if (hours > 0) return `${hours} h ${mins} min`;
|
|
18565
|
+
return `${mins} min`;
|
|
18566
|
+
}
|
|
18567
|
+
function targetFlags(ctx) {
|
|
18568
|
+
return (ctx.tenant && ctx.tenant !== "live" ? ` --tenant ${shellQuote(ctx.tenant)}` : "") + (ctx.env && ctx.env !== "production" ? ` --env ${shellQuote(ctx.env)}` : "") + (ctx.baseUrl ? ` --base-url ${shellQuote(ctx.baseUrl)}` : "");
|
|
18569
|
+
}
|
|
18570
|
+
function verifyCommand(issuerId, ctx) {
|
|
18571
|
+
return `vectros issuers verify ${shellQuote(issuerId)} --context ${shellQuote(ctx.context)}${targetFlags(ctx)} --idp-token-file ./idp-token.txt`;
|
|
18572
|
+
}
|
|
18573
|
+
function deleteCommand(issuerId, ctx) {
|
|
18574
|
+
return `vectros issuers delete ${shellQuote(issuerId)} --context ${shellQuote(ctx.context)}${targetFlags(ctx)} --force`;
|
|
18575
|
+
}
|
|
18576
|
+
function getCommand(issuerId, ctx) {
|
|
18577
|
+
return `vectros issuers get ${shellQuote(issuerId)} --context ${shellQuote(ctx.context)}${targetFlags(ctx)}`;
|
|
18578
|
+
}
|
|
18579
|
+
var WITHHELD = "(withheld: unexpected characters)";
|
|
18580
|
+
var SAFE_TEXT = /^[A-Za-z0-9:/._~+-]{1,512}$/;
|
|
18581
|
+
function printable(value) {
|
|
18582
|
+
return SAFE_TEXT.test(value) ? value : void 0;
|
|
18583
|
+
}
|
|
18584
|
+
function displayChallengeValue(value) {
|
|
18585
|
+
return value === void 0 ? void 0 : printable(value) ?? WITHHELD;
|
|
18586
|
+
}
|
|
18587
|
+
function renderChallengeBlock(view, ctx, now) {
|
|
18588
|
+
const id = view.issuerId ?? "(unknown)";
|
|
18589
|
+
const rawClaim = view.verificationClaim;
|
|
18590
|
+
const rawNonce = view.verificationNonce;
|
|
18591
|
+
const claim = rawClaim === void 0 ? "(not reported)" : printable(rawClaim) ?? WITHHELD;
|
|
18592
|
+
const nonce = rawNonce === void 0 ? "(not reported)" : printable(rawNonce) ?? WITHHELD;
|
|
18593
|
+
const snippetClaim = rawClaim !== void 0 && printable(rawClaim) ? rawClaim : "the claim named above";
|
|
18594
|
+
const lines = [];
|
|
18595
|
+
lines.push(` \u26A0 issuer '${id}' is pending verification: it accepts no token until you prove you control it.`);
|
|
18596
|
+
lines.push("");
|
|
18597
|
+
lines.push(" 1. At your identity provider, add an ADMIN-CONTROLLED rule that puts this exact value in this");
|
|
18598
|
+
lines.push(" claim of the tokens it issues (never map it from an attribute your users can edit):");
|
|
18599
|
+
lines.push(` claim: ${claim}`);
|
|
18600
|
+
lines.push(` value: ${nonce}`);
|
|
18601
|
+
if (view.verificationExpiresAt) {
|
|
18602
|
+
const expires = printable(view.verificationExpiresAt) ?? WITHHELD;
|
|
18603
|
+
const at = Date.parse(view.verificationExpiresAt);
|
|
18604
|
+
const left = Number.isFinite(at) ? ` (in ${humanizeRemaining(at - now)})` : "";
|
|
18605
|
+
lines.push(` expires: ${expires}${left}`);
|
|
18606
|
+
}
|
|
18607
|
+
lines.push("");
|
|
18608
|
+
lines.push(" Auth0 a post-login Action; keep the value in a secret named VECTROS_ISSUER_NONCE:");
|
|
18609
|
+
lines.push(" exports.onExecutePostLogin = async (event, api) => {");
|
|
18610
|
+
lines.push(` const claim = '${snippetClaim}';`);
|
|
18611
|
+
lines.push(" api.idToken.setCustomClaim(claim, event.secrets.VECTROS_ISSUER_NONCE);");
|
|
18612
|
+
lines.push(" api.accessToken.setCustomClaim(claim, event.secrets.VECTROS_ISSUER_NONCE);");
|
|
18613
|
+
lines.push(" };");
|
|
18614
|
+
lines.push(" Okta a token inline hook, or a custom claim on your authorization server, that adds the");
|
|
18615
|
+
lines.push(" claim with this fixed value to the access token.");
|
|
18616
|
+
lines.push(" Entra a claims-mapping policy (or custom claims provider) on the application that adds");
|
|
18617
|
+
lines.push(" the claim with this fixed value.");
|
|
18618
|
+
lines.push(' Keycloak a "Hardcoded claim" protocol mapper on the client, added to the access token and');
|
|
18619
|
+
lines.push(' the ID token. Keycloak reads a dot in "Token Claim Name" as nesting, so write each');
|
|
18620
|
+
lines.push(" dot in the claim name as \\. to keep it a single claim.");
|
|
18621
|
+
lines.push(" Other any admin-controlled rule that adds this claim with this exact value.");
|
|
18622
|
+
lines.push("");
|
|
18623
|
+
lines.push(" 2. Sign in once through that provider and save the JWT it returns to a file. It must be a token");
|
|
18624
|
+
lines.push(" issued for the audience you registered (with Auth0, the ACCESS token, not the ID token). Run:");
|
|
18625
|
+
lines.push(` ${verifyCommand(id, ctx)}`);
|
|
18626
|
+
lines.push("");
|
|
18627
|
+
lines.push(" The provider's OpenID Connect discovery document must list exactly the jwksUri you registered.");
|
|
18628
|
+
lines.push(" The token is checked once and discarded; the rule can be removed after verification.");
|
|
18629
|
+
return `${lines.join("\n")}
|
|
18630
|
+
`;
|
|
18631
|
+
}
|
|
18632
|
+
function renderExpiredBlock(view, ctx, rerun) {
|
|
18633
|
+
const id = view.issuerId ?? "(unknown)";
|
|
18634
|
+
const expired = view.verificationExpiresAt === void 0 ? "(unknown)" : printable(view.verificationExpiresAt) ?? WITHHELD;
|
|
18635
|
+
const lines = [];
|
|
18636
|
+
lines.push(` \u2716 issuer '${id}' is STILL pending verification and its challenge EXPIRED at ${expired} (by this machine's clock).`);
|
|
18637
|
+
lines.push(" It can no longer be verified, so it will never accept a token. Replace it (if that time looks wrong, check the clock first):");
|
|
18638
|
+
lines.push(` ${deleteCommand(id, ctx)}`);
|
|
18639
|
+
lines.push(` ${rerun ?? "then register it again"}`);
|
|
18640
|
+
lines.push(" The new registration gets a new value, so update the rule at your identity provider to match.");
|
|
18641
|
+
return `${lines.join("\n")}
|
|
18642
|
+
`;
|
|
18643
|
+
}
|
|
18644
|
+
|
|
17706
18645
|
// src/config-merge.ts
|
|
17707
18646
|
import { promises as fs2 } from "fs";
|
|
17708
18647
|
import os2 from "os";
|
|
@@ -17801,19 +18740,37 @@ import { createCipheriv, createDecipheriv, randomBytes as randomBytes2, scryptSy
|
|
|
17801
18740
|
// src/os-keychain.ts
|
|
17802
18741
|
var KEYCHAIN_SERVICE = "vectros-cli";
|
|
17803
18742
|
var PROBE_ACCOUNT = "__vectros_probe__";
|
|
17804
|
-
async function loadKeychainBinding() {
|
|
18743
|
+
async function loadKeychainBinding(tryLoad = () => import("@napi-rs/keyring")) {
|
|
17805
18744
|
try {
|
|
17806
|
-
const mod = await
|
|
18745
|
+
const mod = await tryLoad();
|
|
17807
18746
|
if (typeof mod?.Entry === "function") return { binding: mod };
|
|
17808
|
-
return {
|
|
18747
|
+
return {
|
|
18748
|
+
reason: "the @napi-rs/keyring package loaded but exposes no Entry \u2014 expected on no platform",
|
|
18749
|
+
installAccident: true
|
|
18750
|
+
};
|
|
17809
18751
|
} catch (err) {
|
|
17810
|
-
return { reason: loadFailureReason(err) };
|
|
18752
|
+
return { reason: loadFailureReason(err), installAccident: isInstallAccident(err) };
|
|
17811
18753
|
}
|
|
17812
18754
|
}
|
|
17813
|
-
|
|
18755
|
+
var PREBUILT_PLATFORMS = {
|
|
18756
|
+
darwin: ["arm64", "x64"],
|
|
18757
|
+
freebsd: ["x64"],
|
|
18758
|
+
linux: ["arm", "arm64", "riscv64", "x64"],
|
|
18759
|
+
win32: ["arm64", "ia32", "x64"]
|
|
18760
|
+
};
|
|
18761
|
+
function prebuiltShouldExist(platform = process.platform, arch = process.arch) {
|
|
18762
|
+
return PREBUILT_PLATFORMS[platform]?.includes(arch) ?? false;
|
|
18763
|
+
}
|
|
18764
|
+
function isInstallAccident(_err, platform = process.platform, arch = process.arch) {
|
|
18765
|
+
return prebuiltShouldExist(platform, arch);
|
|
18766
|
+
}
|
|
18767
|
+
function loadFailureReason(err, platform = process.platform, arch = process.arch) {
|
|
18768
|
+
if (!prebuiltShouldExist(platform, arch)) {
|
|
18769
|
+
return "the optional @napi-rs/keyring package is not installed for this platform";
|
|
18770
|
+
}
|
|
17814
18771
|
const code = err.code;
|
|
17815
18772
|
if (code === "ERR_MODULE_NOT_FOUND" || code === "MODULE_NOT_FOUND") {
|
|
17816
|
-
return
|
|
18773
|
+
return `the optional @napi-rs/keyring package is missing even though a prebuilt exists for this platform (${platform}/${arch}) \u2014 most likely skipped during install`;
|
|
17817
18774
|
}
|
|
17818
18775
|
return `the optional @napi-rs/keyring package is installed but would not load (${describeErr(err)})`;
|
|
17819
18776
|
}
|
|
@@ -17937,8 +18894,8 @@ var OsKeychainSecretStore = class {
|
|
|
17937
18894
|
* deleteCredential() -> false
|
|
17938
18895
|
* => deleteSecret() -> false. The gate holds; `remove` refuses.
|
|
17939
18896
|
*
|
|
17940
|
-
* Reproduce
|
|
17941
|
-
*
|
|
18897
|
+
* Reproduce by locking the credential store's collection while the CLI still holds a
|
|
18898
|
+
* cached handle to it. CI cannot pin this half — it unlocks the collection so the rest of
|
|
17942
18899
|
* the suite can use it — so the evidence lives here rather than in a test.
|
|
17943
18900
|
*
|
|
17944
18901
|
* The WINDOWS half of the same contract was assumed for seven review rounds and
|
|
@@ -18380,7 +19337,11 @@ var KEYRING_BACKENDS = ["auto", "file"];
|
|
|
18380
19337
|
async function resolveSecretStore(deps = {}) {
|
|
18381
19338
|
const platform = deps.platform ?? process.platform;
|
|
18382
19339
|
const env = deps.env ?? process.env;
|
|
18383
|
-
const file = (reason2) => ({
|
|
19340
|
+
const file = (reason2, installAccident2) => ({
|
|
19341
|
+
kind: "crypto-file",
|
|
19342
|
+
reason: reason2,
|
|
19343
|
+
...installAccident2 ? { installAccident: installAccident2 } : {}
|
|
19344
|
+
});
|
|
18384
19345
|
const requested = env[KEYRING_BACKEND_ENV]?.trim().toLowerCase();
|
|
18385
19346
|
if (requested !== void 0 && requested !== "") {
|
|
18386
19347
|
if (!KEYRING_BACKENDS.includes(requested)) {
|
|
@@ -18397,9 +19358,9 @@ async function resolveSecretStore(deps = {}) {
|
|
|
18397
19358
|
return file("the macOS Keychain can prompt on unattended reads, which would break MCP startup");
|
|
18398
19359
|
}
|
|
18399
19360
|
const loadBinding0 = deps.loadBinding ?? loadKeychainBinding;
|
|
18400
|
-
const { binding, reason } = await loadBinding0();
|
|
19361
|
+
const { binding, reason, installAccident } = await loadBinding0();
|
|
18401
19362
|
if (!binding) {
|
|
18402
|
-
return file(reason ?? "the optional @napi-rs/keyring package is not usable on this platform");
|
|
19363
|
+
return file(reason ?? "the optional @napi-rs/keyring package is not usable on this platform", installAccident);
|
|
18403
19364
|
}
|
|
18404
19365
|
const probe = deps.probe ?? probeKeychain;
|
|
18405
19366
|
if (!probe(binding)) {
|
|
@@ -18428,6 +19389,9 @@ var Keyring = class _Keyring {
|
|
|
18428
19389
|
stores;
|
|
18429
19390
|
/** Where NEW secrets go, and why — surfaced by `keyring doctor`. */
|
|
18430
19391
|
backend;
|
|
19392
|
+
warn;
|
|
19393
|
+
/** Fires at most once per instance — see {@link storeSecretSomewhere}. */
|
|
19394
|
+
installAccidentWarned = false;
|
|
18431
19395
|
/**
|
|
18432
19396
|
* Construct against EXPLICIT stores. The file store defaults to a
|
|
18433
19397
|
* {@link CryptoFileSecretStore} over `dir` — no native binding, nothing outside
|
|
@@ -18442,6 +19406,8 @@ var Keyring = class _Keyring {
|
|
|
18442
19406
|
"os-keychain": deps.keychainStore
|
|
18443
19407
|
};
|
|
18444
19408
|
this.backend = deps.selection ?? { kind: "crypto-file" };
|
|
19409
|
+
this.warn = deps.warn ?? ((msg) => process.stderr.write(`${msg}
|
|
19410
|
+
`));
|
|
18445
19411
|
}
|
|
18446
19412
|
/**
|
|
18447
19413
|
* The keyring a command should use: the stores resolved for THIS machine per
|
|
@@ -18452,7 +19418,7 @@ var Keyring = class _Keyring {
|
|
|
18452
19418
|
static async open(deps = {}) {
|
|
18453
19419
|
if (deps.fileStore ?? deps.keychainStore ?? deps.selection) return new _Keyring(deps);
|
|
18454
19420
|
const { keychain, ...selection } = await resolveSecretStore(deps);
|
|
18455
|
-
return new _Keyring({ dir: deps.dir, keychainStore: keychain, selection });
|
|
19421
|
+
return new _Keyring({ dir: deps.dir, keychainStore: keychain, selection, warn: deps.warn });
|
|
18456
19422
|
}
|
|
18457
19423
|
/**
|
|
18458
19424
|
* The store holding this entry's secret, or undefined when the stamp does not say
|
|
@@ -18569,6 +19535,28 @@ The key itself WAS created server-side and is live \u2014 its id is ${entry.keyI
|
|
|
18569
19535
|
}
|
|
18570
19536
|
return { landedIn, stranded: swept ? void 0 : prior, vetoedBy };
|
|
18571
19537
|
}
|
|
19538
|
+
/**
|
|
19539
|
+
* Warn, at most once per instance, when this machine is about to write a secret to
|
|
19540
|
+
* the file store because of an INSTALL ACCIDENT — not because of an ordinary,
|
|
19541
|
+
* nothing-to-fix absence (unsupported platform, `VECTROS_KEYRING_BACKEND=file`,
|
|
19542
|
+
* macOS's own design choice, or "loaded fine but no OS credential store answers").
|
|
19543
|
+
*
|
|
19544
|
+
* Before this, the CLI degraded to the file store with ZERO signal at write
|
|
19545
|
+
* time, indistinguishable from every OTHER reason a machine ends up there — a
|
|
19546
|
+
* real `@napi-rs/keyring` install failure looked identical to a platform that
|
|
19547
|
+
* was never going to have one, unless you separately ran `keyring doctor`.
|
|
19548
|
+
* This is the signal that was missing, at the moment it matters (a real
|
|
19549
|
+
* secret is about to land somewhere weaker than intended), naming the one
|
|
19550
|
+
* remedy that actually applies to THIS case and no other.
|
|
19551
|
+
*/
|
|
19552
|
+
warnOnceIfInstallAccident() {
|
|
19553
|
+
if (this.installAccidentWarned) return;
|
|
19554
|
+
if (this.backend.kind !== "crypto-file" || !this.backend.installAccident) return;
|
|
19555
|
+
this.installAccidentWarned = true;
|
|
19556
|
+
this.warn(
|
|
19557
|
+
`WARNING: storing this credential in the encrypted file store, not your OS credential store \u2014 ${this.backend.reason ?? "the optional @napi-rs/keyring package failed to load"}. This may be an install accident rather than an unsupported platform: try reinstalling (npm install -g @vectros-ai/cli, or add @napi-rs/keyring as a direct dependency alongside it) to restore OS-credential-store protection. Run \`vectros keyring doctor\` for the full picture.`
|
|
19558
|
+
);
|
|
19559
|
+
}
|
|
18572
19560
|
/**
|
|
18573
19561
|
* Persist a secret, preferring this machine's chosen store but NEVER losing the
|
|
18574
19562
|
* secret if that store refuses. Returns where it actually landed.
|
|
@@ -18581,8 +19569,15 @@ The key itself WAS created server-side and is live \u2014 its id is ${entry.keyI
|
|
|
18581
19569
|
* credential and leave it orphaned server-side with no local record of its
|
|
18582
19570
|
* `keyId`. Degrading to the encrypted file is strictly better than that, and the
|
|
18583
19571
|
* entry's stamp keeps the result honest rather than silently wrong.
|
|
19572
|
+
*
|
|
19573
|
+
* `warnIfDegraded` defaults to true and exists ONLY for {@link probeSecretStore}
|
|
19574
|
+
* to suppress with `false`: the once-per-instance install-accident warning is a
|
|
19575
|
+
* scarce resource (fires at most once per `Keyring`), and the probe's marker
|
|
19576
|
+
* value is never the credential the warning describes. Spending it on the probe
|
|
19577
|
+
* left the REAL write — the very next call, same instance — silent.
|
|
18584
19578
|
*/
|
|
18585
|
-
async storeSecretSomewhere(ref, secret) {
|
|
19579
|
+
async storeSecretSomewhere(ref, secret, { warnIfDegraded = true } = {}) {
|
|
19580
|
+
if (warnIfDegraded) this.warnOnceIfInstallAccident();
|
|
18586
19581
|
const preferred = this.preferred;
|
|
18587
19582
|
if (preferred) {
|
|
18588
19583
|
try {
|
|
@@ -18634,7 +19629,9 @@ The key itself WAS created server-side and is live \u2014 its id is ${entry.keyI
|
|
|
18634
19629
|
async probeSecretStore() {
|
|
18635
19630
|
const fileStore = this.stores["crypto-file"];
|
|
18636
19631
|
const dirWasPresent = fileStore instanceof CryptoFileSecretStore ? await fileStore.secretsDirPresent() : true;
|
|
18637
|
-
const landedIn = await this.storeSecretSomewhere(PROBE_SECRET_REF, PROBE_SECRET_VALUE
|
|
19632
|
+
const landedIn = await this.storeSecretSomewhere(PROBE_SECRET_REF, PROBE_SECRET_VALUE, {
|
|
19633
|
+
warnIfDegraded: false
|
|
19634
|
+
});
|
|
18638
19635
|
await this.stores[landedIn]?.deleteSecret(PROBE_SECRET_REF).catch(() => {
|
|
18639
19636
|
});
|
|
18640
19637
|
if (!dirWasPresent && fileStore instanceof CryptoFileSecretStore) {
|
|
@@ -18935,7 +19932,12 @@ The key itself WAS created server-side and is live \u2014 its id is ${entry.keyI
|
|
|
18935
19932
|
async readSecret(entry) {
|
|
18936
19933
|
const from = storeKindOf(entry);
|
|
18937
19934
|
if (from === void 0) return void 0;
|
|
18938
|
-
const
|
|
19935
|
+
const store = this.stores[from];
|
|
19936
|
+
if (!store) {
|
|
19937
|
+
this.warnOnceIfInstallAccident();
|
|
19938
|
+
return void 0;
|
|
19939
|
+
}
|
|
19940
|
+
const secret = await store.getSecret(entry.secretRef);
|
|
18939
19941
|
if (secret === void 0) return void 0;
|
|
18940
19942
|
if (from !== this.backend.kind) await this.migrateSecret(entry, from, secret);
|
|
18941
19943
|
return secret;
|
|
@@ -19656,7 +20658,7 @@ function nowIso() {
|
|
|
19656
20658
|
}
|
|
19657
20659
|
function warnReplacingAlias(existing, alias, revokedKeyId) {
|
|
19658
20660
|
if (!existing) return;
|
|
19659
|
-
const stillLive = existing.keyId !== void 0 && existing.keyId
|
|
20661
|
+
const stillLive = existing.keyId !== void 0 && !(revokedKeyId ?? []).includes(existing.keyId);
|
|
19660
20662
|
process.stdout.write(
|
|
19661
20663
|
`
|
|
19662
20664
|
\u26A0 Replacing keyring entry '${alias}' (was ${existing.principalId || "<unknown principal>"} in '${existing.context}' @ ${existing.env}, key ${existing.keyId}). Pass --alias to keep both.
|
|
@@ -19667,13 +20669,14 @@ function warnReplacingAlias(existing, alias, revokedKeyId) {
|
|
|
19667
20669
|
}
|
|
19668
20670
|
async function warnOtherAliasesNaming(keyring, revokedKeyId, exceptAlias) {
|
|
19669
20671
|
const affected = (await keyring.list()).filter(
|
|
19670
|
-
(e) => e.alias !== exceptAlias && e.keyId
|
|
20672
|
+
(e) => e.alias !== exceptAlias && e.keyId !== void 0 && revokedKeyId.includes(e.keyId)
|
|
19671
20673
|
);
|
|
19672
20674
|
if (affected.length === 0) return;
|
|
19673
20675
|
const names = affected.map((e) => `'${e.alias}'`).join(", ");
|
|
20676
|
+
const revokedDisplay = revokedKeyId.length === 1 ? `Key ${revokedKeyId[0]}` : `Keys ${revokedKeyId.join(", ")}`;
|
|
19674
20677
|
process.stderr.write(
|
|
19675
20678
|
`
|
|
19676
|
-
\u26A0
|
|
20679
|
+
\u26A0 ${revokedDisplay} ${revokedKeyId.length === 1 ? "was" : "were"} just revoked, but ${affected.length === 1 ? "entry" : "entries"} ${names} still
|
|
19677
20680
|
${affected.length === 1 ? "names" : "name"} it. That secret is now dead \u2014 it keeps reading fine locally and
|
|
19678
20681
|
will simply stop authenticating. Re-point ${affected.length === 1 ? "it" : "them"} with
|
|
19679
20682
|
\`vectros key rotate --to-keyring --alias <name>\`, or drop with \`vectros keyring remove <name>\`.
|
|
@@ -19703,7 +20706,7 @@ async function storeMintedKey(keyring, binding, secret, opts = {}) {
|
|
|
19703
20706
|
was kept too` : `the ${noun} did not
|
|
19704
20707
|
confirm the delete`;
|
|
19705
20708
|
const tidyUp = vetoNoun !== void 0 ? `Clear the copies by hand from your ${vetoNoun} and your ${noun}` : `Clear it by hand from your ${noun}`;
|
|
19706
|
-
if (opts.revokedKeyId
|
|
20709
|
+
if (opts.revokedKeyId?.includes(stranded.keyId)) {
|
|
19707
20710
|
process.stderr.write(
|
|
19708
20711
|
`
|
|
19709
20712
|
\u26A0 The previous key for '${entry.alias}' (${stranded.keyId}) was revoked and replaced, but
|
|
@@ -19774,6 +20777,8 @@ function parseBootstrapArgs(argv) {
|
|
|
19774
20777
|
blankSlate: false,
|
|
19775
20778
|
noSeed: false,
|
|
19776
20779
|
allowTenantWideNamespaces: false,
|
|
20780
|
+
allowScriptAdoption: false,
|
|
20781
|
+
requireVerifiedIssuers: false,
|
|
19777
20782
|
client: "desktop",
|
|
19778
20783
|
tenant: "live",
|
|
19779
20784
|
help: false
|
|
@@ -19838,6 +20843,12 @@ function parseBootstrapArgs(argv) {
|
|
|
19838
20843
|
case "--allow-tenant-wide-namespaces":
|
|
19839
20844
|
args.allowTenantWideNamespaces = true;
|
|
19840
20845
|
break;
|
|
20846
|
+
case "--allow-script-adoption":
|
|
20847
|
+
args.allowScriptAdoption = true;
|
|
20848
|
+
break;
|
|
20849
|
+
case "--require-verified-issuers":
|
|
20850
|
+
args.requireVerifiedIssuers = true;
|
|
20851
|
+
break;
|
|
19841
20852
|
case "--tenant": {
|
|
19842
20853
|
const v = argv[++i];
|
|
19843
20854
|
if (v !== "test" && v !== "live") {
|
|
@@ -19866,7 +20877,7 @@ function parseBootstrapArgs(argv) {
|
|
|
19866
20877
|
return args;
|
|
19867
20878
|
}
|
|
19868
20879
|
function shouldOfferSeedOptOut(args, blueprint, isTty) {
|
|
19869
|
-
return !args.noSeed && (blueprint.seed?.length ?? 0) > 0 && !args.yes &&
|
|
20880
|
+
return !args.noSeed && (blueprint.seed?.length ?? 0) > 0 && !args.yes && isTty;
|
|
19870
20881
|
}
|
|
19871
20882
|
function seedDeclined(answer) {
|
|
19872
20883
|
const a = answer.trim().toLowerCase();
|
|
@@ -19910,6 +20921,31 @@ function seedIndexWarningBlock(failures) {
|
|
|
19910
20921
|
` + lines.join("") + ` They're stored and retrievable by id, but won't appear in search until the issue above is resolved.
|
|
19911
20922
|
`;
|
|
19912
20923
|
}
|
|
20924
|
+
var REQUIRE_VERIFIED_EXIT = 4;
|
|
20925
|
+
function issuerTally(items) {
|
|
20926
|
+
const base = reconcileTally(items, "issuer");
|
|
20927
|
+
if (!base) return null;
|
|
20928
|
+
const pending = items.filter(isPendingVerification).length;
|
|
20929
|
+
return pending > 0 ? base.replace(/\)$/, `; ${pending} pending verification)`) : base;
|
|
20930
|
+
}
|
|
20931
|
+
function pendingIssuerBlock(actions, ctx, now, rerun = "then re-run this `vectros bootstrap` command") {
|
|
20932
|
+
return actions.filter(isPendingVerification).map((a) => challengeExpired(a, now) ? renderExpiredBlock(a, ctx, rerun) : renderChallengeBlock(a, ctx, now)).join("\n");
|
|
20933
|
+
}
|
|
20934
|
+
function finishIssuers(actions, ctx, requireVerified, now, rerun) {
|
|
20935
|
+
const block = pendingIssuerBlock(actions, ctx, now, rerun);
|
|
20936
|
+
if (block) process.stderr.write(`
|
|
20937
|
+
${block}`);
|
|
20938
|
+
const pending = actions.filter(isPendingVerification).length;
|
|
20939
|
+
if (requireVerified && pending > 0) {
|
|
20940
|
+
process.stderr.write(
|
|
20941
|
+
`
|
|
20942
|
+
\u2716 --require-verified-issuers: ${pending} issuer${pending === 1 ? " is" : "s are"} still ${PENDING_VERIFICATION.replace("_", " ")} (exit ${REQUIRE_VERIFIED_EXIT}). The apply itself completed.
|
|
20943
|
+
`
|
|
20944
|
+
);
|
|
20945
|
+
return REQUIRE_VERIFIED_EXIT;
|
|
20946
|
+
}
|
|
20947
|
+
return 0;
|
|
20948
|
+
}
|
|
19913
20949
|
function reconcileTally(items, noun) {
|
|
19914
20950
|
if (items.length === 0) return null;
|
|
19915
20951
|
const created = items.filter((i) => i.action === "created").length;
|
|
@@ -19932,22 +20968,26 @@ function scriptTally(items) {
|
|
|
19932
20968
|
if (items.length === 0) return null;
|
|
19933
20969
|
const created = items.filter((i) => i.action === "created");
|
|
19934
20970
|
const superseded = items.filter((i) => i.action === "superseded");
|
|
19935
|
-
const
|
|
20971
|
+
const adopted = items.filter((i) => i.action === "adopted");
|
|
20972
|
+
const unchanged = items.length - created.length - superseded.length - adopted.length;
|
|
19936
20973
|
const seg = [];
|
|
19937
20974
|
if (created.length) seg.push(`${created.length} first version${created.length === 1 ? "" : "s"} pushed`);
|
|
19938
20975
|
if (superseded.length) {
|
|
19939
20976
|
seg.push(`${superseded.map((i) => `'${i.name}'`).join(", ")} superseded (a new version now current)`);
|
|
19940
20977
|
}
|
|
20978
|
+
if (adopted.length) {
|
|
20979
|
+
seg.push(`${adopted.map((i) => `'${i.name}'`).join(", ")} adopted (previously unmarked, now managed by this blueprint)`);
|
|
20980
|
+
}
|
|
19941
20981
|
if (unchanged) seg.push(`${unchanged} already current \u2014 nothing written`);
|
|
19942
20982
|
return `${items.length} script${items.length === 1 ? "" : "s"} (${seg.join("; ")})`;
|
|
19943
20983
|
}
|
|
19944
|
-
function applyOutcomeSummary(result) {
|
|
20984
|
+
function applyOutcomeSummary(result, opts = {}) {
|
|
19945
20985
|
const parts = [
|
|
19946
20986
|
reconcileTally(result.schemaActions, "schema"),
|
|
19947
20987
|
reconcileTally(result.roleActions, "role"),
|
|
19948
20988
|
// The platform's issuer partial-update endpoint makes an issuer's
|
|
19949
20989
|
// 'reconciled' mean the same "updated to match" as every other kind here.
|
|
19950
|
-
|
|
20990
|
+
issuerTally(result.issuerActions),
|
|
19951
20991
|
namespaceTally(result.namespaceActions),
|
|
19952
20992
|
// Scripts — reported BEFORE triggers, mirroring the order they are provisioned in, and with
|
|
19953
20993
|
// their own wording because "reconciled" would be a lie: a script version is immutable.
|
|
@@ -19960,7 +21000,7 @@ function applyOutcomeSummary(result) {
|
|
|
19960
21000
|
`profile ${result.profileAction}`
|
|
19961
21001
|
].filter((p) => p !== null);
|
|
19962
21002
|
const detail = parts.length ? ` \u2014 ${parts.join("; ")}` : "";
|
|
19963
|
-
const principalNote = result.userAction === "reconciled" ? ` \u26A0 service principal already existed in this tenant before this run (not newly created)
|
|
21003
|
+
const principalNote = (opts.principalNote ?? true) && result.userAction === "reconciled" ? ` \u26A0 service principal already existed in this tenant before this run (not newly created)
|
|
19964
21004
|
` : "";
|
|
19965
21005
|
const pruneNotes = result.triggerPrunes.map(
|
|
19966
21006
|
(p) => ` \u26A0 trigger '${p.name}' removed from the blueprint \u2014 de-provisioned (rule + access profile deleted)
|
|
@@ -19971,13 +21011,31 @@ function applyOutcomeSummary(result) {
|
|
|
19971
21011
|
`)
|
|
19972
21012
|
).join("");
|
|
19973
21013
|
const pruneSkippedNotes = result.triggerPruneSkipped.map(
|
|
19974
|
-
(s) => s.name ? ` \
|
|
21014
|
+
(s) => s.failed === false ? s.name ? ` \u2139 trigger '${s.name}' is not declared here and was left in place \u2014 ${s.reason}.
|
|
21015
|
+
` : ` \u2139 a trigger rule was left in place \u2014 ${s.reason}.
|
|
21016
|
+
` : s.name ? ` \u26A0 trigger '${s.name}' is no longer declared but could NOT be removed \u2014 ${s.reason}. It is still live.
|
|
19975
21017
|
` : ` \u26A0 triggers could not be reconciled this run \u2014 ${s.reason}. A trigger removed from this blueprint may still be live.
|
|
19976
21018
|
`
|
|
19977
21019
|
).join("");
|
|
19978
21020
|
return ` \u2714 context '${result.contextId}' ${result.contextAction}${detail}
|
|
19979
21021
|
${principalNote}${pruneNotes}${pruneSkippedNotes}`;
|
|
19980
21022
|
}
|
|
21023
|
+
function gateSummary(blueprint) {
|
|
21024
|
+
const roleNote = blueprint.accessProfile.roleIds ? ` via role(s) [${blueprint.accessProfile.roleIds.join(", ")}]` : "";
|
|
21025
|
+
const acceptedActions = effectiveAccessProfileActions(blueprint);
|
|
21026
|
+
const allDataPlane = !acceptedActions.some(
|
|
21027
|
+
(a) => KNOWN_CONTROL_PLANE_RESOURCES.includes(a.split(":", 1)[0])
|
|
21028
|
+
);
|
|
21029
|
+
let out = `
|
|
21030
|
+
Blueprint: ${blueprint.name}
|
|
21031
|
+
\u2714 Scope gate: [${acceptedActions.join(", ")}]${roleNote}` + (allDataPlane ? " \u2014 all data-plane \u2713\n" : " \u2014 includes a narrow, allowlisted control-plane exception (see above)\n");
|
|
21032
|
+
if (blueprint.accessProfile.capabilities?.length) {
|
|
21033
|
+
const capGate = evaluateCapabilityGate(blueprint.accessProfile.capabilities);
|
|
21034
|
+
out += ` \u2714 Capability gate: [${capGate.accepted.join(", ")}] \u2014 granted \u2713
|
|
21035
|
+
`;
|
|
21036
|
+
}
|
|
21037
|
+
return out;
|
|
21038
|
+
}
|
|
19981
21039
|
var BOOTSTRAP_HELP = `vectros bootstrap \u2014 provision a least-privilege MCP credential.
|
|
19982
21040
|
|
|
19983
21041
|
Usage:
|
|
@@ -19985,69 +21043,28 @@ Usage:
|
|
|
19985
21043
|
[--set name=value ...] [--values <file>]
|
|
19986
21044
|
[--tenant test|live | --test] [--client desktop|code]
|
|
19987
21045
|
[--yes] [--no-merge] [--rotate] [--confirm-existing-principal]
|
|
21046
|
+
[--require-verified-issuers]
|
|
19988
21047
|
[--token <t> | env VECTROS_BOOTSTRAP_TOKEN]
|
|
19989
21048
|
[--base-url <url>] [--use-scoped-token]
|
|
19990
21049
|
|
|
21050
|
+
To re-apply a blueprint into an existing context WITHOUT minting a key (CI/GitOps), use
|
|
21051
|
+
'vectros blueprint apply' instead.
|
|
21052
|
+
|
|
19991
21053
|
Options:
|
|
19992
|
-
|
|
19993
|
-
|
|
19994
|
-
|
|
19995
|
-
--set name=value Set an install-time input value (repeatable) for a file blueprint's
|
|
19996
|
-
'inputs:' block. Beats --values + the declared default. (Same flag as
|
|
19997
|
-
'vectros blueprint validate/plan'.)
|
|
19998
|
-
--values <file> A YAML/JSON map of input values. (File blueprints only.)
|
|
19999
|
-
--blank Credential only (read-only data-plane profile, no schemas).
|
|
20000
|
-
--no-seed Provision the blueprint's schemas, profile and key but SKIP its
|
|
20001
|
-
sample seed records \u2014 the right choice for a production context
|
|
20002
|
-
(a blueprint's seeds are illustrative, not real data). Unlike
|
|
20003
|
-
--blank, the schemas are still created.
|
|
20004
|
-
--allow-tenant-wide-namespaces
|
|
20005
|
-
Required to apply a blueprint that declares a namespace with
|
|
20006
|
-
tenantWide: true \u2014 visible to every context in the account, not
|
|
20007
|
-
just this one. Without it, bootstrap refuses before provisioning
|
|
20008
|
-
anything, naming the namespace(s). Only an account OWNER's
|
|
20009
|
-
credential can actually complete the registration either way \u2014
|
|
20010
|
-
this flag doesn't change who's allowed, only whether this run
|
|
20011
|
-
tries. Never needed for a blueprint with no tenantWide namespace.
|
|
20012
|
-
--tenant test|live Which tenant the minted key scopes to (default: live).
|
|
20013
|
-
'test' provisions into your isolated test tenant \u2014 a
|
|
20014
|
-
dry-run before pointing real data at the same blueprint.
|
|
20015
|
-
The tenant rides the key (ssk_test_* vs ssk_live_*), so
|
|
20016
|
-
the MCP/client config is otherwise identical.
|
|
20017
|
-
--test Shorthand for --tenant test.
|
|
20018
|
-
--activate Force the freshly-stored key to become your active identity, even
|
|
20019
|
-
if that would switch away from a currently-active LIVE credential
|
|
20020
|
-
(the one case --tenant test does NOT do this automatically).
|
|
20021
|
-
--no-activate Store the key without activating it, regardless of tenant.
|
|
20022
|
-
--base-url <url> Override the API base URL (beats VECTROS_API_BASE_URL).
|
|
20023
|
-
--client <desktop|code> MCP host to configure (default: desktop). 'desktop' merges
|
|
20024
|
-
claude_desktop_config.json (also works for Cursor/Cline);
|
|
20025
|
-
'code' targets Claude Code's project .mcp.json and prints
|
|
20026
|
-
the equivalent 'claude mcp add' command.
|
|
20027
|
-
--token <t> Cognito bridge token; or set VECTROS_BOOTSTRAP_TOKEN.
|
|
20028
|
-
Falls back to your stored \`vectros login\` session
|
|
20029
|
-
(same env) when neither is set \u2014 login once, then
|
|
20030
|
-
bootstrap unattended.
|
|
20031
|
-
--yes, -y Skip confirmations (agent / CI path).
|
|
20032
|
-
--no-merge Still mints/reconciles \u2014 this only changes where the result goes:
|
|
20033
|
-
print the config snippet instead of merging it into your client's
|
|
20034
|
-
config file. Not a dry run; see 'blueprint plan' for that.
|
|
20035
|
-
--rotate Revoke + re-mint THIS machine's key.
|
|
20036
|
-
--confirm-existing-principal
|
|
20037
|
-
Required (with --yes, or with no TTY) to mint a NEW key bound to a
|
|
20038
|
-
service principal that already existed in your tenant before this run
|
|
20039
|
-
\u2014 e.g. a deliberate second machine for an integration you already own.
|
|
20040
|
-
Refused by default so an unexpected collision halts a CI run loudly
|
|
20041
|
-
instead of silently minting a credential bound to the wrong identity.
|
|
20042
|
-
Never needed for --rotate or an ordinary same-machine re-apply.
|
|
20043
|
-
--use-scoped-token Legacy fallback.
|
|
20044
|
-
NOTE: it mints a token WITHOUT the provisioning
|
|
20045
|
-
capability, so it cannot create a blueprint's
|
|
20046
|
-
app-context \u2014 use only with the 'default' context
|
|
20047
|
-
(the dedicated endpoint is required otherwise).
|
|
20048
|
-
--help, -h Show this help.
|
|
21054
|
+
${renderOptionHelp("bootstrap", BLUEPRINT_NAMES)}
|
|
21055
|
+
|
|
21056
|
+
No --blueprint/--blank in a TTY \u2192 interactive menu.
|
|
20049
21057
|
|
|
20050
|
-
|
|
21058
|
+
Exit codes:
|
|
21059
|
+
0 applied; also --help, and answering no at "Apply this plan?" ("Aborted - nothing minted")
|
|
21060
|
+
1 failed: a bad flag, an unknown or malformed blueprint, a missing or invalid --set input, a
|
|
21061
|
+
declared issuer scope the platform did not apply, or any error while applying (the message says
|
|
21062
|
+
what did and did not run)
|
|
21063
|
+
2 refused by a gate: the scope gate or the capability gate rejected the blueprint, an identity it
|
|
21064
|
+
references could not be resolved, or it declares a tenant-wide namespace without the opt-in (the
|
|
21065
|
+
message says what, if anything, was already provisioned)
|
|
21066
|
+
3 needs a confirmation it did not get (an existing service principal, a key that cannot be recovered)
|
|
21067
|
+
4 applied in full, but --require-verified-issuers was given and an issuer is pending verification`;
|
|
20051
21068
|
async function resolveBlueprint(args, prompt, isTty) {
|
|
20052
21069
|
if (args.blankSlate) {
|
|
20053
21070
|
if (args.set.length > 0 || args.values) {
|
|
@@ -20149,7 +21166,7 @@ async function runBootstrapCli(argv, deps = {}) {
|
|
|
20149
21166
|
try {
|
|
20150
21167
|
args = parseBootstrapArgs(argv);
|
|
20151
21168
|
} catch (err) {
|
|
20152
|
-
process.stderr.write(`${err instanceof Error ? err.message : String(err)}
|
|
21169
|
+
process.stderr.write(`${redactCredentials(err instanceof Error ? err.message : String(err))}
|
|
20153
21170
|
|
|
20154
21171
|
${BOOTSTRAP_HELP}
|
|
20155
21172
|
`);
|
|
@@ -20191,21 +21208,7 @@ ${BOOTSTRAP_HELP}
|
|
|
20191
21208
|
}
|
|
20192
21209
|
throw err;
|
|
20193
21210
|
}
|
|
20194
|
-
|
|
20195
|
-
const acceptedActions = effectiveAccessProfileActions(blueprint);
|
|
20196
|
-
const allDataPlane = !acceptedActions.some(
|
|
20197
|
-
(a) => KNOWN_CONTROL_PLANE_RESOURCES.includes(a.split(":", 1)[0])
|
|
20198
|
-
);
|
|
20199
|
-
process.stdout.write(
|
|
20200
|
-
`
|
|
20201
|
-
Blueprint: ${blueprint.name}
|
|
20202
|
-
\u2714 Scope gate: [${acceptedActions.join(", ")}]${roleNote}` + (allDataPlane ? " \u2014 all data-plane \u2713\n" : " \u2014 includes a narrow, allowlisted control-plane exception (see above)\n")
|
|
20203
|
-
);
|
|
20204
|
-
if (blueprint.accessProfile.capabilities?.length) {
|
|
20205
|
-
const capGate = evaluateCapabilityGate(blueprint.accessProfile.capabilities);
|
|
20206
|
-
process.stdout.write(` \u2714 Capability gate: [${capGate.accepted.join(", ")}] \u2014 granted \u2713
|
|
20207
|
-
`);
|
|
20208
|
-
}
|
|
21211
|
+
process.stdout.write(gateSummary(blueprint));
|
|
20209
21212
|
let noSeed = args.noSeed;
|
|
20210
21213
|
let seedDeclinedInteractively = false;
|
|
20211
21214
|
if (shouldOfferSeedOptOut(args, blueprint, isTty)) {
|
|
@@ -20231,7 +21234,7 @@ ${renderPlan(blueprint, planSteps)}
|
|
|
20231
21234
|
`);
|
|
20232
21235
|
}
|
|
20233
21236
|
}
|
|
20234
|
-
if (!args.yes &&
|
|
21237
|
+
if (!args.yes && isTty) {
|
|
20235
21238
|
const ans = (await promptCtx.prompt("\n Apply this plan? [y/N]: ")).trim().toLowerCase();
|
|
20236
21239
|
if (ans !== "y" && ans !== "yes") {
|
|
20237
21240
|
process.stdout.write(" Aborted \u2014 nothing minted.\n");
|
|
@@ -20365,6 +21368,7 @@ ${renderPlan(blueprint, planSteps)}
|
|
|
20365
21368
|
machineId,
|
|
20366
21369
|
rotate: args.rotate,
|
|
20367
21370
|
skipSeed: noSeed,
|
|
21371
|
+
allowScriptAdoption: args.allowScriptAdoption,
|
|
20368
21372
|
readExistingKey,
|
|
20369
21373
|
persistMintedKey,
|
|
20370
21374
|
provisioned,
|
|
@@ -20377,6 +21381,12 @@ ${renderPlan(blueprint, planSteps)}
|
|
|
20377
21381
|
}),
|
|
20378
21382
|
log
|
|
20379
21383
|
});
|
|
21384
|
+
const finishApply = () => finishIssuers(
|
|
21385
|
+
result.issuerActions,
|
|
21386
|
+
{ context: blueprint.contextId, tenant: args.tenant, env: args.env, baseUrl: args.baseUrl },
|
|
21387
|
+
args.requireVerifiedIssuers,
|
|
21388
|
+
(deps.now ?? Date.now)()
|
|
21389
|
+
);
|
|
20380
21390
|
process.stdout.write(
|
|
20381
21391
|
applyOutcomeSummary(result) + ` \u2714 ${args.tenant} key (${result.keyAction}) \u2192 ${result.keyName}
|
|
20382
21392
|
` + seedSummaryLine({
|
|
@@ -20412,7 +21422,7 @@ ${renderPlan(blueprint, planSteps)}
|
|
|
20412
21422
|
|
|
20413
21423
|
${snippet}
|
|
20414
21424
|
${codeAddHint}`);
|
|
20415
|
-
return
|
|
21425
|
+
return finishApply();
|
|
20416
21426
|
}
|
|
20417
21427
|
let doMerge = args.yes;
|
|
20418
21428
|
if (!doMerge && isTty) {
|
|
@@ -20426,7 +21436,7 @@ ${codeAddHint}`);
|
|
|
20426
21436
|
|
|
20427
21437
|
${snippet}
|
|
20428
21438
|
${codeAddHint}`);
|
|
20429
|
-
return
|
|
21439
|
+
return finishApply();
|
|
20430
21440
|
}
|
|
20431
21441
|
try {
|
|
20432
21442
|
const merged = await safeMergeConfigFile({ configPath, entry });
|
|
@@ -20447,7 +21457,7 @@ ${codeAddHint}`);
|
|
|
20447
21457
|
${snippet}
|
|
20448
21458
|
${codeAddHint}`);
|
|
20449
21459
|
}
|
|
20450
|
-
return
|
|
21460
|
+
return finishApply();
|
|
20451
21461
|
} catch (err) {
|
|
20452
21462
|
if (err instanceof LostKeyError) {
|
|
20453
21463
|
process.stderr.write(`
|
|
@@ -20476,6 +21486,15 @@ ${codeAddHint}`);
|
|
|
20476
21486
|
);
|
|
20477
21487
|
return 2;
|
|
20478
21488
|
}
|
|
21489
|
+
if (err instanceof IssuerScopeNotAppliedError) {
|
|
21490
|
+
process.stderr.write(
|
|
21491
|
+
`
|
|
21492
|
+
\u2716 ${err.message}
|
|
21493
|
+
The app-context, service principal, and any declared namespaces were already provisioned. No schemas, profile, roles, key, or seed records were.
|
|
21494
|
+
`
|
|
21495
|
+
);
|
|
21496
|
+
return 1;
|
|
21497
|
+
}
|
|
20479
21498
|
if (err instanceof TenantWideNamespacesNotAllowedError) {
|
|
20480
21499
|
process.stderr.write(`
|
|
20481
21500
|
\u2716 ${err.message}
|
|
@@ -20498,126 +21517,614 @@ ${formatCliError(err, { action: "Bootstrap failed" })}`);
|
|
|
20498
21517
|
// src/cli-blueprint.ts
|
|
20499
21518
|
import { writeFile as writeFile2, access } from "fs/promises";
|
|
20500
21519
|
|
|
20501
|
-
// src/
|
|
20502
|
-
|
|
20503
|
-
|
|
20504
|
-
|
|
20505
|
-
if (typeof lf === "string") return [lf];
|
|
20506
|
-
if (lf.fieldNames !== void 0) return lf.fieldNames;
|
|
20507
|
-
return lf.fieldName !== void 0 ? [lf.fieldName] : [];
|
|
20508
|
-
}
|
|
20509
|
-
function compositeLegFindings(schema, lf, fieldsById, rangeEnabledFieldNames) {
|
|
20510
|
-
const findings = [];
|
|
20511
|
-
const id = `[${lf.fieldNames.join(", ")}]`;
|
|
20512
|
-
const seenLegs = /* @__PURE__ */ new Set();
|
|
20513
|
-
for (const leg of lf.fieldNames) {
|
|
20514
|
-
if (seenLegs.has(leg)) {
|
|
20515
|
-
findings.push({
|
|
20516
|
-
severity: "error",
|
|
20517
|
-
rule: "lookup-composite-repeated-leg",
|
|
20518
|
-
message: `schema '${schema.typeName}': composite lookupField ${id} repeats the leg '${leg}'. Each leg occupies its own position in the sort key, so a repeat doubles the key cost of every row to express a conjunction of a field with itself.`
|
|
20519
|
-
});
|
|
20520
|
-
}
|
|
20521
|
-
seenLegs.add(leg);
|
|
20522
|
-
const field = fieldsById.get(leg);
|
|
20523
|
-
if (!field) continue;
|
|
20524
|
-
if (field.fieldType === "array" || field.fieldType === "object") {
|
|
20525
|
-
findings.push({
|
|
20526
|
-
severity: "error",
|
|
20527
|
-
rule: "lookup-composite-leg-multi-valued",
|
|
20528
|
-
message: `schema '${schema.typeName}': composite lookupField ${id} uses '${leg}' as a leg, but it is declared '${field.fieldType}'. A leg occupies one sort-key position and holds one value \u2014 a multi-valued or structured field would index only one rendering and the conjunction would return a subset while appearing complete. Use a scalar field as a leg.`
|
|
20529
|
-
});
|
|
20530
|
-
}
|
|
20531
|
-
if (field.fieldType === "reference" && field.cardinality === "many") {
|
|
20532
|
-
findings.push({
|
|
20533
|
-
severity: "error",
|
|
20534
|
-
rule: "lookup-composite-leg-multi-valued",
|
|
20535
|
-
message: `schema '${schema.typeName}': composite lookupField ${id} uses '${leg}' as a leg, but it is a reference with cardinality 'many'. One sort-key position holds one value, so a multi-valued leg would index only one target and the conjunction would silently return a subset. Use a cardinality 'one' reference, or a scalar field.`
|
|
20536
|
-
});
|
|
20537
|
-
}
|
|
20538
|
-
if (rangeEnabledFieldNames.has(leg)) {
|
|
20539
|
-
findings.push({
|
|
20540
|
-
severity: "error",
|
|
20541
|
-
rule: "lookup-composite-leg-range-enabled",
|
|
20542
|
-
message: `schema '${schema.typeName}': composite lookupField ${id} uses '${leg}' as a leg, but '${leg}' has its own lookupField declaring rangeEnabled \u2014 that stores it as an ordered row keyed on its own value, not an equality index. A leg is matched exactly. Declare the composite over non-range fields, and keep the range lookup as its own declaration.`
|
|
20543
|
-
});
|
|
20544
|
-
}
|
|
20545
|
-
if (lf.sortBy !== void 0 && leg === lf.sortBy) {
|
|
20546
|
-
findings.push({
|
|
20547
|
-
severity: "error",
|
|
20548
|
-
rule: "lookup-composite-leg-is-sortby",
|
|
20549
|
-
message: `schema '${schema.typeName}': composite lookupField ${id} cannot use '${leg}' as both a leg and its sortBy target. Within a fixed tuple every matching record has the same value for that leg, so the sort payload would be constant and a sortFrom/sortTo window would return either everything or nothing \u2014 at double the sort-key cost. Sort by a different field, 'createdAt', or 'lastUpdated'.`
|
|
20550
|
-
});
|
|
20551
|
-
}
|
|
20552
|
-
}
|
|
20553
|
-
return findings;
|
|
21520
|
+
// src/cli-apply.ts
|
|
21521
|
+
import * as readline3 from "readline/promises";
|
|
21522
|
+
function bootstrapOnly(spelling) {
|
|
21523
|
+
return spellingsFor("bootstrap").includes(spelling) && !spellingsFor("apply").includes(spelling);
|
|
20554
21524
|
}
|
|
20555
|
-
function
|
|
20556
|
-
const
|
|
20557
|
-
|
|
20558
|
-
|
|
20559
|
-
|
|
20560
|
-
|
|
20561
|
-
|
|
20562
|
-
|
|
20563
|
-
|
|
20564
|
-
|
|
20565
|
-
|
|
20566
|
-
|
|
20567
|
-
|
|
20568
|
-
|
|
20569
|
-
|
|
20570
|
-
|
|
20571
|
-
|
|
20572
|
-
|
|
20573
|
-
|
|
20574
|
-
|
|
20575
|
-
|
|
20576
|
-
|
|
20577
|
-
|
|
20578
|
-
|
|
20579
|
-
|
|
20580
|
-
|
|
20581
|
-
|
|
20582
|
-
|
|
20583
|
-
|
|
20584
|
-
|
|
20585
|
-
|
|
20586
|
-
|
|
20587
|
-
|
|
20588
|
-
|
|
20589
|
-
|
|
20590
|
-
for (const lf of schema.lookupFields ?? []) {
|
|
20591
|
-
for (const name of lookupFieldNames(lf)) {
|
|
20592
|
-
if (!fieldIds.has(name)) {
|
|
20593
|
-
findings.push({
|
|
20594
|
-
severity: "error",
|
|
20595
|
-
rule: "lookup-field-unknown",
|
|
20596
|
-
message: `schema '${schema.typeName}': lookupField '${name}' is not a declared field.`
|
|
20597
|
-
});
|
|
21525
|
+
function parseApplyArgs(argv) {
|
|
21526
|
+
const args = {
|
|
21527
|
+
set: [],
|
|
21528
|
+
env: "production",
|
|
21529
|
+
yes: false,
|
|
21530
|
+
seed: false,
|
|
21531
|
+
allowTenantWideNamespaces: false,
|
|
21532
|
+
allowScriptAdoption: false,
|
|
21533
|
+
requireVerifiedIssuers: false,
|
|
21534
|
+
confirmExistingPrincipal: false,
|
|
21535
|
+
prune: false,
|
|
21536
|
+
help: false
|
|
21537
|
+
};
|
|
21538
|
+
const value = (flag, i) => {
|
|
21539
|
+
const v = argv[i + 1];
|
|
21540
|
+
if (v === void 0 || v.startsWith("-") && v !== "-") throw new Error(`${flag} needs a value`);
|
|
21541
|
+
if (v === "") throw new Error(`${flag} needs a non-empty value`);
|
|
21542
|
+
return v;
|
|
21543
|
+
};
|
|
21544
|
+
for (let i = 0; i < argv.length; i++) {
|
|
21545
|
+
const a = argv[i];
|
|
21546
|
+
switch (a) {
|
|
21547
|
+
case "--set":
|
|
21548
|
+
args.set.push(value(a, i));
|
|
21549
|
+
i++;
|
|
21550
|
+
break;
|
|
21551
|
+
case "--values":
|
|
21552
|
+
args.values = value(a, i);
|
|
21553
|
+
i++;
|
|
21554
|
+
break;
|
|
21555
|
+
case "--env": {
|
|
21556
|
+
const v = value(a, i);
|
|
21557
|
+
i++;
|
|
21558
|
+
if (v !== "staging" && v !== "production") {
|
|
21559
|
+
throw new Error(`--env must be 'staging' or 'production' (got '${v}')`);
|
|
20598
21560
|
}
|
|
21561
|
+
args.env = v;
|
|
21562
|
+
break;
|
|
20599
21563
|
}
|
|
20600
|
-
|
|
20601
|
-
|
|
20602
|
-
|
|
20603
|
-
|
|
20604
|
-
|
|
20605
|
-
|
|
20606
|
-
|
|
20607
|
-
|
|
20608
|
-
|
|
20609
|
-
|
|
20610
|
-
|
|
20611
|
-
|
|
20612
|
-
|
|
21564
|
+
case "--base-url":
|
|
21565
|
+
args.baseUrl = value(a, i);
|
|
21566
|
+
i++;
|
|
21567
|
+
break;
|
|
21568
|
+
case "--token":
|
|
21569
|
+
args.token = value(a, i);
|
|
21570
|
+
i++;
|
|
21571
|
+
break;
|
|
21572
|
+
case "--tenant": {
|
|
21573
|
+
const v = value(a, i);
|
|
21574
|
+
i++;
|
|
21575
|
+
if (v !== "test" && v !== "live") throw new Error(`--tenant must be 'test' or 'live' (got '${v}')`);
|
|
21576
|
+
if (args.tenant !== void 0) throw new Error("--tenant was given more than once");
|
|
21577
|
+
args.tenant = v;
|
|
21578
|
+
break;
|
|
20613
21579
|
}
|
|
20614
|
-
|
|
20615
|
-
|
|
20616
|
-
|
|
20617
|
-
|
|
20618
|
-
|
|
20619
|
-
|
|
20620
|
-
|
|
21580
|
+
case "--yes":
|
|
21581
|
+
case "-y":
|
|
21582
|
+
args.yes = true;
|
|
21583
|
+
break;
|
|
21584
|
+
case "--seed":
|
|
21585
|
+
args.seed = true;
|
|
21586
|
+
break;
|
|
21587
|
+
case "--allow-tenant-wide-namespaces":
|
|
21588
|
+
args.allowTenantWideNamespaces = true;
|
|
21589
|
+
break;
|
|
21590
|
+
case "--allow-script-adoption":
|
|
21591
|
+
args.allowScriptAdoption = true;
|
|
21592
|
+
break;
|
|
21593
|
+
case "--require-verified-issuers":
|
|
21594
|
+
args.requireVerifiedIssuers = true;
|
|
21595
|
+
break;
|
|
21596
|
+
case "--confirm-existing-principal":
|
|
21597
|
+
args.confirmExistingPrincipal = true;
|
|
21598
|
+
break;
|
|
21599
|
+
case "--prune":
|
|
21600
|
+
args.prune = true;
|
|
21601
|
+
break;
|
|
21602
|
+
case "--help":
|
|
21603
|
+
case "-h":
|
|
21604
|
+
args.help = true;
|
|
21605
|
+
break;
|
|
21606
|
+
default:
|
|
21607
|
+
if (a === "--test") {
|
|
21608
|
+
throw new Error("apply has no --test shorthand: write --tenant test (there is no default tenant)");
|
|
21609
|
+
}
|
|
21610
|
+
if (a === "--no-seed") {
|
|
21611
|
+
throw new Error("apply does not seed by default: pass --seed to write the sample records");
|
|
21612
|
+
}
|
|
21613
|
+
if (a === "--blueprint") {
|
|
21614
|
+
throw new Error("apply takes the blueprint as an argument, not a flag: vectros blueprint apply <file|name> --tenant test|live");
|
|
21615
|
+
}
|
|
21616
|
+
if (a === "--blank" || a === "--blank-slate") {
|
|
21617
|
+
throw new Error("apply has no blank slate: it needs a blueprint (vectros blueprint apply <file|name> --tenant test|live)");
|
|
21618
|
+
}
|
|
21619
|
+
if (bootstrapOnly(a)) {
|
|
21620
|
+
throw new Error(
|
|
21621
|
+
`${a} belongs to 'vectros bootstrap': apply mints no key and touches no client configuration`
|
|
21622
|
+
);
|
|
21623
|
+
}
|
|
21624
|
+
if (a.startsWith("-")) {
|
|
21625
|
+
const name = a.split("=")[0];
|
|
21626
|
+
throw new Error(
|
|
21627
|
+
`Unknown apply flag: ${name}` + (a.includes("=") ? " (a flag's value is a separate argument, not name=value)" : "")
|
|
21628
|
+
);
|
|
21629
|
+
}
|
|
21630
|
+
if (args.blueprint !== void 0) throw new Error("Unexpected extra argument: apply takes one blueprint");
|
|
21631
|
+
args.blueprint = a;
|
|
21632
|
+
}
|
|
21633
|
+
}
|
|
21634
|
+
return args;
|
|
21635
|
+
}
|
|
21636
|
+
function applySeedNotice(blueprint, seed) {
|
|
21637
|
+
const declared = blueprint.seed?.length ?? 0;
|
|
21638
|
+
if (declared === 0) {
|
|
21639
|
+
return seed ? "\n Sample records: --seed was passed, but this blueprint declares none.\n" : "";
|
|
21640
|
+
}
|
|
21641
|
+
return seed ? `
|
|
21642
|
+
Sample records: --seed was passed, so the ${declared} sample record(s) in the plan above WILL be written.
|
|
21643
|
+
` : `
|
|
21644
|
+
Sample records: this blueprint declares ${declared}; they will NOT be written (apply seeds only with --seed).
|
|
21645
|
+
`;
|
|
21646
|
+
}
|
|
21647
|
+
function applySeedSummary(blueprint, seed, seededCount) {
|
|
21648
|
+
const declared = blueprint.seed?.length ?? 0;
|
|
21649
|
+
if (declared === 0) return seed ? " \u2714 Sample records: none written \u2014 this blueprint declares none\n" : "";
|
|
21650
|
+
if (seed) {
|
|
21651
|
+
const shortfall = seededCount < declared ? " \u2014 the rest were skipped (this blueprint declares no schema for their type)" : "";
|
|
21652
|
+
return ` \u2714 Sample records: seeded ${seededCount} of ${declared} (--seed)${shortfall}
|
|
21653
|
+
`;
|
|
21654
|
+
}
|
|
21655
|
+
return ` \u2714 Sample records: none written \u2014 this blueprint declares ${declared}; pass --seed to write them
|
|
21656
|
+
`;
|
|
21657
|
+
}
|
|
21658
|
+
var APPLY_HELP = `vectros blueprint apply \u2014 provision a blueprint into a tenant, minting no key.
|
|
21659
|
+
|
|
21660
|
+
Usage:
|
|
21661
|
+
vectros blueprint apply <file|name> --tenant test|live [--yes] [--prune] [--env staging|production]
|
|
21662
|
+
[--set name=value ...] [--values <file>] [--seed]
|
|
21663
|
+
[--confirm-existing-principal] [--require-verified-issuers]
|
|
21664
|
+
[--allow-tenant-wide-namespaces] [--allow-script-adoption]
|
|
21665
|
+
[--token <t> | env VECTROS_BOOTSTRAP_TOKEN] [--base-url <url>]
|
|
21666
|
+
|
|
21667
|
+
<file|name> is a path to your own blueprint (YAML/JSON) or the name of a bundled one
|
|
21668
|
+
(${BLUEPRINT_NAMES.join(", ")}). Everything it declares is created or reconciled \u2014 the context, its
|
|
21669
|
+
service principal and access profile, schemas, roles, issuers, triggers and scripts (and, only with --seed, its
|
|
21670
|
+
sample seed records) \u2014 but NO scoped key is minted, nothing is written to a keyring and no MCP client config is
|
|
21671
|
+
touched, so a CI runner can re-apply a changed blueprint. To also get a credential, use 'vectros bootstrap'. It
|
|
21672
|
+
authenticates the same way bootstrap does and refuses on the same gates (scope, capability, tenant-wide namespaces)
|
|
21673
|
+
before anything changes. (A script pushed onto an existing hand-authored name is checked when the scripts are
|
|
21674
|
+
pushed, partway through: --allow-script-adoption.)
|
|
21675
|
+
|
|
21676
|
+
Before it changes anything it prints the account, tenant, context and API it is pointed at, then the plan, and
|
|
21677
|
+
asks to proceed unless --yes is given. With no terminal --yes is required.
|
|
21678
|
+
|
|
21679
|
+
Options:
|
|
21680
|
+
${renderOptionHelp("apply", BLUEPRINT_NAMES)}
|
|
21681
|
+
|
|
21682
|
+
Deletion: apply deletes nothing unless --prune is passed. With --prune it removes each trigger in the context
|
|
21683
|
+
whose stored 'provisionedBy' marker equals this blueprint's name and whose principal's external id is the one this
|
|
21684
|
+
blueprint would derive, and which the file no longer declares. Both are stored markers, not verified identities,
|
|
21685
|
+
and the name comes from the blueprint FILE (after any \${{ inputs.* }} substitution, so --set and --values can change
|
|
21686
|
+
it): a blueprint that reuses another one's name and omits some of its triggers will remove them. A trigger counts
|
|
21687
|
+
as matched when it carries this blueprint's marker and the file no longer declares it; if such a row cannot be
|
|
21688
|
+
removed (no id or principal to remove it by, or a failed read or delete) apply exits 1. Rows with no marker are left
|
|
21689
|
+
alone and reported; rows marked by another blueprint are left alone. Apply still replaces an access profile's scopes
|
|
21690
|
+
and a role's rules in place, as bootstrap does.
|
|
21691
|
+
|
|
21692
|
+
Seed records: unlike bootstrap, apply writes NO sample seed records unless --seed is passed, in either tenant (a
|
|
21693
|
+
blueprint's seeds are illustrative, not real data, and a re-apply from a pipeline must not write them by omission).
|
|
21694
|
+
The plan and the summary say plainly whether they will be or were written. --no-seed is refused here: apply already
|
|
21695
|
+
does not seed, so the flag would do nothing.
|
|
21696
|
+
|
|
21697
|
+
Exit codes:
|
|
21698
|
+
0 applied; also --help, and answering no at "Apply this plan?" (nothing was changed)
|
|
21699
|
+
1 failed while applying, or --prune could not remove a trigger it matched (steps earlier in the plan may
|
|
21700
|
+
already have been applied; a re-run converges)
|
|
21701
|
+
2 refused, before anything was changed: a usage error (no --tenant, no --yes without a terminal, no
|
|
21702
|
+
credential, an unknown flag, an unknown or malformed blueprint, a missing --set input, an invalid base
|
|
21703
|
+
URL), the scope or capability gate, or a tenant-wide namespace without the opt-in. Also 2, but AFTER the
|
|
21704
|
+
context and the service principal were provisioned: an identity the blueprint references could not be
|
|
21705
|
+
resolved (the message says so)
|
|
21706
|
+
3 needs a confirmation it did not get (an existing service principal: --confirm-existing-principal)
|
|
21707
|
+
4 applied in full, but --require-verified-issuers was given and an issuer is pending verification`;
|
|
21708
|
+
function blueprintRefusal(err) {
|
|
21709
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
21710
|
+
if (message.startsWith("Unknown blueprint ")) {
|
|
21711
|
+
return `No bundled blueprint matches the argument you gave. Bundled blueprints: ${BLUEPRINT_NAMES.join(", ")} (or pass a path like ./my.yaml).`;
|
|
21712
|
+
}
|
|
21713
|
+
return message;
|
|
21714
|
+
}
|
|
21715
|
+
var refuse = (message) => {
|
|
21716
|
+
process.stderr.write(`
|
|
21717
|
+
\u2716 ${redactCredentials(message)}
|
|
21718
|
+
Nothing was changed.
|
|
21719
|
+
`);
|
|
21720
|
+
return 2;
|
|
21721
|
+
};
|
|
21722
|
+
async function runApplyCli(argv, deps = {}) {
|
|
21723
|
+
const log = deps.log ?? createLogger2();
|
|
21724
|
+
let args;
|
|
21725
|
+
try {
|
|
21726
|
+
args = parseApplyArgs(argv);
|
|
21727
|
+
} catch (err) {
|
|
21728
|
+
process.stderr.write(
|
|
21729
|
+
`
|
|
21730
|
+
\u2716 ${redactCredentials(err instanceof Error ? err.message : String(err))}
|
|
21731
|
+
See 'vectros blueprint apply --help'. Nothing was changed.
|
|
21732
|
+
`
|
|
21733
|
+
);
|
|
21734
|
+
return 2;
|
|
21735
|
+
}
|
|
21736
|
+
if (args.help) {
|
|
21737
|
+
process.stdout.write(`${APPLY_HELP}
|
|
21738
|
+
`);
|
|
21739
|
+
return 0;
|
|
21740
|
+
}
|
|
21741
|
+
if (!args.blueprint) {
|
|
21742
|
+
return refuse("apply needs a blueprint: vectros blueprint apply <file|name> --tenant test|live");
|
|
21743
|
+
}
|
|
21744
|
+
if (!args.tenant) {
|
|
21745
|
+
return refuse(
|
|
21746
|
+
"apply needs --tenant test|live. There is no default, so a run never lands on 'live' by omission."
|
|
21747
|
+
);
|
|
21748
|
+
}
|
|
21749
|
+
const tenant = args.tenant;
|
|
21750
|
+
const isTty = deps.isTty ?? Boolean(process.stdin.isTTY);
|
|
21751
|
+
if (!args.yes && !isTty) {
|
|
21752
|
+
return refuse(
|
|
21753
|
+
"apply needs --yes when there is no terminal to confirm on: it changes a tenant" + (args.prune ? " and, with --prune, deletes triggers" : "") + ", so it never runs unattended without it. Re-run with --yes."
|
|
21754
|
+
);
|
|
21755
|
+
}
|
|
21756
|
+
let baseUrl;
|
|
21757
|
+
try {
|
|
21758
|
+
baseUrl = envToBaseUrl(args.env, args.baseUrl ?? process.env.VECTROS_API_BASE_URL);
|
|
21759
|
+
} catch (err) {
|
|
21760
|
+
return refuse(err instanceof Error ? err.message : String(err));
|
|
21761
|
+
}
|
|
21762
|
+
const promptCtx = deps.createPrompt ? deps.createPrompt() : (() => {
|
|
21763
|
+
const rl = readline3.createInterface({ input: process.stdin, output: process.stdout });
|
|
21764
|
+
return { prompt: (q) => rl.question(q), close: () => rl.close() };
|
|
21765
|
+
})();
|
|
21766
|
+
try {
|
|
21767
|
+
let blueprint;
|
|
21768
|
+
try {
|
|
21769
|
+
blueprint = await resolveBlueprint(
|
|
21770
|
+
{ blueprint: args.blueprint, set: args.set, values: args.values, blankSlate: false },
|
|
21771
|
+
promptCtx.prompt,
|
|
21772
|
+
isTty
|
|
21773
|
+
);
|
|
21774
|
+
validateBlueprint(blueprint);
|
|
21775
|
+
assertTenantWideNamespacesAllowed(blueprint, args.allowTenantWideNamespaces);
|
|
21776
|
+
} catch (err) {
|
|
21777
|
+
if (err instanceof ScopeGateError || err instanceof CapabilityGateError) {
|
|
21778
|
+
process.stderr.write(
|
|
21779
|
+
`
|
|
21780
|
+
\u2716 ${err instanceof ScopeGateError ? "Scope" : "Capability"} gate refused blueprint:
|
|
21781
|
+
`
|
|
21782
|
+
);
|
|
21783
|
+
for (const r of err.rejected) process.stderr.write(` \u2022 ${formatRejection(r)}
|
|
21784
|
+
`);
|
|
21785
|
+
process.stderr.write(" Nothing was changed.\n");
|
|
21786
|
+
return 2;
|
|
21787
|
+
}
|
|
21788
|
+
return refuse(blueprintRefusal(err));
|
|
21789
|
+
}
|
|
21790
|
+
process.stdout.write(gateSummary(blueprint));
|
|
21791
|
+
const noSeed = !args.seed;
|
|
21792
|
+
let cognitoToken;
|
|
21793
|
+
try {
|
|
21794
|
+
cognitoToken = await resolveBridgeCognitoToken(args, promptCtx.prompt, isTty, deps.resolveCognitoToken);
|
|
21795
|
+
} catch (err) {
|
|
21796
|
+
return refuse(err instanceof Error ? err.message : String(err));
|
|
21797
|
+
}
|
|
21798
|
+
const mintBridge = deps.mintBridgeToken ?? mintBridgeToken;
|
|
21799
|
+
const mintContext = deps.mintContextToken ?? mintContextToken;
|
|
21800
|
+
let client;
|
|
21801
|
+
let tenantId;
|
|
21802
|
+
try {
|
|
21803
|
+
const bridge = await mintBridge({ cognitoToken, apiBaseUrl: baseUrl, tenant });
|
|
21804
|
+
client = new VectrosClient({ token: bridge.token, environment: baseUrl });
|
|
21805
|
+
const identity = await (deps.resolveIdentity ?? resolveIdentity)({ log, apiKey: bridge.token, environment: baseUrl });
|
|
21806
|
+
if (!identity.tenantId || typeof identity.tenantId !== "string") {
|
|
21807
|
+
throw new Error("the extended ping (tenant_id) must be deployed for apply to show which account it is pointed at");
|
|
21808
|
+
}
|
|
21809
|
+
tenantId = identity.tenantId;
|
|
21810
|
+
} catch (err) {
|
|
21811
|
+
process.stderr.write(`
|
|
21812
|
+
${formatCliError(err, { action: "Could not authenticate or resolve the account" })} Nothing was changed.
|
|
21813
|
+
`);
|
|
21814
|
+
return 1;
|
|
21815
|
+
}
|
|
21816
|
+
process.stdout.write(
|
|
21817
|
+
`
|
|
21818
|
+
Target
|
|
21819
|
+
account ${tenantId}
|
|
21820
|
+
tenant ${tenant}
|
|
21821
|
+
context ${blueprint.contextId}
|
|
21822
|
+
api ${baseUrl}
|
|
21823
|
+
`
|
|
21824
|
+
);
|
|
21825
|
+
process.stdout.write(
|
|
21826
|
+
`
|
|
21827
|
+
${renderPlan(blueprint, buildPlan(blueprint, { noSeed, noKey: true }), { prune: args.prune })}
|
|
21828
|
+
`
|
|
21829
|
+
);
|
|
21830
|
+
process.stdout.write(applySeedNotice(blueprint, args.seed));
|
|
21831
|
+
const declaredIdentities = Object.entries(blueprint.identities ?? {});
|
|
21832
|
+
if (declaredIdentities.length > 0) {
|
|
21833
|
+
process.stdout.write("\n Identities to ensure (tenant-wide, idempotent by external id):\n");
|
|
21834
|
+
for (const [name, decl] of declaredIdentities) {
|
|
21835
|
+
process.stdout.write(` ~ ${name} (${decl.kind}) \u2014 external id '${decl.externalId}'
|
|
21836
|
+
`);
|
|
21837
|
+
}
|
|
21838
|
+
}
|
|
21839
|
+
if (args.prune) {
|
|
21840
|
+
process.stdout.write(
|
|
21841
|
+
`
|
|
21842
|
+
\u26A0 --prune: this apply will also remove any trigger provisioned under blueprint '${blueprint.name}' that the file no longer declares \u2014 matched by stored marker, not a verified identity.
|
|
21843
|
+
`
|
|
21844
|
+
);
|
|
21845
|
+
}
|
|
21846
|
+
if (!args.yes) {
|
|
21847
|
+
const ans = (await promptCtx.prompt("\n Apply this plan? [y/N]: ")).trim().toLowerCase();
|
|
21848
|
+
if (ans !== "y" && ans !== "yes") {
|
|
21849
|
+
process.stdout.write(" Aborted \u2014 nothing was changed.\n");
|
|
21850
|
+
return 0;
|
|
21851
|
+
}
|
|
21852
|
+
try {
|
|
21853
|
+
client = new VectrosClient({
|
|
21854
|
+
token: (await mintBridge({ cognitoToken, apiBaseUrl: baseUrl, tenant })).token,
|
|
21855
|
+
environment: baseUrl
|
|
21856
|
+
});
|
|
21857
|
+
} catch (err) {
|
|
21858
|
+
process.stderr.write(
|
|
21859
|
+
`
|
|
21860
|
+
${formatCliError(err, { action: "Could not mint a fresh bootstrap token after your confirmation" })} Nothing was changed.
|
|
21861
|
+
`
|
|
21862
|
+
);
|
|
21863
|
+
return 1;
|
|
21864
|
+
}
|
|
21865
|
+
}
|
|
21866
|
+
let confirmedBy;
|
|
21867
|
+
const confirmExistingPrincipal = async (d) => {
|
|
21868
|
+
if (!args.yes && isTty) {
|
|
21869
|
+
const ans = await promptCtx.prompt(
|
|
21870
|
+
`
|
|
21871
|
+
\u26A0 '${d.externalId}' already exists in your tenant (context '${d.contextId}') \u2014 this apply will REWRITE that
|
|
21872
|
+
existing identity's access profile to the blueprint's grants. Expected when you re-apply a blueprint you
|
|
21873
|
+
already manage. If you don't recognize this identity, do NOT proceed: it may be a name collision with
|
|
21874
|
+
something you don't control.
|
|
21875
|
+
Proceed and rewrite its access profile? [y/N]: `
|
|
21876
|
+
);
|
|
21877
|
+
const ok = existingPrincipalConfirmed(ans);
|
|
21878
|
+
if (ok) confirmedBy = "prompt";
|
|
21879
|
+
return ok;
|
|
21880
|
+
}
|
|
21881
|
+
if (args.confirmExistingPrincipal) confirmedBy = "flag";
|
|
21882
|
+
return args.confirmExistingPrincipal;
|
|
21883
|
+
};
|
|
21884
|
+
const remintBootstrapForContext = buildRemintBootstrapForContext({
|
|
21885
|
+
cognitoToken,
|
|
21886
|
+
apiBaseUrl: baseUrl,
|
|
21887
|
+
tenant,
|
|
21888
|
+
mintBridge
|
|
21889
|
+
});
|
|
21890
|
+
const provisioned = await (deps.provisionContext ?? provisionContext)(client, {
|
|
21891
|
+
blueprint,
|
|
21892
|
+
allowTenantWideNamespaces: args.allowTenantWideNamespaces,
|
|
21893
|
+
remintBootstrapForContext,
|
|
21894
|
+
// No key is minted or read here, so there is never a local record: the check fires for any principal
|
|
21895
|
+
// that already existed.
|
|
21896
|
+
readExistingKey: async () => void 0,
|
|
21897
|
+
confirmExistingPrincipal,
|
|
21898
|
+
skipKey: true,
|
|
21899
|
+
log
|
|
21900
|
+
});
|
|
21901
|
+
if (provisioned.userAction === "reconciled") {
|
|
21902
|
+
process.stdout.write(
|
|
21903
|
+
` \u26A0 reusing existing service principal '${blueprint.servicePrincipal.externalId}' (id ${provisioned.servicePrincipalUserId}` + (provisioned.servicePrincipalCreatedAt ? `, created ${provisioned.servicePrincipalCreatedAt}` : "") + `) \u2014 proceeding because ` + (confirmedBy === "prompt" ? "you confirmed it at the prompt.\n" : "--confirm-existing-principal was passed (in a pipeline that is standing consent).\n")
|
|
21904
|
+
);
|
|
21905
|
+
}
|
|
21906
|
+
const resolvedBlueprint = await (deps.resolveApplyTimeIdentities ?? resolveApplyTimeIdentities)(
|
|
21907
|
+
blueprint,
|
|
21908
|
+
provisioned,
|
|
21909
|
+
client,
|
|
21910
|
+
log
|
|
21911
|
+
);
|
|
21912
|
+
const result = await (deps.runLoader ?? runLoader)(client, {
|
|
21913
|
+
blueprint: resolvedBlueprint,
|
|
21914
|
+
tenantId,
|
|
21915
|
+
machineId: "apply",
|
|
21916
|
+
skipKey: true,
|
|
21917
|
+
skipSeed: noSeed,
|
|
21918
|
+
// Off unless asked for: apply never deletes on its own. `blueprint-test` sets the same switch off.
|
|
21919
|
+
pruneTriggers: args.prune,
|
|
21920
|
+
allowScriptAdoption: args.allowScriptAdoption,
|
|
21921
|
+
readExistingKey: async () => void 0,
|
|
21922
|
+
persistMintedKey: async () => {
|
|
21923
|
+
},
|
|
21924
|
+
provisioned,
|
|
21925
|
+
remintBootstrapForContext,
|
|
21926
|
+
remintForContext: buildRemintForContext({ cognitoToken, apiBaseUrl: baseUrl, tenant, mintContext }),
|
|
21927
|
+
log
|
|
21928
|
+
});
|
|
21929
|
+
process.stdout.write(
|
|
21930
|
+
applyOutcomeSummary(result, { principalNote: false }) + ` \u2714 Applied '${blueprint.name}' to the ${tenant} tenant, context '${blueprint.contextId}'. No scoped key was minted and no client configuration was touched.
|
|
21931
|
+
` + applySeedSummary(blueprint, args.seed, result.seededCount) + seedIndexWarningBlock(result.seedIndexFailures) + (args.prune ? "" : ` \u2139 Triggers were not pruned (--prune was not passed): a trigger this blueprint provisioned earlier and its file no longer declares is left in place. Pass --prune to remove it.
|
|
21932
|
+
`)
|
|
21933
|
+
);
|
|
21934
|
+
const issuerCode = finishIssuers(
|
|
21935
|
+
result.issuerActions,
|
|
21936
|
+
{ context: blueprint.contextId, tenant, env: args.env, baseUrl: args.baseUrl },
|
|
21937
|
+
args.requireVerifiedIssuers,
|
|
21938
|
+
(deps.now ?? Date.now)(),
|
|
21939
|
+
"then re-run this `vectros blueprint apply` command"
|
|
21940
|
+
);
|
|
21941
|
+
const pruneFailures = result.triggerPruneSkipped.filter((s) => s.failed !== false);
|
|
21942
|
+
if (args.prune && pruneFailures.length > 0) {
|
|
21943
|
+
process.stderr.write(
|
|
21944
|
+
`
|
|
21945
|
+
\u2716 --prune: ${pruneFailures.length} removal${pruneFailures.length === 1 ? "" : "s"} could not be carried out (exit 1). The rest of the apply completed; the trigger(s) named above may still be live.
|
|
21946
|
+
`
|
|
21947
|
+
);
|
|
21948
|
+
return 1;
|
|
21949
|
+
}
|
|
21950
|
+
return issuerCode;
|
|
21951
|
+
} catch (err) {
|
|
21952
|
+
return reportApplyError(err);
|
|
21953
|
+
} finally {
|
|
21954
|
+
promptCtx.close();
|
|
21955
|
+
}
|
|
21956
|
+
}
|
|
21957
|
+
function reportApplyError(err) {
|
|
21958
|
+
if (err instanceof ExistingPrincipalNotConfirmedError) {
|
|
21959
|
+
process.stderr.write(
|
|
21960
|
+
`
|
|
21961
|
+
\u2716 ${err.message}
|
|
21962
|
+
|
|
21963
|
+
If this is expected (a re-apply of a blueprint you manage), re-run with --confirm-existing-principal
|
|
21964
|
+
(with --yes in a pipeline, where it is standing consent), or confirm at the prompt.
|
|
21965
|
+
If you don't recognize this identity, investigate the blueprint's servicePrincipal.externalId first.
|
|
21966
|
+
Only the app-context and the principal lookup had run; no grant or anything else was changed.
|
|
21967
|
+
`
|
|
21968
|
+
);
|
|
21969
|
+
return 3;
|
|
21970
|
+
}
|
|
21971
|
+
if (err instanceof BlueprintIdentityError || err instanceof UnresolvedIdentitiesError) {
|
|
21972
|
+
process.stderr.write(
|
|
21973
|
+
`
|
|
21974
|
+
\u2716 ${err.message}
|
|
21975
|
+
The app-context, service principal, and any declared namespaces were already provisioned. No schemas, profile, roles or seed records were.
|
|
21976
|
+
`
|
|
21977
|
+
);
|
|
21978
|
+
return 2;
|
|
21979
|
+
}
|
|
21980
|
+
if (err instanceof IssuerScopeNotAppliedError) {
|
|
21981
|
+
process.stderr.write(
|
|
21982
|
+
`
|
|
21983
|
+
\u2716 ${err.message}
|
|
21984
|
+
The app-context, service principal, and any declared namespaces were already provisioned. No schemas, profile, roles or seed records were.
|
|
21985
|
+
`
|
|
21986
|
+
);
|
|
21987
|
+
return 1;
|
|
21988
|
+
}
|
|
21989
|
+
if (err instanceof TenantWideNamespacesNotAllowedError) {
|
|
21990
|
+
process.stderr.write(`
|
|
21991
|
+
\u2716 ${err.message}
|
|
21992
|
+
Nothing was changed.
|
|
21993
|
+
`);
|
|
21994
|
+
return 2;
|
|
21995
|
+
}
|
|
21996
|
+
process.stderr.write(
|
|
21997
|
+
`
|
|
21998
|
+
${formatCliError(err, { action: "Apply failed" })} The apply stopped part-way: steps earlier in the plan may already have been applied. Re-running converges.
|
|
21999
|
+
`
|
|
22000
|
+
);
|
|
22001
|
+
if (process.env.VECTROS_MCP_DEBUG && err instanceof Error && err.stack) {
|
|
22002
|
+
process.stderr.write(`${err.stack}
|
|
22003
|
+
`);
|
|
22004
|
+
}
|
|
22005
|
+
return 1;
|
|
22006
|
+
}
|
|
22007
|
+
|
|
22008
|
+
// src/lint.ts
|
|
22009
|
+
var ENUM_COMPATIBLE_TYPES = /* @__PURE__ */ new Set(["enum", "string", "string[]"]);
|
|
22010
|
+
var BUILT_IN_SORT_BY = /* @__PURE__ */ new Set(["createdAt", "lastUpdated"]);
|
|
22011
|
+
function lookupFieldNames(lf) {
|
|
22012
|
+
if (typeof lf === "string") return [lf];
|
|
22013
|
+
if (lf.fieldNames !== void 0) return lf.fieldNames;
|
|
22014
|
+
return lf.fieldName !== void 0 ? [lf.fieldName] : [];
|
|
22015
|
+
}
|
|
22016
|
+
function compositeLegFindings(schema, lf, fieldsById, rangeEnabledFieldNames) {
|
|
22017
|
+
const findings = [];
|
|
22018
|
+
const id = `[${lf.fieldNames.join(", ")}]`;
|
|
22019
|
+
const seenLegs = /* @__PURE__ */ new Set();
|
|
22020
|
+
for (const leg of lf.fieldNames) {
|
|
22021
|
+
if (seenLegs.has(leg)) {
|
|
22022
|
+
findings.push({
|
|
22023
|
+
severity: "error",
|
|
22024
|
+
rule: "lookup-composite-repeated-leg",
|
|
22025
|
+
message: `schema '${schema.typeName}': composite lookupField ${id} repeats the leg '${leg}'. Each leg occupies its own position in the sort key, so a repeat doubles the key cost of every row to express a conjunction of a field with itself.`
|
|
22026
|
+
});
|
|
22027
|
+
}
|
|
22028
|
+
seenLegs.add(leg);
|
|
22029
|
+
const field = fieldsById.get(leg);
|
|
22030
|
+
if (!field) continue;
|
|
22031
|
+
if (field.fieldType === "array" || field.fieldType === "object") {
|
|
22032
|
+
findings.push({
|
|
22033
|
+
severity: "error",
|
|
22034
|
+
rule: "lookup-composite-leg-multi-valued",
|
|
22035
|
+
message: `schema '${schema.typeName}': composite lookupField ${id} uses '${leg}' as a leg, but it is declared '${field.fieldType}'. A leg occupies one sort-key position and holds one value \u2014 a multi-valued or structured field would index only one rendering and the conjunction would return a subset while appearing complete. Use a scalar field as a leg.`
|
|
22036
|
+
});
|
|
22037
|
+
}
|
|
22038
|
+
if (field.fieldType === "reference" && field.cardinality === "many") {
|
|
22039
|
+
findings.push({
|
|
22040
|
+
severity: "error",
|
|
22041
|
+
rule: "lookup-composite-leg-multi-valued",
|
|
22042
|
+
message: `schema '${schema.typeName}': composite lookupField ${id} uses '${leg}' as a leg, but it is a reference with cardinality 'many'. One sort-key position holds one value, so a multi-valued leg would index only one target and the conjunction would silently return a subset. Use a cardinality 'one' reference, or a scalar field.`
|
|
22043
|
+
});
|
|
22044
|
+
}
|
|
22045
|
+
if (rangeEnabledFieldNames.has(leg)) {
|
|
22046
|
+
findings.push({
|
|
22047
|
+
severity: "error",
|
|
22048
|
+
rule: "lookup-composite-leg-range-enabled",
|
|
22049
|
+
message: `schema '${schema.typeName}': composite lookupField ${id} uses '${leg}' as a leg, but '${leg}' has its own lookupField declaring rangeEnabled \u2014 that stores it as an ordered row keyed on its own value, not an equality index. A leg is matched exactly. Declare the composite over non-range fields, and keep the range lookup as its own declaration.`
|
|
22050
|
+
});
|
|
22051
|
+
}
|
|
22052
|
+
if (lf.sortBy !== void 0 && leg === lf.sortBy) {
|
|
22053
|
+
findings.push({
|
|
22054
|
+
severity: "error",
|
|
22055
|
+
rule: "lookup-composite-leg-is-sortby",
|
|
22056
|
+
message: `schema '${schema.typeName}': composite lookupField ${id} cannot use '${leg}' as both a leg and its sortBy target. Within a fixed tuple every matching record has the same value for that leg, so the sort payload would be constant and a sortFrom/sortTo window would return either everything or nothing \u2014 at double the sort-key cost. Sort by a different field, 'createdAt', or 'lastUpdated'.`
|
|
22057
|
+
});
|
|
22058
|
+
}
|
|
22059
|
+
}
|
|
22060
|
+
return findings;
|
|
22061
|
+
}
|
|
22062
|
+
function lintBlueprint(bp) {
|
|
22063
|
+
const findings = [];
|
|
22064
|
+
const typeNames = /* @__PURE__ */ new Set();
|
|
22065
|
+
const expectedDimsByTypeName = /* @__PURE__ */ new Map();
|
|
22066
|
+
for (const schema of bp.schemas) {
|
|
22067
|
+
typeNames.add(schema.typeName);
|
|
22068
|
+
if (schema.expectedScopeDims && schema.expectedScopeDims.length > 0) {
|
|
22069
|
+
expectedDimsByTypeName.set(schema.typeName, schema.expectedScopeDims);
|
|
22070
|
+
}
|
|
22071
|
+
const fieldIds = /* @__PURE__ */ new Set();
|
|
22072
|
+
const fieldsById = /* @__PURE__ */ new Map();
|
|
22073
|
+
for (const f of schema.fields) {
|
|
22074
|
+
if (fieldIds.has(f.fieldId)) {
|
|
22075
|
+
findings.push({
|
|
22076
|
+
severity: "error",
|
|
22077
|
+
rule: "duplicate-field-id",
|
|
22078
|
+
message: `schema '${schema.typeName}': duplicate fieldId '${f.fieldId}'.`
|
|
22079
|
+
});
|
|
22080
|
+
}
|
|
22081
|
+
fieldIds.add(f.fieldId);
|
|
22082
|
+
fieldsById.set(f.fieldId, f);
|
|
22083
|
+
if (f.enumValues && f.enumValues.length > 0 && !ENUM_COMPATIBLE_TYPES.has(f.fieldType)) {
|
|
22084
|
+
findings.push({
|
|
22085
|
+
severity: "warning",
|
|
22086
|
+
rule: "enum-values-on-incompatible-type",
|
|
22087
|
+
message: `schema '${schema.typeName}', field '${f.fieldId}': enumValues set on fieldType '${f.fieldType}' (expected enum/string). The values will likely be ignored.`
|
|
22088
|
+
});
|
|
22089
|
+
}
|
|
22090
|
+
}
|
|
22091
|
+
const rangeEnabledFieldNames = /* @__PURE__ */ new Set();
|
|
22092
|
+
for (const lf of schema.lookupFields ?? []) {
|
|
22093
|
+
if (typeof lf !== "string" && lf.fieldName !== void 0 && lf.rangeEnabled) {
|
|
22094
|
+
rangeEnabledFieldNames.add(lf.fieldName);
|
|
22095
|
+
}
|
|
22096
|
+
}
|
|
22097
|
+
for (const lf of schema.lookupFields ?? []) {
|
|
22098
|
+
for (const name of lookupFieldNames(lf)) {
|
|
22099
|
+
if (!fieldIds.has(name)) {
|
|
22100
|
+
findings.push({
|
|
22101
|
+
severity: "error",
|
|
22102
|
+
rule: "lookup-field-unknown",
|
|
22103
|
+
message: `schema '${schema.typeName}': lookupField '${name}' is not a declared field.`
|
|
22104
|
+
});
|
|
22105
|
+
}
|
|
22106
|
+
}
|
|
22107
|
+
if (typeof lf !== "string" && lf.fieldNames !== void 0) {
|
|
22108
|
+
findings.push(
|
|
22109
|
+
...compositeLegFindings(schema, { fieldNames: lf.fieldNames, sortBy: lf.sortBy }, fieldsById, rangeEnabledFieldNames)
|
|
22110
|
+
);
|
|
22111
|
+
}
|
|
22112
|
+
if (typeof lf !== "string" && lf.sortBy !== void 0) {
|
|
22113
|
+
if (!BUILT_IN_SORT_BY.has(lf.sortBy) && !fieldIds.has(lf.sortBy)) {
|
|
22114
|
+
findings.push({
|
|
22115
|
+
severity: "warning",
|
|
22116
|
+
rule: "lookup-sortby-unknown-field",
|
|
22117
|
+
message: `schema '${schema.typeName}': lookupField sortBy '${lf.sortBy}' is not a declared field (nor 'createdAt'/'lastUpdated'). The platform does not reject this \u2014 it silently treats the sort target as an untyped field no record ever populates, which usually isn't what was intended. Likely a typo.`
|
|
22118
|
+
});
|
|
22119
|
+
}
|
|
22120
|
+
}
|
|
22121
|
+
}
|
|
22122
|
+
}
|
|
22123
|
+
for (const rec of bp.seed ?? []) {
|
|
22124
|
+
if (!typeNames.has(rec.typeName)) {
|
|
22125
|
+
findings.push({
|
|
22126
|
+
severity: "error",
|
|
22127
|
+
rule: "seed-unknown-record-type",
|
|
20621
22128
|
message: `seed '${rec.externalId}': typeName '${rec.typeName}' has no matching schema (the loader would skip it).`
|
|
20622
22129
|
});
|
|
20623
22130
|
}
|
|
@@ -20779,7 +22286,10 @@ function defaultScaffold(name) {
|
|
|
20779
22286
|
return `# ${name}.blueprint.yaml \u2014 a Vectros blueprint.
|
|
20780
22287
|
# Edit, then check it with: vectros blueprint validate ./${name}.blueprint.yaml
|
|
20781
22288
|
# Preview what it provisions: vectros blueprint plan ./${name}.blueprint.yaml
|
|
20782
|
-
# Apply it:
|
|
22289
|
+
# Apply it (no key): vectros blueprint apply ./${name}.blueprint.yaml --tenant test
|
|
22290
|
+
# (writes the sample seed records below only with --seed)
|
|
22291
|
+
# Apply it and mint a key: vectros bootstrap --blueprint ./${name}.blueprint.yaml
|
|
22292
|
+
# (writes them unless you pass --no-seed)
|
|
20783
22293
|
|
|
20784
22294
|
name: ${name} # stable id + idempotency key. Re-running NEVER duplicates.
|
|
20785
22295
|
version: 1.0.0
|
|
@@ -20798,9 +22308,9 @@ schemas:
|
|
|
20798
22308
|
fieldType: string # string | number | boolean | date | enum | array | object
|
|
20799
22309
|
required: true
|
|
20800
22310
|
searchable: true # contributes to full-text + semantic search
|
|
20801
|
-
|
|
20802
|
-
|
|
20803
|
-
|
|
22311
|
+
# Do NOT declare an externalId field here: it is a first-class identifier the
|
|
22312
|
+
# platform reserves (a seed's externalId is set at the TOP LEVEL of the seed, below),
|
|
22313
|
+
# and it has its own built-in finder, so it is not listed under lookupFields either.
|
|
20804
22314
|
lookupFields: [title] # extra O(1) exact-match lookup indexes (max 10)
|
|
20805
22315
|
|
|
20806
22316
|
accessProfile:
|
|
@@ -20836,7 +22346,8 @@ servicePrincipal:
|
|
|
20836
22346
|
seed:
|
|
20837
22347
|
- surface: record # 'record' (createRecord) or 'document' (text-ingest: needs title + text)
|
|
20838
22348
|
typeName: thing
|
|
20839
|
-
externalId: seed-welcome # stable \u2192 idempotent; re-apply converges
|
|
22349
|
+
externalId: seed-welcome # stable \u2192 idempotent; re-apply converges. Top level ONLY:
|
|
22350
|
+
# never inside 'fields', never as a schema field.
|
|
20840
22351
|
fields: { title: "Hello from ${name}" }
|
|
20841
22352
|
`;
|
|
20842
22353
|
}
|
|
@@ -20904,7 +22415,10 @@ Usage:
|
|
|
20904
22415
|
vectros blueprint init <name> [--from <bundled>] [--out <path>] [--force]
|
|
20905
22416
|
Scaffold a new blueprint YAML to edit (no creds).
|
|
20906
22417
|
vectros blueprint validate <file|name> Structural + scope-gate + lint checks (no creds).
|
|
20907
|
-
vectros blueprint plan <file|name> Validate, then preview what bootstrap WOULD provision.
|
|
22418
|
+
vectros blueprint plan <file|name> Validate, then preview what bootstrap WOULD provision (apply: same, minus the sample seeds unless --seed).
|
|
22419
|
+
vectros blueprint apply <file|name> --tenant test|live
|
|
22420
|
+
Provision it into a tenant, minting NO key (needs credentials;
|
|
22421
|
+
'vectros blueprint apply --help' for its flags and exit codes).
|
|
20908
22422
|
vectros blueprint list List the bundled blueprint library.
|
|
20909
22423
|
|
|
20910
22424
|
<file|name> is a path to your own blueprint (YAML .yaml/.yml or JSON .json) OR
|
|
@@ -21029,10 +22543,17 @@ ${renderPlan(blueprint, steps)}
|
|
|
21029
22543
|
}
|
|
21030
22544
|
}
|
|
21031
22545
|
const setEcho = [...args.set].join(" --set ");
|
|
22546
|
+
const inputs = (args.set.length ? ` --set ${setEcho}` : "") + (args.values ? ` --values ${args.values}` : "");
|
|
21032
22547
|
process.stdout.write(
|
|
21033
22548
|
`
|
|
21034
22549
|
${steps.length} step(s). This is a preview \u2014 nothing was minted.
|
|
21035
|
-
Apply with: vectros
|
|
22550
|
+
Apply with: vectros blueprint apply ${args.file} --tenant test${inputs}
|
|
22551
|
+
(no scoped key is minted; write --tenant live for your live tenant), or
|
|
22552
|
+
vectros bootstrap --blueprint ${args.file}${inputs}
|
|
22553
|
+
(which also mints a key for your MCP client).
|
|
22554
|
+
` + (blueprint.seed?.length ? ` The plan above includes the sample seed records: bootstrap writes them unless --no-seed;
|
|
22555
|
+
blueprint apply writes them only when you pass --seed.
|
|
22556
|
+
` : "")
|
|
21036
22557
|
);
|
|
21037
22558
|
return 0;
|
|
21038
22559
|
}
|
|
@@ -21096,7 +22617,8 @@ function runList() {
|
|
|
21096
22617
|
}
|
|
21097
22618
|
process.stdout.write(
|
|
21098
22619
|
`
|
|
21099
|
-
Apply one with: vectros
|
|
22620
|
+
Apply one with: vectros blueprint apply <name> --tenant test (no key)
|
|
22621
|
+
vectros bootstrap --blueprint <name> (also mints a key)
|
|
21100
22622
|
Scaffold your own: vectros blueprint init my-thing
|
|
21101
22623
|
Validate it: vectros blueprint validate ./my-thing.blueprint.yaml
|
|
21102
22624
|
`
|
|
@@ -21104,7 +22626,7 @@ function runList() {
|
|
|
21104
22626
|
return 0;
|
|
21105
22627
|
}
|
|
21106
22628
|
async function runBlueprintCli(argv, deps = {}) {
|
|
21107
|
-
|
|
22629
|
+
if (argv[0] === "apply") return runApplyCli(argv.slice(1), deps);
|
|
21108
22630
|
let args;
|
|
21109
22631
|
try {
|
|
21110
22632
|
args = parseBlueprintArgs(argv);
|
|
@@ -21578,7 +23100,7 @@ ${list}
|
|
|
21578
23100
|
return storeAndReport(dest, key, auth, {
|
|
21579
23101
|
...opts,
|
|
21580
23102
|
principalId: `usr_${userId}`,
|
|
21581
|
-
revokedKeyId: existing?.keyId
|
|
23103
|
+
revokedKeyId: existing?.keyId ? [existing.keyId] : void 0
|
|
21582
23104
|
});
|
|
21583
23105
|
emitMintedKey(key, opts.format);
|
|
21584
23106
|
return 0;
|
|
@@ -22594,7 +24116,7 @@ async function runRoleDelete(opts, deps = {}) {
|
|
|
22594
24116
|
}
|
|
22595
24117
|
|
|
22596
24118
|
// src/commands/context.ts
|
|
22597
|
-
import * as
|
|
24119
|
+
import * as readline4 from "readline/promises";
|
|
22598
24120
|
function renderContext(c) {
|
|
22599
24121
|
return formatKeyValues([
|
|
22600
24122
|
["context", c.contextId],
|
|
@@ -22675,7 +24197,7 @@ async function runContextDestroy(opts, deps = {}) {
|
|
|
22675
24197
|
return 2;
|
|
22676
24198
|
}
|
|
22677
24199
|
const promptCtx = deps.createPrompt ? deps.createPrompt() : (() => {
|
|
22678
|
-
const rl =
|
|
24200
|
+
const rl = readline4.createInterface({ input: process.stdin, output: process.stdout });
|
|
22679
24201
|
return { prompt: (q) => rl.question(q), close: () => rl.close() };
|
|
22680
24202
|
})();
|
|
22681
24203
|
try {
|
|
@@ -22746,7 +24268,8 @@ ${renderContext(ctx)}
|
|
|
22746
24268
|
}
|
|
22747
24269
|
|
|
22748
24270
|
// src/commands/issuers.ts
|
|
22749
|
-
import * as
|
|
24271
|
+
import * as readline5 from "readline/promises";
|
|
24272
|
+
import { createReadStream } from "fs";
|
|
22750
24273
|
function parseSelfSignupPolicies(raw) {
|
|
22751
24274
|
if (!raw || raw.length === 0) return void 0;
|
|
22752
24275
|
return raw.map((entry) => {
|
|
@@ -22785,6 +24308,24 @@ function parseCapturedClaims(raw) {
|
|
|
22785
24308
|
}
|
|
22786
24309
|
return [...raw];
|
|
22787
24310
|
}
|
|
24311
|
+
function parseRestrictedToDomain(raw) {
|
|
24312
|
+
if (raw === void 0) return void 0;
|
|
24313
|
+
const domain = raw.trim();
|
|
24314
|
+
if (domain.length === 0) {
|
|
24315
|
+
throw new Error(
|
|
24316
|
+
"--restricted-to-domain must be a non-blank domain (e.g. 'acme.com'). To remove the scope from an existing issuer, use --clear-restricted-to-domain."
|
|
24317
|
+
);
|
|
24318
|
+
}
|
|
24319
|
+
if (!isRestrictedToDomainName(domain.toLowerCase())) {
|
|
24320
|
+
throw new Error(
|
|
24321
|
+
`--restricted-to-domain must be a DNS name such as 'acme.com' \u2014 letters, digits and dashes in dot-separated labels, at least two labels, no scheme, path or port (write an internationalized domain in its punycode xn-- form); got '${domain}'. To remove the scope from an existing issuer, use --clear-restricted-to-domain.`
|
|
24322
|
+
);
|
|
24323
|
+
}
|
|
24324
|
+
return domain;
|
|
24325
|
+
}
|
|
24326
|
+
function renderRestrictedToDomain(domain) {
|
|
24327
|
+
return domain ? domain : "\u2014";
|
|
24328
|
+
}
|
|
22788
24329
|
function renderCapturedClaims(claims) {
|
|
22789
24330
|
if (!claims || claims.length === 0) return "\u2014";
|
|
22790
24331
|
return claims.join(", ");
|
|
@@ -22794,6 +24335,7 @@ function renderSelfSignup(policies) {
|
|
|
22794
24335
|
return policies.map((p) => `${p.signup_type} \u2192 ${p.role_id}`).join(", ");
|
|
22795
24336
|
}
|
|
22796
24337
|
function renderIssuer(i) {
|
|
24338
|
+
const pending = isPendingVerification(i);
|
|
22797
24339
|
return formatKeyValues([
|
|
22798
24340
|
["issuerId", i.issuerId],
|
|
22799
24341
|
["issuer", i.issuer],
|
|
@@ -22806,9 +24348,22 @@ function renderIssuer(i) {
|
|
|
22806
24348
|
["status", i.status],
|
|
22807
24349
|
["createdAt", i.createdAt],
|
|
22808
24350
|
["selfSignup", renderSelfSignup(i.selfSignupPolicies)],
|
|
22809
|
-
["capturedClaims", renderCapturedClaims(i.capturedClaims)]
|
|
24351
|
+
["capturedClaims", renderCapturedClaims(i.capturedClaims)],
|
|
24352
|
+
["restrictedToDomain", renderRestrictedToDomain(i.restrictedToDomain)],
|
|
24353
|
+
...pending ? [
|
|
24354
|
+
["verificationClaim", displayChallengeValue(i.verificationClaim)],
|
|
24355
|
+
["verificationNonce", displayChallengeValue(i.verificationNonce)],
|
|
24356
|
+
["verificationExpiresAt", displayChallengeValue(i.verificationExpiresAt)]
|
|
24357
|
+
] : []
|
|
22810
24358
|
]);
|
|
22811
24359
|
}
|
|
24360
|
+
function challengeContext(opts) {
|
|
24361
|
+
return { context: opts.context, tenant: opts.tenant, env: opts.env, baseUrl: opts.baseUrl };
|
|
24362
|
+
}
|
|
24363
|
+
function renderPendingNotes(i, ctx, now) {
|
|
24364
|
+
if (!isPendingVerification(i)) return "";
|
|
24365
|
+
return challengeExpired(i, now) ? renderExpiredBlock(i, ctx, "then register it again with the same arguments") : renderChallengeBlock(i, ctx, now);
|
|
24366
|
+
}
|
|
22812
24367
|
async function runIssuersList(opts, deps = {}) {
|
|
22813
24368
|
return withAuth({ ...opts, provisioning: true, context: opts.context }, deps, async (auth) => {
|
|
22814
24369
|
const client = auth.client;
|
|
@@ -22828,7 +24383,8 @@ async function runIssuersList(opts, deps = {}) {
|
|
|
22828
24383
|
{ header: "audience", get: (i) => i.audience },
|
|
22829
24384
|
{ header: "status", get: (i) => i.status },
|
|
22830
24385
|
{ header: "self-signup", get: (i) => renderSelfSignup(i.selfSignupPolicies) },
|
|
22831
|
-
{ header: "captured claims", get: (i) => renderCapturedClaims(i.capturedClaims) }
|
|
24386
|
+
{ header: "captured claims", get: (i) => renderCapturedClaims(i.capturedClaims) },
|
|
24387
|
+
{ header: "restricted to domain", get: (i) => renderRestrictedToDomain(i.restrictedToDomain) }
|
|
22832
24388
|
]) + "\n"
|
|
22833
24389
|
);
|
|
22834
24390
|
return 0;
|
|
@@ -22848,9 +24404,11 @@ async function runIssuersGet(opts, deps = {}) {
|
|
|
22848
24404
|
`);
|
|
22849
24405
|
return 0;
|
|
22850
24406
|
}
|
|
24407
|
+
const notes = renderPendingNotes(issuer, challengeContext(opts), (deps.now ?? Date.now)());
|
|
22851
24408
|
process.stdout.write(`
|
|
22852
24409
|
${renderIssuer(issuer)}
|
|
22853
|
-
`
|
|
24410
|
+
${notes ? `
|
|
24411
|
+
${notes}` : ""}`);
|
|
22854
24412
|
return 0;
|
|
22855
24413
|
} catch (err) {
|
|
22856
24414
|
process.stderr.write(formatCliError(err, { action: "issuers get failed" }));
|
|
@@ -22861,9 +24419,11 @@ ${renderIssuer(issuer)}
|
|
|
22861
24419
|
async function runIssuersRegister(opts, deps = {}) {
|
|
22862
24420
|
let selfSignupPolicies;
|
|
22863
24421
|
let capturedClaims;
|
|
24422
|
+
let restrictedToDomain;
|
|
22864
24423
|
try {
|
|
22865
24424
|
selfSignupPolicies = parseSelfSignupPolicies(opts.selfSignupPolicy);
|
|
22866
24425
|
capturedClaims = parseCapturedClaims(opts.capturedClaim);
|
|
24426
|
+
restrictedToDomain = parseRestrictedToDomain(opts.restrictedToDomain);
|
|
22867
24427
|
} catch (err) {
|
|
22868
24428
|
process.stderr.write(` \u2716 ${formatError(err)}
|
|
22869
24429
|
`);
|
|
@@ -22882,18 +24442,45 @@ async function runIssuersRegister(opts, deps = {}) {
|
|
|
22882
24442
|
emailClaim: opts.emailClaim,
|
|
22883
24443
|
userinfoUri: opts.userinfoUri,
|
|
22884
24444
|
selfSignupPolicies,
|
|
22885
|
-
capturedClaims
|
|
24445
|
+
capturedClaims,
|
|
24446
|
+
restrictedToDomain
|
|
22886
24447
|
});
|
|
24448
|
+
if (restrictedToDomain !== void 0 && (issuer.restrictedToDomain ?? "").toLowerCase() !== restrictedToDomain.toLowerCase()) {
|
|
24449
|
+
if (isPendingVerification(issuer)) {
|
|
24450
|
+
const ctx = challengeContext(opts);
|
|
24451
|
+
process.stderr.write(
|
|
24452
|
+
` \u2716 issuer '${opts.issuerId}' is ${issuer.created === false ? "already registered and " : ""}still pending verification, so the requested
|
|
24453
|
+
--restricted-to-domain '${restrictedToDomain}' was NOT applied: register never changes an existing registration, and a pending
|
|
24454
|
+
registration cannot be re-scoped. Either replace it with a scoped one (a domain-scoped issuer needs no login):
|
|
24455
|
+
${deleteCommand(opts.issuerId, ctx)}
|
|
24456
|
+
and register it again with the scope, or verify it first, which makes it active and scopable with issuers update:
|
|
24457
|
+
${verifyCommand(opts.issuerId, ctx)}
|
|
24458
|
+
`
|
|
24459
|
+
);
|
|
24460
|
+
return 1;
|
|
24461
|
+
}
|
|
24462
|
+
const stored = issuer.restrictedToDomain ? `restricted to '${issuer.restrictedToDomain}'` : "with no domain scope";
|
|
24463
|
+
const why = issuer.created === false ? `is already registered ${stored}, and register never changes an existing registration` : `was registered ${stored}: the platform did not apply the requested scope`;
|
|
24464
|
+
process.stderr.write(
|
|
24465
|
+
` \u2716 issuer '${opts.issuerId}' ${why} \u2014 the requested
|
|
24466
|
+
--restricted-to-domain '${restrictedToDomain}' was NOT applied. Run:
|
|
24467
|
+
vectros issuers update ${shellQuote(opts.issuerId)} --context ${shellQuote(opts.context)}${targetFlags(challengeContext(opts))} --restricted-to-domain ${shellQuote(restrictedToDomain)}
|
|
24468
|
+
`
|
|
24469
|
+
);
|
|
24470
|
+
return 1;
|
|
24471
|
+
}
|
|
22887
24472
|
if (opts.json) {
|
|
22888
24473
|
process.stdout.write(`${formatJson(issuer)}
|
|
22889
24474
|
`);
|
|
22890
24475
|
return 0;
|
|
22891
24476
|
}
|
|
22892
24477
|
const verb = issuer.created === false ? "already registered \u2014 returned unchanged" : "registered";
|
|
24478
|
+
const notes = renderPendingNotes(issuer, challengeContext(opts), (deps.now ?? Date.now)());
|
|
22893
24479
|
process.stdout.write(`
|
|
22894
24480
|
\u2714 issuer '${opts.issuerId}' ${verb}
|
|
22895
24481
|
${renderIssuer(issuer)}
|
|
22896
|
-
`
|
|
24482
|
+
${notes ? `
|
|
24483
|
+
${notes}` : ""}`);
|
|
22897
24484
|
return 0;
|
|
22898
24485
|
} catch (err) {
|
|
22899
24486
|
process.stderr.write(formatCliError(err, { action: "issuers register failed" }));
|
|
@@ -22904,14 +24491,22 @@ ${renderIssuer(issuer)}
|
|
|
22904
24491
|
async function runIssuersUpdate(opts, deps = {}) {
|
|
22905
24492
|
let selfSignupPolicies;
|
|
22906
24493
|
let capturedClaims;
|
|
24494
|
+
let restrictedToDomain;
|
|
22907
24495
|
try {
|
|
22908
24496
|
selfSignupPolicies = parseSelfSignupPolicies(opts.selfSignupPolicy);
|
|
22909
24497
|
capturedClaims = parseCapturedClaims(opts.capturedClaim);
|
|
24498
|
+
restrictedToDomain = parseRestrictedToDomain(opts.restrictedToDomain);
|
|
22910
24499
|
} catch (err) {
|
|
22911
24500
|
process.stderr.write(` \u2716 ${formatError(err)}
|
|
22912
24501
|
`);
|
|
22913
24502
|
return 2;
|
|
22914
24503
|
}
|
|
24504
|
+
if (opts.clearRestrictedToDomain && restrictedToDomain !== void 0) {
|
|
24505
|
+
process.stderr.write(
|
|
24506
|
+
" \u2716 --clear-restricted-to-domain and --restricted-to-domain are mutually exclusive.\n"
|
|
24507
|
+
);
|
|
24508
|
+
return 2;
|
|
24509
|
+
}
|
|
22915
24510
|
if (opts.clearSelfSignupPolicies && selfSignupPolicies && selfSignupPolicies.length > 0) {
|
|
22916
24511
|
process.stderr.write(
|
|
22917
24512
|
" \u2716 --clear-self-signup-policies and --self-signup-policy are mutually exclusive.\n"
|
|
@@ -22934,7 +24529,8 @@ async function runIssuersUpdate(opts, deps = {}) {
|
|
|
22934
24529
|
userinfoUri: opts.userinfoUri,
|
|
22935
24530
|
status: opts.status,
|
|
22936
24531
|
selfSignupPolicies: opts.clearSelfSignupPolicies ? [] : selfSignupPolicies,
|
|
22937
|
-
capturedClaims: opts.clearCapturedClaims ? [] : capturedClaims
|
|
24532
|
+
capturedClaims: opts.clearCapturedClaims ? [] : capturedClaims,
|
|
24533
|
+
restrictedToDomain: opts.clearRestrictedToDomain ? "" : restrictedToDomain
|
|
22938
24534
|
});
|
|
22939
24535
|
if (opts.json) {
|
|
22940
24536
|
process.stdout.write(`${formatJson(issuer)}
|
|
@@ -22965,7 +24561,7 @@ async function runIssuersDelete(opts, deps = {}) {
|
|
|
22965
24561
|
return 2;
|
|
22966
24562
|
}
|
|
22967
24563
|
const promptCtx = deps.createPrompt ? deps.createPrompt() : (() => {
|
|
22968
|
-
const rl =
|
|
24564
|
+
const rl = readline5.createInterface({ input: process.stdin, output: process.stdout });
|
|
22969
24565
|
return { prompt: (q) => rl.question(q), close: () => rl.close() };
|
|
22970
24566
|
})();
|
|
22971
24567
|
try {
|
|
@@ -23007,6 +24603,173 @@ async function runIssuersDelete(opts, deps = {}) {
|
|
|
23007
24603
|
}
|
|
23008
24604
|
});
|
|
23009
24605
|
}
|
|
24606
|
+
var JWT_SHAPE = /^[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]*$/;
|
|
24607
|
+
function decodeTokenBytes(bytes) {
|
|
24608
|
+
if (bytes.length >= 2 && bytes[0] === 255 && bytes[1] === 254) return bytes.subarray(2).toString("utf16le");
|
|
24609
|
+
if (bytes.length >= 2 && bytes[0] === 254 && bytes[1] === 255) {
|
|
24610
|
+
const swapped = Buffer.from(bytes.subarray(2));
|
|
24611
|
+
swapped.swap16();
|
|
24612
|
+
return swapped.toString("utf16le");
|
|
24613
|
+
}
|
|
24614
|
+
if (bytes.length >= 3 && bytes[0] === 239 && bytes[1] === 187 && bytes[2] === 191) {
|
|
24615
|
+
return bytes.subarray(3).toString("utf8");
|
|
24616
|
+
}
|
|
24617
|
+
return bytes.toString("utf8");
|
|
24618
|
+
}
|
|
24619
|
+
var MAX_TOKEN_BYTES = 64 * 1024;
|
|
24620
|
+
var TokenTooLargeError = class extends Error {
|
|
24621
|
+
constructor() {
|
|
24622
|
+
super(`the input is larger than ${MAX_TOKEN_BYTES / 1024} KB, which no token is; nothing was read further and nothing was sent.`);
|
|
24623
|
+
this.name = "TokenTooLargeError";
|
|
24624
|
+
}
|
|
24625
|
+
};
|
|
24626
|
+
async function readAllFrom(stream, maxBytes = MAX_TOKEN_BYTES) {
|
|
24627
|
+
const chunks = [];
|
|
24628
|
+
let total = 0;
|
|
24629
|
+
for await (const chunk of stream) {
|
|
24630
|
+
const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
24631
|
+
total += buf.length;
|
|
24632
|
+
if (total > maxBytes) throw new TokenTooLargeError();
|
|
24633
|
+
chunks.push(buf);
|
|
24634
|
+
}
|
|
24635
|
+
return decodeTokenBytes(Buffer.concat(chunks));
|
|
24636
|
+
}
|
|
24637
|
+
var drainStdin = () => readAllFrom(process.stdin);
|
|
24638
|
+
var readTokenFile = (path4) => readAllFrom(createReadStream(path4));
|
|
24639
|
+
function fileFailureReason(err) {
|
|
24640
|
+
if (err instanceof TokenTooLargeError) return err.message;
|
|
24641
|
+
switch (err?.code) {
|
|
24642
|
+
case "ENOENT":
|
|
24643
|
+
return "no such file.";
|
|
24644
|
+
case "EISDIR":
|
|
24645
|
+
return "that is a directory.";
|
|
24646
|
+
case "EACCES":
|
|
24647
|
+
case "EPERM":
|
|
24648
|
+
return "permission denied.";
|
|
24649
|
+
default:
|
|
24650
|
+
return "it could not be read.";
|
|
24651
|
+
}
|
|
24652
|
+
}
|
|
24653
|
+
async function resolveIdpToken(opts, deps) {
|
|
24654
|
+
if (opts.idpToken !== void 0 && opts.idpTokenFile !== void 0) {
|
|
24655
|
+
throw new Error("--idp-token and --idp-token-file are mutually exclusive.");
|
|
24656
|
+
}
|
|
24657
|
+
const readStdin = deps.readStdin ?? drainStdin;
|
|
24658
|
+
const readTextFile = deps.readTextFile ?? readTokenFile;
|
|
24659
|
+
let raw;
|
|
24660
|
+
if (opts.idpToken !== void 0) {
|
|
24661
|
+
raw = opts.idpToken;
|
|
24662
|
+
} else if (opts.idpTokenFile !== void 0 && opts.idpTokenFile !== "-") {
|
|
24663
|
+
if (looksLikeCredential(opts.idpTokenFile)) {
|
|
24664
|
+
throw new Error(
|
|
24665
|
+
"--idp-token-file takes the PATH of a file holding the token, but the value given looks like a token (or is too long to be a path). Save the token to a file and pass that path (for a file in this directory, write ./name), or pipe it on stdin."
|
|
24666
|
+
);
|
|
24667
|
+
}
|
|
24668
|
+
try {
|
|
24669
|
+
raw = await readTextFile(opts.idpTokenFile);
|
|
24670
|
+
} catch (err) {
|
|
24671
|
+
const name = safeBaseName(opts.idpTokenFile);
|
|
24672
|
+
throw new Error(`could not read the file given to --idp-token-file${name ? ` (${name})` : ""}: ${fileFailureReason(err)}`);
|
|
24673
|
+
}
|
|
24674
|
+
} else if (opts.idpTokenFile === "-" || !(deps.isTty ?? Boolean(process.stdin.isTTY))) {
|
|
24675
|
+
if (opts.idpTokenFile === "-" && (deps.isTty ?? Boolean(process.stdin.isTTY))) {
|
|
24676
|
+
throw new Error(
|
|
24677
|
+
"stdin is a terminal, so `--idp-token-file -` would have you paste the token where it is displayed. Pipe it instead (some-command | vectros issuers verify ...), or save it to a file and pass --idp-token-file <path>."
|
|
24678
|
+
);
|
|
24679
|
+
}
|
|
24680
|
+
if (opts.idpTokenFile !== "-") {
|
|
24681
|
+
process.stderr.write(" \u2026 no token flag given: reading the token from stdin (pass --idp-token-file <path> to avoid this).\n");
|
|
24682
|
+
}
|
|
24683
|
+
raw = await readStdin();
|
|
24684
|
+
} else {
|
|
24685
|
+
throw new Error(
|
|
24686
|
+
"no token given. Save the JWT your identity provider returned to a file and pass --idp-token-file <path>, or pipe it on stdin. (Not --token: that is your Vectros credential.)"
|
|
24687
|
+
);
|
|
24688
|
+
}
|
|
24689
|
+
if (raw.length > MAX_TOKEN_BYTES) throw new TokenTooLargeError();
|
|
24690
|
+
const token = raw.trim().replace(/^bearer\s+/i, "");
|
|
24691
|
+
if (token.length === 0) throw new Error("the token is empty.");
|
|
24692
|
+
if (!JWT_SHAPE.test(token)) {
|
|
24693
|
+
throw new Error(
|
|
24694
|
+
"the value does not look like a JWT (three dot-separated base64url segments). Pass the whole access token or ID token your identity provider issued for the login, not a decoded payload. An opaque access token (not a JWT) cannot be used: configure the provider to issue a JWT for this audience."
|
|
24695
|
+
);
|
|
24696
|
+
}
|
|
24697
|
+
return token;
|
|
24698
|
+
}
|
|
24699
|
+
async function runIssuersVerify(opts, deps = {}) {
|
|
24700
|
+
let token;
|
|
24701
|
+
try {
|
|
24702
|
+
token = await resolveIdpToken(opts, deps);
|
|
24703
|
+
} catch (err) {
|
|
24704
|
+
process.stderr.write(` \u2716 ${formatError(err)}
|
|
24705
|
+
`);
|
|
24706
|
+
return 2;
|
|
24707
|
+
}
|
|
24708
|
+
if (opts.idpToken !== void 0) {
|
|
24709
|
+
process.stderr.write(
|
|
24710
|
+
" \u26A0 --idp-token puts the token in your shell history and the process list; prefer --idp-token-file or stdin.\n"
|
|
24711
|
+
);
|
|
24712
|
+
}
|
|
24713
|
+
return withAuth({ ...opts, provisioning: true, context: opts.context }, deps, async (auth) => {
|
|
24714
|
+
const client = auth.client;
|
|
24715
|
+
try {
|
|
24716
|
+
const issuer = await client.auth.verifyIssuer({ issuerId: opts.issuerId, token });
|
|
24717
|
+
const active = issuer.status === "active";
|
|
24718
|
+
if (opts.json) {
|
|
24719
|
+
process.stdout.write(`${formatJson(issuer)}
|
|
24720
|
+
`);
|
|
24721
|
+
return active ? 0 : 1;
|
|
24722
|
+
}
|
|
24723
|
+
if (!active) {
|
|
24724
|
+
process.stderr.write(
|
|
24725
|
+
` \u2716 the platform accepted the call but issuer '${opts.issuerId}' reports status '${issuer.status ?? "(none)"}', not 'active'. Run: ${getCommand(opts.issuerId, challengeContext(opts))}
|
|
24726
|
+
`
|
|
24727
|
+
);
|
|
24728
|
+
return 1;
|
|
24729
|
+
}
|
|
24730
|
+
process.stdout.write(
|
|
24731
|
+
`
|
|
24732
|
+
\u2714 issuer '${opts.issuerId}' verified: it is now active and accepts tokens from this provider.
|
|
24733
|
+
${renderIssuer(issuer)}
|
|
24734
|
+
|
|
24735
|
+
The token was checked once and discarded. You can remove the rule you added at the provider.
|
|
24736
|
+
`
|
|
24737
|
+
);
|
|
24738
|
+
return 0;
|
|
24739
|
+
} catch (err) {
|
|
24740
|
+
const info = extractServerError(err);
|
|
24741
|
+
process.stderr.write(formatCliError(err, { action: "issuers verify failed" }));
|
|
24742
|
+
if (info.statusCode === 409) {
|
|
24743
|
+
process.stderr.write(
|
|
24744
|
+
` The registration changed or was removed while it was being verified, and this request activated
|
|
24745
|
+
nothing. Run: ${getCommand(opts.issuerId, challengeContext(opts))}
|
|
24746
|
+
(another verification may already have activated it); otherwise retry.
|
|
24747
|
+
`
|
|
24748
|
+
);
|
|
24749
|
+
} else if (info.statusCode === 400) {
|
|
24750
|
+
process.stderr.write(
|
|
24751
|
+
` This did not activate the registration. The message above says why; to see where it stands, run:
|
|
24752
|
+
${getCommand(opts.issuerId, challengeContext(opts))}
|
|
24753
|
+
`
|
|
24754
|
+
);
|
|
24755
|
+
} else if (info.statusCode === 404) {
|
|
24756
|
+
process.stderr.write(
|
|
24757
|
+
` No registration with this id is visible to you in this context. A pending registration whose challenge
|
|
24758
|
+
has expired is removed by the platform. Run: ${getCommand(opts.issuerId, challengeContext(opts))}
|
|
24759
|
+
`
|
|
24760
|
+
);
|
|
24761
|
+
} else {
|
|
24762
|
+
process.stderr.write(
|
|
24763
|
+
` This request did not report success, and the platform may still have finished it (a timeout or a
|
|
24764
|
+
dropped connection can leave the registration verified). Before retrying, run:
|
|
24765
|
+
${getCommand(opts.issuerId, challengeContext(opts))}
|
|
24766
|
+
`
|
|
24767
|
+
);
|
|
24768
|
+
}
|
|
24769
|
+
return 1;
|
|
24770
|
+
}
|
|
24771
|
+
});
|
|
24772
|
+
}
|
|
23010
24773
|
|
|
23011
24774
|
// src/commands/namespace.ts
|
|
23012
24775
|
function renderNamespace(n) {
|
|
@@ -23112,7 +24875,7 @@ ${renderNamespace(ns)}
|
|
|
23112
24875
|
|
|
23113
24876
|
// src/commands/scripts.ts
|
|
23114
24877
|
import { readFile as readFile3 } from "fs/promises";
|
|
23115
|
-
import * as
|
|
24878
|
+
import * as readline6 from "readline/promises";
|
|
23116
24879
|
var SOURCE_MAX_BYTES = 3e5;
|
|
23117
24880
|
var INPUT_CONTRACT_MAX_BYTES = 5e4;
|
|
23118
24881
|
var utf8Bytes2 = (s) => new TextEncoder().encode(s).length;
|
|
@@ -23250,6 +25013,7 @@ async function runScriptsList(opts, deps = {}) {
|
|
|
23250
25013
|
const rows = await drainPages(
|
|
23251
25014
|
(startFrom) => client.scripts.listScripts({
|
|
23252
25015
|
limit: PAGE_LIMIT,
|
|
25016
|
+
includeSource: true,
|
|
23253
25017
|
...opts.name ? { name: opts.name } : {},
|
|
23254
25018
|
...startFrom ? { startFrom } : {}
|
|
23255
25019
|
})
|
|
@@ -23310,10 +25074,9 @@ async function runScriptsDelete(opts, deps = {}) {
|
|
|
23310
25074
|
`
|
|
23311
25075
|
\u26A0 Deleting script version '${opts.id}' is permanent \u2014 the stored source is the only copy.
|
|
23312
25076
|
Other versions of the same name are unaffected.
|
|
23313
|
-
|
|
23314
|
-
|
|
23315
|
-
|
|
23316
|
-
against the API or the portal before removing a version a rule may still use.
|
|
25077
|
+
The platform refuses this delete if a trigger rule still references this version \u2014
|
|
25078
|
+
directly, or via 'latest' if this is the name's current newest version \u2014 and names the
|
|
25079
|
+
offending rule instead of deleting. If no rule references it, the delete proceeds.
|
|
23317
25080
|
And if this is the name's LATEST version, its identity is REUSED: the next push of that
|
|
23318
25081
|
name recomputes the same version number and the same id, so anything holding a pinned
|
|
23319
25082
|
'${opts.id}' or a name@version reference will silently resolve to different source.
|
|
@@ -23331,7 +25094,7 @@ async function runScriptsDelete(opts, deps = {}) {
|
|
|
23331
25094
|
return 2;
|
|
23332
25095
|
}
|
|
23333
25096
|
const promptCtx = deps.createPrompt ? deps.createPrompt() : (() => {
|
|
23334
|
-
const rl =
|
|
25097
|
+
const rl = readline6.createInterface({ input: process.stdin, output: process.stdout });
|
|
23335
25098
|
return { prompt: (q) => rl.question(q), close: () => rl.close() };
|
|
23336
25099
|
})();
|
|
23337
25100
|
try {
|
|
@@ -23446,16 +25209,36 @@ function defaultOpenBrowser(url) {
|
|
|
23446
25209
|
void open(url).catch(() => {
|
|
23447
25210
|
});
|
|
23448
25211
|
}
|
|
23449
|
-
|
|
23450
|
-
|
|
23451
|
-
|
|
23452
|
-
|
|
23453
|
-
|
|
23454
|
-
|
|
23455
|
-
|
|
23456
|
-
|
|
23457
|
-
|
|
23458
|
-
|
|
25212
|
+
var PAGE_SIGNED_IN = '<html><body style="font-family:sans-serif"><h3>Vectros CLI</h3><p>You can close this tab and return to the terminal.</p></body></html>';
|
|
25213
|
+
var PAGE_SIGN_IN_FAILED = '<html><body style="font-family:sans-serif"><h3>Vectros CLI</h3><p>The sign-in did not complete. Return to the terminal for details.</p></body></html>';
|
|
25214
|
+
var TEXT_NOT_THIS_SIGN_IN = "This response could not be matched to the sign-in in progress. Return to your terminal: if the sign-in did not finish there, press Ctrl+C and run `vectros login` again.";
|
|
25215
|
+
var REQUEST_TIMEOUT_MS = 1e4;
|
|
25216
|
+
async function startLoopback(ports, expectedState, onCode, onIgnored) {
|
|
25217
|
+
const server = createServer(
|
|
25218
|
+
{
|
|
25219
|
+
headersTimeout: REQUEST_TIMEOUT_MS,
|
|
25220
|
+
requestTimeout: REQUEST_TIMEOUT_MS,
|
|
25221
|
+
keepAliveTimeout: 1e3,
|
|
25222
|
+
connectionsCheckingInterval: 1e3
|
|
25223
|
+
},
|
|
25224
|
+
(req, res) => {
|
|
25225
|
+
const classified = classifyCallback(req.method, req.url, expectedState);
|
|
25226
|
+
if (classified.kind === "accepted") {
|
|
25227
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
25228
|
+
res.end(classified.params.error ? PAGE_SIGN_IN_FAILED : PAGE_SIGNED_IN);
|
|
25229
|
+
onCode(classified.params);
|
|
25230
|
+
return;
|
|
25231
|
+
}
|
|
25232
|
+
if (classified.kind === "wrong-state") {
|
|
25233
|
+
res.writeHead(400, { "Content-Type": "text/plain" });
|
|
25234
|
+
res.end(TEXT_NOT_THIS_SIGN_IN);
|
|
25235
|
+
onIgnored();
|
|
25236
|
+
return;
|
|
25237
|
+
}
|
|
25238
|
+
res.writeHead(404, { "Content-Type": "text/html" });
|
|
25239
|
+
res.end("<html><body>Not found</body></html>");
|
|
25240
|
+
}
|
|
25241
|
+
);
|
|
23459
25242
|
for (const port of ports) {
|
|
23460
25243
|
const ok = await new Promise((resolve) => {
|
|
23461
25244
|
const onErr = () => {
|
|
@@ -23468,7 +25251,10 @@ async function startLoopback(ports, onCode) {
|
|
|
23468
25251
|
resolve(true);
|
|
23469
25252
|
});
|
|
23470
25253
|
});
|
|
23471
|
-
if (ok)
|
|
25254
|
+
if (ok) {
|
|
25255
|
+
const address = server.address();
|
|
25256
|
+
return { server, port: typeof address === "object" && address ? address.port : port };
|
|
25257
|
+
}
|
|
23472
25258
|
}
|
|
23473
25259
|
throw new Error(`No free loopback port among ${ports.join(", ")} \u2014 close whatever is using them and retry.`);
|
|
23474
25260
|
}
|
|
@@ -23482,7 +25268,7 @@ async function runLogin(opts, deps = {}) {
|
|
|
23482
25268
|
try {
|
|
23483
25269
|
config = resolveCognitoConfig(opts.env);
|
|
23484
25270
|
} catch (err) {
|
|
23485
|
-
if (err instanceof MissingCognitoClientIdError) {
|
|
25271
|
+
if (err instanceof MissingCognitoClientIdError || err instanceof InvalidBaseUrlError || err instanceof InvalidCognitoRegionError) {
|
|
23486
25272
|
process.stderr.write(` \u2716 ${err.message}
|
|
23487
25273
|
`);
|
|
23488
25274
|
return 2;
|
|
@@ -23497,9 +25283,26 @@ async function runLogin(opts, deps = {}) {
|
|
|
23497
25283
|
const codePromise = new Promise((resolve) => {
|
|
23498
25284
|
resolveCode = resolve;
|
|
23499
25285
|
});
|
|
25286
|
+
let warnedIgnored = false;
|
|
25287
|
+
let settled = false;
|
|
25288
|
+
const warnIgnored = () => {
|
|
25289
|
+
if (settled || warnedIgnored) return;
|
|
25290
|
+
warnedIgnored = true;
|
|
25291
|
+
process.stderr.write(
|
|
25292
|
+
" ! ignored a response that did not carry this sign-in's state, so it cannot be treated as the answer to this sign-in.\n Still waiting for the real one. If the browser showed an error, or nothing seems to happen, press Ctrl+C and run `vectros login` again.\n"
|
|
25293
|
+
);
|
|
25294
|
+
};
|
|
23500
25295
|
let loopback;
|
|
23501
25296
|
try {
|
|
23502
|
-
loopback = await startLoopback(
|
|
25297
|
+
loopback = await startLoopback(
|
|
25298
|
+
ports,
|
|
25299
|
+
state,
|
|
25300
|
+
(params) => {
|
|
25301
|
+
settled = true;
|
|
25302
|
+
resolveCode(params);
|
|
25303
|
+
},
|
|
25304
|
+
warnIgnored
|
|
25305
|
+
);
|
|
23503
25306
|
} catch (err) {
|
|
23504
25307
|
process.stderr.write(` \u2716 ${formatError(err)}
|
|
23505
25308
|
`);
|
|
@@ -23529,10 +25332,7 @@ async function runLogin(opts, deps = {}) {
|
|
|
23529
25332
|
}
|
|
23530
25333
|
const params = await withTimeout(codePromise, timeoutMs);
|
|
23531
25334
|
if (params.error) {
|
|
23532
|
-
throw new Error(`authorization failed: ${params.error
|
|
23533
|
-
}
|
|
23534
|
-
if (params.state !== state) {
|
|
23535
|
-
throw new Error("state mismatch on the callback \u2014 aborting (possible CSRF).");
|
|
25335
|
+
throw new Error(`authorization failed: ${describeAuthorizationError(params.error, params.errorDescription)}`);
|
|
23536
25336
|
}
|
|
23537
25337
|
if (!params.code) {
|
|
23538
25338
|
throw new Error("no authorization code on the callback.");
|
|
@@ -23553,13 +25353,20 @@ async function runLogin(opts, deps = {}) {
|
|
|
23553
25353
|
return 0;
|
|
23554
25354
|
} catch (err) {
|
|
23555
25355
|
process.stderr.write(`
|
|
23556
|
-
\u2716 login failed: ${formatError(err)}
|
|
25356
|
+
\u2716 login failed: ${stripControlCharacters(formatError(err), 1e3)}
|
|
23557
25357
|
`);
|
|
23558
25358
|
return 1;
|
|
23559
25359
|
} finally {
|
|
23560
25360
|
loopback.server.close();
|
|
25361
|
+
loopback.server.closeAllConnections();
|
|
23561
25362
|
}
|
|
23562
25363
|
}
|
|
25364
|
+
var OAUTH_ERROR_CODE = /^[A-Za-z0-9_.-]{1,64}$/;
|
|
25365
|
+
function describeAuthorizationError(error, description) {
|
|
25366
|
+
const code = OAUTH_ERROR_CODE.test(error) ? error : "an unrecognised error";
|
|
25367
|
+
const text = description ? stripControlCharacters(description) : "";
|
|
25368
|
+
return text ? `${code} \u2014 ${text}` : code;
|
|
25369
|
+
}
|
|
23563
25370
|
async function runLogout(_opts, deps = {}) {
|
|
23564
25371
|
const removed = await clearSession(deps.baseDir);
|
|
23565
25372
|
process.stdout.write(removed ? " \u2714 Logged out (session cleared).\n" : " No stored session.\n");
|
|
@@ -23760,11 +25567,17 @@ async function runAssertions(client, bootstrapClient, deps, blueprint, tenantId,
|
|
|
23760
25567
|
for (const { issuerId } of result.issuerActions) {
|
|
23761
25568
|
try {
|
|
23762
25569
|
const issuer = await bootstrapClient.auth.getIssuer({ issuerId });
|
|
23763
|
-
const
|
|
25570
|
+
const declaredScope = blueprint.issuers?.find((i) => i.issuerId === issuerId)?.restrictedToDomain;
|
|
25571
|
+
const scopeOk = declaredScope === void 0 || (issuer.restrictedToDomain ?? "").toLowerCase() === declaredScope.toLowerCase();
|
|
25572
|
+
const ok = issuer.issuerId === issuerId && scopeOk;
|
|
23764
25573
|
assertions.push({
|
|
23765
25574
|
name: `issuer:${issuerId}`,
|
|
23766
25575
|
ok,
|
|
23767
|
-
detail:
|
|
25576
|
+
detail: !scopeOk ? `getIssuer(${issuerId}) reports restrictedToDomain '${issuer.restrictedToDomain ?? "(none)"}', the blueprint declares '${declaredScope}'` : ok ? (
|
|
25577
|
+
// A pending issuer is a PASS here: this harness cannot perform the identity-provider login that
|
|
25578
|
+
// verification needs, and registration is what the blueprint declares. Say so rather than hide it.
|
|
25579
|
+
`issuer '${issuerId}' registered + readable` + (issuer.status === "pending_verification" ? " (pending verification: it accepts no token until a login at its identity provider proves control)" : "")
|
|
25580
|
+
) : `getIssuer(${issuerId}) returned issuerId '${issuer.issuerId ?? "(none)"}'`
|
|
23768
25581
|
});
|
|
23769
25582
|
} catch (e) {
|
|
23770
25583
|
assertions.push({ name: `issuer:${issuerId}`, ok: false, detail: `getIssuer(${issuerId}) failed: ${errMsg(e)}` });
|
|
@@ -24075,6 +25888,11 @@ async function runBlueprintHarness(client, input, deps) {
|
|
|
24075
25888
|
// recording that it happened and nothing able to restore it. Opt out rather than grow the
|
|
24076
25889
|
// snapshot a leg teardown could not honour anyway.
|
|
24077
25890
|
pruneTriggers: false
|
|
25891
|
+
// `allowScriptAdoption` is left at its default `false` — the SAME created-only contract above
|
|
25892
|
+
// applies here: this harness may run against a shared tenant, and adopting a pre-existing,
|
|
25893
|
+
// unmarked script name would start this run's teardown treating (and, worse, later deleting)
|
|
25894
|
+
// versions on a name it does not own. A test blueprint that needs `scripts:` should ship a
|
|
25895
|
+
// fresh, collision-free name, exactly as the harness already expects for schemas/roles/issuers.
|
|
24078
25896
|
});
|
|
24079
25897
|
log.debug({ component: "harness", contextId: result.contextId, keyName: result.keyName }, "applied");
|
|
24080
25898
|
assertions = await runAssertions(
|
|
@@ -24288,7 +26106,7 @@ async function issueOrRotateKey(client, tenantId, context, userId, keyName) {
|
|
|
24288
26106
|
"The key already exists and its secret could not be re-minted (the prior key may not have cleared yet). Retry shortly."
|
|
24289
26107
|
);
|
|
24290
26108
|
}
|
|
24291
|
-
return { key: rotated, revokedKeyId: existing?.keyId };
|
|
26109
|
+
return { key: rotated, revokedKeyId: existing?.keyId ? [existing.keyId] : void 0 };
|
|
24292
26110
|
}
|
|
24293
26111
|
async function runJoin(opts, deps = {}) {
|
|
24294
26112
|
const hasRole = Boolean(opts.role?.length);
|
|
@@ -24435,9 +26253,9 @@ function emitJoined(opts) {
|
|
|
24435
26253
|
|
|
24436
26254
|
// src/build-info.ts
|
|
24437
26255
|
var BUILD_INFO = {
|
|
24438
|
-
cli: true ? "0.
|
|
24439
|
-
sdk: true ? "0.
|
|
24440
|
-
blueprints: true ? "0.
|
|
26256
|
+
cli: true ? "0.23.0" : "dev",
|
|
26257
|
+
sdk: true ? "0.45.0" : "dev",
|
|
26258
|
+
blueprints: true ? "0.20.0" : "dev"
|
|
24441
26259
|
};
|
|
24442
26260
|
function formatBuildInfo() {
|
|
24443
26261
|
return `vectros ${BUILD_INFO.cli} (sdk ${BUILD_INFO.sdk}, blueprints ${BUILD_INFO.blueprints})`;
|
|
@@ -24464,11 +26282,13 @@ function addCommonAuthOptions(cmd) {
|
|
|
24464
26282
|
"emit machine-readable JSON instead of the human-readable view"
|
|
24465
26283
|
);
|
|
24466
26284
|
}
|
|
26285
|
+
var TENANT_FLAG_INVALID = /* @__PURE__ */ Symbol("parseTenantFlag:invalid");
|
|
24467
26286
|
function parseTenantFlag(raw) {
|
|
24468
26287
|
if (raw !== void 0 && raw !== "test" && raw !== "live") {
|
|
24469
26288
|
process.stderr.write(` \u2716 --tenant must be 'test' or 'live' (got '${raw}')
|
|
24470
26289
|
`);
|
|
24471
|
-
process.
|
|
26290
|
+
process.exitCode = 2;
|
|
26291
|
+
return TENANT_FLAG_INVALID;
|
|
24472
26292
|
}
|
|
24473
26293
|
return raw;
|
|
24474
26294
|
}
|
|
@@ -24483,20 +26303,20 @@ function toCommonOptions(flags) {
|
|
|
24483
26303
|
}
|
|
24484
26304
|
function buildProgram(deps = {}) {
|
|
24485
26305
|
const program = new Command();
|
|
24486
|
-
program.name("vectros").description("The Vectros provisioning CLI \u2014 provision contexts, identities, access, and keys.").version(formatBuildInfo(), "-v, --version", "show the CLI + bundled SDK/blueprints versions").showHelpAfterError().addHelpText(
|
|
26306
|
+
program.name("vectros").description("The Vectros provisioning CLI \u2014 provision contexts, identities, access, and keys.").version(formatBuildInfo(), "-v, --version", "show the CLI + bundled SDK/blueprints versions").showHelpAfterError().configureOutput({ outputError: (text, write) => write(redactCredentials(text)) }).addHelpText(
|
|
24487
26307
|
"after",
|
|
24488
|
-
"\nProvisioning + authoring (own help):\n bootstrap One-shot least-privilege MCP credential. vectros bootstrap --help\n blueprint Author + inspect blueprints.
|
|
26308
|
+
"\nProvisioning + authoring (own help):\n bootstrap One-shot least-privilege MCP credential. vectros bootstrap --help\n blueprint Author + inspect blueprints; apply one. vectros blueprint --help\n"
|
|
24489
26309
|
);
|
|
24490
26310
|
const login = program.command("login").description("Sign in via your browser (Cognito PKCE) and store a session.").addOption(new Option("--env <env>", "target environment").choices(["staging", "production"]).default("production").hideHelp()).option("--no-browser", "print the sign-in URL instead of opening a browser (headless/CI/WSL)");
|
|
24491
26311
|
login.action(async (flags) => {
|
|
24492
|
-
process.
|
|
26312
|
+
process.exitCode = await runLogin({ env: flags.env, json: false, noBrowser: flags.browser === false });
|
|
24493
26313
|
});
|
|
24494
26314
|
program.command("logout").description("Clear the stored login session.").action(async () => {
|
|
24495
|
-
process.
|
|
26315
|
+
process.exitCode = await runLogout({ env: "production", json: false });
|
|
24496
26316
|
});
|
|
24497
26317
|
const whoami = program.command("whoami").description("Show the authenticated session (tenant, env, principal).");
|
|
24498
26318
|
addCommonAuthOptions(whoami).action(async (flags) => {
|
|
24499
|
-
process.
|
|
26319
|
+
process.exitCode = await runWhoami(toCommonOptions(flags), deps);
|
|
24500
26320
|
});
|
|
24501
26321
|
const blueprintTest = program.command("blueprint-test").description("Apply a blueprint to a live env, assert it provisioned a working credential, then tear it down.").argument("<file>", "blueprint file path (./my.yaml) or a bundled blueprint name").option("--keep", "leave the provisioned graph in place (skip teardown) for inspection").option("--tenant <tenant>", "target tenant for the run: 'test' or 'live' (default live)");
|
|
24502
26322
|
addCommonAuthOptions(blueprintTest).action(
|
|
@@ -24504,14 +26324,13 @@ function buildProgram(deps = {}) {
|
|
|
24504
26324
|
if (flags.tenant !== void 0 && flags.tenant !== "test" && flags.tenant !== "live") {
|
|
24505
26325
|
process.stderr.write(` \u2716 --tenant must be 'test' or 'live' (got '${flags.tenant}')
|
|
24506
26326
|
`);
|
|
24507
|
-
process.
|
|
24508
|
-
|
|
24509
|
-
|
|
24510
|
-
|
|
24511
|
-
|
|
24512
|
-
|
|
24513
|
-
|
|
24514
|
-
)
|
|
26327
|
+
process.exitCode = 2;
|
|
26328
|
+
return;
|
|
26329
|
+
}
|
|
26330
|
+
process.exitCode = await runBlueprintTest(
|
|
26331
|
+
file,
|
|
26332
|
+
{ ...toCommonOptions(flags), keep: Boolean(flags.keep), tenant: flags.tenant },
|
|
26333
|
+
deps
|
|
24515
26334
|
);
|
|
24516
26335
|
}
|
|
24517
26336
|
);
|
|
@@ -24532,9 +26351,7 @@ function registerAssumeCommand(program, deps) {
|
|
|
24532
26351
|
const assume = program.command("assume").description("Re-mint your own verb token with one or more identity.<namespace> values switched (needs a role's assumable grant).").requiredOption("--context <c>", "app context to operate in").option("--scope <ns:value>", "namespace value to assume, repeatable (e.g. org:orgB)", collect, []);
|
|
24533
26352
|
addCommonAuthOptions(assume).action(
|
|
24534
26353
|
async (flags) => {
|
|
24535
|
-
process.
|
|
24536
|
-
await runAssume({ ...toCommonOptions(flags), context: flags.context, scope: flags.scope ?? [] }, deps)
|
|
24537
|
-
);
|
|
26354
|
+
process.exitCode = await runAssume({ ...toCommonOptions(flags), context: flags.context, scope: flags.scope ?? [] }, deps);
|
|
24538
26355
|
}
|
|
24539
26356
|
);
|
|
24540
26357
|
}
|
|
@@ -24548,23 +26365,22 @@ function registerJoinCommand(program, deps) {
|
|
|
24548
26365
|
if (flags.tenant !== void 0 && flags.tenant !== "test" && flags.tenant !== "live") {
|
|
24549
26366
|
process.stderr.write(` \u2716 --tenant must be 'test' or 'live' (got '${flags.tenant}')
|
|
24550
26367
|
`);
|
|
24551
|
-
process.
|
|
26368
|
+
process.exitCode = 2;
|
|
26369
|
+
return;
|
|
24552
26370
|
}
|
|
24553
|
-
process.
|
|
24554
|
-
|
|
24555
|
-
|
|
24556
|
-
|
|
24557
|
-
|
|
24558
|
-
|
|
24559
|
-
|
|
24560
|
-
|
|
24561
|
-
|
|
24562
|
-
|
|
24563
|
-
|
|
24564
|
-
|
|
24565
|
-
|
|
24566
|
-
deps
|
|
24567
|
-
)
|
|
26371
|
+
process.exitCode = await runJoin(
|
|
26372
|
+
{
|
|
26373
|
+
...toCommonOptions(flags),
|
|
26374
|
+
contextArg,
|
|
26375
|
+
role: flags.role,
|
|
26376
|
+
actions: flags.actions,
|
|
26377
|
+
as: flags.as,
|
|
26378
|
+
scope: flags.scope,
|
|
26379
|
+
alias: flags.alias,
|
|
26380
|
+
tenant: flags.tenant,
|
|
26381
|
+
activate: flags.activate
|
|
26382
|
+
},
|
|
26383
|
+
deps
|
|
24568
26384
|
);
|
|
24569
26385
|
}
|
|
24570
26386
|
);
|
|
@@ -24573,29 +26389,27 @@ function registerKeyringCommands(program) {
|
|
|
24573
26389
|
const KEYRING_SHOW_FORMATS = ["human", "env", "raw"];
|
|
24574
26390
|
const keyring = program.command("keyring").description("Inspect and manage the local multi-principal keyring.");
|
|
24575
26391
|
keyring.command("list").description("List keyring entries and which one is active.").option("--json", "emit machine-readable JSON").action(async (flags) => {
|
|
24576
|
-
process.
|
|
26392
|
+
process.exitCode = await runKeyringList({ json: Boolean(flags.json) });
|
|
24577
26393
|
});
|
|
24578
26394
|
keyring.command("use").description("Make an entry the active identity.").argument("<alias>", "the keyring entry to activate").option("--json", "emit machine-readable JSON").action(async (alias, flags) => {
|
|
24579
|
-
process.
|
|
26395
|
+
process.exitCode = await runKeyringUse(alias, { json: Boolean(flags.json) });
|
|
24580
26396
|
});
|
|
24581
26397
|
keyring.command("show").description("Reveal the active (or a named) entry\u2019s key. `--format raw` prints just the secret \u2014 the form scripts and the MCP server resolve a key through.").option("--alias <name>", "show this entry instead of the active one").addOption(new Option("--format <fmt>", "how to emit the key").choices(KEYRING_SHOW_FORMATS).default("human")).action(async (flags) => {
|
|
24582
|
-
process.
|
|
26398
|
+
process.exitCode = await runKeyringShow({ alias: flags.alias, format: flags.format });
|
|
24583
26399
|
});
|
|
24584
26400
|
keyring.command("doctor").description("Health-check the keyring: which entry is active, whether each secret is readable, which entries hold live keys (the ones that act on real data), and which store is holding your secrets.").option("--json", "emit machine-readable JSON").action(async (flags) => {
|
|
24585
|
-
process.
|
|
26401
|
+
process.exitCode = await runKeyringDoctor({ json: Boolean(flags.json) });
|
|
24586
26402
|
});
|
|
24587
26403
|
keyring.command("prune").description("Drop orphaned entries that have no stored secret. Entries whose secret will not decrypt, or is held in the OS credential store (which cannot report absence), are reported and never removed.").option("--dry-run", "report what would be removed without removing anything").option("--force", "skip the interactive confirmation (required when stdin is not a TTY)").option("--json", "emit machine-readable JSON").action(async (flags) => {
|
|
24588
|
-
process.
|
|
24589
|
-
await runKeyringPrune({ dryRun: Boolean(flags.dryRun), force: Boolean(flags.force), json: Boolean(flags.json) })
|
|
24590
|
-
);
|
|
26404
|
+
process.exitCode = await runKeyringPrune({ dryRun: Boolean(flags.dryRun), force: Boolean(flags.force), json: Boolean(flags.json) });
|
|
24591
26405
|
});
|
|
24592
26406
|
keyring.command("remove").description(
|
|
24593
26407
|
"Drop an entry and its stored secret. Refuses if the secret cannot be confirmed deleted \u2014 removing the entry would strand a live key by discarding the only record of its id."
|
|
24594
26408
|
).argument("<alias>", "the keyring entry to remove").option("--json", "emit machine-readable JSON").action(async (alias, flags) => {
|
|
24595
|
-
process.
|
|
26409
|
+
process.exitCode = await runKeyringRemove(alias, { json: Boolean(flags.json) });
|
|
24596
26410
|
});
|
|
24597
26411
|
program.command("switch").description("Shortcut for `keyring use` \u2014 flip the active identity.").argument("<alias>", "the keyring entry to activate").option("--json", "emit machine-readable JSON").action(async (alias, flags) => {
|
|
24598
|
-
process.
|
|
26412
|
+
process.exitCode = await runKeyringUse(alias, { json: Boolean(flags.json) });
|
|
24599
26413
|
});
|
|
24600
26414
|
}
|
|
24601
26415
|
function registerContextCommands(program, deps) {
|
|
@@ -24606,18 +26420,17 @@ function registerContextCommands(program, deps) {
|
|
|
24606
26420
|
if (flags.tenant !== void 0 && flags.tenant !== "test" && flags.tenant !== "live") {
|
|
24607
26421
|
process.stderr.write(` \u2716 --tenant must be 'test' or 'live' (got '${flags.tenant}')
|
|
24608
26422
|
`);
|
|
24609
|
-
process.
|
|
26423
|
+
process.exitCode = 2;
|
|
26424
|
+
return;
|
|
24610
26425
|
}
|
|
24611
|
-
process.
|
|
24612
|
-
|
|
24613
|
-
|
|
24614
|
-
|
|
24615
|
-
|
|
24616
|
-
|
|
24617
|
-
|
|
24618
|
-
|
|
24619
|
-
deps
|
|
24620
|
-
)
|
|
26426
|
+
process.exitCode = await runContextCreate(
|
|
26427
|
+
{
|
|
26428
|
+
...toCommonOptions(flags),
|
|
26429
|
+
contextId,
|
|
26430
|
+
name: flags.name,
|
|
26431
|
+
tenant: flags.tenant
|
|
26432
|
+
},
|
|
26433
|
+
deps
|
|
24621
26434
|
);
|
|
24622
26435
|
}
|
|
24623
26436
|
);
|
|
@@ -24626,13 +26439,12 @@ function registerContextCommands(program, deps) {
|
|
|
24626
26439
|
if (flags.tenant !== void 0 && flags.tenant !== "test" && flags.tenant !== "live") {
|
|
24627
26440
|
process.stderr.write(` \u2716 --tenant must be 'test' or 'live' (got '${flags.tenant}')
|
|
24628
26441
|
`);
|
|
24629
|
-
process.
|
|
26442
|
+
process.exitCode = 2;
|
|
26443
|
+
return;
|
|
24630
26444
|
}
|
|
24631
|
-
process.
|
|
24632
|
-
|
|
24633
|
-
|
|
24634
|
-
deps
|
|
24635
|
-
)
|
|
26445
|
+
process.exitCode = await runContextList(
|
|
26446
|
+
{ ...toCommonOptions(flags), mine: Boolean(flags.mine), tenant: flags.tenant },
|
|
26447
|
+
deps
|
|
24636
26448
|
);
|
|
24637
26449
|
});
|
|
24638
26450
|
const get = context.command("get").description("Show one app context.").argument("<contextId>").option("--tenant <tenant>", "tenant the context lives in: 'test' or 'live' (default live)");
|
|
@@ -24640,13 +26452,12 @@ function registerContextCommands(program, deps) {
|
|
|
24640
26452
|
if (flags.tenant !== void 0 && flags.tenant !== "test" && flags.tenant !== "live") {
|
|
24641
26453
|
process.stderr.write(` \u2716 --tenant must be 'test' or 'live' (got '${flags.tenant}')
|
|
24642
26454
|
`);
|
|
24643
|
-
process.
|
|
26455
|
+
process.exitCode = 2;
|
|
26456
|
+
return;
|
|
24644
26457
|
}
|
|
24645
|
-
process.
|
|
24646
|
-
|
|
24647
|
-
|
|
24648
|
-
deps
|
|
24649
|
-
)
|
|
26458
|
+
process.exitCode = await runContextGet(
|
|
26459
|
+
{ ...toCommonOptions(flags), contextId, tenant: flags.tenant },
|
|
26460
|
+
deps
|
|
24650
26461
|
);
|
|
24651
26462
|
});
|
|
24652
26463
|
const destroy = context.command("destroy").description(
|
|
@@ -24657,18 +26468,17 @@ function registerContextCommands(program, deps) {
|
|
|
24657
26468
|
if (flags.tenant !== void 0 && flags.tenant !== "test" && flags.tenant !== "live") {
|
|
24658
26469
|
process.stderr.write(` \u2716 --tenant must be 'test' or 'live' (got '${flags.tenant}')
|
|
24659
26470
|
`);
|
|
24660
|
-
process.
|
|
26471
|
+
process.exitCode = 2;
|
|
26472
|
+
return;
|
|
24661
26473
|
}
|
|
24662
|
-
process.
|
|
24663
|
-
|
|
24664
|
-
|
|
24665
|
-
|
|
24666
|
-
|
|
24667
|
-
|
|
24668
|
-
|
|
24669
|
-
|
|
24670
|
-
deps
|
|
24671
|
-
)
|
|
26474
|
+
process.exitCode = await runContextDestroy(
|
|
26475
|
+
{
|
|
26476
|
+
...toCommonOptions(flags),
|
|
26477
|
+
contextId,
|
|
26478
|
+
force: Boolean(flags.force),
|
|
26479
|
+
tenant: flags.tenant
|
|
26480
|
+
},
|
|
26481
|
+
deps
|
|
24672
26482
|
);
|
|
24673
26483
|
}
|
|
24674
26484
|
);
|
|
@@ -24679,18 +26489,16 @@ function registerIssuersCommands(program, deps) {
|
|
|
24679
26489
|
addCommonAuthOptions(list).action(
|
|
24680
26490
|
async (flags) => {
|
|
24681
26491
|
const tenant = parseTenantFlag(flags.tenant);
|
|
24682
|
-
|
|
24683
|
-
|
|
24684
|
-
);
|
|
26492
|
+
if (tenant === TENANT_FLAG_INVALID) return;
|
|
26493
|
+
process.exitCode = await runIssuersList({ ...toCommonOptions(flags), context: flags.context, tenant }, deps);
|
|
24685
26494
|
}
|
|
24686
26495
|
);
|
|
24687
26496
|
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)");
|
|
24688
26497
|
addCommonAuthOptions(get).action(
|
|
24689
26498
|
async (issuerId, flags) => {
|
|
24690
26499
|
const tenant = parseTenantFlag(flags.tenant);
|
|
24691
|
-
|
|
24692
|
-
|
|
24693
|
-
);
|
|
26500
|
+
if (tenant === TENANT_FLAG_INVALID) return;
|
|
26501
|
+
process.exitCode = await runIssuersGet({ ...toCommonOptions(flags), context: flags.context, issuerId, tenant }, deps);
|
|
24694
26502
|
}
|
|
24695
26503
|
);
|
|
24696
26504
|
const register = issuers.command("register").description("Register (idempotent by issuerId) a trusted third-party IdP issuer.").requiredOption("--context <c>", "app context an exchanged token targets").requiredOption("--issuer-id <id>", "short slug for this issuer (3-31 chars; lowercase letter + lowercase/digit/dash)").requiredOption("--issuer <url>", "the IdP's iss claim value").requiredOption("--jwks-uri <url>", "the IdP's remote JWKS endpoint").requiredOption("--audience <aud>", "the required aud claim value (globally unique with --issuer)").option("--sub-claim <claim>", "claim carrying the subject identifier (default 'sub')").option("--email-claim <claim>", "claim carrying the subject's email (default 'email')").option("--userinfo-uri <url>", "OIDC userinfo endpoint, used as an email-resolution fallback").option(
|
|
@@ -24702,32 +26510,35 @@ function registerIssuersCommands(program, deps) {
|
|
|
24702
26510
|
"--captured-claim <name>",
|
|
24703
26511
|
"additional OIDC claim to capture from this issuer\u2019s tokens beyond the email claim, repeatable (e.g. phone_number); omit to capture nothing beyond email",
|
|
24704
26512
|
collectOptional
|
|
26513
|
+
).option(
|
|
26514
|
+
"--restricted-to-domain <domain>",
|
|
26515
|
+
"scope this registration to a domain already verified for your account; a token matches only when its 'hd' claim equals it (a token with no 'hd' claim is not matched)"
|
|
24705
26516
|
).option("--tenant <tenant>", "tenant the context lives in: 'test' or 'live' (default live)");
|
|
24706
26517
|
addCommonAuthOptions(register).action(
|
|
24707
26518
|
async (flags) => {
|
|
24708
26519
|
const tenant = parseTenantFlag(flags.tenant);
|
|
24709
|
-
|
|
24710
|
-
|
|
24711
|
-
|
|
24712
|
-
|
|
24713
|
-
|
|
24714
|
-
|
|
24715
|
-
|
|
24716
|
-
|
|
24717
|
-
|
|
24718
|
-
|
|
24719
|
-
|
|
24720
|
-
|
|
24721
|
-
|
|
24722
|
-
|
|
24723
|
-
|
|
24724
|
-
|
|
24725
|
-
|
|
24726
|
-
|
|
26520
|
+
if (tenant === TENANT_FLAG_INVALID) return;
|
|
26521
|
+
process.exitCode = await runIssuersRegister(
|
|
26522
|
+
{
|
|
26523
|
+
...toCommonOptions(flags),
|
|
26524
|
+
context: flags.context,
|
|
26525
|
+
issuerId: flags.issuerId,
|
|
26526
|
+
issuer: flags.issuer,
|
|
26527
|
+
jwksUri: flags.jwksUri,
|
|
26528
|
+
audience: flags.audience,
|
|
26529
|
+
subClaim: flags.subClaim,
|
|
26530
|
+
emailClaim: flags.emailClaim,
|
|
26531
|
+
userinfoUri: flags.userinfoUri,
|
|
26532
|
+
selfSignupPolicy: flags.selfSignupPolicy,
|
|
26533
|
+
capturedClaim: flags.capturedClaim,
|
|
26534
|
+
restrictedToDomain: flags.restrictedToDomain,
|
|
26535
|
+
tenant
|
|
26536
|
+
},
|
|
26537
|
+
deps
|
|
24727
26538
|
);
|
|
24728
26539
|
}
|
|
24729
26540
|
);
|
|
24730
|
-
const update = issuers.command("update").description("Patch the safe, mutable fields of a registered issuer (trust-anchor fields are immutable).").argument("<issuerId>").requiredOption("--context <c>", "app context the issuer is registered under").option("--sub-claim <claim>", "update the subject-identifier claim").option("--email-claim <claim>", "update the subject-email claim").option("--userinfo-uri <url>", "update the OIDC userinfo fallback endpoint").option("--status <status>", "'active' or 'suspended' \u2014 a suspended issuer's tokens are rejected at exchange time").option(
|
|
26541
|
+
const update = issuers.command("update").description("Patch the safe, mutable fields of a registered issuer (trust-anchor fields are immutable).").argument("<issuerId>").requiredOption("--context <c>", "app context the issuer is registered under").option("--sub-claim <claim>", "update the subject-identifier claim").option("--email-claim <claim>", "update the subject-email claim").option("--userinfo-uri <url>", "update the OIDC userinfo fallback endpoint").option("--status <status>", "'active' or 'suspended' \u2014 a suspended issuer's tokens are rejected at exchange time; refused while the registration is pending verification (see 'issuers verify')").option(
|
|
24731
26542
|
"--self-signup-policy <type:roleId>",
|
|
24732
26543
|
"replace self-signup policies, repeatable (e.g. default:member) \u2014 omit entirely to leave unchanged",
|
|
24733
26544
|
collect,
|
|
@@ -24736,28 +26547,32 @@ function registerIssuersCommands(program, deps) {
|
|
|
24736
26547
|
"--captured-claim <name>",
|
|
24737
26548
|
"replace the additional OIDC claims captured beyond the email claim, repeatable \u2014 omit entirely to leave unchanged",
|
|
24738
26549
|
collectOptional
|
|
24739
|
-
).option("--clear-captured-claims", "capture nothing beyond the email claim (sends an empty list; XOR --captured-claim)").option(
|
|
26550
|
+
).option("--clear-captured-claims", "capture nothing beyond the email claim (sends an empty list; XOR --captured-claim)").option(
|
|
26551
|
+
"--restricted-to-domain <domain>",
|
|
26552
|
+
"scope the registration to a domain already verified for your account; a token with no matching 'hd' claim then stops matching \u2014 omit to leave unchanged. Refused on a registration that is pending verification: verify it first"
|
|
26553
|
+
).option("--clear-restricted-to-domain", "remove the domain scope (XOR --restricted-to-domain)").option("--tenant <tenant>", "tenant the context lives in: 'test' or 'live' (default live)");
|
|
24740
26554
|
addCommonAuthOptions(update).action(
|
|
24741
26555
|
async (issuerId, flags) => {
|
|
24742
26556
|
const tenant = parseTenantFlag(flags.tenant);
|
|
24743
|
-
|
|
24744
|
-
|
|
24745
|
-
|
|
24746
|
-
|
|
24747
|
-
|
|
24748
|
-
|
|
24749
|
-
|
|
24750
|
-
|
|
24751
|
-
|
|
24752
|
-
|
|
24753
|
-
|
|
24754
|
-
|
|
24755
|
-
|
|
24756
|
-
|
|
24757
|
-
|
|
24758
|
-
|
|
24759
|
-
|
|
24760
|
-
|
|
26557
|
+
if (tenant === TENANT_FLAG_INVALID) return;
|
|
26558
|
+
process.exitCode = await runIssuersUpdate(
|
|
26559
|
+
{
|
|
26560
|
+
...toCommonOptions(flags),
|
|
26561
|
+
context: flags.context,
|
|
26562
|
+
issuerId,
|
|
26563
|
+
subClaim: flags.subClaim,
|
|
26564
|
+
emailClaim: flags.emailClaim,
|
|
26565
|
+
userinfoUri: flags.userinfoUri,
|
|
26566
|
+
status: flags.status,
|
|
26567
|
+
selfSignupPolicy: flags.selfSignupPolicy,
|
|
26568
|
+
clearSelfSignupPolicies: Boolean(flags.clearSelfSignupPolicies),
|
|
26569
|
+
capturedClaim: flags.capturedClaim,
|
|
26570
|
+
clearCapturedClaims: Boolean(flags.clearCapturedClaims),
|
|
26571
|
+
restrictedToDomain: flags.restrictedToDomain,
|
|
26572
|
+
clearRestrictedToDomain: Boolean(flags.clearRestrictedToDomain),
|
|
26573
|
+
tenant
|
|
26574
|
+
},
|
|
26575
|
+
deps
|
|
24761
26576
|
);
|
|
24762
26577
|
}
|
|
24763
26578
|
);
|
|
@@ -24765,11 +26580,33 @@ function registerIssuersCommands(program, deps) {
|
|
|
24765
26580
|
addCommonAuthOptions(del).action(
|
|
24766
26581
|
async (issuerId, flags) => {
|
|
24767
26582
|
const tenant = parseTenantFlag(flags.tenant);
|
|
24768
|
-
|
|
24769
|
-
|
|
24770
|
-
|
|
24771
|
-
|
|
24772
|
-
|
|
26583
|
+
if (tenant === TENANT_FLAG_INVALID) return;
|
|
26584
|
+
process.exitCode = await runIssuersDelete(
|
|
26585
|
+
{ ...toCommonOptions(flags), context: flags.context, issuerId, force: Boolean(flags.force), tenant },
|
|
26586
|
+
deps
|
|
26587
|
+
);
|
|
26588
|
+
}
|
|
26589
|
+
);
|
|
26590
|
+
const verify = issuers.command("verify").description(
|
|
26591
|
+
"Prove you control a registered issuer, activating it: answers a 'pending_verification' registration's challenge with a JWT from a real login through the identity provider (see 'issuers get' for the challenge)."
|
|
26592
|
+
).argument("<issuerId>").requiredOption("--context <c>", "app context the issuer is registered under").option(
|
|
26593
|
+
"--idp-token <jwt>",
|
|
26594
|
+
"the identity provider's JWT for a real login (NOT your Vectros credential; that is --token). Visible in your shell history and process list, so prefer --idp-token-file or stdin"
|
|
26595
|
+
).option("--idp-token-file <path>", "a file holding the identity provider's JWT ('-' reads stdin); stdin is also read when no token flag is given and input is piped").option("--tenant <tenant>", "tenant the context lives in: 'test' or 'live' (default live)");
|
|
26596
|
+
addCommonAuthOptions(verify).action(
|
|
26597
|
+
async (issuerId, flags) => {
|
|
26598
|
+
const tenant = parseTenantFlag(flags.tenant);
|
|
26599
|
+
if (tenant === TENANT_FLAG_INVALID) return;
|
|
26600
|
+
process.exitCode = await runIssuersVerify(
|
|
26601
|
+
{
|
|
26602
|
+
...toCommonOptions(flags),
|
|
26603
|
+
context: flags.context,
|
|
26604
|
+
issuerId,
|
|
26605
|
+
idpToken: flags.idpToken,
|
|
26606
|
+
idpTokenFile: flags.idpTokenFile,
|
|
26607
|
+
tenant
|
|
26608
|
+
},
|
|
26609
|
+
deps
|
|
24773
26610
|
);
|
|
24774
26611
|
}
|
|
24775
26612
|
);
|
|
@@ -24785,33 +26622,30 @@ function registerNamespaceCommands(program, deps) {
|
|
|
24785
26622
|
` \u2716 --specificity-rank must be an integer between 0 and 1000000 (got '${flags.specificityRank}').
|
|
24786
26623
|
`
|
|
24787
26624
|
);
|
|
24788
|
-
process.
|
|
26625
|
+
process.exitCode = 2;
|
|
26626
|
+
return;
|
|
24789
26627
|
}
|
|
24790
|
-
process.
|
|
24791
|
-
|
|
24792
|
-
|
|
24793
|
-
|
|
24794
|
-
|
|
24795
|
-
|
|
24796
|
-
|
|
24797
|
-
|
|
24798
|
-
|
|
24799
|
-
|
|
24800
|
-
deps
|
|
24801
|
-
)
|
|
26628
|
+
process.exitCode = await runNamespaceRegister(
|
|
26629
|
+
{
|
|
26630
|
+
...toCommonOptions(flags),
|
|
26631
|
+
namespace: name,
|
|
26632
|
+
context: flags.context,
|
|
26633
|
+
specificityRank,
|
|
26634
|
+
entityBacked: Boolean(flags.entityBacked),
|
|
26635
|
+
defaultSchemaId: flags.defaultSchemaId
|
|
26636
|
+
},
|
|
26637
|
+
deps
|
|
24802
26638
|
);
|
|
24803
26639
|
}
|
|
24804
26640
|
);
|
|
24805
26641
|
const list = namespace.command("list").description("List scope-namespace registrations \u2014 tenant-wide, or one context\u2019s own.").option("--context <c>", "list one app context\u2019s own registrations instead of the tenant-wide set");
|
|
24806
26642
|
addCommonAuthOptions(list).action(async (flags) => {
|
|
24807
|
-
process.
|
|
26643
|
+
process.exitCode = await runNamespaceList({ ...toCommonOptions(flags), context: flags.context }, deps);
|
|
24808
26644
|
});
|
|
24809
26645
|
const get = namespace.command("get").description("Show one namespace registration \u2014 tenant-wide, or one context\u2019s own.").argument("<name>").option("--context <c>", "resolve for one app context\u2019s own registration instead of the tenant-wide one");
|
|
24810
26646
|
addCommonAuthOptions(get).action(
|
|
24811
26647
|
async (name, flags) => {
|
|
24812
|
-
process.
|
|
24813
|
-
await runNamespaceGet({ ...toCommonOptions(flags), namespace: name, context: flags.context }, deps)
|
|
24814
|
-
);
|
|
26648
|
+
process.exitCode = await runNamespaceGet({ ...toCommonOptions(flags), namespace: name, context: flags.context }, deps);
|
|
24815
26649
|
}
|
|
24816
26650
|
);
|
|
24817
26651
|
}
|
|
@@ -24820,59 +26654,59 @@ function registerScriptsCommands(program, deps) {
|
|
|
24820
26654
|
const push = scripts.command("push").description("Store a new version of a script name (always creates a version unless --if-changed).").argument("<name>", "script name: 1-64 chars of letters, digits, '_' and '-'").requiredOption("--context <c>", "app context to push into").requiredOption("--source-file <path>", "file holding the script source (max 300,000 UTF-8 bytes)").option("--input-contract <text>", "free-text description of the expected input shape (descriptive only)").option("--input-contract-file <path>", "read the input contract from a file instead").option("--if-changed", "push nothing when the current version already carries this exact content").option("--tenant <tenant>", "target tenant: 'test' or 'live' (default live)");
|
|
24821
26655
|
addCommonAuthOptions(push).action(
|
|
24822
26656
|
async (name, flags) => {
|
|
24823
|
-
|
|
24824
|
-
|
|
24825
|
-
|
|
24826
|
-
|
|
24827
|
-
|
|
24828
|
-
|
|
24829
|
-
|
|
24830
|
-
|
|
24831
|
-
|
|
24832
|
-
|
|
24833
|
-
|
|
24834
|
-
|
|
24835
|
-
|
|
24836
|
-
|
|
26657
|
+
const tenant = parseTenantFlag(flags.tenant);
|
|
26658
|
+
if (tenant === TENANT_FLAG_INVALID) return;
|
|
26659
|
+
process.exitCode = await runScriptsPush(
|
|
26660
|
+
{
|
|
26661
|
+
...toCommonOptions(flags),
|
|
26662
|
+
name,
|
|
26663
|
+
context: flags.context,
|
|
26664
|
+
sourceFile: flags.sourceFile,
|
|
26665
|
+
inputContract: flags.inputContract,
|
|
26666
|
+
inputContractFile: flags.inputContractFile,
|
|
26667
|
+
ifChanged: Boolean(flags.ifChanged),
|
|
26668
|
+
tenant
|
|
26669
|
+
},
|
|
26670
|
+
deps
|
|
24837
26671
|
);
|
|
24838
26672
|
}
|
|
24839
26673
|
);
|
|
24840
26674
|
const list = scripts.command("list").description("List script versions \u2014 every version of one --name (oldest first), or all scripts (newest first).").requiredOption("--context <c>", "app context to read").option("--name <name>", "list every version of this script name instead of a flat cross-name list").option("--tenant <tenant>", "target tenant: 'test' or 'live' (default live)");
|
|
24841
26675
|
addCommonAuthOptions(list).action(
|
|
24842
26676
|
async (flags) => {
|
|
24843
|
-
|
|
24844
|
-
|
|
24845
|
-
|
|
24846
|
-
|
|
24847
|
-
|
|
26677
|
+
const tenant = parseTenantFlag(flags.tenant);
|
|
26678
|
+
if (tenant === TENANT_FLAG_INVALID) return;
|
|
26679
|
+
process.exitCode = await runScriptsList(
|
|
26680
|
+
{ ...toCommonOptions(flags), context: flags.context, name: flags.name, tenant },
|
|
26681
|
+
deps
|
|
24848
26682
|
);
|
|
24849
26683
|
}
|
|
24850
26684
|
);
|
|
24851
26685
|
const get = scripts.command("get").description("Show one script version, including its full source.").argument("<id>", "the script VERSION id (each version has its own)").requiredOption("--context <c>", "app context to read").option("--tenant <tenant>", "target tenant: 'test' or 'live' (default live)");
|
|
24852
26686
|
addCommonAuthOptions(get).action(
|
|
24853
26687
|
async (id, flags) => {
|
|
24854
|
-
|
|
24855
|
-
|
|
24856
|
-
|
|
24857
|
-
|
|
24858
|
-
|
|
26688
|
+
const tenant = parseTenantFlag(flags.tenant);
|
|
26689
|
+
if (tenant === TENANT_FLAG_INVALID) return;
|
|
26690
|
+
process.exitCode = await runScriptsGet(
|
|
26691
|
+
{ ...toCommonOptions(flags), id, context: flags.context, tenant },
|
|
26692
|
+
deps
|
|
24859
26693
|
);
|
|
24860
26694
|
}
|
|
24861
26695
|
);
|
|
24862
26696
|
const del = scripts.command("delete").description("Permanently delete ONE script version (other versions of the name are unaffected).").argument("<id>", "the script VERSION id").requiredOption("--context <c>", "app context to delete from").option("--tenant <tenant>", "target tenant: 'test' or 'live' (default live)").option("--force", "skip the interactive confirmation (required in a non-interactive shell)");
|
|
24863
26697
|
addCommonAuthOptions(del).action(
|
|
24864
26698
|
async (id, flags) => {
|
|
24865
|
-
|
|
24866
|
-
|
|
24867
|
-
|
|
24868
|
-
|
|
24869
|
-
|
|
24870
|
-
|
|
24871
|
-
|
|
24872
|
-
|
|
24873
|
-
|
|
24874
|
-
|
|
24875
|
-
|
|
26699
|
+
const tenant = parseTenantFlag(flags.tenant);
|
|
26700
|
+
if (tenant === TENANT_FLAG_INVALID) return;
|
|
26701
|
+
process.exitCode = await runScriptsDelete(
|
|
26702
|
+
{
|
|
26703
|
+
...toCommonOptions(flags),
|
|
26704
|
+
id,
|
|
26705
|
+
context: flags.context,
|
|
26706
|
+
tenant,
|
|
26707
|
+
force: Boolean(flags.force)
|
|
26708
|
+
},
|
|
26709
|
+
deps
|
|
24876
26710
|
);
|
|
24877
26711
|
}
|
|
24878
26712
|
);
|
|
@@ -24892,9 +26726,11 @@ function registerRoleCommands(program, deps) {
|
|
|
24892
26726
|
).option("--description <text>", "free-text description").option("--tenant <tenant>", "tenant to target: 'test' or 'live' (default live)");
|
|
24893
26727
|
addCommonAuthOptions(create).action(
|
|
24894
26728
|
async (flags) => {
|
|
26729
|
+
const tenant = parseTenantFlag(flags.tenant);
|
|
26730
|
+
if (tenant === TENANT_FLAG_INVALID) return;
|
|
24895
26731
|
process.exitCode = await runRoleCreate({
|
|
24896
26732
|
...toCommonOptions(flags),
|
|
24897
|
-
tenant
|
|
26733
|
+
tenant,
|
|
24898
26734
|
context: flags.context,
|
|
24899
26735
|
roleId: flags.roleId,
|
|
24900
26736
|
name: flags.name,
|
|
@@ -24908,16 +26744,20 @@ function registerRoleCommands(program, deps) {
|
|
|
24908
26744
|
);
|
|
24909
26745
|
const list = role.command("list").description("List the roles defined in a context.").requiredOption("--context <c>", "app context").option("--tenant <tenant>", "tenant to target: 'test' or 'live' (default live)");
|
|
24910
26746
|
addCommonAuthOptions(list).action(async (flags) => {
|
|
26747
|
+
const tenant = parseTenantFlag(flags.tenant);
|
|
26748
|
+
if (tenant === TENANT_FLAG_INVALID) return;
|
|
24911
26749
|
process.exitCode = await runRoleList(
|
|
24912
|
-
{ ...toCommonOptions(flags), tenant
|
|
26750
|
+
{ ...toCommonOptions(flags), tenant, context: flags.context },
|
|
24913
26751
|
deps
|
|
24914
26752
|
);
|
|
24915
26753
|
});
|
|
24916
26754
|
const get = role.command("get").description("Show one role by id.").requiredOption("--context <c>", "app context").requiredOption("--role-id <id>", "role id").option("--tenant <tenant>", "tenant to target: 'test' or 'live' (default live)");
|
|
24917
26755
|
addCommonAuthOptions(get).action(
|
|
24918
26756
|
async (flags) => {
|
|
26757
|
+
const tenant = parseTenantFlag(flags.tenant);
|
|
26758
|
+
if (tenant === TENANT_FLAG_INVALID) return;
|
|
24919
26759
|
process.exitCode = await runRoleGet(
|
|
24920
|
-
{ ...toCommonOptions(flags), tenant
|
|
26760
|
+
{ ...toCommonOptions(flags), tenant, context: flags.context, roleId: flags.roleId },
|
|
24921
26761
|
deps
|
|
24922
26762
|
);
|
|
24923
26763
|
}
|
|
@@ -24925,8 +26765,10 @@ function registerRoleCommands(program, deps) {
|
|
|
24925
26765
|
const del = role.command("delete").description("Delete a role (referencing profiles must be reassigned).").requiredOption("--context <c>", "app context").requiredOption("--role-id <id>", "role id").option("--tenant <tenant>", "tenant to target: 'test' or 'live' (default live)");
|
|
24926
26766
|
addAuthOptions(del).action(
|
|
24927
26767
|
async (flags) => {
|
|
26768
|
+
const tenant = parseTenantFlag(flags.tenant);
|
|
26769
|
+
if (tenant === TENANT_FLAG_INVALID) return;
|
|
24928
26770
|
process.exitCode = await runRoleDelete(
|
|
24929
|
-
{ ...toCommonOptions(flags), tenant
|
|
26771
|
+
{ ...toCommonOptions(flags), tenant, context: flags.context, roleId: flags.roleId },
|
|
24930
26772
|
deps
|
|
24931
26773
|
);
|
|
24932
26774
|
}
|
|
@@ -24937,9 +26779,11 @@ function registerAccessCommands(program, deps) {
|
|
|
24937
26779
|
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)").option("--tenant <tenant>", "tenant to target: 'test' or 'live' (default live)");
|
|
24938
26780
|
addCommonAuthOptions(grant).action(
|
|
24939
26781
|
async (flags) => {
|
|
26782
|
+
const tenant = parseTenantFlag(flags.tenant);
|
|
26783
|
+
if (tenant === TENANT_FLAG_INVALID) return;
|
|
24940
26784
|
process.exitCode = await runAccessGrant({
|
|
24941
26785
|
...toCommonOptions(flags),
|
|
24942
|
-
tenant
|
|
26786
|
+
tenant,
|
|
24943
26787
|
principal: flags.principal,
|
|
24944
26788
|
context: flags.context,
|
|
24945
26789
|
role: flags.role,
|
|
@@ -24952,8 +26796,10 @@ function registerAccessCommands(program, deps) {
|
|
|
24952
26796
|
const revoke = access2.command("revoke").description("Remove a principal\u2019s binding in a context.").requiredOption("--principal <p>", "principal: 'usr_<userId>', 'key_<keyId>', or 'me' (your own user)").requiredOption("--context <c>", "app context").option("--tenant <tenant>", "tenant to target: 'test' or 'live' (default live)");
|
|
24953
26797
|
addAuthOptions(revoke).action(
|
|
24954
26798
|
async (flags) => {
|
|
26799
|
+
const tenant = parseTenantFlag(flags.tenant);
|
|
26800
|
+
if (tenant === TENANT_FLAG_INVALID) return;
|
|
24955
26801
|
process.exitCode = await runAccessRevoke(
|
|
24956
|
-
{ ...toCommonOptions(flags), tenant
|
|
26802
|
+
{ ...toCommonOptions(flags), tenant, principal: flags.principal, context: flags.context },
|
|
24957
26803
|
deps
|
|
24958
26804
|
);
|
|
24959
26805
|
}
|
|
@@ -24961,8 +26807,10 @@ function registerAccessCommands(program, deps) {
|
|
|
24961
26807
|
const list = access2.command("list").description("List a context\u2019s members (--context) or a principal\u2019s contexts (--principal).").option("--context <c>", "list members of this context").option("--principal <p>", "list contexts this principal is bound to ('me' = your own user)").option("--tenant <tenant>", "tenant to target: 'test' or 'live' (default live)");
|
|
24962
26808
|
addCommonAuthOptions(list).action(
|
|
24963
26809
|
async (flags) => {
|
|
26810
|
+
const tenant = parseTenantFlag(flags.tenant);
|
|
26811
|
+
if (tenant === TENANT_FLAG_INVALID) return;
|
|
24964
26812
|
process.exitCode = await runAccessList(
|
|
24965
|
-
{ ...toCommonOptions(flags), tenant
|
|
26813
|
+
{ ...toCommonOptions(flags), tenant, context: flags.context, principal: flags.principal },
|
|
24966
26814
|
deps
|
|
24967
26815
|
);
|
|
24968
26816
|
}
|
|
@@ -24970,8 +26818,10 @@ function registerAccessCommands(program, deps) {
|
|
|
24970
26818
|
const get = access2.command("get").description("Show one principal\u2019s binding in a context.").requiredOption("--principal <p>", "principal: 'usr_<userId>', 'key_<keyId>', or 'me' (your own user)").requiredOption("--context <c>", "app context").option("--tenant <tenant>", "tenant to target: 'test' or 'live' (default live)");
|
|
24971
26819
|
addCommonAuthOptions(get).action(
|
|
24972
26820
|
async (flags) => {
|
|
26821
|
+
const tenant = parseTenantFlag(flags.tenant);
|
|
26822
|
+
if (tenant === TENANT_FLAG_INVALID) return;
|
|
24973
26823
|
process.exitCode = await runAccessGet(
|
|
24974
|
-
{ ...toCommonOptions(flags), tenant
|
|
26824
|
+
{ ...toCommonOptions(flags), tenant, principal: flags.principal, context: flags.context },
|
|
24975
26825
|
deps
|
|
24976
26826
|
);
|
|
24977
26827
|
}
|
|
@@ -24979,8 +26829,10 @@ function registerAccessCommands(program, deps) {
|
|
|
24979
26829
|
const explain = access2.command("explain").description("Show what a principal\u2019s binding grants: its clauses + a concrete access matrix.").requiredOption("--principal <p>", "principal: 'usr_<userId>', 'key_<keyId>', or 'me' (your own user)").requiredOption("--context <c>", "app context").option("--tenant <tenant>", "tenant to target: 'test' or 'live' (default live)");
|
|
24980
26830
|
addCommonAuthOptions(explain).action(
|
|
24981
26831
|
async (flags) => {
|
|
26832
|
+
const tenant = parseTenantFlag(flags.tenant);
|
|
26833
|
+
if (tenant === TENANT_FLAG_INVALID) return;
|
|
24982
26834
|
process.exitCode = await runAccessExplain(
|
|
24983
|
-
{ ...toCommonOptions(flags), tenant
|
|
26835
|
+
{ ...toCommonOptions(flags), tenant, principal: flags.principal, context: flags.context },
|
|
24984
26836
|
deps
|
|
24985
26837
|
);
|
|
24986
26838
|
}
|
|
@@ -24997,31 +26849,27 @@ function registerIdentityCommands(program, deps) {
|
|
|
24997
26849
|
const create = identity.command("create").description("Create (idempotent by externalId) a principal.").addOption(dimensionOption()).requiredOption("--external-id <id>", "your stable id for the principal (idempotency key)").option("--name <name>", "display name (entity namespaces only, not --type user)").option("--email <email>", "email (user only)").option("--service", "user only: mint a SERVICE principal (default HUMAN)").option("--scope <ns:value>", "entity only: a parent ownership edge (repeatable, \u22642, e.g. org:<id>)", collect, []).option("--metadata <json>", "JSON object of metadata");
|
|
24998
26850
|
addCommonAuthOptions(create).action(
|
|
24999
26851
|
async (flags) => {
|
|
25000
|
-
process.
|
|
25001
|
-
|
|
25002
|
-
|
|
25003
|
-
|
|
25004
|
-
|
|
25005
|
-
|
|
25006
|
-
|
|
25007
|
-
|
|
25008
|
-
|
|
25009
|
-
|
|
25010
|
-
}, deps)
|
|
25011
|
-
);
|
|
26852
|
+
process.exitCode = await runIdentityCreate({
|
|
26853
|
+
...toCommonOptions(flags),
|
|
26854
|
+
type: flags.type,
|
|
26855
|
+
externalId: flags.externalId,
|
|
26856
|
+
name: flags.name,
|
|
26857
|
+
email: flags.email,
|
|
26858
|
+
service: flags.service,
|
|
26859
|
+
scope: flags.scope,
|
|
26860
|
+
metadata: flags.metadata
|
|
26861
|
+
}, deps);
|
|
25012
26862
|
}
|
|
25013
26863
|
);
|
|
25014
26864
|
const list = identity.command("list").description("List principals of a dimension (optionally filtered).").addOption(dimensionOption()).option("--external-id <id>", "filter to a stable id").option("--limit <n>", "page size (1\u2013100)", (v) => Number.parseInt(v, 10));
|
|
25015
26865
|
addCommonAuthOptions(list).action(
|
|
25016
26866
|
async (flags) => {
|
|
25017
|
-
process.
|
|
25018
|
-
|
|
25019
|
-
|
|
25020
|
-
|
|
25021
|
-
|
|
25022
|
-
|
|
25023
|
-
}, deps)
|
|
25024
|
-
);
|
|
26867
|
+
process.exitCode = await runIdentityList({
|
|
26868
|
+
...toCommonOptions(flags),
|
|
26869
|
+
type: flags.type,
|
|
26870
|
+
externalId: flags.externalId,
|
|
26871
|
+
limit: flags.limit
|
|
26872
|
+
}, deps);
|
|
25025
26873
|
}
|
|
25026
26874
|
);
|
|
25027
26875
|
const listAll = identity.command("list-all").description(
|
|
@@ -25030,38 +26878,35 @@ function registerIdentityCommands(program, deps) {
|
|
|
25030
26878
|
addCommonAuthOptions(listAll).action(
|
|
25031
26879
|
async (flags) => {
|
|
25032
26880
|
const tenant = parseTenantFlag(flags.tenant);
|
|
25033
|
-
|
|
25034
|
-
|
|
25035
|
-
|
|
25036
|
-
|
|
25037
|
-
)
|
|
26881
|
+
if (tenant === TENANT_FLAG_INVALID) return;
|
|
26882
|
+
process.exitCode = await runIdentityListAll(
|
|
26883
|
+
{ ...toCommonOptions(flags), externalId: flags.externalId, limit: flags.limit, tenant },
|
|
26884
|
+
deps
|
|
25038
26885
|
);
|
|
25039
26886
|
}
|
|
25040
26887
|
);
|
|
25041
26888
|
const get = identity.command("get").description("Show one principal by its Vectros id.").addOption(dimensionOption()).requiredOption("--id <vectrosId>", "the Vectros-assigned id (from create/list)");
|
|
25042
26889
|
addCommonAuthOptions(get).action(async (flags) => {
|
|
25043
|
-
process.
|
|
26890
|
+
process.exitCode = await runIdentityGet({ ...toCommonOptions(flags), type: flags.type, id: flags.id }, deps);
|
|
25044
26891
|
});
|
|
25045
26892
|
const del = identity.command("delete").description("Delete a principal by its Vectros id.").addOption(dimensionOption()).requiredOption("--id <vectrosId>", "the Vectros-assigned id");
|
|
25046
26893
|
addAuthOptions(del).action(async (flags) => {
|
|
25047
|
-
process.
|
|
26894
|
+
process.exitCode = await runIdentityDelete({ ...toCommonOptions(flags), type: flags.type, id: flags.id }, deps);
|
|
25048
26895
|
});
|
|
25049
26896
|
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");
|
|
25050
26897
|
addCommonAuthOptions(invite).action(
|
|
25051
26898
|
async (flags) => {
|
|
25052
|
-
process.
|
|
25053
|
-
|
|
25054
|
-
|
|
25055
|
-
|
|
25056
|
-
|
|
25057
|
-
|
|
25058
|
-
|
|
25059
|
-
|
|
25060
|
-
|
|
25061
|
-
|
|
25062
|
-
|
|
25063
|
-
deps
|
|
25064
|
-
)
|
|
26899
|
+
process.exitCode = await runIdentityInvite(
|
|
26900
|
+
{
|
|
26901
|
+
...toCommonOptions(flags),
|
|
26902
|
+
context: flags.context,
|
|
26903
|
+
role: flags.role,
|
|
26904
|
+
email: flags.email,
|
|
26905
|
+
firstName: flags.firstName,
|
|
26906
|
+
acceptUrl: flags.acceptUrl,
|
|
26907
|
+
noSendEmail: flags.sendEmail === false
|
|
26908
|
+
},
|
|
26909
|
+
deps
|
|
25065
26910
|
);
|
|
25066
26911
|
}
|
|
25067
26912
|
);
|
|
@@ -25087,17 +26932,16 @@ function registerKeyCommands(program, deps) {
|
|
|
25087
26932
|
);
|
|
25088
26933
|
addAuthOptions(issue).action(async (flags) => {
|
|
25089
26934
|
const tenant = parseTenantFlag(flags.tenant);
|
|
25090
|
-
|
|
25091
|
-
|
|
25092
|
-
|
|
25093
|
-
|
|
25094
|
-
|
|
25095
|
-
|
|
25096
|
-
|
|
25097
|
-
|
|
25098
|
-
|
|
25099
|
-
|
|
25100
|
-
);
|
|
26935
|
+
if (tenant === TENANT_FLAG_INVALID) return;
|
|
26936
|
+
process.exitCode = await runKeyIssue({
|
|
26937
|
+
...toCommonOptions(flags),
|
|
26938
|
+
principal: flags.principal,
|
|
26939
|
+
context: flags.context,
|
|
26940
|
+
name: flags.name,
|
|
26941
|
+
label: flags.label,
|
|
26942
|
+
tenant,
|
|
26943
|
+
...toKeyDestination(flags)
|
|
26944
|
+
}, deps);
|
|
25101
26945
|
});
|
|
25102
26946
|
const list = key.command("list").description("List active scoped keys in one context (default, or --context <id>); --all-contexts for every context.").option("--principal <p>", "only keys for this principal (usr_<id> or <id>)").option("--context <c>", "only keys in this context (default: the default context)").option(
|
|
25103
26947
|
"--all-contexts",
|
|
@@ -25107,20 +26951,20 @@ function registerKeyCommands(program, deps) {
|
|
|
25107
26951
|
async (flags) => {
|
|
25108
26952
|
if (flags.allContexts && flags.tenant !== void 0) {
|
|
25109
26953
|
process.stderr.write(" \u2716 --tenant is not combinable with --all-contexts (which already spans both tenants).\n");
|
|
25110
|
-
process.
|
|
26954
|
+
process.exitCode = 2;
|
|
26955
|
+
return;
|
|
25111
26956
|
}
|
|
25112
26957
|
const tenant = parseTenantFlag(flags.tenant);
|
|
25113
|
-
|
|
25114
|
-
|
|
25115
|
-
|
|
25116
|
-
|
|
25117
|
-
|
|
25118
|
-
|
|
25119
|
-
|
|
25120
|
-
|
|
25121
|
-
|
|
25122
|
-
|
|
25123
|
-
)
|
|
26958
|
+
if (tenant === TENANT_FLAG_INVALID) return;
|
|
26959
|
+
process.exitCode = await runKeyList(
|
|
26960
|
+
{
|
|
26961
|
+
...toCommonOptions(flags),
|
|
26962
|
+
principal: flags.principal,
|
|
26963
|
+
context: flags.context,
|
|
26964
|
+
allContexts: Boolean(flags.allContexts),
|
|
26965
|
+
tenant
|
|
26966
|
+
},
|
|
26967
|
+
deps
|
|
25124
26968
|
);
|
|
25125
26969
|
}
|
|
25126
26970
|
);
|
|
@@ -25128,14 +26972,16 @@ function registerKeyCommands(program, deps) {
|
|
|
25128
26972
|
addCommonAuthOptions(get).action(
|
|
25129
26973
|
async (keyId, flags) => {
|
|
25130
26974
|
const tenant = parseTenantFlag(flags.tenant);
|
|
25131
|
-
|
|
26975
|
+
if (tenant === TENANT_FLAG_INVALID) return;
|
|
26976
|
+
process.exitCode = await runKeyGet(keyId, { ...toCommonOptions(flags), context: flags.context, tenant }, deps);
|
|
25132
26977
|
}
|
|
25133
26978
|
);
|
|
25134
26979
|
const revoke = key.command("revoke").description("Revoke a key (soft-delete; ~5 min to take effect).").argument("<keyId>").option("--context <c>", "context the key is bound to (default: the default context)").option("--tenant <tenant>", "tenant the key lives in: 'test' or 'live' (default live)");
|
|
25135
26980
|
addAuthOptions(revoke).action(
|
|
25136
26981
|
async (keyId, flags) => {
|
|
25137
26982
|
const tenant = parseTenantFlag(flags.tenant);
|
|
25138
|
-
|
|
26983
|
+
if (tenant === TENANT_FLAG_INVALID) return;
|
|
26984
|
+
process.exitCode = await runKeyRevoke(keyId, { ...toCommonOptions(flags), context: flags.context, tenant }, deps);
|
|
25139
26985
|
}
|
|
25140
26986
|
);
|
|
25141
26987
|
const rotate = addKeyDestinationOptions(
|
|
@@ -25146,17 +26992,16 @@ function registerKeyCommands(program, deps) {
|
|
|
25146
26992
|
);
|
|
25147
26993
|
addAuthOptions(rotate).action(async (flags) => {
|
|
25148
26994
|
const tenant = parseTenantFlag(flags.tenant);
|
|
25149
|
-
|
|
25150
|
-
|
|
25151
|
-
|
|
25152
|
-
|
|
25153
|
-
|
|
25154
|
-
|
|
25155
|
-
|
|
25156
|
-
|
|
25157
|
-
|
|
25158
|
-
|
|
25159
|
-
);
|
|
26995
|
+
if (tenant === TENANT_FLAG_INVALID) return;
|
|
26996
|
+
process.exitCode = await runKeyRotate({
|
|
26997
|
+
...toCommonOptions(flags),
|
|
26998
|
+
principal: flags.principal,
|
|
26999
|
+
context: flags.context,
|
|
27000
|
+
name: flags.name,
|
|
27001
|
+
forceNew: flags.forceNew,
|
|
27002
|
+
tenant,
|
|
27003
|
+
...toKeyDestination(flags)
|
|
27004
|
+
}, deps);
|
|
25160
27005
|
});
|
|
25161
27006
|
}
|
|
25162
27007
|
|
|
@@ -25169,7 +27014,8 @@ async function main() {
|
|
|
25169
27014
|
const sub = process.argv[2];
|
|
25170
27015
|
const passthrough = sub ? PASSTHROUGH_COMMANDS[sub] : void 0;
|
|
25171
27016
|
if (passthrough) {
|
|
25172
|
-
process.
|
|
27017
|
+
process.exitCode = await passthrough(process.argv.slice(3));
|
|
27018
|
+
return;
|
|
25173
27019
|
}
|
|
25174
27020
|
await buildProgram().parseAsync(process.argv);
|
|
25175
27021
|
}
|
|
@@ -25180,6 +27026,6 @@ main().catch((err) => {
|
|
|
25180
27026
|
process.stderr.write(`${err.stack}
|
|
25181
27027
|
`);
|
|
25182
27028
|
}
|
|
25183
|
-
process.
|
|
27029
|
+
process.exitCode = 2;
|
|
25184
27030
|
});
|
|
25185
27031
|
//# sourceMappingURL=cli.mjs.map
|