@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.js
CHANGED
|
@@ -518,7 +518,7 @@ var require_graceful_fs = __commonJS({
|
|
|
518
518
|
function patch(fs5) {
|
|
519
519
|
polyfills(fs5);
|
|
520
520
|
fs5.gracefulify = patch;
|
|
521
|
-
fs5.createReadStream =
|
|
521
|
+
fs5.createReadStream = createReadStream2;
|
|
522
522
|
fs5.createWriteStream = createWriteStream;
|
|
523
523
|
var fs$readFile = fs5.readFile;
|
|
524
524
|
fs5.readFile = readFile4;
|
|
@@ -728,7 +728,7 @@ var require_graceful_fs = __commonJS({
|
|
|
728
728
|
}
|
|
729
729
|
});
|
|
730
730
|
}
|
|
731
|
-
function
|
|
731
|
+
function createReadStream2(path4, options) {
|
|
732
732
|
return new fs5.ReadStream(path4, options);
|
|
733
733
|
}
|
|
734
734
|
function createWriteStream(path4, options) {
|
|
@@ -5013,6 +5013,7 @@ var AuthClient = class {
|
|
|
5013
5013
|
*
|
|
5014
5014
|
* @throws {@link Vectros.ForbiddenError}
|
|
5015
5015
|
* @throws {@link Vectros.NotFoundError}
|
|
5016
|
+
* @throws {@link Vectros.ConflictError}
|
|
5016
5017
|
* @throws {@link Vectros.TooManyRequestsError}
|
|
5017
5018
|
*
|
|
5018
5019
|
* @example
|
|
@@ -5055,6 +5056,8 @@ var AuthClient = class {
|
|
|
5055
5056
|
throw new ForbiddenError(_response.error.body, _response.rawResponse);
|
|
5056
5057
|
case 404:
|
|
5057
5058
|
throw new NotFoundError(_response.error.body, _response.rawResponse);
|
|
5059
|
+
case 409:
|
|
5060
|
+
throw new ConflictError(_response.error.body, _response.rawResponse);
|
|
5058
5061
|
case 429:
|
|
5059
5062
|
throw new TooManyRequestsError(_response.error.body, _response.rawResponse);
|
|
5060
5063
|
default:
|
|
@@ -5131,7 +5134,7 @@ var AuthClient = class {
|
|
|
5131
5134
|
return handleNonStatusCodeError(_response.error, _response.rawResponse, "GET", "/v1/app-contexts/{contextId}");
|
|
5132
5135
|
}
|
|
5133
5136
|
/**
|
|
5134
|
-
* Updates
|
|
5137
|
+
* 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.
|
|
5135
5138
|
*
|
|
5136
5139
|
* @param {Vectros.UpdateAppContextRequest} request
|
|
5137
5140
|
* @param {AuthClient.RequestOptions} requestOptions - Request-specific configuration.
|
|
@@ -5199,7 +5202,7 @@ var AuthClient = class {
|
|
|
5199
5202
|
return handleNonStatusCodeError(_response.error, _response.rawResponse, "PUT", "/v1/app-contexts/{contextId}");
|
|
5200
5203
|
}
|
|
5201
5204
|
/**
|
|
5202
|
-
* Permanently deletes an app context and everything in it — every record, document, folder, schema, role,
|
|
5205
|
+
* 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.
|
|
5203
5206
|
*
|
|
5204
5207
|
* @param {Vectros.DeleteAppContextRequest} request
|
|
5205
5208
|
* @param {AuthClient.RequestOptions} requestOptions - Request-specific configuration.
|
|
@@ -5734,7 +5737,7 @@ var AuthClient = class {
|
|
|
5734
5737
|
return handleNonStatusCodeError(_response.error, _response.rawResponse, "GET", "/v1/auth/issuers/{issuerId}");
|
|
5735
5738
|
}
|
|
5736
5739
|
/**
|
|
5737
|
-
* 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.
|
|
5740
|
+
* 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.
|
|
5738
5741
|
*
|
|
5739
5742
|
* @param {Vectros.IssuerUpdateRequest} request
|
|
5740
5743
|
* @param {AuthClient.RequestOptions} requestOptions - Request-specific configuration.
|
|
@@ -5801,7 +5804,7 @@ var AuthClient = class {
|
|
|
5801
5804
|
return handleNonStatusCodeError(_response.error, _response.rawResponse, "PUT", "/v1/auth/issuers/{issuerId}");
|
|
5802
5805
|
}
|
|
5803
5806
|
/**
|
|
5804
|
-
* 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.
|
|
5807
|
+
* 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.
|
|
5805
5808
|
*
|
|
5806
5809
|
* @param {Vectros.DeleteIssuerRequest} request
|
|
5807
5810
|
* @param {AuthClient.RequestOptions} requestOptions - Request-specific configuration.
|
|
@@ -5927,7 +5930,7 @@ var AuthClient = class {
|
|
|
5927
5930
|
return handleNonStatusCodeError(_response.error, _response.rawResponse, "GET", "/v1/auth/issuers");
|
|
5928
5931
|
}
|
|
5929
5932
|
/**
|
|
5930
|
-
* 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.
|
|
5933
|
+
* 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.
|
|
5931
5934
|
*
|
|
5932
5935
|
* @param {Vectros.IssuerRequest} request
|
|
5933
5936
|
* @param {AuthClient.RequestOptions} requestOptions - Request-specific configuration.
|
|
@@ -5993,6 +5996,82 @@ var AuthClient = class {
|
|
|
5993
5996
|
}
|
|
5994
5997
|
return handleNonStatusCodeError(_response.error, _response.rawResponse, "POST", "/v1/auth/issuers");
|
|
5995
5998
|
}
|
|
5999
|
+
/**
|
|
6000
|
+
* 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.
|
|
6001
|
+
*
|
|
6002
|
+
* @param {Vectros.IssuerVerifyRequest} request
|
|
6003
|
+
* @param {AuthClient.RequestOptions} requestOptions - Request-specific configuration.
|
|
6004
|
+
*
|
|
6005
|
+
* @throws {@link Vectros.BadRequestError}
|
|
6006
|
+
* @throws {@link Vectros.ForbiddenError}
|
|
6007
|
+
* @throws {@link Vectros.NotFoundError}
|
|
6008
|
+
* @throws {@link Vectros.ConflictError}
|
|
6009
|
+
* @throws {@link Vectros.TooManyRequestsError}
|
|
6010
|
+
*
|
|
6011
|
+
* @example
|
|
6012
|
+
* await client.auth.verifyIssuer({
|
|
6013
|
+
* issuerId: "auth0-prod",
|
|
6014
|
+
* token: "token"
|
|
6015
|
+
* })
|
|
6016
|
+
*/
|
|
6017
|
+
verifyIssuer(request, requestOptions) {
|
|
6018
|
+
return HttpResponsePromise.fromPromise(this.__verifyIssuer(request, requestOptions));
|
|
6019
|
+
}
|
|
6020
|
+
async __verifyIssuer(request, requestOptions) {
|
|
6021
|
+
const { issuerId, ..._body } = request;
|
|
6022
|
+
const _authRequest = await this._options.authProvider.getAuthRequest();
|
|
6023
|
+
const _headers = mergeHeaders(
|
|
6024
|
+
_authRequest.headers,
|
|
6025
|
+
this._options?.headers,
|
|
6026
|
+
requestOptions?.headers
|
|
6027
|
+
);
|
|
6028
|
+
const _response = await fetcher({
|
|
6029
|
+
url: url_exports.join(
|
|
6030
|
+
await Supplier.get(this._options.baseUrl) ?? await Supplier.get(this._options.environment),
|
|
6031
|
+
`v1/auth/issuers/${url_exports.encodePathParam(issuerId)}/verify`
|
|
6032
|
+
),
|
|
6033
|
+
method: "POST",
|
|
6034
|
+
headers: _headers,
|
|
6035
|
+
contentType: "application/json",
|
|
6036
|
+
queryString: url_exports.queryBuilder().mergeAdditional(requestOptions?.queryParams).build(),
|
|
6037
|
+
requestType: "json",
|
|
6038
|
+
body: _body,
|
|
6039
|
+
timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1e3,
|
|
6040
|
+
maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries,
|
|
6041
|
+
abortSignal: requestOptions?.abortSignal,
|
|
6042
|
+
fetchFn: this._options?.fetch,
|
|
6043
|
+
logging: this._options.logging
|
|
6044
|
+
});
|
|
6045
|
+
if (_response.ok) {
|
|
6046
|
+
return { data: _response.body, rawResponse: _response.rawResponse };
|
|
6047
|
+
}
|
|
6048
|
+
if (_response.error.reason === "status-code") {
|
|
6049
|
+
switch (_response.error.statusCode) {
|
|
6050
|
+
case 400:
|
|
6051
|
+
throw new BadRequestError(_response.error.body, _response.rawResponse);
|
|
6052
|
+
case 403:
|
|
6053
|
+
throw new ForbiddenError(_response.error.body, _response.rawResponse);
|
|
6054
|
+
case 404:
|
|
6055
|
+
throw new NotFoundError(_response.error.body, _response.rawResponse);
|
|
6056
|
+
case 409:
|
|
6057
|
+
throw new ConflictError(_response.error.body, _response.rawResponse);
|
|
6058
|
+
case 429:
|
|
6059
|
+
throw new TooManyRequestsError(_response.error.body, _response.rawResponse);
|
|
6060
|
+
default:
|
|
6061
|
+
throw new VectrosError({
|
|
6062
|
+
statusCode: _response.error.statusCode,
|
|
6063
|
+
body: _response.error.body,
|
|
6064
|
+
rawResponse: _response.rawResponse
|
|
6065
|
+
});
|
|
6066
|
+
}
|
|
6067
|
+
}
|
|
6068
|
+
return handleNonStatusCodeError(
|
|
6069
|
+
_response.error,
|
|
6070
|
+
_response.rawResponse,
|
|
6071
|
+
"POST",
|
|
6072
|
+
"/v1/auth/issuers/{issuerId}/verify"
|
|
6073
|
+
);
|
|
6074
|
+
}
|
|
5996
6075
|
/**
|
|
5997
6076
|
* 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.
|
|
5998
6077
|
*
|
|
@@ -7425,7 +7504,7 @@ var DocumentsClient = class {
|
|
|
7425
7504
|
return handleNonStatusCodeError(_response.error, _response.rawResponse, "GET", "/v1/documents/{id}/versions");
|
|
7426
7505
|
}
|
|
7427
7506
|
/**
|
|
7428
|
-
* 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
|
|
7507
|
+
* 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.
|
|
7429
7508
|
*
|
|
7430
7509
|
* @param {Vectros.FileUploadRequest} request
|
|
7431
7510
|
* @param {DocumentsClient.RequestOptions} requestOptions - Request-specific configuration.
|
|
@@ -9191,7 +9270,7 @@ var IdentityClient = class {
|
|
|
9191
9270
|
return handleNonStatusCodeError(_response.error, _response.rawResponse, "GET", "/v1/users/exists-by-email");
|
|
9192
9271
|
}
|
|
9193
9272
|
/**
|
|
9194
|
-
* Returns the audit trail of changes to a user, most recent first. Identity history is always recorded
|
|
9273
|
+
* 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.
|
|
9195
9274
|
*
|
|
9196
9275
|
* @param {Vectros.GetUserVersionsRequest} request
|
|
9197
9276
|
* @param {IdentityClient.RequestOptions} requestOptions - Request-specific configuration.
|
|
@@ -10261,7 +10340,7 @@ var RecordsClient = class {
|
|
|
10261
10340
|
return handleNonStatusCodeError(_response.error, _response.rawResponse, "POST", "/v1/records/lookup");
|
|
10262
10341
|
}
|
|
10263
10342
|
/**
|
|
10264
|
-
* 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.
|
|
10343
|
+
* 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.
|
|
10265
10344
|
*
|
|
10266
10345
|
* @param {Vectros.GetRecordTombstoneRequest} request
|
|
10267
10346
|
* @param {RecordsClient.RequestOptions} requestOptions - Request-specific configuration.
|
|
@@ -10668,7 +10747,7 @@ var SchemasClient = class {
|
|
|
10668
10747
|
return handleNonStatusCodeError(_response.error, _response.rawResponse, "PUT", "/v1/schemas/{id}");
|
|
10669
10748
|
}
|
|
10670
10749
|
/**
|
|
10671
|
-
* 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.
|
|
10750
|
+
* 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.
|
|
10672
10751
|
*
|
|
10673
10752
|
* @param {Vectros.DeleteSchemaRequest} request
|
|
10674
10753
|
* @param {SchemasClient.RequestOptions} requestOptions - Request-specific configuration.
|
|
@@ -10803,7 +10882,7 @@ var ScriptsClient = class {
|
|
|
10803
10882
|
*
|
|
10804
10883
|
* **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`).
|
|
10805
10884
|
*
|
|
10806
|
-
* **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
|
|
10885
|
+
* **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()`.
|
|
10807
10886
|
*
|
|
10808
10887
|
* **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.
|
|
10809
10888
|
*
|
|
@@ -10898,11 +10977,14 @@ var ScriptsClient = class {
|
|
|
10898
10977
|
return handleNonStatusCodeError(_response.error, _response.rawResponse, "POST", "/v1/scripts/execute");
|
|
10899
10978
|
}
|
|
10900
10979
|
/**
|
|
10901
|
-
* 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.
|
|
10980
|
+
* 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.
|
|
10902
10981
|
*
|
|
10903
10982
|
* @param {Vectros.ListScriptsRequest} request
|
|
10904
10983
|
* @param {ScriptsClient.RequestOptions} requestOptions - Request-specific configuration.
|
|
10905
10984
|
*
|
|
10985
|
+
* @throws {@link Vectros.BadRequestError}
|
|
10986
|
+
* @throws {@link Vectros.NotFoundError}
|
|
10987
|
+
*
|
|
10906
10988
|
* @example
|
|
10907
10989
|
* await client.scripts.listScripts({
|
|
10908
10990
|
* name: "notify-assignee",
|
|
@@ -10913,11 +10995,13 @@ var ScriptsClient = class {
|
|
|
10913
10995
|
return HttpResponsePromise.fromPromise(this.__listScripts(request, requestOptions));
|
|
10914
10996
|
}
|
|
10915
10997
|
async __listScripts(request = {}, requestOptions) {
|
|
10916
|
-
const { name, startFrom, limit } = request;
|
|
10998
|
+
const { name, startFrom, limit, includeSource, latest } = request;
|
|
10917
10999
|
const _queryParams = {
|
|
10918
11000
|
name,
|
|
10919
11001
|
startFrom,
|
|
10920
|
-
limit
|
|
11002
|
+
limit,
|
|
11003
|
+
includeSource,
|
|
11004
|
+
latest
|
|
10921
11005
|
};
|
|
10922
11006
|
const _authRequest = await this._options.authProvider.getAuthRequest();
|
|
10923
11007
|
const _headers = mergeHeaders(
|
|
@@ -10943,16 +11027,23 @@ var ScriptsClient = class {
|
|
|
10943
11027
|
return { data: _response.body, rawResponse: _response.rawResponse };
|
|
10944
11028
|
}
|
|
10945
11029
|
if (_response.error.reason === "status-code") {
|
|
10946
|
-
|
|
10947
|
-
|
|
10948
|
-
|
|
10949
|
-
|
|
10950
|
-
|
|
11030
|
+
switch (_response.error.statusCode) {
|
|
11031
|
+
case 400:
|
|
11032
|
+
throw new BadRequestError(_response.error.body, _response.rawResponse);
|
|
11033
|
+
case 404:
|
|
11034
|
+
throw new NotFoundError(_response.error.body, _response.rawResponse);
|
|
11035
|
+
default:
|
|
11036
|
+
throw new VectrosError({
|
|
11037
|
+
statusCode: _response.error.statusCode,
|
|
11038
|
+
body: _response.error.body,
|
|
11039
|
+
rawResponse: _response.rawResponse
|
|
11040
|
+
});
|
|
11041
|
+
}
|
|
10951
11042
|
}
|
|
10952
11043
|
return handleNonStatusCodeError(_response.error, _response.rawResponse, "GET", "/v1/scripts");
|
|
10953
11044
|
}
|
|
10954
11045
|
/**
|
|
10955
|
-
* 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.
|
|
11046
|
+
* 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.
|
|
10956
11047
|
*
|
|
10957
11048
|
* @param {Vectros.ScriptRequest} request
|
|
10958
11049
|
* @param {ScriptsClient.RequestOptions} requestOptions - Request-specific configuration.
|
|
@@ -11139,12 +11230,13 @@ var ScriptsClient = class {
|
|
|
11139
11230
|
return handleNonStatusCodeError(_response.error, _response.rawResponse, "PUT", "/v1/scripts/{id}");
|
|
11140
11231
|
}
|
|
11141
11232
|
/**
|
|
11142
|
-
* 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.
|
|
11233
|
+
* 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.
|
|
11143
11234
|
*
|
|
11144
11235
|
* @param {Vectros.DeleteScriptRequest} request
|
|
11145
11236
|
* @param {ScriptsClient.RequestOptions} requestOptions - Request-specific configuration.
|
|
11146
11237
|
*
|
|
11147
11238
|
* @throws {@link Vectros.NotFoundError}
|
|
11239
|
+
* @throws {@link Vectros.ConflictError}
|
|
11148
11240
|
* @throws {@link Vectros.TooManyRequestsError}
|
|
11149
11241
|
*
|
|
11150
11242
|
* @example
|
|
@@ -11184,6 +11276,8 @@ var ScriptsClient = class {
|
|
|
11184
11276
|
switch (_response.error.statusCode) {
|
|
11185
11277
|
case 404:
|
|
11186
11278
|
throw new NotFoundError(_response.error.body, _response.rawResponse);
|
|
11279
|
+
case 409:
|
|
11280
|
+
throw new ConflictError(_response.error.body, _response.rawResponse);
|
|
11187
11281
|
case 429:
|
|
11188
11282
|
throw new TooManyRequestsError(_response.error.body, _response.rawResponse);
|
|
11189
11283
|
default:
|
|
@@ -11729,6 +11823,25 @@ var logging;
|
|
|
11729
11823
|
var import_zod = require("zod");
|
|
11730
11824
|
var import_zod2 = require("zod");
|
|
11731
11825
|
var CONTEXT_ID_RE = /^[a-z][a-z0-9-]{2,30}$/;
|
|
11826
|
+
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;
|
|
11827
|
+
function safeDisplayString(base) {
|
|
11828
|
+
return base.refine((v) => !FORGERY_CHARS_RE.test(v), {
|
|
11829
|
+
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"
|
|
11830
|
+
});
|
|
11831
|
+
}
|
|
11832
|
+
function safeUrlString(base) {
|
|
11833
|
+
return base.refine(
|
|
11834
|
+
(v) => {
|
|
11835
|
+
if (!/^[\x20-\x7e]*$/.test(v)) return false;
|
|
11836
|
+
const authorityMatch = /^https:\/\/([^/?#]*)/i.exec(v);
|
|
11837
|
+
if (!authorityMatch) return false;
|
|
11838
|
+
return /^[A-Za-z0-9.-]+(:\d+)?$/.test(authorityMatch[1]);
|
|
11839
|
+
},
|
|
11840
|
+
{
|
|
11841
|
+
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"
|
|
11842
|
+
}
|
|
11843
|
+
);
|
|
11844
|
+
}
|
|
11732
11845
|
var MAX_SCOPE_DIMENSIONS = 2;
|
|
11733
11846
|
var ValidationRulesSchema = import_zod.z.object({
|
|
11734
11847
|
required: import_zod.z.boolean().optional(),
|
|
@@ -11758,7 +11871,7 @@ var RenderHintsSchema = import_zod.z.object({
|
|
|
11758
11871
|
displayField: import_zod.z.boolean().optional()
|
|
11759
11872
|
}).strict();
|
|
11760
11873
|
var BlueprintFieldDefSchema = import_zod.z.object({
|
|
11761
|
-
fieldId: import_zod.z.string().min(1),
|
|
11874
|
+
fieldId: safeDisplayString(import_zod.z.string().min(1)),
|
|
11762
11875
|
fieldType: import_zod.z.string().min(1),
|
|
11763
11876
|
required: import_zod.z.boolean().optional(),
|
|
11764
11877
|
searchable: import_zod.z.boolean().optional(),
|
|
@@ -11849,13 +11962,13 @@ var BlueprintFieldDefSchema = import_zod.z.object({
|
|
|
11849
11962
|
}
|
|
11850
11963
|
});
|
|
11851
11964
|
var BlueprintLookupFieldSchema = import_zod.z.union([
|
|
11852
|
-
import_zod.z.string().min(1),
|
|
11965
|
+
safeDisplayString(import_zod.z.string().min(1)),
|
|
11853
11966
|
import_zod.z.object({
|
|
11854
11967
|
// A lookup field declares either ONE field (fieldName) or 2-3 fields
|
|
11855
11968
|
// matched together (fieldNames) — never both, never neither. `.strict()`
|
|
11856
11969
|
// below only rejects unknown KEYS; it does not enforce this combination,
|
|
11857
11970
|
// so the `.superRefine()` after it carries the actual rule.
|
|
11858
|
-
fieldName: import_zod.z.string().min(1).optional(),
|
|
11971
|
+
fieldName: safeDisplayString(import_zod.z.string().min(1)).optional(),
|
|
11859
11972
|
// Composite (conjunctive) lookup: match on all of these fields at once —
|
|
11860
11973
|
// "every record where `status` is `open` AND `area` is `billing`", exact,
|
|
11861
11974
|
// in a stable order. Order is significant and locked at create: a query
|
|
@@ -11864,7 +11977,7 @@ var BlueprintLookupFieldSchema = import_zod.z.union([
|
|
|
11864
11977
|
// lookup for that. Record-only: the schema's `allowedSurfaces` must be
|
|
11865
11978
|
// exactly `['record']` (checked at the schema level below) — the
|
|
11866
11979
|
// platform's composite index has no document/user/entity reader yet.
|
|
11867
|
-
fieldNames: import_zod.z.array(import_zod.z.string().min(1)).min(2).max(3).optional(),
|
|
11980
|
+
fieldNames: import_zod.z.array(safeDisplayString(import_zod.z.string().min(1))).min(2).max(3).optional(),
|
|
11868
11981
|
// Not available on a composite (`fieldNames`) lookup — checked below.
|
|
11869
11982
|
unique: import_zod.z.boolean().optional(),
|
|
11870
11983
|
// Opt this field into ordered range + prefix lookups (from/to/prefix) on
|
|
@@ -11914,7 +12027,14 @@ var BlueprintCapabilitiesSchema = import_zod.z.object({
|
|
|
11914
12027
|
triggersEnabled: import_zod.z.boolean().optional()
|
|
11915
12028
|
}).strict();
|
|
11916
12029
|
var BlueprintSchemaSchema = import_zod.z.object({
|
|
11917
|
-
|
|
12030
|
+
// safeDisplayString: buildPlan's schema step renders this raw into a single summary
|
|
12031
|
+
// line (`schema '${s.typeName}' (...)`) — a gap found after the rest of this file's
|
|
12032
|
+
// forgery-character pass had already shipped, the same "unconstrained free-text field
|
|
12033
|
+
// reaches a rendered plan line" shape as issuer/allowedActions/etc. `displayName`
|
|
12034
|
+
// here is NOT wrapped: unlike
|
|
12035
|
+
// servicePrincipal's own displayName, this one is never interpolated into a rendered
|
|
12036
|
+
// summary anywhere in loader.ts today (only forwarded to the schema-upsert payload).
|
|
12037
|
+
typeName: safeDisplayString(import_zod.z.string().min(1)),
|
|
11918
12038
|
displayName: import_zod.z.string().min(1),
|
|
11919
12039
|
description: import_zod.z.string().optional(),
|
|
11920
12040
|
// Default search-index mode for instances of this type. HYBRID, SEMANTIC and
|
|
@@ -12051,7 +12171,7 @@ var BlueprintAccessProfileSchema = import_zod.z.object({
|
|
|
12051
12171
|
// `.superRefine` on this schema enforces the XOR, mirroring the platform's
|
|
12052
12172
|
// own access-profile scopes/role-composition XOR check). Exactly one of
|
|
12053
12173
|
// the two authors this profile's grant.
|
|
12054
|
-
allowedActions: import_zod.z.array(import_zod.z.string().min(1)).min(1).optional(),
|
|
12174
|
+
allowedActions: import_zod.z.array(safeDisplayString(import_zod.z.string().min(1))).min(1).optional(),
|
|
12055
12175
|
// Additive role composition: this profile's effective grant is
|
|
12056
12176
|
// each named role's own clauses, concatenated in the order listed, instead of
|
|
12057
12177
|
// an inline clause authored here. Every id must name a role this SAME
|
|
@@ -12080,7 +12200,14 @@ var BlueprintAccessProfileSchema = import_zod.z.object({
|
|
|
12080
12200
|
// NOT see tenant-level/seed records). Every KEY is validated below
|
|
12081
12201
|
// (`lintDataScopeKeys`) — `userId` or a grammar-valid `scope:<ns>`, no
|
|
12082
12202
|
// other shape.
|
|
12083
|
-
|
|
12203
|
+
// safeDisplayString on the value: not currently rendered by VALUE anywhere in buildPlan
|
|
12204
|
+
// (only `Object.keys(...)` of a profile's dataScope is shown today, unlike a role clause's
|
|
12205
|
+
// dataScope, which `renderPlanDataScope` prints values for — the reason that sibling field
|
|
12206
|
+
// WAS wrapped). Wrapped anyway for consistency with every other dataScope-shaped field in
|
|
12207
|
+
// this file, and so a future preview enhancement that starts showing these values (a very
|
|
12208
|
+
// plausible one, given the existing renderPlanDataScope precedent) doesn't silently reopen
|
|
12209
|
+
// this exact class on a field that looks, byte-for-byte, like every other one already closed.
|
|
12210
|
+
dataScope: import_zod.z.record(import_zod.z.array(import_zod.z.union([safeDisplayString(import_zod.z.string().min(1)), import_zod.z.null()]))).optional(),
|
|
12084
12211
|
// Optional identity overrides for the service principal's profile — scope
|
|
12085
12212
|
// values its key STAMPS onto everything it writes (a credential can only
|
|
12086
12213
|
// stamp ownership its identity carries). Keys are the namespaced `scope:<ns>`
|
|
@@ -12156,8 +12283,8 @@ var BlueprintAccessProfileSchema = import_zod.z.object({
|
|
|
12156
12283
|
}
|
|
12157
12284
|
});
|
|
12158
12285
|
var BlueprintRoleClauseSchema = import_zod.z.object({
|
|
12159
|
-
allowedActions: import_zod.z.array(import_zod.z.string().min(1)).min(1),
|
|
12160
|
-
dataScope: import_zod.z.record(import_zod.z.array(import_zod.z.union([import_zod.z.string().min(1), import_zod.z.null()]))).optional(),
|
|
12286
|
+
allowedActions: import_zod.z.array(safeDisplayString(import_zod.z.string().min(1))).min(1),
|
|
12287
|
+
dataScope: import_zod.z.record(import_zod.z.array(import_zod.z.union([safeDisplayString(import_zod.z.string().min(1)), import_zod.z.null()]))).optional(),
|
|
12161
12288
|
dataScopeRef: import_zod.z.string().min(1).optional(),
|
|
12162
12289
|
capabilities: GrantedCapabilitiesSchema,
|
|
12163
12290
|
assignableRoles: AssignableRolesSchema
|
|
@@ -12171,10 +12298,10 @@ var BlueprintRoleClauseSchema = import_zod.z.object({
|
|
|
12171
12298
|
}
|
|
12172
12299
|
});
|
|
12173
12300
|
var BlueprintFragmentsSchema = import_zod.z.record(
|
|
12174
|
-
import_zod.z.record(import_zod.z.array(import_zod.z.union([import_zod.z.string().min(1), import_zod.z.null()])))
|
|
12301
|
+
import_zod.z.record(import_zod.z.array(import_zod.z.union([safeDisplayString(import_zod.z.string().min(1)), import_zod.z.null()])))
|
|
12175
12302
|
);
|
|
12176
|
-
var BlueprintRolesSchema = import_zod.z.record(import_zod.z.array(BlueprintRoleClauseSchema).min(1));
|
|
12177
|
-
var BlueprintRoleAssumableSchema = import_zod.z.record(import_zod.z.array(import_zod.z.string().min(1)).min(1));
|
|
12303
|
+
var BlueprintRolesSchema = import_zod.z.record(safeDisplayString(import_zod.z.string().min(1)), import_zod.z.array(BlueprintRoleClauseSchema).min(1));
|
|
12304
|
+
var BlueprintRoleAssumableSchema = import_zod.z.record(import_zod.z.array(safeDisplayString(import_zod.z.string().min(1))).min(1));
|
|
12178
12305
|
var BlueprintRoleAssumableMapSchema = import_zod.z.record(BlueprintRoleAssumableSchema);
|
|
12179
12306
|
var BlueprintTriggerFiringSourceSchema = import_zod.z.object({
|
|
12180
12307
|
schemaName: import_zod.z.string().min(1),
|
|
@@ -12182,7 +12309,12 @@ var BlueprintTriggerFiringSourceSchema = import_zod.z.object({
|
|
|
12182
12309
|
}).strict();
|
|
12183
12310
|
var SCRIPT_VERSION_INTEGER_RE = /^[1-9][0-9]*$/;
|
|
12184
12311
|
var BlueprintTriggerScriptRefSchema = import_zod.z.object({
|
|
12185
|
-
|
|
12312
|
+
// safeDisplayString: rendered raw into buildPlan's trigger step (`${scriptRef.name}@...`).
|
|
12313
|
+
// When naming a script THIS blueprint ships, `name` is transitively safe already (it must
|
|
12314
|
+
// equal a `scripts` map key, itself grammar-pinned by SCRIPT_NAME_RE) — but a scriptRef
|
|
12315
|
+
// naming a script this blueprint does NOT ship is, by design, "entirely unrestricted" (see
|
|
12316
|
+
// this file's own doc on that case), so the wrap is load-bearing for exactly that path.
|
|
12317
|
+
name: safeDisplayString(import_zod.z.string().min(1)),
|
|
12186
12318
|
version: import_zod.z.string().min(1)
|
|
12187
12319
|
}).strict().superRefine((ref, ctx) => {
|
|
12188
12320
|
if (ref.version !== "latest" && !SCRIPT_VERSION_INTEGER_RE.test(ref.version)) {
|
|
@@ -12202,9 +12334,9 @@ var BlueprintTriggerManifestSchema = import_zod.z.array(
|
|
|
12202
12334
|
var BlueprintTriggerEntrySchema = import_zod.z.object({
|
|
12203
12335
|
firingSource: BlueprintTriggerFiringSourceSchema,
|
|
12204
12336
|
scriptRef: BlueprintTriggerScriptRefSchema,
|
|
12205
|
-
allowedActions: import_zod.z.array(import_zod.z.string().min(1)).min(1).optional(),
|
|
12337
|
+
allowedActions: import_zod.z.array(safeDisplayString(import_zod.z.string().min(1))).min(1).optional(),
|
|
12206
12338
|
roleIds: import_zod.z.array(import_zod.z.string().min(1)).min(1).optional(),
|
|
12207
|
-
dataScope: import_zod.z.record(import_zod.z.array(import_zod.z.union([import_zod.z.string().min(1), import_zod.z.null()]))).optional(),
|
|
12339
|
+
dataScope: import_zod.z.record(import_zod.z.array(import_zod.z.union([safeDisplayString(import_zod.z.string().min(1)), import_zod.z.null()]))).optional(),
|
|
12208
12340
|
capabilities: GrantedCapabilitiesSchema,
|
|
12209
12341
|
manifest: BlueprintTriggerManifestSchema,
|
|
12210
12342
|
/**
|
|
@@ -12228,7 +12360,7 @@ var BlueprintTriggerEntrySchema = import_zod.z.object({
|
|
|
12228
12360
|
* together are capped at 224 KB serialised — a firing whose projected input exceeds that is not
|
|
12229
12361
|
* run, and is recorded as an `INPUT_TOO_LARGE` failure naming the size.
|
|
12230
12362
|
*/
|
|
12231
|
-
fields: import_zod.z.array(import_zod.z.string().min(1))
|
|
12363
|
+
fields: import_zod.z.array(safeDisplayString(import_zod.z.string().min(1)))
|
|
12232
12364
|
}).strict().superRefine((entry, ctx) => {
|
|
12233
12365
|
const hasInline = entry.allowedActions !== void 0;
|
|
12234
12366
|
const hasRoleIds = entry.roleIds !== void 0;
|
|
@@ -12257,7 +12389,7 @@ var BlueprintTriggerEntrySchema = import_zod.z.object({
|
|
|
12257
12389
|
}
|
|
12258
12390
|
}
|
|
12259
12391
|
});
|
|
12260
|
-
var BlueprintTriggersSchema = import_zod.z.record(BlueprintTriggerEntrySchema);
|
|
12392
|
+
var BlueprintTriggersSchema = import_zod.z.record(safeDisplayString(import_zod.z.string().min(1)), BlueprintTriggerEntrySchema);
|
|
12261
12393
|
var SCRIPT_NAME_RE = /^[A-Za-z0-9_-]{1,64}$/;
|
|
12262
12394
|
var SCRIPT_SOURCE_MAX_BYTES = 3e5;
|
|
12263
12395
|
var SCRIPT_INPUT_CONTRACT_MAX_BYTES = 5e4;
|
|
@@ -12303,8 +12435,15 @@ var IdentityKindSchema = import_zod.z.string().min(1).superRefine((val, ctx) =>
|
|
|
12303
12435
|
});
|
|
12304
12436
|
var IdentityDeclSchema = import_zod.z.object({
|
|
12305
12437
|
kind: IdentityKindSchema,
|
|
12306
|
-
|
|
12307
|
-
|
|
12438
|
+
// safeDisplayString: `@vectros-ai/cli`'s `runBootstrapCli` and `blueprint plan` both print
|
|
12439
|
+
// this RAW to stdout ("~ <name> (<kind>) — external id '<externalId>'"), immediately before
|
|
12440
|
+
// the "Apply this plan? [y/N]" confirmation gate itself — the highest-severity gap found in
|
|
12441
|
+
// this pass, since it sits at the confirmation prompt's own disclosure text, not merely
|
|
12442
|
+
// somewhere upstream of it. `displayName` is wrapped too though not currently
|
|
12443
|
+
// rendered in any preview (defense-in-depth, matching this file's practice for its adjacent
|
|
12444
|
+
// sibling fields) — it does reach a platform API payload as an entity's `name`.
|
|
12445
|
+
externalId: safeDisplayString(import_zod.z.string().min(1)),
|
|
12446
|
+
displayName: safeDisplayString(import_zod.z.string().min(1)).optional(),
|
|
12308
12447
|
metadata: import_zod.z.record(import_zod.z.unknown()).optional()
|
|
12309
12448
|
}).strict();
|
|
12310
12449
|
var IDENTITY_NAME_RE = /^[A-Za-z_]\w*$/;
|
|
@@ -12315,10 +12454,14 @@ var IdentitiesDeclSchema = import_zod.z.record(
|
|
|
12315
12454
|
IdentityDeclSchema
|
|
12316
12455
|
);
|
|
12317
12456
|
var BlueprintServicePrincipalSchema = import_zod.z.object({
|
|
12318
|
-
externalId: import_zod.z.string().min(1),
|
|
12319
|
-
displayName: import_zod.z.string().min(1)
|
|
12457
|
+
externalId: safeDisplayString(import_zod.z.string().min(1)),
|
|
12458
|
+
displayName: safeDisplayString(import_zod.z.string().min(1))
|
|
12320
12459
|
}).strict();
|
|
12321
12460
|
var ISSUER_ID_RE = CONTEXT_ID_RE;
|
|
12461
|
+
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])?)+$/;
|
|
12462
|
+
function isRestrictedToDomainName(value) {
|
|
12463
|
+
return RESTRICTED_DOMAIN_RE.test(value);
|
|
12464
|
+
}
|
|
12322
12465
|
var MAX_CAPTURED_CLAIMS = 20;
|
|
12323
12466
|
var MAX_CAPTURED_CLAIM_NAME_LENGTH = 64;
|
|
12324
12467
|
function assertNoDuplicateClaimNames(field) {
|
|
@@ -12342,10 +12485,17 @@ var BlueprintIssuerSchema = import_zod.z.object({
|
|
|
12342
12485
|
issuerId: import_zod.z.string().regex(ISSUER_ID_RE, {
|
|
12343
12486
|
message: "issuerId must be 3-31 chars, start with a lowercase letter, then lowercase letters/digits/dashes (e.g. 'auth0-prod')"
|
|
12344
12487
|
}),
|
|
12345
|
-
/**
|
|
12346
|
-
|
|
12347
|
-
|
|
12348
|
-
|
|
12488
|
+
/**
|
|
12489
|
+
* The IdP's `iss` claim value, exactly as it appears in tokens it issues. `safeUrlString`,
|
|
12490
|
+
* not `safeDisplayString`: this is the trust-anchor field the plan preview exists to
|
|
12491
|
+
* disclose, so it gets the stronger positive check (see that function's own doc) rather
|
|
12492
|
+
* than the blocklist every OTHER free-text field uses — a homoglyph host here is exactly
|
|
12493
|
+
* what an operator approving a plan must be able to trust reading. Virtually every real
|
|
12494
|
+
* OIDC issuer is URL-shaped in practice, even though the spec does not strictly require it.
|
|
12495
|
+
*/
|
|
12496
|
+
issuer: safeUrlString(import_zod.z.string().min(1)),
|
|
12497
|
+
/** The IdP's remote JWKS endpoint, used to verify presented tokens' signatures. `safeUrlString` — see `issuer`'s own comment just above. */
|
|
12498
|
+
jwksUri: safeUrlString(import_zod.z.string().min(1)),
|
|
12349
12499
|
/**
|
|
12350
12500
|
* The `aud` claim value a presented subject_token must carry. Must be globally
|
|
12351
12501
|
* unique in combination with `issuer` — use a distinct audience per
|
|
@@ -12373,7 +12523,29 @@ var BlueprintIssuerSchema = import_zod.z.object({
|
|
|
12373
12523
|
* back to calling this endpoint (with the presented token as the bearer credential) and reads
|
|
12374
12524
|
* `emailClaim` from its JSON response instead. Omit to leave the fallback disabled.
|
|
12375
12525
|
*/
|
|
12376
|
-
|
|
12526
|
+
// `safeUrlString` — see `issuer`'s own comment above for why these three trust-anchor
|
|
12527
|
+
// fields get the stronger positive check instead of `safeDisplayString`'s blocklist.
|
|
12528
|
+
userinfoUri: safeUrlString(import_zod.z.string().min(1)).optional(),
|
|
12529
|
+
/**
|
|
12530
|
+
* Opt-in domain scope: a domain ALREADY VERIFIED for your account (verified once, by the
|
|
12531
|
+
* account owner, in the developer portal). The registration's `(issuer, audience)` uniqueness
|
|
12532
|
+
* is then scoped to that domain, so an unrelated registration of the same pair never blocks
|
|
12533
|
+
* it, and a presented token matches only when its `hd` claim equals the domain (Google
|
|
12534
|
+
* Workspace emits `hd` natively; other identity providers need a claims-mapping rule that
|
|
12535
|
+
* emits a claim named exactly `hd`). A token with no matching `hd` claim is not matched at all,
|
|
12536
|
+
* so scoping an issuer that already has users signing in without one locks those users out.
|
|
12537
|
+
*
|
|
12538
|
+
* Written the way the platform stores it: a lowercase, ASCII DNS name (`acme.com`), never a URL.
|
|
12539
|
+
* An internationalized domain must be given in its punycode (`xn--…`) form, the same rule
|
|
12540
|
+
* `issuer` follows, so what an operator reads in the plan preview is the domain that is
|
|
12541
|
+
* actually scoped. An unverified domain fails the apply with the platform's `400`.
|
|
12542
|
+
*
|
|
12543
|
+
* Omit to register a domain-less issuer. Omitting it on a re-apply leaves an existing scope
|
|
12544
|
+
* unchanged; removing a scope is `vectros issuers update --clear-restricted-to-domain`.
|
|
12545
|
+
*/
|
|
12546
|
+
restrictedToDomain: import_zod.z.string().regex(RESTRICTED_DOMAIN_RE, {
|
|
12547
|
+
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"
|
|
12548
|
+
}).optional(),
|
|
12377
12549
|
/**
|
|
12378
12550
|
* Opt-in self-service signup: a list of {signupType, roleId} pairs. When a first-time exchange
|
|
12379
12551
|
* caller presents no invite token but names a signupType matching one of these (or omits it and
|
|
@@ -12413,9 +12585,18 @@ var BlueprintIssuerSchema = import_zod.z.object({
|
|
|
12413
12585
|
* separate opt-ins, and capturing a claim does not by itself expose it on a profile.
|
|
12414
12586
|
*/
|
|
12415
12587
|
capturedClaims: import_zod.z.array(
|
|
12416
|
-
|
|
12417
|
-
|
|
12418
|
-
|
|
12588
|
+
// `safeDisplayString` (a `.refine`) then the blank-name `.refine` — both ordered AFTER
|
|
12589
|
+
// `.max`, since `.refine` returns a ZodEffects, which has no `.max` (`.refine` itself
|
|
12590
|
+
// chains fine onto a ZodEffects — it's on the ZodType base, not ZodString-specific).
|
|
12591
|
+
// `.min(1)` would be a LENGTH check and would admit a whitespace-only name while the
|
|
12592
|
+
// message claims "non-blank"; the platform rejects on isBlank(). Note the value is
|
|
12593
|
+
// REJECTED, not trimmed — it reaches the wire exactly as authored, matching how the
|
|
12594
|
+
// platform stores it.
|
|
12595
|
+
safeDisplayString(
|
|
12596
|
+
import_zod.z.string().max(MAX_CAPTURED_CLAIM_NAME_LENGTH, {
|
|
12597
|
+
message: `a capturedClaims entry must be at most ${MAX_CAPTURED_CLAIM_NAME_LENGTH} characters`
|
|
12598
|
+
})
|
|
12599
|
+
).refine((name) => name.trim().length > 0, {
|
|
12419
12600
|
message: "a capturedClaims entry must be a non-blank OIDC claim name"
|
|
12420
12601
|
})
|
|
12421
12602
|
).max(MAX_CAPTURED_CLAIMS, {
|
|
@@ -12586,14 +12767,24 @@ function lintNamespaces(value, ctx) {
|
|
|
12586
12767
|
});
|
|
12587
12768
|
}
|
|
12588
12769
|
var SeedCommonShape = {
|
|
12589
|
-
/**
|
|
12590
|
-
|
|
12770
|
+
/**
|
|
12771
|
+
* The schema typeName this seed instantiates (should match a declared schema).
|
|
12772
|
+
* `safeDisplayString`, matching `BlueprintSchemaSchema.typeName`: NOT currently
|
|
12773
|
+
* rendered into any plan-preview line (the seed step only counts records/documents),
|
|
12774
|
+
* but it is the same field concept and costs nothing to close now rather than leave
|
|
12775
|
+
* as a silent gap the next preview enhancement reopens.
|
|
12776
|
+
*/
|
|
12777
|
+
typeName: safeDisplayString(import_zod.z.string().min(1)),
|
|
12591
12778
|
/**
|
|
12592
12779
|
* Stable, caller-supplied id — the loader's idempotency key AND the value other
|
|
12593
12780
|
* seeds resolve a `reference` against (across surfaces: a record seed may
|
|
12594
12781
|
* reference a document seed by its externalId, and vice versa).
|
|
12782
|
+
* `safeDisplayString`: not currently rendered in a plan-preview line (same as this schema's
|
|
12783
|
+
* `typeName` just above until this pass), but the same "costs nothing to close now" standard
|
|
12784
|
+
* applies — a short caller-supplied id, not free-form prose, so no YAML block-scalar
|
|
12785
|
+
* collision risk like `description`'s.
|
|
12595
12786
|
*/
|
|
12596
|
-
externalId: import_zod.z.string().min(1),
|
|
12787
|
+
externalId: safeDisplayString(import_zod.z.string().min(1)),
|
|
12597
12788
|
/**
|
|
12598
12789
|
* Optional scope ownership for the seeded item, as `namespace:value` entries
|
|
12599
12790
|
* (≤2, e.g. `['org:${{ identities.team }}']`; tokens substitute at apply
|
|
@@ -13252,15 +13443,31 @@ var BLUEPRINT_FIELD_PHASES = Object.freeze({
|
|
|
13252
13443
|
});
|
|
13253
13444
|
var BlueprintSchema = import_zod.z.object({
|
|
13254
13445
|
/** Stable blueprint id (the `--blueprint <name>` selector + idempotency key). */
|
|
13255
|
-
name: import_zod.z.string().min(1),
|
|
13256
|
-
version: import_zod.z.string().min(1),
|
|
13446
|
+
name: safeDisplayString(import_zod.z.string().min(1)),
|
|
13447
|
+
version: safeDisplayString(import_zod.z.string().min(1)),
|
|
13448
|
+
// NOT safeDisplayString — deliberately, after that wrap broke a real, multi-line-authored
|
|
13449
|
+
// blueprint's `description` in review. `description` is never interpolated into any
|
|
13450
|
+
// rendered plan-preview line (`buildPlan`'s header prints
|
|
13451
|
+
// `name`/`version`/`contextId`, never `description` — confirmed by grep, not assumed), so
|
|
13452
|
+
// wrapping it protected a sink that does not exist. It also actively broke real content:
|
|
13453
|
+
// this is exactly the field authors reach for a YAML folded (`>`) or literal (`|`) block
|
|
13454
|
+
// scalar to write, and clip chomping (the YAML default) always leaves exactly one trailing
|
|
13455
|
+
// `\n` — which `safeDisplayString` rejects as a C0 control, indistinguishable from a
|
|
13456
|
+
// forgery attempt. See `safeDisplayString`'s own doc for why this is a property of the
|
|
13457
|
+
// wrapper, not a one-off carve-out for this field alone.
|
|
13257
13458
|
description: import_zod.z.string().min(1),
|
|
13258
13459
|
/** The app-context the profile + scoped key bind to (e.g. "mcp"). */
|
|
13259
13460
|
contextId: import_zod.z.string().regex(CONTEXT_ID_RE, {
|
|
13260
13461
|
message: "contextId must be 3-31 chars, start with a lowercase letter, then lowercase letters/digits/dashes (e.g. 'mcp')"
|
|
13261
13462
|
}),
|
|
13262
|
-
/**
|
|
13263
|
-
|
|
13463
|
+
/**
|
|
13464
|
+
* Human-readable app-context name; defaults to `MCP — <name>` (see {@link contextNameOf})
|
|
13465
|
+
* when absent. `safeDisplayString`: `contextNameOf` renders this raw into the FIRST line of
|
|
13466
|
+
* every `buildPlan` summary (the `context '...' (...)` step) — a gap found after the rest
|
|
13467
|
+
* of this file's forgery-character pass had already shipped; same shape as
|
|
13468
|
+
* `name`/`version`/`description` right above it.
|
|
13469
|
+
*/
|
|
13470
|
+
contextName: safeDisplayString(import_zod.z.string().min(1)).optional(),
|
|
13264
13471
|
/**
|
|
13265
13472
|
* The deploying organization's own display name — distinct from {@link contextName}.
|
|
13266
13473
|
* `contextName` is the blueprint author's fixed identity for the app (the same for every
|
|
@@ -13268,8 +13475,13 @@ var BlueprintSchema = import_zod.z.object({
|
|
|
13268
13475
|
* deployer-supplied `${{ inputs.x }}` value rather than hardcoded. Used for branding on
|
|
13269
13476
|
* platform-sent correspondence (e.g. invite emails) alongside `contextName` — not a
|
|
13270
13477
|
* replacement for it. See {@link companyNameOf}.
|
|
13478
|
+
*
|
|
13479
|
+
* `safeDisplayString`: not interpolated into any CLI plan-preview line today (confirmed —
|
|
13480
|
+
* `loader.ts` never reads `companyName`), but it IS forwarded to the platform for the
|
|
13481
|
+
* invite-email correspondence described above, a second class of human-read surface this
|
|
13482
|
+
* value reaches. Wrapped for the same reason as every sibling scalar identity field here.
|
|
13271
13483
|
*/
|
|
13272
|
-
companyName: import_zod.z.string().min(1).optional(),
|
|
13484
|
+
companyName: safeDisplayString(import_zod.z.string().min(1)).optional(),
|
|
13273
13485
|
schemas: import_zod.z.array(BlueprintSchemaSchema).default([]),
|
|
13274
13486
|
accessProfile: BlueprintAccessProfileSchema,
|
|
13275
13487
|
servicePrincipal: BlueprintServicePrincipalSchema,
|
|
@@ -13346,9 +13558,16 @@ var BlueprintSchema = import_zod.z.object({
|
|
|
13346
13558
|
* context's name and needs a broader scope — see `@vectros-ai/cli`'s own notes.)
|
|
13347
13559
|
*/
|
|
13348
13560
|
identityProjectionClaims: import_zod.z.array(
|
|
13349
|
-
|
|
13350
|
-
|
|
13351
|
-
|
|
13561
|
+
// safeDisplayString: rendered raw into buildPlan's context step (`identity projection:
|
|
13562
|
+
// [...]`) — same rendered-plan-preview shape as `capturedClaims` (its structural pair,
|
|
13563
|
+
// and fixed alongside it originally), missed here in the first pass; same ordering rule
|
|
13564
|
+
// applies (safeDisplayString's own `.refine` before the blank-name `.refine` below,
|
|
13565
|
+
// both after `.max` since `.refine` returns a ZodEffects with no `.max`).
|
|
13566
|
+
safeDisplayString(
|
|
13567
|
+
import_zod.z.string().max(MAX_CAPTURED_CLAIM_NAME_LENGTH, {
|
|
13568
|
+
message: `an identityProjectionClaims entry must be at most ${MAX_CAPTURED_CLAIM_NAME_LENGTH} characters`
|
|
13569
|
+
})
|
|
13570
|
+
).refine((name) => name.trim().length > 0, {
|
|
13352
13571
|
message: "an identityProjectionClaims entry must be a non-blank claim name"
|
|
13353
13572
|
})
|
|
13354
13573
|
).max(MAX_CAPTURED_CLAIMS, {
|
|
@@ -15500,6 +15719,357 @@ function renderActionableError(info) {
|
|
|
15500
15719
|
return null;
|
|
15501
15720
|
}
|
|
15502
15721
|
|
|
15722
|
+
// src/redact.ts
|
|
15723
|
+
var JOSE_SHAPED = /eyJ[A-Za-z0-9_-]{6,}(?:\.[A-Za-z0-9_-]*){0,4}/g;
|
|
15724
|
+
var LONG_SEGMENTS = /[A-Za-z0-9_-]{20,}(?:\.[A-Za-z0-9_-]{16,}){1,4}/g;
|
|
15725
|
+
var BEARER = /\bBearer\s+[A-Za-z0-9._~+/=-]{8,}/gi;
|
|
15726
|
+
var VECTROS_SECRET_ENV = /(?:ssk|sk|st)_(?:live|test)_[A-Za-z0-9._-]+/gi;
|
|
15727
|
+
var VECTROS_SECRET_BARE = /(?<![A-Za-z0-9])(?:ssk|sk|st)_[A-Za-z0-9._-]{8,}/gi;
|
|
15728
|
+
function redactCredentials(text) {
|
|
15729
|
+
return text.replace(BEARER, "Bearer [redacted]").replace(JOSE_SHAPED, "[redacted]").replace(LONG_SEGMENTS, "[redacted]").replace(VECTROS_SECRET_ENV, "[redacted]").replace(VECTROS_SECRET_BARE, "[redacted]");
|
|
15730
|
+
}
|
|
15731
|
+
function looksLikeCredential(value) {
|
|
15732
|
+
const trimmed = value.trim();
|
|
15733
|
+
if (trimmed.length > 200) return true;
|
|
15734
|
+
if (/eyJ[A-Za-z0-9_-]{8,}/.test(trimmed)) return true;
|
|
15735
|
+
const bare = trimmed.replace(/^["'`]+|["'`,;]+$/g, "").replace(/^bearer\s+/i, "");
|
|
15736
|
+
const segments = bare.split(".");
|
|
15737
|
+
return segments.length >= 3 && segments.filter((s) => s.length >= 16).length >= 2;
|
|
15738
|
+
}
|
|
15739
|
+
function safeBaseName(path4) {
|
|
15740
|
+
const base = path4.split(/[\\/]/).filter(Boolean).pop();
|
|
15741
|
+
return base !== void 0 && base.length <= 40 && /^[A-Za-z0-9._ ()-]+$/.test(base) ? base : void 0;
|
|
15742
|
+
}
|
|
15743
|
+
|
|
15744
|
+
// src/provision-options.ts
|
|
15745
|
+
var BLUEPRINT_NAMES_TOKEN = "{{BLUEPRINT_NAMES}}";
|
|
15746
|
+
var PROVISION_OPTIONS = [
|
|
15747
|
+
{
|
|
15748
|
+
flag: "--blueprint",
|
|
15749
|
+
value: "<name|file>",
|
|
15750
|
+
modes: ["bootstrap"],
|
|
15751
|
+
help: [
|
|
15752
|
+
"A bundled blueprint ({{BLUEPRINT_NAMES}}),",
|
|
15753
|
+
'OR a path to your own blueprint YAML/JSON (./my.yaml) \u2014 the "copy &',
|
|
15754
|
+
'own" path. File blueprints get the identical scope gate (no override).'
|
|
15755
|
+
]
|
|
15756
|
+
},
|
|
15757
|
+
{
|
|
15758
|
+
flag: "--set",
|
|
15759
|
+
value: "name=value",
|
|
15760
|
+
modes: ["bootstrap", "apply"],
|
|
15761
|
+
help: [
|
|
15762
|
+
"Set an install-time input value (repeatable) for a file blueprint's",
|
|
15763
|
+
"'inputs:' block. Beats --values + the declared default. (Same flag as",
|
|
15764
|
+
"'vectros blueprint validate/plan'.)"
|
|
15765
|
+
]
|
|
15766
|
+
},
|
|
15767
|
+
{
|
|
15768
|
+
flag: "--values",
|
|
15769
|
+
value: "<file>",
|
|
15770
|
+
modes: ["bootstrap", "apply"],
|
|
15771
|
+
help: [
|
|
15772
|
+
"A YAML/JSON map of input values. (File blueprints only.)"
|
|
15773
|
+
]
|
|
15774
|
+
},
|
|
15775
|
+
{
|
|
15776
|
+
flag: "--blank",
|
|
15777
|
+
modes: ["bootstrap"],
|
|
15778
|
+
help: [
|
|
15779
|
+
"Credential only (read-only data-plane profile, no schemas)."
|
|
15780
|
+
]
|
|
15781
|
+
},
|
|
15782
|
+
{
|
|
15783
|
+
flag: "--no-seed",
|
|
15784
|
+
modes: ["bootstrap"],
|
|
15785
|
+
help: [
|
|
15786
|
+
"Provision the blueprint's schemas, profile and key but SKIP its",
|
|
15787
|
+
"sample seed records \u2014 the right choice for a production context",
|
|
15788
|
+
"(a blueprint's seeds are illustrative, not real data). Unlike",
|
|
15789
|
+
"--blank, the schemas are still created."
|
|
15790
|
+
]
|
|
15791
|
+
},
|
|
15792
|
+
{
|
|
15793
|
+
// The opposite default to bootstrap's: a re-apply from CI must not write sample data into a tenant by omission.
|
|
15794
|
+
flag: "--seed",
|
|
15795
|
+
modes: ["apply"],
|
|
15796
|
+
help: [
|
|
15797
|
+
"Also write the blueprint's sample seed records. Off by default, in BOTH tenants: apply",
|
|
15798
|
+
"never writes sample data unless you ask (a blueprint's seeds are illustrative, not real",
|
|
15799
|
+
"data). Seeds are written by external id, so a re-run with --seed is idempotent.",
|
|
15800
|
+
"(bootstrap is the other way round: it seeds unless --no-seed. apply refuses --no-seed.)"
|
|
15801
|
+
]
|
|
15802
|
+
},
|
|
15803
|
+
{
|
|
15804
|
+
flag: "--allow-tenant-wide-namespaces",
|
|
15805
|
+
modes: ["bootstrap", "apply"],
|
|
15806
|
+
help: {
|
|
15807
|
+
bootstrap: [
|
|
15808
|
+
"Required to apply a blueprint that declares a namespace with",
|
|
15809
|
+
"tenantWide: true \u2014 visible to every context in the account, not",
|
|
15810
|
+
"just this one. Without it, bootstrap refuses before provisioning",
|
|
15811
|
+
"anything, naming the namespace(s). Only an account OWNER's",
|
|
15812
|
+
"credential can actually complete the registration either way \u2014",
|
|
15813
|
+
"this flag doesn't change who's allowed, only whether this run",
|
|
15814
|
+
"tries. Never needed for a blueprint with no tenantWide namespace."
|
|
15815
|
+
],
|
|
15816
|
+
apply: [
|
|
15817
|
+
"Required to apply a blueprint that declares a namespace with",
|
|
15818
|
+
"tenantWide: true \u2014 visible to every context in the account, not",
|
|
15819
|
+
"just this one. Without it, apply refuses before provisioning",
|
|
15820
|
+
"anything, naming the namespace(s). Only an account OWNER's",
|
|
15821
|
+
"credential can actually complete the registration either way \u2014",
|
|
15822
|
+
"this flag doesn't change who's allowed, only whether this run",
|
|
15823
|
+
"tries. Never needed for a blueprint with no tenantWide namespace."
|
|
15824
|
+
]
|
|
15825
|
+
}
|
|
15826
|
+
},
|
|
15827
|
+
{
|
|
15828
|
+
flag: "--allow-script-adoption",
|
|
15829
|
+
stacked: true,
|
|
15830
|
+
modes: ["bootstrap", "apply"],
|
|
15831
|
+
help: {
|
|
15832
|
+
bootstrap: [
|
|
15833
|
+
"Required to push a script version onto a name that already exists in",
|
|
15834
|
+
"this context but carries no blueprint provenance marker (hand-authored,",
|
|
15835
|
+
"or pushed before this marker existed) \u2014 without it, bootstrap refuses",
|
|
15836
|
+
"before minting anything for that name, naming it. Never needed for a",
|
|
15837
|
+
"brand-new script name, or for re-applying a blueprint that already owns",
|
|
15838
|
+
"the name it's pushing to. Does NOT help if the name is already owned by",
|
|
15839
|
+
"a DIFFERENT blueprint \u2014 that is refused unconditionally, no flag past it."
|
|
15840
|
+
],
|
|
15841
|
+
apply: [
|
|
15842
|
+
"Required to push a script version onto a name that already exists in",
|
|
15843
|
+
"this context but carries no blueprint provenance marker (hand-authored,",
|
|
15844
|
+
"or pushed before this marker existed) \u2014 without it, apply refuses",
|
|
15845
|
+
"before changing anything for that name, naming it. Never needed for a",
|
|
15846
|
+
"brand-new script name, or for re-applying a blueprint that already owns",
|
|
15847
|
+
"the name it's pushing to. Does NOT help if the name is already owned by",
|
|
15848
|
+
"a DIFFERENT blueprint \u2014 that is refused unconditionally, no flag past it."
|
|
15849
|
+
]
|
|
15850
|
+
}
|
|
15851
|
+
},
|
|
15852
|
+
{
|
|
15853
|
+
flag: "--tenant",
|
|
15854
|
+
value: "test|live",
|
|
15855
|
+
modes: ["bootstrap", "apply"],
|
|
15856
|
+
help: {
|
|
15857
|
+
bootstrap: [
|
|
15858
|
+
"Which tenant the minted key scopes to (default: live).",
|
|
15859
|
+
"'test' provisions into your isolated test tenant \u2014 a",
|
|
15860
|
+
"dry-run before pointing real data at the same blueprint.",
|
|
15861
|
+
"The tenant rides the key (ssk_test_* vs ssk_live_*), so",
|
|
15862
|
+
"the MCP/client config is otherwise identical."
|
|
15863
|
+
],
|
|
15864
|
+
apply: [
|
|
15865
|
+
"Which tenant to apply into. REQUIRED: there is no default, so a run",
|
|
15866
|
+
"never lands on 'live' by omission. 'test' is your isolated test tenant",
|
|
15867
|
+
"\u2014 apply there first."
|
|
15868
|
+
]
|
|
15869
|
+
}
|
|
15870
|
+
},
|
|
15871
|
+
{
|
|
15872
|
+
flag: "--test",
|
|
15873
|
+
modes: ["bootstrap"],
|
|
15874
|
+
help: [
|
|
15875
|
+
"Shorthand for --tenant test."
|
|
15876
|
+
]
|
|
15877
|
+
},
|
|
15878
|
+
{
|
|
15879
|
+
flag: "--activate",
|
|
15880
|
+
modes: ["bootstrap"],
|
|
15881
|
+
help: [
|
|
15882
|
+
"Force the freshly-stored key to become your active identity, even",
|
|
15883
|
+
"if that would switch away from a currently-active LIVE credential",
|
|
15884
|
+
"(the one case --tenant test does NOT do this automatically)."
|
|
15885
|
+
]
|
|
15886
|
+
},
|
|
15887
|
+
{
|
|
15888
|
+
flag: "--no-activate",
|
|
15889
|
+
modes: ["bootstrap"],
|
|
15890
|
+
help: [
|
|
15891
|
+
"Store the key without activating it, regardless of tenant."
|
|
15892
|
+
]
|
|
15893
|
+
},
|
|
15894
|
+
{
|
|
15895
|
+
flag: "--base-url",
|
|
15896
|
+
value: "<url>",
|
|
15897
|
+
modes: ["bootstrap", "apply"],
|
|
15898
|
+
help: [
|
|
15899
|
+
"Override the API base URL (beats VECTROS_API_BASE_URL)."
|
|
15900
|
+
]
|
|
15901
|
+
},
|
|
15902
|
+
{
|
|
15903
|
+
flag: "--client",
|
|
15904
|
+
value: "<desktop|code>",
|
|
15905
|
+
modes: ["bootstrap"],
|
|
15906
|
+
help: [
|
|
15907
|
+
"MCP host to configure (default: desktop). 'desktop' merges",
|
|
15908
|
+
"claude_desktop_config.json (also works for Cursor/Cline);",
|
|
15909
|
+
"'code' targets Claude Code's project .mcp.json and prints",
|
|
15910
|
+
"the equivalent 'claude mcp add' command."
|
|
15911
|
+
]
|
|
15912
|
+
},
|
|
15913
|
+
{
|
|
15914
|
+
flag: "--token",
|
|
15915
|
+
value: "<t>",
|
|
15916
|
+
modes: ["bootstrap", "apply"],
|
|
15917
|
+
help: {
|
|
15918
|
+
bootstrap: [
|
|
15919
|
+
"Cognito bridge token; or set VECTROS_BOOTSTRAP_TOKEN.",
|
|
15920
|
+
"Falls back to your stored `vectros login` session",
|
|
15921
|
+
"(same env) when neither is set \u2014 login once, then",
|
|
15922
|
+
"bootstrap unattended."
|
|
15923
|
+
],
|
|
15924
|
+
apply: [
|
|
15925
|
+
"Cognito bridge token; or set VECTROS_BOOTSTRAP_TOKEN.",
|
|
15926
|
+
"Falls back to your stored `vectros login` session",
|
|
15927
|
+
"(same env) when neither is set \u2014 login once, then",
|
|
15928
|
+
"apply unattended."
|
|
15929
|
+
]
|
|
15930
|
+
}
|
|
15931
|
+
},
|
|
15932
|
+
{
|
|
15933
|
+
flag: "--yes",
|
|
15934
|
+
aliases: ["-y"],
|
|
15935
|
+
modes: ["bootstrap", "apply"],
|
|
15936
|
+
help: {
|
|
15937
|
+
bootstrap: [
|
|
15938
|
+
"Skip confirmations (agent / CI path)."
|
|
15939
|
+
],
|
|
15940
|
+
apply: [
|
|
15941
|
+
"Skip the 'Apply this plan?' confirmation. REQUIRED when there is no terminal",
|
|
15942
|
+
"(CI, an agent): apply changes a tenant, so it never runs unattended without this."
|
|
15943
|
+
]
|
|
15944
|
+
}
|
|
15945
|
+
},
|
|
15946
|
+
{
|
|
15947
|
+
flag: "--no-merge",
|
|
15948
|
+
modes: ["bootstrap"],
|
|
15949
|
+
help: [
|
|
15950
|
+
"Still mints/reconciles \u2014 this only changes where the result goes:",
|
|
15951
|
+
"print the config snippet instead of merging it into your client's",
|
|
15952
|
+
"config file. Not a dry run; see 'blueprint plan' for that. Does NOT",
|
|
15953
|
+
"skip the 'Apply this plan?' confirmation \u2014 pass --yes for that."
|
|
15954
|
+
]
|
|
15955
|
+
},
|
|
15956
|
+
{
|
|
15957
|
+
flag: "--rotate",
|
|
15958
|
+
modes: ["bootstrap"],
|
|
15959
|
+
help: [
|
|
15960
|
+
"Revoke + re-mint THIS machine's key."
|
|
15961
|
+
]
|
|
15962
|
+
},
|
|
15963
|
+
{
|
|
15964
|
+
flag: "--require-verified-issuers",
|
|
15965
|
+
modes: ["bootstrap", "apply"],
|
|
15966
|
+
help: {
|
|
15967
|
+
bootstrap: [
|
|
15968
|
+
"Exit 4 (the apply itself still completes) when an issuer this blueprint registers",
|
|
15969
|
+
"is still pending verification. Without it a finished apply exits 0 and prints the",
|
|
15970
|
+
"challenge and the command that finishes verification: an issuer registered without",
|
|
15971
|
+
"restrictedToDomain accepts no token until you prove control of the identity",
|
|
15972
|
+
"provider, which needs a real login that no bootstrap can perform."
|
|
15973
|
+
],
|
|
15974
|
+
apply: [
|
|
15975
|
+
"Exit 4 (the apply itself still completes) when an issuer this blueprint registers",
|
|
15976
|
+
"is still pending verification. Without it a finished apply exits 0 and prints the",
|
|
15977
|
+
"challenge and the command that finishes verification: an issuer registered without",
|
|
15978
|
+
"restrictedToDomain accepts no token until you prove control of the identity",
|
|
15979
|
+
"provider, which needs a real login that no apply can perform."
|
|
15980
|
+
]
|
|
15981
|
+
}
|
|
15982
|
+
},
|
|
15983
|
+
{
|
|
15984
|
+
flag: "--confirm-existing-principal",
|
|
15985
|
+
modes: ["bootstrap", "apply"],
|
|
15986
|
+
help: {
|
|
15987
|
+
bootstrap: [
|
|
15988
|
+
"Required (with --yes, or with no TTY) to mint a NEW key bound to a",
|
|
15989
|
+
"service principal that already existed in your tenant before this run",
|
|
15990
|
+
"\u2014 e.g. a deliberate second machine for an integration you already own.",
|
|
15991
|
+
"Refused by default so an unexpected collision halts a CI run loudly",
|
|
15992
|
+
"instead of silently minting a credential bound to the wrong identity.",
|
|
15993
|
+
"Never needed for --rotate or an ordinary same-machine re-apply."
|
|
15994
|
+
],
|
|
15995
|
+
apply: [
|
|
15996
|
+
"Required (with --yes, or with no TTY) when the blueprint's service principal",
|
|
15997
|
+
"already exists in your tenant. Apply never reads a local key record, so that is",
|
|
15998
|
+
"every apply once the principal exists, on any machine, including one that ran",
|
|
15999
|
+
"bootstrap. It lets apply rewrite that principal's access profile (the",
|
|
16000
|
+
"blueprint's own service principal only). In a pipeline this flag is STANDING",
|
|
16001
|
+
"consent: it also waves through a blueprint whose servicePrincipal.externalId",
|
|
16002
|
+
"collides with an unrelated identity, so keep the blueprint's source reviewed.",
|
|
16003
|
+
"Every run that proceeds prints which principal it reused and why."
|
|
16004
|
+
]
|
|
16005
|
+
}
|
|
16006
|
+
},
|
|
16007
|
+
{
|
|
16008
|
+
flag: "--prune",
|
|
16009
|
+
modes: ["apply"],
|
|
16010
|
+
help: [
|
|
16011
|
+
"Also remove the triggers this blueprint provisioned earlier and its file no longer",
|
|
16012
|
+
"declares. OFF by default: without it apply never deletes anything. A trigger is",
|
|
16013
|
+
"matched by a STORED MARKER \u2014 its provisionedBy equals the blueprint's name, and its",
|
|
16014
|
+
"principal's external id is the one this blueprint would derive \u2014 not a verified",
|
|
16015
|
+
"identity. The name comes from the blueprint FILE, after any input substitution (so",
|
|
16016
|
+
"--set and --values can change it), and the scope gate bounds what a blueprint may",
|
|
16017
|
+
"grant, not what it may delete. A matched row that cannot be removed (no id or",
|
|
16018
|
+
"principal, a failed read or delete) makes apply exit 1. Rows with no marker are left",
|
|
16019
|
+
"alone and reported; another blueprint's rows are left alone. Needs --yes when there",
|
|
16020
|
+
"is no terminal."
|
|
16021
|
+
]
|
|
16022
|
+
},
|
|
16023
|
+
{
|
|
16024
|
+
flag: "--use-scoped-token",
|
|
16025
|
+
modes: ["bootstrap"],
|
|
16026
|
+
help: [
|
|
16027
|
+
"Legacy fallback.",
|
|
16028
|
+
"NOTE: it mints a token WITHOUT the provisioning",
|
|
16029
|
+
"capability, so it cannot create a blueprint's",
|
|
16030
|
+
"app-context \u2014 use only with the 'default' context",
|
|
16031
|
+
"(the dedicated endpoint is required otherwise)."
|
|
16032
|
+
]
|
|
16033
|
+
},
|
|
16034
|
+
{
|
|
16035
|
+
flag: "--help",
|
|
16036
|
+
aliases: ["-h"],
|
|
16037
|
+
modes: ["bootstrap", "apply"],
|
|
16038
|
+
help: [
|
|
16039
|
+
"Show this help."
|
|
16040
|
+
]
|
|
16041
|
+
},
|
|
16042
|
+
{ flag: "--env", value: "<staging|production>", modes: ["bootstrap", "apply"] },
|
|
16043
|
+
{ flag: "--blank-slate", modes: ["bootstrap"] }
|
|
16044
|
+
];
|
|
16045
|
+
function optionsFor(mode) {
|
|
16046
|
+
return PROVISION_OPTIONS.filter((o) => o.modes.includes(mode));
|
|
16047
|
+
}
|
|
16048
|
+
function spellingsFor(mode) {
|
|
16049
|
+
return optionsFor(mode).flatMap((o) => [o.flag, ...o.aliases ?? []]);
|
|
16050
|
+
}
|
|
16051
|
+
function helpLines(option, mode) {
|
|
16052
|
+
if (!option.modes.includes(mode) || option.help === void 0) return void 0;
|
|
16053
|
+
return Array.isArray(option.help) ? option.help : option.help[mode];
|
|
16054
|
+
}
|
|
16055
|
+
var LABEL_COLUMN = 25;
|
|
16056
|
+
var TEXT_INDENT = " ".repeat(LABEL_COLUMN + 2);
|
|
16057
|
+
function renderOptionHelp(mode, blueprintNames) {
|
|
16058
|
+
const blocks = [];
|
|
16059
|
+
for (const option of optionsFor(mode)) {
|
|
16060
|
+
const lines = helpLines(option, mode);
|
|
16061
|
+
if (!lines) continue;
|
|
16062
|
+
const label = [option.flag + (option.value ? ` ${option.value}` : ""), ...option.aliases ?? []].join(", ");
|
|
16063
|
+
const text = lines.map((l) => l.split(BLUEPRINT_NAMES_TOKEN).join(blueprintNames.join(", ")));
|
|
16064
|
+
if (label.length <= LABEL_COLUMN - 2 && !option.stacked) {
|
|
16065
|
+
blocks.push([` ${label.padEnd(LABEL_COLUMN)}${text[0]}`, ...text.slice(1).map((l) => TEXT_INDENT + l)].join("\n"));
|
|
16066
|
+
} else {
|
|
16067
|
+
blocks.push([` ${label}`, ...text.map((l) => TEXT_INDENT + l)].join("\n"));
|
|
16068
|
+
}
|
|
16069
|
+
}
|
|
16070
|
+
return blocks.join("\n");
|
|
16071
|
+
}
|
|
16072
|
+
|
|
15503
16073
|
// src/base-url.ts
|
|
15504
16074
|
var ALLOWED_HOST_APEX = "vectros.ai";
|
|
15505
16075
|
var ALLOWED_HOST_SUFFIX = ".vectros.ai";
|
|
@@ -15563,6 +16133,9 @@ function validateBaseUrl(rawUrl, opts = {}) {
|
|
|
15563
16133
|
if (host === ALLOWED_HOST_APEX || host.endsWith(ALLOWED_HOST_SUFFIX)) {
|
|
15564
16134
|
return rawUrl;
|
|
15565
16135
|
}
|
|
16136
|
+
if ((opts.additionalAllowedHosts ?? []).some((h) => host === h.toLowerCase())) {
|
|
16137
|
+
return rawUrl;
|
|
16138
|
+
}
|
|
15566
16139
|
throw new InvalidBaseUrlError(
|
|
15567
16140
|
`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.`
|
|
15568
16141
|
);
|
|
@@ -15742,6 +16315,13 @@ var import_promises = require("fs/promises");
|
|
|
15742
16315
|
var import_node_os = require("os");
|
|
15743
16316
|
var import_node_path = require("path");
|
|
15744
16317
|
|
|
16318
|
+
// src/terminal-text.ts
|
|
16319
|
+
var STRIPPED = /[\p{Cc}\p{Cf}\p{Zl}\p{Zp}]/gu;
|
|
16320
|
+
function stripControlCharacters(value, maxLength = 200) {
|
|
16321
|
+
const characters = Array.from(value.replace(STRIPPED, ""));
|
|
16322
|
+
return characters.length > maxLength ? `${characters.slice(0, maxLength).join("")}...` : characters.join("");
|
|
16323
|
+
}
|
|
16324
|
+
|
|
15745
16325
|
// src/pkce.ts
|
|
15746
16326
|
var import_node_crypto = require("crypto");
|
|
15747
16327
|
|
|
@@ -15750,6 +16330,9 @@ var PER_ENV = {
|
|
|
15750
16330
|
staging: { domainPrefix: "vectros-developer-staging", region: "us-east-1" },
|
|
15751
16331
|
production: { domainPrefix: "vectros-developer", region: "us-east-1" }
|
|
15752
16332
|
};
|
|
16333
|
+
var COGNITO_ALLOWED_HOSTS = Object.values(PER_ENV).map(
|
|
16334
|
+
(p) => `${p.domainPrefix}.auth.${p.region}.amazoncognito.com`
|
|
16335
|
+
);
|
|
15753
16336
|
var BAKED_CLIENT_IDS = {
|
|
15754
16337
|
staging: "61hjvm25kkjb1eei8t2d3jkcl8",
|
|
15755
16338
|
// validated against deployed staging hosted UI (2026-06-05)
|
|
@@ -15764,13 +16347,30 @@ var MissingCognitoClientIdError = class extends Error {
|
|
|
15764
16347
|
this.name = "MissingCognitoClientIdError";
|
|
15765
16348
|
}
|
|
15766
16349
|
};
|
|
16350
|
+
var REGION_SHAPE = /^[a-z]{2}(?:-[a-z]+)+-\d+$/;
|
|
16351
|
+
var InvalidCognitoRegionError = class extends Error {
|
|
16352
|
+
constructor(region) {
|
|
16353
|
+
super(
|
|
16354
|
+
`Invalid VECTROS_COGNITO_REGION ${JSON.stringify(region)} \u2014 expected an AWS region shape (e.g. "us-east-1").`
|
|
16355
|
+
);
|
|
16356
|
+
this.name = "InvalidCognitoRegionError";
|
|
16357
|
+
}
|
|
16358
|
+
};
|
|
15767
16359
|
function resolveCognitoConfig(env) {
|
|
15768
16360
|
const per = PER_ENV[env];
|
|
15769
|
-
const
|
|
15770
|
-
const
|
|
16361
|
+
const domainOverride = process.env.VECTROS_COGNITO_DOMAIN;
|
|
16362
|
+
const regionOverride = process.env.VECTROS_COGNITO_REGION;
|
|
16363
|
+
if (domainOverride === void 0 && regionOverride !== void 0 && !REGION_SHAPE.test(regionOverride)) {
|
|
16364
|
+
throw new InvalidCognitoRegionError(regionOverride);
|
|
16365
|
+
}
|
|
16366
|
+
const region = regionOverride ?? per.region;
|
|
16367
|
+
const rawDomain = domainOverride ?? `https://${per.domainPrefix}.auth.${region}.amazoncognito.com`;
|
|
16368
|
+
const domain = validateBaseUrl(rawDomain, {
|
|
16369
|
+
additionalAllowedHosts: COGNITO_ALLOWED_HOSTS
|
|
16370
|
+
}).replace(/\/$/, "");
|
|
15771
16371
|
const clientId = process.env.VECTROS_COGNITO_CLIENT_ID ?? BAKED_CLIENT_IDS[env];
|
|
15772
16372
|
if (!clientId) throw new MissingCognitoClientIdError(env);
|
|
15773
|
-
return { domain
|
|
16373
|
+
return { domain, clientId, region };
|
|
15774
16374
|
}
|
|
15775
16375
|
var LOOPBACK_PORTS = [8976, 8977, 8978];
|
|
15776
16376
|
var OAUTH_SCOPES = ["openid", "email", "profile"];
|
|
@@ -15830,6 +16430,24 @@ function parseCallback(requestUrl) {
|
|
|
15830
16430
|
errorDescription: url.searchParams.get("error_description") ?? void 0
|
|
15831
16431
|
};
|
|
15832
16432
|
}
|
|
16433
|
+
var CALLBACK_PATH = "/callback";
|
|
16434
|
+
function stateMatches(received, expected) {
|
|
16435
|
+
if (received === void 0) return false;
|
|
16436
|
+
const a = Buffer.from(received);
|
|
16437
|
+
const b = Buffer.from(expected);
|
|
16438
|
+
return a.length === b.length && (0, import_node_crypto.timingSafeEqual)(a, b);
|
|
16439
|
+
}
|
|
16440
|
+
function classifyCallback(method, requestUrl, expectedState) {
|
|
16441
|
+
if (method !== "GET") return { kind: "not-a-callback" };
|
|
16442
|
+
const target = requestUrl ?? "/";
|
|
16443
|
+
const queryStart = target.indexOf("?");
|
|
16444
|
+
const path4 = queryStart === -1 ? target : target.slice(0, queryStart);
|
|
16445
|
+
if (path4 !== CALLBACK_PATH) return { kind: "not-a-callback" };
|
|
16446
|
+
const params = parseCallback(target);
|
|
16447
|
+
if (!params.code && !params.error) return { kind: "not-a-callback" };
|
|
16448
|
+
if (!stateMatches(params.state, expectedState)) return { kind: "wrong-state" };
|
|
16449
|
+
return { kind: "accepted", params };
|
|
16450
|
+
}
|
|
15833
16451
|
|
|
15834
16452
|
// src/auth-store.ts
|
|
15835
16453
|
var DIR_NAME = ".vectros";
|
|
@@ -15875,7 +16493,7 @@ async function postToken(config, body, fetchImpl) {
|
|
|
15875
16493
|
});
|
|
15876
16494
|
const text = await res.text().catch(() => "");
|
|
15877
16495
|
if (!res.ok) {
|
|
15878
|
-
throw new Error(`token endpoint HTTP ${res.status}${text ? ` \u2014 ${text}` : ""}`);
|
|
16496
|
+
throw new Error(`token endpoint HTTP ${res.status}${text ? ` \u2014 ${stripControlCharacters(text, 500)}` : ""}`);
|
|
15879
16497
|
}
|
|
15880
16498
|
try {
|
|
15881
16499
|
return JSON.parse(text);
|
|
@@ -16568,7 +17186,7 @@ async function withAuth(opts, deps, body) {
|
|
|
16568
17186
|
log
|
|
16569
17187
|
});
|
|
16570
17188
|
} catch (err) {
|
|
16571
|
-
if (err instanceof MissingCredentialError) {
|
|
17189
|
+
if (err instanceof MissingCredentialError || err instanceof InvalidBaseUrlError || err instanceof InvalidCognitoRegionError) {
|
|
16572
17190
|
process.stderr.write(` \u2716 ${err.message}
|
|
16573
17191
|
`);
|
|
16574
17192
|
return 2;
|
|
@@ -16644,6 +17262,12 @@ async function drainPages(fetch2) {
|
|
|
16644
17262
|
return out;
|
|
16645
17263
|
}
|
|
16646
17264
|
|
|
17265
|
+
// src/shell-quote.ts
|
|
17266
|
+
function shellQuote(value) {
|
|
17267
|
+
if (/^[A-Za-z0-9_@%+=:,./-]+$/.test(value)) return value;
|
|
17268
|
+
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
17269
|
+
}
|
|
17270
|
+
|
|
16647
17271
|
// src/loader.ts
|
|
16648
17272
|
function actionOf(created) {
|
|
16649
17273
|
return created ? "created" : "reconciled";
|
|
@@ -16655,15 +17279,42 @@ var SCRIPT_CONFLICT_RETRY_DELAY_MS = 750;
|
|
|
16655
17279
|
function isVersionConflict(err) {
|
|
16656
17280
|
return typeof err === "object" && err !== null && err.statusCode === 409;
|
|
16657
17281
|
}
|
|
16658
|
-
|
|
16659
|
-
|
|
16660
|
-
|
|
17282
|
+
function isForbidden(err) {
|
|
17283
|
+
return typeof err === "object" && err !== null && err.statusCode === 403;
|
|
17284
|
+
}
|
|
17285
|
+
function withAdoptionContext(err, name) {
|
|
17286
|
+
if (!(err instanceof Error) || !isForbidden(err)) return err;
|
|
17287
|
+
return new Error(
|
|
17288
|
+
`${err.message}
|
|
17289
|
+
|
|
17290
|
+
(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'.)`
|
|
16661
17291
|
);
|
|
16662
|
-
|
|
16663
|
-
|
|
16664
|
-
|
|
16665
|
-
|
|
16666
|
-
|
|
17292
|
+
}
|
|
17293
|
+
function isScriptNameNotFound(err) {
|
|
17294
|
+
return typeof err === "object" && err !== null && err.statusCode === 404;
|
|
17295
|
+
}
|
|
17296
|
+
function isScriptPageEnvelope(result) {
|
|
17297
|
+
return "data" in result;
|
|
17298
|
+
}
|
|
17299
|
+
async function readLatestScriptVersion(ctxClient, name) {
|
|
17300
|
+
let result;
|
|
17301
|
+
try {
|
|
17302
|
+
result = await ctxClient.scripts.listScripts({ name, latest: true, includeSource: true });
|
|
17303
|
+
} catch (err) {
|
|
17304
|
+
if (isScriptNameNotFound(err)) return void 0;
|
|
17305
|
+
throw err;
|
|
17306
|
+
}
|
|
17307
|
+
if (result == null) {
|
|
17308
|
+
throw new Error(
|
|
17309
|
+
`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.`
|
|
17310
|
+
);
|
|
17311
|
+
}
|
|
17312
|
+
if (isScriptPageEnvelope(result)) {
|
|
17313
|
+
throw new Error(
|
|
17314
|
+
`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.`
|
|
17315
|
+
);
|
|
17316
|
+
}
|
|
17317
|
+
return result;
|
|
16667
17318
|
}
|
|
16668
17319
|
function requireScriptId(row, name) {
|
|
16669
17320
|
if (!row.id) {
|
|
@@ -16674,8 +17325,34 @@ function requireScriptId(row, name) {
|
|
|
16674
17325
|
function scriptMatches(stored, declared) {
|
|
16675
17326
|
return stored.source === declared.source && (stored.declaredInputContract ?? void 0) === declared.declaredInputContract;
|
|
16676
17327
|
}
|
|
17328
|
+
var ScriptOwnershipConflictError = class extends Error {
|
|
17329
|
+
constructor(name, owner, blueprintName) {
|
|
17330
|
+
super(
|
|
17331
|
+
`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.`
|
|
17332
|
+
);
|
|
17333
|
+
this.name = "ScriptOwnershipConflictError";
|
|
17334
|
+
}
|
|
17335
|
+
};
|
|
17336
|
+
var ScriptAdoptionNotAllowedError = class extends Error {
|
|
17337
|
+
constructor(name) {
|
|
17338
|
+
super(
|
|
17339
|
+
`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.`
|
|
17340
|
+
);
|
|
17341
|
+
this.name = "ScriptAdoptionNotAllowedError";
|
|
17342
|
+
}
|
|
17343
|
+
};
|
|
17344
|
+
function assertScriptPushOwnable(name, latest, blueprintName, allowScriptAdoption) {
|
|
17345
|
+
if (latest === void 0) return { adopting: false };
|
|
17346
|
+
const owner = latest.provisionedBy?.trim() || void 0;
|
|
17347
|
+
if (owner === void 0) {
|
|
17348
|
+
if (!allowScriptAdoption) throw new ScriptAdoptionNotAllowedError(name);
|
|
17349
|
+
return { adopting: true };
|
|
17350
|
+
}
|
|
17351
|
+
if (owner !== blueprintName) throw new ScriptOwnershipConflictError(name, owner, blueprintName);
|
|
17352
|
+
return { adopting: false };
|
|
17353
|
+
}
|
|
16677
17354
|
async function ensureScripts(args) {
|
|
16678
|
-
const { ctxClient, scripts, log } = args;
|
|
17355
|
+
const { ctxClient, scripts, blueprintName, allowScriptAdoption, log } = args;
|
|
16679
17356
|
const actions = [];
|
|
16680
17357
|
for (const [name, entry] of Object.entries(scripts)) {
|
|
16681
17358
|
let latest = await readLatestScriptVersion(ctxClient, name);
|
|
@@ -16686,18 +17363,32 @@ async function ensureScripts(args) {
|
|
|
16686
17363
|
id: requireScriptId(latest, name),
|
|
16687
17364
|
scriptVersion: latest.scriptVersion ?? 0
|
|
16688
17365
|
});
|
|
16689
|
-
|
|
16690
|
-
|
|
16691
|
-
|
|
16692
|
-
|
|
17366
|
+
const foreignOwner = latest.provisionedBy?.trim() || void 0;
|
|
17367
|
+
if (foreignOwner !== void 0 && foreignOwner !== blueprintName) {
|
|
17368
|
+
log.warn(
|
|
17369
|
+
{ component: "loader", scriptName: name, provisionedBy: foreignOwner },
|
|
17370
|
+
`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`
|
|
17371
|
+
);
|
|
17372
|
+
} else {
|
|
17373
|
+
log.debug(
|
|
17374
|
+
{ component: "loader", scriptName: name, scriptVersion: latest.scriptVersion },
|
|
17375
|
+
"script already matches the declared source \u2014 no version pushed"
|
|
17376
|
+
);
|
|
17377
|
+
}
|
|
16693
17378
|
continue;
|
|
16694
17379
|
}
|
|
16695
|
-
|
|
17380
|
+
let { adopting } = assertScriptPushOwnable(name, latest, blueprintName, allowScriptAdoption);
|
|
17381
|
+
const body = {
|
|
17382
|
+
name,
|
|
17383
|
+
source: entry.source,
|
|
17384
|
+
declaredInputContract: entry.declaredInputContract,
|
|
17385
|
+
provisionedBy: blueprintName
|
|
17386
|
+
};
|
|
16696
17387
|
let pushed;
|
|
16697
17388
|
try {
|
|
16698
17389
|
pushed = await ctxClient.scripts.createScript(body);
|
|
16699
17390
|
} catch (err) {
|
|
16700
|
-
if (!isVersionConflict(err)) throw err;
|
|
17391
|
+
if (!isVersionConflict(err)) throw adopting ? withAdoptionContext(err, name) : err;
|
|
16701
17392
|
log.debug(
|
|
16702
17393
|
{ component: "loader", scriptName: name },
|
|
16703
17394
|
"concurrent push detected (409) \u2014 pausing, then re-reading the current version"
|
|
@@ -16723,10 +17414,11 @@ async function ensureScripts(args) {
|
|
|
16723
17414
|
);
|
|
16724
17415
|
continue;
|
|
16725
17416
|
}
|
|
17417
|
+
({ adopting } = assertScriptPushOwnable(name, latest, blueprintName, allowScriptAdoption));
|
|
16726
17418
|
try {
|
|
16727
17419
|
pushed = await ctxClient.scripts.createScript(body);
|
|
16728
17420
|
} catch (retryErr) {
|
|
16729
|
-
if (!isVersionConflict(retryErr)) throw retryErr;
|
|
17421
|
+
if (!isVersionConflict(retryErr)) throw adopting ? withAdoptionContext(retryErr, name) : retryErr;
|
|
16730
17422
|
throw new Error(
|
|
16731
17423
|
`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.`
|
|
16732
17424
|
);
|
|
@@ -16738,13 +17430,13 @@ async function ensureScripts(args) {
|
|
|
16738
17430
|
const firstVersion = latest === void 0;
|
|
16739
17431
|
actions.push({
|
|
16740
17432
|
name,
|
|
16741
|
-
action: firstVersion ? "created" : "superseded",
|
|
17433
|
+
action: firstVersion ? "created" : adopting ? "adopted" : "superseded",
|
|
16742
17434
|
id: pushed.id,
|
|
16743
17435
|
scriptVersion: pushed.scriptVersion ?? 0
|
|
16744
17436
|
});
|
|
16745
17437
|
log.debug(
|
|
16746
17438
|
{ component: "loader", scriptName: name, scriptVersion: pushed.scriptVersion },
|
|
16747
|
-
firstVersion ? "script pushed \u2014 first version for this name in this context" : "script source changed \u2014 new version pushed over an existing name"
|
|
17439
|
+
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"
|
|
16748
17440
|
);
|
|
16749
17441
|
}
|
|
16750
17442
|
return actions;
|
|
@@ -16763,14 +17455,16 @@ async function pruneUndeclaredTriggers(args) {
|
|
|
16763
17455
|
{ component: "loader", err: extractServerError(err) },
|
|
16764
17456
|
"could not list trigger rules \u2014 skipping the trigger reconcile entirely; a trigger removed from this blueprint may still be live"
|
|
16765
17457
|
);
|
|
16766
|
-
return { pruned, skipped: [{ reason: "the context's trigger rules could not be listed" }] };
|
|
17458
|
+
return { pruned, skipped: [{ reason: "the context's trigger rules could not be listed", failed: true }] };
|
|
16767
17459
|
}
|
|
16768
17460
|
for (const row of live) {
|
|
16769
17461
|
if (!row.name || !row.id || !row.principalId) {
|
|
16770
17462
|
if (!declaredNames.has(row.name ?? "")) {
|
|
17463
|
+
const ours = row.provisionedBy === blueprintName;
|
|
16771
17464
|
skipped.push({
|
|
16772
17465
|
name: row.name,
|
|
16773
|
-
reason: "the trigger rule carries no id/name/principal, so it cannot be safely de-provisioned"
|
|
17466
|
+
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",
|
|
17467
|
+
failed: ours
|
|
16774
17468
|
});
|
|
16775
17469
|
}
|
|
16776
17470
|
continue;
|
|
@@ -16779,7 +17473,8 @@ async function pruneUndeclaredTriggers(args) {
|
|
|
16779
17473
|
if (!row.provisionedBy) {
|
|
16780
17474
|
skipped.push({
|
|
16781
17475
|
name: row.name,
|
|
16782
|
-
reason: "it carries no blueprint provenance marker, so it cannot be attributed to this blueprint"
|
|
17476
|
+
reason: "it carries no blueprint provenance marker, so it cannot be attributed to this blueprint",
|
|
17477
|
+
failed: false
|
|
16783
17478
|
});
|
|
16784
17479
|
continue;
|
|
16785
17480
|
}
|
|
@@ -16800,7 +17495,8 @@ async function pruneUndeclaredTriggers(args) {
|
|
|
16800
17495
|
);
|
|
16801
17496
|
skipped.push({
|
|
16802
17497
|
name: row.name,
|
|
16803
|
-
reason: "its service principal could not be read, so ownership could not be fully established"
|
|
17498
|
+
reason: "its service principal could not be read, so ownership could not be fully established",
|
|
17499
|
+
failed: true
|
|
16804
17500
|
});
|
|
16805
17501
|
continue;
|
|
16806
17502
|
}
|
|
@@ -16811,7 +17507,8 @@ async function pruneUndeclaredTriggers(args) {
|
|
|
16811
17507
|
);
|
|
16812
17508
|
skipped.push({
|
|
16813
17509
|
name: row.name,
|
|
16814
|
-
reason: "it names a service principal this blueprint did not mint for a trigger of that name, so its provenance is inconsistent"
|
|
17510
|
+
reason: "it names a service principal this blueprint did not mint for a trigger of that name, so its provenance is inconsistent",
|
|
17511
|
+
failed: false
|
|
16815
17512
|
});
|
|
16816
17513
|
continue;
|
|
16817
17514
|
}
|
|
@@ -16824,7 +17521,7 @@ async function pruneUndeclaredTriggers(args) {
|
|
|
16824
17521
|
{ component: "loader", triggerName: row.name, principalId: row.principalId, err: extractServerError(err) },
|
|
16825
17522
|
"could not delete a removed trigger's access profile \u2014 leaving the trigger rule in place so the next apply retries"
|
|
16826
17523
|
);
|
|
16827
|
-
skipped.push({ name: row.name, reason: "its access profile could not be deleted, so its grant may still be live" });
|
|
17524
|
+
skipped.push({ name: row.name, reason: "its access profile could not be deleted, so its grant may still be live", failed: true });
|
|
16828
17525
|
continue;
|
|
16829
17526
|
}
|
|
16830
17527
|
profileDeleted = false;
|
|
@@ -16836,7 +17533,7 @@ async function pruneUndeclaredTriggers(args) {
|
|
|
16836
17533
|
{ component: "loader", triggerName: row.name, err: extractServerError(err) },
|
|
16837
17534
|
"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"
|
|
16838
17535
|
);
|
|
16839
|
-
skipped.push({ name: row.name, reason: "its grant was removed but the rule row itself could not be deleted" });
|
|
17536
|
+
skipped.push({ name: row.name, reason: "its grant was removed but the rule row itself could not be deleted", failed: true });
|
|
16840
17537
|
continue;
|
|
16841
17538
|
}
|
|
16842
17539
|
pruned.push({ name: row.name, principalId: row.principalId, profileDeleted });
|
|
@@ -16915,9 +17612,10 @@ function describeNamespaceMismatch(decl, existing) {
|
|
|
16915
17612
|
return mismatches.length > 0 ? mismatches.join("; ") : null;
|
|
16916
17613
|
}
|
|
16917
17614
|
var LostKeyError = class extends Error {
|
|
16918
|
-
constructor(keyName) {
|
|
17615
|
+
constructor(keyName, rotateAttempted = false, revokedKeyId) {
|
|
17616
|
+
const revokedDisplay = revokedKeyId?.map((id) => `'${id}'`).join(", ");
|
|
16919
17617
|
super(
|
|
16920
|
-
`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.`
|
|
17618
|
+
`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.`)
|
|
16921
17619
|
);
|
|
16922
17620
|
this.keyName = keyName;
|
|
16923
17621
|
this.name = "LostKeyError";
|
|
@@ -16925,18 +17623,20 @@ var LostKeyError = class extends Error {
|
|
|
16925
17623
|
keyName;
|
|
16926
17624
|
};
|
|
16927
17625
|
var ExistingPrincipalNotConfirmedError = class extends Error {
|
|
16928
|
-
constructor(contextId, userId, externalId) {
|
|
17626
|
+
constructor(contextId, userId, externalId, mintsKey = true) {
|
|
16929
17627
|
super(
|
|
16930
|
-
`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.`
|
|
17628
|
+
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.`
|
|
16931
17629
|
);
|
|
16932
17630
|
this.contextId = contextId;
|
|
16933
17631
|
this.userId = userId;
|
|
16934
17632
|
this.externalId = externalId;
|
|
17633
|
+
this.mintsKey = mintsKey;
|
|
16935
17634
|
this.name = "ExistingPrincipalNotConfirmedError";
|
|
16936
17635
|
}
|
|
16937
17636
|
contextId;
|
|
16938
17637
|
userId;
|
|
16939
17638
|
externalId;
|
|
17639
|
+
mintsKey;
|
|
16940
17640
|
};
|
|
16941
17641
|
function mapEnumValues(enumValues) {
|
|
16942
17642
|
if (!enumValues) return void 0;
|
|
@@ -17039,6 +17739,30 @@ function assertNoUnresolvedIdentities(blueprint) {
|
|
|
17039
17739
|
);
|
|
17040
17740
|
}
|
|
17041
17741
|
}
|
|
17742
|
+
var IssuerScopeNotAppliedError = class extends Error {
|
|
17743
|
+
constructor(issuerId, contextId, declared, returned, phase) {
|
|
17744
|
+
super(
|
|
17745
|
+
`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)}`
|
|
17746
|
+
);
|
|
17747
|
+
this.issuerId = issuerId;
|
|
17748
|
+
this.contextId = contextId;
|
|
17749
|
+
this.declared = declared;
|
|
17750
|
+
this.returned = returned;
|
|
17751
|
+
this.phase = phase;
|
|
17752
|
+
this.name = "IssuerScopeNotAppliedError";
|
|
17753
|
+
}
|
|
17754
|
+
issuerId;
|
|
17755
|
+
contextId;
|
|
17756
|
+
declared;
|
|
17757
|
+
returned;
|
|
17758
|
+
phase;
|
|
17759
|
+
};
|
|
17760
|
+
function assertIssuerScopeApplied(issuer, returned, phase) {
|
|
17761
|
+
const declared = issuer.restrictedToDomain;
|
|
17762
|
+
if (declared === void 0) return;
|
|
17763
|
+
if ((returned ?? "").toLowerCase() === declared.toLowerCase()) return;
|
|
17764
|
+
throw new IssuerScopeNotAppliedError(issuer.issuerId, issuer.contextId, declared, returned, phase);
|
|
17765
|
+
}
|
|
17042
17766
|
var TenantWideNamespacesNotAllowedError = class extends Error {
|
|
17043
17767
|
constructor(message) {
|
|
17044
17768
|
super(message);
|
|
@@ -17094,7 +17818,12 @@ async function provisionContext(client, input) {
|
|
|
17094
17818
|
externalId: blueprint.servicePrincipal.externalId
|
|
17095
17819
|
}) : true;
|
|
17096
17820
|
if (!proceed) {
|
|
17097
|
-
throw new ExistingPrincipalNotConfirmedError(
|
|
17821
|
+
throw new ExistingPrincipalNotConfirmedError(
|
|
17822
|
+
blueprint.contextId,
|
|
17823
|
+
userId,
|
|
17824
|
+
blueprint.servicePrincipal.externalId,
|
|
17825
|
+
!input.skipKey
|
|
17826
|
+
);
|
|
17098
17827
|
}
|
|
17099
17828
|
log.debug(
|
|
17100
17829
|
{ component: "loader", contextId: blueprint.contextId, userId },
|
|
@@ -17112,7 +17841,14 @@ async function provisionContext(client, input) {
|
|
|
17112
17841
|
namespaceActions.push({ namespace: ns.namespace, action });
|
|
17113
17842
|
log.debug({ component: "loader", namespace: ns.namespace, contextId: blueprint.contextId, action }, "namespace ensured");
|
|
17114
17843
|
}
|
|
17115
|
-
return {
|
|
17844
|
+
return {
|
|
17845
|
+
contextAction,
|
|
17846
|
+
servicePrincipalUserId: userId,
|
|
17847
|
+
...user.createdAt === void 0 ? {} : { servicePrincipalCreatedAt: user.createdAt },
|
|
17848
|
+
userAction,
|
|
17849
|
+
namespaceActions,
|
|
17850
|
+
ctxBootstrapClient
|
|
17851
|
+
};
|
|
17116
17852
|
}
|
|
17117
17853
|
async function runLoader(client, input) {
|
|
17118
17854
|
const {
|
|
@@ -17120,13 +17856,18 @@ async function runLoader(client, input) {
|
|
|
17120
17856
|
tenantId,
|
|
17121
17857
|
machineId,
|
|
17122
17858
|
rotate = false,
|
|
17859
|
+
skipKey = false,
|
|
17123
17860
|
skipSeed = false,
|
|
17124
17861
|
seedIndexPoll,
|
|
17125
17862
|
provisioned,
|
|
17126
17863
|
readExistingKey,
|
|
17127
17864
|
persistMintedKey,
|
|
17865
|
+
allowScriptAdoption = false,
|
|
17128
17866
|
log
|
|
17129
17867
|
} = input;
|
|
17868
|
+
if (skipKey && rotate) {
|
|
17869
|
+
throw new Error("rotate and skipKey cannot be combined: rotate replaces this machine's key, and skipKey mints none.");
|
|
17870
|
+
}
|
|
17130
17871
|
assertNoUnresolvedIdentities(blueprint);
|
|
17131
17872
|
const { contextAction, servicePrincipalUserId: userId, userAction, namespaceActions } = provisioned;
|
|
17132
17873
|
assertBlueprintGates(blueprint);
|
|
@@ -17150,13 +17891,15 @@ async function runLoader(client, input) {
|
|
|
17150
17891
|
subClaim: issuer.subClaim,
|
|
17151
17892
|
emailClaim: issuer.emailClaim,
|
|
17152
17893
|
userinfoUri: issuer.userinfoUri,
|
|
17894
|
+
restrictedToDomain: issuer.restrictedToDomain,
|
|
17153
17895
|
selfSignupPolicies,
|
|
17154
17896
|
capturedClaims: issuer.capturedClaims
|
|
17155
17897
|
});
|
|
17156
17898
|
const action = actionOf(issuerResp.created);
|
|
17157
|
-
|
|
17899
|
+
let observed = issuerResp;
|
|
17900
|
+
if (action === "created") assertIssuerScopeApplied(issuer, issuerResp.restrictedToDomain, "registered");
|
|
17158
17901
|
if (action === "reconciled") {
|
|
17159
|
-
await ctxBootstrapClient.auth.updateIssuer({
|
|
17902
|
+
const updated = await ctxBootstrapClient.auth.updateIssuer({
|
|
17160
17903
|
issuerId: issuer.issuerId,
|
|
17161
17904
|
// Trust-anchor/routing-pin fields — echoed back, never actually
|
|
17162
17905
|
// changed here (the platform rejects a differing value).
|
|
@@ -17167,15 +17910,37 @@ async function runLoader(client, input) {
|
|
|
17167
17910
|
subClaim: issuer.subClaim,
|
|
17168
17911
|
emailClaim: issuer.emailClaim,
|
|
17169
17912
|
userinfoUri: issuer.userinfoUri,
|
|
17913
|
+
// Undeclared stays `undefined` (leave the stored scope unchanged) — never '', which the
|
|
17914
|
+
// platform reads as "remove the scope". That "never ''" invariant is enforced by the blueprint
|
|
17915
|
+
// schema, not here: the schema's grammar has no empty spelling, and this CLI is only ever run
|
|
17916
|
+
// as a binary, so a blueprint always passes through `parseBlueprint` before it reaches this loop.
|
|
17917
|
+
restrictedToDomain: issuer.restrictedToDomain,
|
|
17170
17918
|
selfSignupPolicies,
|
|
17171
17919
|
capturedClaims: issuer.capturedClaims
|
|
17172
17920
|
});
|
|
17921
|
+
assertIssuerScopeApplied(issuer, updated.restrictedToDomain, "reconciled");
|
|
17922
|
+
if (updated.status !== void 0) {
|
|
17923
|
+
observed = {
|
|
17924
|
+
status: updated.status,
|
|
17925
|
+
verificationClaim: updated.verificationClaim,
|
|
17926
|
+
verificationNonce: updated.verificationNonce,
|
|
17927
|
+
verificationExpiresAt: updated.verificationExpiresAt
|
|
17928
|
+
};
|
|
17929
|
+
}
|
|
17173
17930
|
log.debug({ component: "loader", issuerId: issuer.issuerId }, "issuer already registered \u2014 reconciled safe fields to the declared values");
|
|
17174
17931
|
} else {
|
|
17175
17932
|
log.debug({ component: "loader", issuerId: issuer.issuerId, action }, "issuer registered");
|
|
17176
17933
|
}
|
|
17177
|
-
|
|
17178
|
-
|
|
17934
|
+
issuerActions.push({
|
|
17935
|
+
issuerId: issuer.issuerId,
|
|
17936
|
+
action,
|
|
17937
|
+
...observed.status !== void 0 ? { status: observed.status } : {},
|
|
17938
|
+
...observed.verificationClaim !== void 0 ? { verificationClaim: observed.verificationClaim } : {},
|
|
17939
|
+
...observed.verificationNonce !== void 0 ? { verificationNonce: observed.verificationNonce } : {},
|
|
17940
|
+
...observed.verificationExpiresAt !== void 0 ? { verificationExpiresAt: observed.verificationExpiresAt } : {}
|
|
17941
|
+
});
|
|
17942
|
+
}
|
|
17943
|
+
const ctxClient = input.remintForContext ? await input.remintForContext(blueprint.contextId) : client;
|
|
17179
17944
|
log.debug({ component: "loader", contextId: blueprint.contextId }, "re-minted per-context client");
|
|
17180
17945
|
const schemaIds = {};
|
|
17181
17946
|
const schemaActions = [];
|
|
@@ -17275,7 +18040,13 @@ async function runLoader(client, input) {
|
|
|
17275
18040
|
body: profileBody
|
|
17276
18041
|
});
|
|
17277
18042
|
log.debug({ component: "loader", principalId: `usr_${userId}`, accessProfileId: profile.id, profileAction }, "access profile ensured + reconciled to declared scopes");
|
|
17278
|
-
const scriptActions = blueprint.scripts ? await ensureScripts({
|
|
18043
|
+
const scriptActions = blueprint.scripts ? await ensureScripts({
|
|
18044
|
+
ctxClient,
|
|
18045
|
+
scripts: blueprint.scripts,
|
|
18046
|
+
blueprintName: blueprint.name,
|
|
18047
|
+
allowScriptAdoption,
|
|
18048
|
+
log
|
|
18049
|
+
}) : [];
|
|
17279
18050
|
const triggerActions = [];
|
|
17280
18051
|
for (const [triggerName, entry] of Object.entries(blueprint.triggers ?? {})) {
|
|
17281
18052
|
const schemaId = schemaIds[entry.firingSource.schemaName];
|
|
@@ -17368,7 +18139,9 @@ async function runLoader(client, input) {
|
|
|
17368
18139
|
{ component: "loader", contextId: blueprint.contextId },
|
|
17369
18140
|
"trigger reconcile-by-absence skipped \u2014 no per-context client is wired, so a removed trigger may still be live"
|
|
17370
18141
|
);
|
|
17371
|
-
triggerPruneSkipped = [
|
|
18142
|
+
triggerPruneSkipped = [
|
|
18143
|
+
{ reason: "no per-context client was wired, so triggers could not be reconciled in the blueprint's own context", failed: true }
|
|
18144
|
+
];
|
|
17372
18145
|
} else {
|
|
17373
18146
|
({ pruned: triggerPrunes, skipped: triggerPruneSkipped } = await pruneUndeclaredTriggers({
|
|
17374
18147
|
ctxClient,
|
|
@@ -17378,38 +18151,76 @@ async function runLoader(client, input) {
|
|
|
17378
18151
|
log
|
|
17379
18152
|
}));
|
|
17380
18153
|
}
|
|
17381
|
-
const keyName = keyNameFor(blueprint.name, machineId);
|
|
17382
|
-
let keyAction = "created";
|
|
18154
|
+
const keyName = skipKey ? "" : keyNameFor(blueprint.name, machineId);
|
|
18155
|
+
let keyAction = skipKey ? "skipped" : "created";
|
|
17383
18156
|
let revokedKeyId;
|
|
17384
|
-
|
|
17385
|
-
|
|
17386
|
-
|
|
17387
|
-
|
|
17388
|
-
|
|
17389
|
-
|
|
17390
|
-
|
|
17391
|
-
|
|
17392
|
-
|
|
17393
|
-
|
|
17394
|
-
|
|
17395
|
-
|
|
17396
|
-
|
|
17397
|
-
|
|
17398
|
-
|
|
17399
|
-
|
|
17400
|
-
|
|
17401
|
-
|
|
17402
|
-
|
|
17403
|
-
|
|
17404
|
-
|
|
17405
|
-
|
|
17406
|
-
|
|
18157
|
+
let rawKey = "";
|
|
18158
|
+
let keyId = "";
|
|
18159
|
+
if (!skipKey) {
|
|
18160
|
+
if (rotate) {
|
|
18161
|
+
const existing = await readExistingKey(blueprint.contextId);
|
|
18162
|
+
if (existing?.keyId) {
|
|
18163
|
+
await ctxClient.auth.revokeScopedKey({ keyId: existing.keyId });
|
|
18164
|
+
log.debug({ component: "loader", keyId: existing.keyId }, "revoked prior key for rotate");
|
|
18165
|
+
revokedKeyId = [existing.keyId];
|
|
18166
|
+
} else {
|
|
18167
|
+
let activeKeys;
|
|
18168
|
+
try {
|
|
18169
|
+
activeKeys = await drainPages(() => ctxClient.auth.listScopedKeys());
|
|
18170
|
+
} catch (err) {
|
|
18171
|
+
throw new Error(
|
|
18172
|
+
`--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.`,
|
|
18173
|
+
{ cause: err }
|
|
18174
|
+
);
|
|
18175
|
+
}
|
|
18176
|
+
const matches = activeKeys.filter(
|
|
18177
|
+
(k) => k.contextId === blueprint.contextId && k.userId === userId && k.keyName === keyName && k.status === "active" && k.keyId
|
|
18178
|
+
);
|
|
18179
|
+
if (matches.length > 1) {
|
|
18180
|
+
log.warn(
|
|
18181
|
+
{ component: "loader", keyName, contextId: blueprint.contextId, count: matches.length },
|
|
18182
|
+
"rotate: more than one active key matched this context/user/name tuple server-side \u2014 revoking all of them"
|
|
18183
|
+
);
|
|
18184
|
+
}
|
|
18185
|
+
if (matches.length > 0) {
|
|
18186
|
+
const revokedIds = [];
|
|
18187
|
+
for (const match of matches) {
|
|
18188
|
+
await ctxClient.auth.revokeScopedKey({ keyId: match.keyId });
|
|
18189
|
+
revokedIds.push(match.keyId);
|
|
18190
|
+
}
|
|
18191
|
+
log.debug(
|
|
18192
|
+
{ component: "loader", keyIds: revokedIds },
|
|
18193
|
+
"revoked prior key(s) for rotate \u2014 resolved server-side, no local record existed"
|
|
18194
|
+
);
|
|
18195
|
+
revokedKeyId = revokedIds;
|
|
18196
|
+
} else {
|
|
18197
|
+
log.warn(
|
|
18198
|
+
{ component: "loader", keyName, contextId: blueprint.contextId },
|
|
18199
|
+
"rotate: no local record and no matching active key found server-side \u2014 nothing to revoke"
|
|
18200
|
+
);
|
|
18201
|
+
}
|
|
18202
|
+
}
|
|
18203
|
+
if (revokedKeyId) keyAction = "rotated";
|
|
18204
|
+
}
|
|
18205
|
+
const keyResp = await ctxClient.auth.createScopedKey({ keyName, tenantId, contextId: blueprint.contextId, userId });
|
|
18206
|
+
if (keyResp.rawKey) {
|
|
18207
|
+
keyId = keyResp.keyId ?? "";
|
|
18208
|
+
rawKey = keyResp.rawKey;
|
|
18209
|
+
if (keyId) {
|
|
18210
|
+
await persistMintedKey(blueprint.contextId, { keyId, rawKey, keyName, principalId: `usr_${userId}`, revokedKeyId });
|
|
18211
|
+
}
|
|
18212
|
+
if (keyAction !== "rotated") keyAction = "created";
|
|
18213
|
+
} else {
|
|
18214
|
+
const existing = await readExistingKey(blueprint.contextId);
|
|
18215
|
+
if (!existing) {
|
|
18216
|
+
throw new LostKeyError(keyName, rotate, revokedKeyId);
|
|
18217
|
+
}
|
|
18218
|
+
rawKey = existing.rawKey;
|
|
18219
|
+
keyId = existing.keyId;
|
|
18220
|
+
keyAction = "reused";
|
|
17407
18221
|
}
|
|
17408
|
-
|
|
17409
|
-
keyId = existing.keyId;
|
|
17410
|
-
keyAction = "reused";
|
|
18222
|
+
log.debug({ component: "loader", keyName, keyAction }, "scoped key resolved");
|
|
17411
18223
|
}
|
|
17412
|
-
log.debug({ component: "loader", keyName, keyAction }, "scoped key resolved");
|
|
17413
18224
|
let seededCount = 0;
|
|
17414
18225
|
const seededRecordIds = [];
|
|
17415
18226
|
const seededDocumentIds = [];
|
|
@@ -17516,6 +18327,9 @@ async function runLoader(client, input) {
|
|
|
17516
18327
|
seedIndexFailures
|
|
17517
18328
|
};
|
|
17518
18329
|
}
|
|
18330
|
+
function sanitizeForPlanDisplay(value, maxLen = 200) {
|
|
18331
|
+
return stripControlCharacters(value, maxLen);
|
|
18332
|
+
}
|
|
17519
18333
|
function renderPlanDataScope(ds) {
|
|
17520
18334
|
const entries = Object.entries(ds ?? {});
|
|
17521
18335
|
if (entries.length === 0) return "(none \u2014 unconstrained)";
|
|
@@ -17542,11 +18356,15 @@ function buildPlan(blueprint, opts = {}) {
|
|
|
17542
18356
|
});
|
|
17543
18357
|
}
|
|
17544
18358
|
for (const iss of blueprint.issuers ?? []) {
|
|
17545
|
-
const
|
|
17546
|
-
const
|
|
18359
|
+
const issuerUrl = sanitizeForPlanDisplay(iss.issuer);
|
|
18360
|
+
const jwksNote = `, jwksUri: ${sanitizeForPlanDisplay(iss.jwksUri)}`;
|
|
18361
|
+
const userinfoNote = iss.userinfoUri ? `, userinfoUri: ${sanitizeForPlanDisplay(iss.userinfoUri)}` : "";
|
|
18362
|
+
const domainNote = iss.restrictedToDomain ? `, restricted to domain: ${sanitizeForPlanDisplay(iss.restrictedToDomain)}` : "";
|
|
18363
|
+
const selfSignup = iss.selfSignupPolicies?.length ? `, self-signup: [${iss.selfSignupPolicies.map((p) => `${p.signupType}\u2192${p.roleId}`).join(", ")}]` : "";
|
|
18364
|
+
const captured = iss.capturedClaims?.length ? `, captured claims: [${iss.capturedClaims.map((c) => sanitizeForPlanDisplay(c)).join(", ")}]` : "";
|
|
17547
18365
|
steps.push({
|
|
17548
18366
|
kind: "issuer",
|
|
17549
|
-
summary: `issuer '${iss.issuerId}' (${
|
|
18367
|
+
summary: `issuer '${iss.issuerId}' (${issuerUrl} \u2192 context '${iss.contextId}'${jwksNote}${userinfoNote}${domainNote}${selfSignup}${captured}) [${BLUEPRINT_FIELD_PHASES.issuers}-token phase]`
|
|
17550
18368
|
});
|
|
17551
18369
|
}
|
|
17552
18370
|
for (const s of blueprint.schemas) {
|
|
@@ -17636,7 +18454,9 @@ function buildPlan(blueprint, opts = {}) {
|
|
|
17636
18454
|
});
|
|
17637
18455
|
}
|
|
17638
18456
|
}
|
|
17639
|
-
|
|
18457
|
+
if (!opts.noKey) {
|
|
18458
|
+
steps.push({ kind: "key", summary: `key ${blueprint.name}-<machine> (mint 1 scoped ssk_*)` });
|
|
18459
|
+
}
|
|
17640
18460
|
const seeds = blueprint.seed ?? [];
|
|
17641
18461
|
if (seeds.length > 0 && !opts.noSeed) {
|
|
17642
18462
|
const docs = seeds.filter((s) => s.surface === "document").length;
|
|
@@ -17648,36 +18468,57 @@ function buildPlan(blueprint, opts = {}) {
|
|
|
17648
18468
|
}
|
|
17649
18469
|
return steps;
|
|
17650
18470
|
}
|
|
17651
|
-
function renderPlan(blueprint, steps) {
|
|
18471
|
+
function renderPlan(blueprint, steps, opts = {}) {
|
|
17652
18472
|
const header = ` Plan for blueprint '${blueprint.name}' (v${blueprint.version}) \u2192 context '${blueprint.contextId}':`;
|
|
17653
18473
|
const legendLines = [
|
|
17654
18474
|
` Apply is a reconcile \u2014 each item is created if absent, else updated in place to`,
|
|
17655
18475
|
` match. Nothing is recreated, and nothing is dropped except as noted below.`
|
|
17656
18476
|
];
|
|
17657
|
-
|
|
17658
|
-
|
|
17659
|
-
|
|
17660
|
-
|
|
17661
|
-
|
|
17662
|
-
|
|
17663
|
-
|
|
17664
|
-
|
|
18477
|
+
if (opts.prune === false) {
|
|
18478
|
+
legendLines.push(
|
|
18479
|
+
` Nothing is deleted: this apply does not prune. A trigger this blueprint provisioned`,
|
|
18480
|
+
` earlier and its file no longer declares is LEFT IN PLACE; pass --prune to remove it.`,
|
|
18481
|
+
` (Access profiles and roles are still replaced in place: their scopes become the declared set.)`
|
|
18482
|
+
);
|
|
18483
|
+
} else {
|
|
18484
|
+
legendLines.push(
|
|
18485
|
+
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`,
|
|
18486
|
+
` previously provisioned and no longer declares is DELETED (the rule and its access`,
|
|
18487
|
+
` profile; its service principal is kept). This applies even when this plan shows no`,
|
|
18488
|
+
` trigger steps: removing the last one is exactly what leaves none to show. Triggers`,
|
|
18489
|
+
` created outside this blueprint are never touched. Each deletion is named in the`,
|
|
18490
|
+
` apply output. bootstrap does this on every apply; 'vectros blueprint apply' only`,
|
|
18491
|
+
` when --prune is passed.`,
|
|
18492
|
+
` "This blueprint's" is decided by a STORED MARKER, not a verified identity: the row's`,
|
|
18493
|
+
` provisionedBy equals this blueprint's name (read from the blueprint file) and its`,
|
|
18494
|
+
` principal's external id is the one this blueprint would derive for that trigger.`
|
|
18495
|
+
);
|
|
18496
|
+
}
|
|
17665
18497
|
legendLines.push(
|
|
17666
18498
|
` Counter-exception: a script is never updated in place \u2014 a changed source ADDS a new`,
|
|
17667
18499
|
` immutable version and every earlier one persists, which is also how a 'latest' rule`,
|
|
17668
18500
|
` picks the change up. And scripts are NOT reconciled by absence: a script this blueprint`,
|
|
17669
18501
|
` previously shipped and no longer declares is LEFT IN PLACE, including every stored`,
|
|
17670
|
-
` version.
|
|
17671
|
-
`
|
|
17672
|
-
`
|
|
18502
|
+
` version. Each version does carry this blueprint's own provenance marker, and a push`,
|
|
18503
|
+
` ONTO an already-marked-or-existing name is gated on it (see 'script' steps above) \u2014 but`,
|
|
18504
|
+
` that gate has no bearing on DELETION: there is no second, principal-shaped factor to`,
|
|
18505
|
+
` pair the marker with the way trigger rows have one, so a wrong delete stays unrecoverable`,
|
|
18506
|
+
` and would break any trigger still referencing it. Remove one deliberately with`,
|
|
17673
18507
|
` 'vectros scripts delete', after 'vectros scripts list' shows you what is there.`
|
|
17674
18508
|
);
|
|
18509
|
+
if (blueprint.issuers?.some((i) => i.restrictedToDomain === void 0)) {
|
|
18510
|
+
legendLines.push(
|
|
18511
|
+
` Note: a newly registered issuer without restrictedToDomain starts pending verification on platform`,
|
|
18512
|
+
` 0.45.0 and later: it accepts no token until you prove you control the identity provider.`,
|
|
18513
|
+
` the apply prints the challenge and the command that finishes it.`
|
|
18514
|
+
);
|
|
18515
|
+
}
|
|
17675
18516
|
if (steps.some((s) => s.kind === "issuer")) {
|
|
17676
18517
|
legendLines.push(
|
|
17677
18518
|
` Exception: an issuer's trust-anchor fields (issuer, jwksUri, audience,`,
|
|
17678
18519
|
` contextId) are immutable once registered \u2014 only subClaim/emailClaim/`,
|
|
17679
|
-
` userinfoUri/selfSignupPolicies actually reconcile
|
|
17680
|
-
` authorable in a blueprint at all).`
|
|
18520
|
+
` userinfoUri/restrictedToDomain/selfSignupPolicies/capturedClaims actually reconcile`,
|
|
18521
|
+
` on a re-apply (status is not authorable in a blueprint at all).`
|
|
17681
18522
|
);
|
|
17682
18523
|
}
|
|
17683
18524
|
if (steps.some((s) => s.kind === "namespace")) {
|
|
@@ -17698,6 +18539,104 @@ function renderPlan(blueprint, steps) {
|
|
|
17698
18539
|
return [header, legend, "", ...lines].join("\n");
|
|
17699
18540
|
}
|
|
17700
18541
|
|
|
18542
|
+
// src/issuer-challenge.ts
|
|
18543
|
+
var PENDING_VERIFICATION = "pending_verification";
|
|
18544
|
+
function isPendingVerification(view) {
|
|
18545
|
+
return view.status === PENDING_VERIFICATION;
|
|
18546
|
+
}
|
|
18547
|
+
function challengeExpired(view, nowMs) {
|
|
18548
|
+
if (!view.verificationExpiresAt) return false;
|
|
18549
|
+
const at = Date.parse(view.verificationExpiresAt);
|
|
18550
|
+
return Number.isFinite(at) && at <= nowMs;
|
|
18551
|
+
}
|
|
18552
|
+
function humanizeRemaining(ms) {
|
|
18553
|
+
const minutes = Math.floor(ms / 6e4);
|
|
18554
|
+
if (minutes < 1) return "under a minute";
|
|
18555
|
+
const days = Math.floor(minutes / 1440);
|
|
18556
|
+
const hours = Math.floor(minutes % 1440 / 60);
|
|
18557
|
+
const mins = minutes % 60;
|
|
18558
|
+
if (days > 0) return `${days} day${days === 1 ? "" : "s"} ${hours} h`;
|
|
18559
|
+
if (hours > 0) return `${hours} h ${mins} min`;
|
|
18560
|
+
return `${mins} min`;
|
|
18561
|
+
}
|
|
18562
|
+
function targetFlags(ctx) {
|
|
18563
|
+
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)}` : "");
|
|
18564
|
+
}
|
|
18565
|
+
function verifyCommand(issuerId, ctx) {
|
|
18566
|
+
return `vectros issuers verify ${shellQuote(issuerId)} --context ${shellQuote(ctx.context)}${targetFlags(ctx)} --idp-token-file ./idp-token.txt`;
|
|
18567
|
+
}
|
|
18568
|
+
function deleteCommand(issuerId, ctx) {
|
|
18569
|
+
return `vectros issuers delete ${shellQuote(issuerId)} --context ${shellQuote(ctx.context)}${targetFlags(ctx)} --force`;
|
|
18570
|
+
}
|
|
18571
|
+
function getCommand(issuerId, ctx) {
|
|
18572
|
+
return `vectros issuers get ${shellQuote(issuerId)} --context ${shellQuote(ctx.context)}${targetFlags(ctx)}`;
|
|
18573
|
+
}
|
|
18574
|
+
var WITHHELD = "(withheld: unexpected characters)";
|
|
18575
|
+
var SAFE_TEXT = /^[A-Za-z0-9:/._~+-]{1,512}$/;
|
|
18576
|
+
function printable(value) {
|
|
18577
|
+
return SAFE_TEXT.test(value) ? value : void 0;
|
|
18578
|
+
}
|
|
18579
|
+
function displayChallengeValue(value) {
|
|
18580
|
+
return value === void 0 ? void 0 : printable(value) ?? WITHHELD;
|
|
18581
|
+
}
|
|
18582
|
+
function renderChallengeBlock(view, ctx, now) {
|
|
18583
|
+
const id = view.issuerId ?? "(unknown)";
|
|
18584
|
+
const rawClaim = view.verificationClaim;
|
|
18585
|
+
const rawNonce = view.verificationNonce;
|
|
18586
|
+
const claim = rawClaim === void 0 ? "(not reported)" : printable(rawClaim) ?? WITHHELD;
|
|
18587
|
+
const nonce = rawNonce === void 0 ? "(not reported)" : printable(rawNonce) ?? WITHHELD;
|
|
18588
|
+
const snippetClaim = rawClaim !== void 0 && printable(rawClaim) ? rawClaim : "the claim named above";
|
|
18589
|
+
const lines = [];
|
|
18590
|
+
lines.push(` \u26A0 issuer '${id}' is pending verification: it accepts no token until you prove you control it.`);
|
|
18591
|
+
lines.push("");
|
|
18592
|
+
lines.push(" 1. At your identity provider, add an ADMIN-CONTROLLED rule that puts this exact value in this");
|
|
18593
|
+
lines.push(" claim of the tokens it issues (never map it from an attribute your users can edit):");
|
|
18594
|
+
lines.push(` claim: ${claim}`);
|
|
18595
|
+
lines.push(` value: ${nonce}`);
|
|
18596
|
+
if (view.verificationExpiresAt) {
|
|
18597
|
+
const expires = printable(view.verificationExpiresAt) ?? WITHHELD;
|
|
18598
|
+
const at = Date.parse(view.verificationExpiresAt);
|
|
18599
|
+
const left = Number.isFinite(at) ? ` (in ${humanizeRemaining(at - now)})` : "";
|
|
18600
|
+
lines.push(` expires: ${expires}${left}`);
|
|
18601
|
+
}
|
|
18602
|
+
lines.push("");
|
|
18603
|
+
lines.push(" Auth0 a post-login Action; keep the value in a secret named VECTROS_ISSUER_NONCE:");
|
|
18604
|
+
lines.push(" exports.onExecutePostLogin = async (event, api) => {");
|
|
18605
|
+
lines.push(` const claim = '${snippetClaim}';`);
|
|
18606
|
+
lines.push(" api.idToken.setCustomClaim(claim, event.secrets.VECTROS_ISSUER_NONCE);");
|
|
18607
|
+
lines.push(" api.accessToken.setCustomClaim(claim, event.secrets.VECTROS_ISSUER_NONCE);");
|
|
18608
|
+
lines.push(" };");
|
|
18609
|
+
lines.push(" Okta a token inline hook, or a custom claim on your authorization server, that adds the");
|
|
18610
|
+
lines.push(" claim with this fixed value to the access token.");
|
|
18611
|
+
lines.push(" Entra a claims-mapping policy (or custom claims provider) on the application that adds");
|
|
18612
|
+
lines.push(" the claim with this fixed value.");
|
|
18613
|
+
lines.push(' Keycloak a "Hardcoded claim" protocol mapper on the client, added to the access token and');
|
|
18614
|
+
lines.push(' the ID token. Keycloak reads a dot in "Token Claim Name" as nesting, so write each');
|
|
18615
|
+
lines.push(" dot in the claim name as \\. to keep it a single claim.");
|
|
18616
|
+
lines.push(" Other any admin-controlled rule that adds this claim with this exact value.");
|
|
18617
|
+
lines.push("");
|
|
18618
|
+
lines.push(" 2. Sign in once through that provider and save the JWT it returns to a file. It must be a token");
|
|
18619
|
+
lines.push(" issued for the audience you registered (with Auth0, the ACCESS token, not the ID token). Run:");
|
|
18620
|
+
lines.push(` ${verifyCommand(id, ctx)}`);
|
|
18621
|
+
lines.push("");
|
|
18622
|
+
lines.push(" The provider's OpenID Connect discovery document must list exactly the jwksUri you registered.");
|
|
18623
|
+
lines.push(" The token is checked once and discarded; the rule can be removed after verification.");
|
|
18624
|
+
return `${lines.join("\n")}
|
|
18625
|
+
`;
|
|
18626
|
+
}
|
|
18627
|
+
function renderExpiredBlock(view, ctx, rerun) {
|
|
18628
|
+
const id = view.issuerId ?? "(unknown)";
|
|
18629
|
+
const expired = view.verificationExpiresAt === void 0 ? "(unknown)" : printable(view.verificationExpiresAt) ?? WITHHELD;
|
|
18630
|
+
const lines = [];
|
|
18631
|
+
lines.push(` \u2716 issuer '${id}' is STILL pending verification and its challenge EXPIRED at ${expired} (by this machine's clock).`);
|
|
18632
|
+
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):");
|
|
18633
|
+
lines.push(` ${deleteCommand(id, ctx)}`);
|
|
18634
|
+
lines.push(` ${rerun ?? "then register it again"}`);
|
|
18635
|
+
lines.push(" The new registration gets a new value, so update the rule at your identity provider to match.");
|
|
18636
|
+
return `${lines.join("\n")}
|
|
18637
|
+
`;
|
|
18638
|
+
}
|
|
18639
|
+
|
|
17701
18640
|
// src/config-merge.ts
|
|
17702
18641
|
var import_node_fs2 = require("fs");
|
|
17703
18642
|
var import_node_os3 = __toESM(require("os"));
|
|
@@ -17796,19 +18735,37 @@ var import_proper_lockfile = __toESM(require_proper_lockfile());
|
|
|
17796
18735
|
// src/os-keychain.ts
|
|
17797
18736
|
var KEYCHAIN_SERVICE = "vectros-cli";
|
|
17798
18737
|
var PROBE_ACCOUNT = "__vectros_probe__";
|
|
17799
|
-
async function loadKeychainBinding() {
|
|
18738
|
+
async function loadKeychainBinding(tryLoad = () => import("@napi-rs/keyring")) {
|
|
17800
18739
|
try {
|
|
17801
|
-
const mod = await
|
|
18740
|
+
const mod = await tryLoad();
|
|
17802
18741
|
if (typeof mod?.Entry === "function") return { binding: mod };
|
|
17803
|
-
return {
|
|
18742
|
+
return {
|
|
18743
|
+
reason: "the @napi-rs/keyring package loaded but exposes no Entry \u2014 expected on no platform",
|
|
18744
|
+
installAccident: true
|
|
18745
|
+
};
|
|
17804
18746
|
} catch (err) {
|
|
17805
|
-
return { reason: loadFailureReason(err) };
|
|
18747
|
+
return { reason: loadFailureReason(err), installAccident: isInstallAccident(err) };
|
|
17806
18748
|
}
|
|
17807
18749
|
}
|
|
17808
|
-
|
|
18750
|
+
var PREBUILT_PLATFORMS = {
|
|
18751
|
+
darwin: ["arm64", "x64"],
|
|
18752
|
+
freebsd: ["x64"],
|
|
18753
|
+
linux: ["arm", "arm64", "riscv64", "x64"],
|
|
18754
|
+
win32: ["arm64", "ia32", "x64"]
|
|
18755
|
+
};
|
|
18756
|
+
function prebuiltShouldExist(platform = process.platform, arch = process.arch) {
|
|
18757
|
+
return PREBUILT_PLATFORMS[platform]?.includes(arch) ?? false;
|
|
18758
|
+
}
|
|
18759
|
+
function isInstallAccident(_err, platform = process.platform, arch = process.arch) {
|
|
18760
|
+
return prebuiltShouldExist(platform, arch);
|
|
18761
|
+
}
|
|
18762
|
+
function loadFailureReason(err, platform = process.platform, arch = process.arch) {
|
|
18763
|
+
if (!prebuiltShouldExist(platform, arch)) {
|
|
18764
|
+
return "the optional @napi-rs/keyring package is not installed for this platform";
|
|
18765
|
+
}
|
|
17809
18766
|
const code = err.code;
|
|
17810
18767
|
if (code === "ERR_MODULE_NOT_FOUND" || code === "MODULE_NOT_FOUND") {
|
|
17811
|
-
return
|
|
18768
|
+
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`;
|
|
17812
18769
|
}
|
|
17813
18770
|
return `the optional @napi-rs/keyring package is installed but would not load (${describeErr(err)})`;
|
|
17814
18771
|
}
|
|
@@ -17932,8 +18889,8 @@ var OsKeychainSecretStore = class {
|
|
|
17932
18889
|
* deleteCredential() -> false
|
|
17933
18890
|
* => deleteSecret() -> false. The gate holds; `remove` refuses.
|
|
17934
18891
|
*
|
|
17935
|
-
* Reproduce
|
|
17936
|
-
*
|
|
18892
|
+
* Reproduce by locking the credential store's collection while the CLI still holds a
|
|
18893
|
+
* cached handle to it. CI cannot pin this half — it unlocks the collection so the rest of
|
|
17937
18894
|
* the suite can use it — so the evidence lives here rather than in a test.
|
|
17938
18895
|
*
|
|
17939
18896
|
* The WINDOWS half of the same contract was assumed for seven review rounds and
|
|
@@ -18375,7 +19332,11 @@ var KEYRING_BACKENDS = ["auto", "file"];
|
|
|
18375
19332
|
async function resolveSecretStore(deps = {}) {
|
|
18376
19333
|
const platform = deps.platform ?? process.platform;
|
|
18377
19334
|
const env = deps.env ?? process.env;
|
|
18378
|
-
const file = (reason2) => ({
|
|
19335
|
+
const file = (reason2, installAccident2) => ({
|
|
19336
|
+
kind: "crypto-file",
|
|
19337
|
+
reason: reason2,
|
|
19338
|
+
...installAccident2 ? { installAccident: installAccident2 } : {}
|
|
19339
|
+
});
|
|
18379
19340
|
const requested = env[KEYRING_BACKEND_ENV]?.trim().toLowerCase();
|
|
18380
19341
|
if (requested !== void 0 && requested !== "") {
|
|
18381
19342
|
if (!KEYRING_BACKENDS.includes(requested)) {
|
|
@@ -18392,9 +19353,9 @@ async function resolveSecretStore(deps = {}) {
|
|
|
18392
19353
|
return file("the macOS Keychain can prompt on unattended reads, which would break MCP startup");
|
|
18393
19354
|
}
|
|
18394
19355
|
const loadBinding0 = deps.loadBinding ?? loadKeychainBinding;
|
|
18395
|
-
const { binding, reason } = await loadBinding0();
|
|
19356
|
+
const { binding, reason, installAccident } = await loadBinding0();
|
|
18396
19357
|
if (!binding) {
|
|
18397
|
-
return file(reason ?? "the optional @napi-rs/keyring package is not usable on this platform");
|
|
19358
|
+
return file(reason ?? "the optional @napi-rs/keyring package is not usable on this platform", installAccident);
|
|
18398
19359
|
}
|
|
18399
19360
|
const probe = deps.probe ?? probeKeychain;
|
|
18400
19361
|
if (!probe(binding)) {
|
|
@@ -18423,6 +19384,9 @@ var Keyring = class _Keyring {
|
|
|
18423
19384
|
stores;
|
|
18424
19385
|
/** Where NEW secrets go, and why — surfaced by `keyring doctor`. */
|
|
18425
19386
|
backend;
|
|
19387
|
+
warn;
|
|
19388
|
+
/** Fires at most once per instance — see {@link storeSecretSomewhere}. */
|
|
19389
|
+
installAccidentWarned = false;
|
|
18426
19390
|
/**
|
|
18427
19391
|
* Construct against EXPLICIT stores. The file store defaults to a
|
|
18428
19392
|
* {@link CryptoFileSecretStore} over `dir` — no native binding, nothing outside
|
|
@@ -18437,6 +19401,8 @@ var Keyring = class _Keyring {
|
|
|
18437
19401
|
"os-keychain": deps.keychainStore
|
|
18438
19402
|
};
|
|
18439
19403
|
this.backend = deps.selection ?? { kind: "crypto-file" };
|
|
19404
|
+
this.warn = deps.warn ?? ((msg) => process.stderr.write(`${msg}
|
|
19405
|
+
`));
|
|
18440
19406
|
}
|
|
18441
19407
|
/**
|
|
18442
19408
|
* The keyring a command should use: the stores resolved for THIS machine per
|
|
@@ -18447,7 +19413,7 @@ var Keyring = class _Keyring {
|
|
|
18447
19413
|
static async open(deps = {}) {
|
|
18448
19414
|
if (deps.fileStore ?? deps.keychainStore ?? deps.selection) return new _Keyring(deps);
|
|
18449
19415
|
const { keychain, ...selection } = await resolveSecretStore(deps);
|
|
18450
|
-
return new _Keyring({ dir: deps.dir, keychainStore: keychain, selection });
|
|
19416
|
+
return new _Keyring({ dir: deps.dir, keychainStore: keychain, selection, warn: deps.warn });
|
|
18451
19417
|
}
|
|
18452
19418
|
/**
|
|
18453
19419
|
* The store holding this entry's secret, or undefined when the stamp does not say
|
|
@@ -18564,6 +19530,28 @@ The key itself WAS created server-side and is live \u2014 its id is ${entry.keyI
|
|
|
18564
19530
|
}
|
|
18565
19531
|
return { landedIn, stranded: swept ? void 0 : prior, vetoedBy };
|
|
18566
19532
|
}
|
|
19533
|
+
/**
|
|
19534
|
+
* Warn, at most once per instance, when this machine is about to write a secret to
|
|
19535
|
+
* the file store because of an INSTALL ACCIDENT — not because of an ordinary,
|
|
19536
|
+
* nothing-to-fix absence (unsupported platform, `VECTROS_KEYRING_BACKEND=file`,
|
|
19537
|
+
* macOS's own design choice, or "loaded fine but no OS credential store answers").
|
|
19538
|
+
*
|
|
19539
|
+
* Before this, the CLI degraded to the file store with ZERO signal at write
|
|
19540
|
+
* time, indistinguishable from every OTHER reason a machine ends up there — a
|
|
19541
|
+
* real `@napi-rs/keyring` install failure looked identical to a platform that
|
|
19542
|
+
* was never going to have one, unless you separately ran `keyring doctor`.
|
|
19543
|
+
* This is the signal that was missing, at the moment it matters (a real
|
|
19544
|
+
* secret is about to land somewhere weaker than intended), naming the one
|
|
19545
|
+
* remedy that actually applies to THIS case and no other.
|
|
19546
|
+
*/
|
|
19547
|
+
warnOnceIfInstallAccident() {
|
|
19548
|
+
if (this.installAccidentWarned) return;
|
|
19549
|
+
if (this.backend.kind !== "crypto-file" || !this.backend.installAccident) return;
|
|
19550
|
+
this.installAccidentWarned = true;
|
|
19551
|
+
this.warn(
|
|
19552
|
+
`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.`
|
|
19553
|
+
);
|
|
19554
|
+
}
|
|
18567
19555
|
/**
|
|
18568
19556
|
* Persist a secret, preferring this machine's chosen store but NEVER losing the
|
|
18569
19557
|
* secret if that store refuses. Returns where it actually landed.
|
|
@@ -18576,8 +19564,15 @@ The key itself WAS created server-side and is live \u2014 its id is ${entry.keyI
|
|
|
18576
19564
|
* credential and leave it orphaned server-side with no local record of its
|
|
18577
19565
|
* `keyId`. Degrading to the encrypted file is strictly better than that, and the
|
|
18578
19566
|
* entry's stamp keeps the result honest rather than silently wrong.
|
|
19567
|
+
*
|
|
19568
|
+
* `warnIfDegraded` defaults to true and exists ONLY for {@link probeSecretStore}
|
|
19569
|
+
* to suppress with `false`: the once-per-instance install-accident warning is a
|
|
19570
|
+
* scarce resource (fires at most once per `Keyring`), and the probe's marker
|
|
19571
|
+
* value is never the credential the warning describes. Spending it on the probe
|
|
19572
|
+
* left the REAL write — the very next call, same instance — silent.
|
|
18579
19573
|
*/
|
|
18580
|
-
async storeSecretSomewhere(ref, secret) {
|
|
19574
|
+
async storeSecretSomewhere(ref, secret, { warnIfDegraded = true } = {}) {
|
|
19575
|
+
if (warnIfDegraded) this.warnOnceIfInstallAccident();
|
|
18581
19576
|
const preferred = this.preferred;
|
|
18582
19577
|
if (preferred) {
|
|
18583
19578
|
try {
|
|
@@ -18629,7 +19624,9 @@ The key itself WAS created server-side and is live \u2014 its id is ${entry.keyI
|
|
|
18629
19624
|
async probeSecretStore() {
|
|
18630
19625
|
const fileStore = this.stores["crypto-file"];
|
|
18631
19626
|
const dirWasPresent = fileStore instanceof CryptoFileSecretStore ? await fileStore.secretsDirPresent() : true;
|
|
18632
|
-
const landedIn = await this.storeSecretSomewhere(PROBE_SECRET_REF, PROBE_SECRET_VALUE
|
|
19627
|
+
const landedIn = await this.storeSecretSomewhere(PROBE_SECRET_REF, PROBE_SECRET_VALUE, {
|
|
19628
|
+
warnIfDegraded: false
|
|
19629
|
+
});
|
|
18633
19630
|
await this.stores[landedIn]?.deleteSecret(PROBE_SECRET_REF).catch(() => {
|
|
18634
19631
|
});
|
|
18635
19632
|
if (!dirWasPresent && fileStore instanceof CryptoFileSecretStore) {
|
|
@@ -18930,7 +19927,12 @@ The key itself WAS created server-side and is live \u2014 its id is ${entry.keyI
|
|
|
18930
19927
|
async readSecret(entry) {
|
|
18931
19928
|
const from = storeKindOf(entry);
|
|
18932
19929
|
if (from === void 0) return void 0;
|
|
18933
|
-
const
|
|
19930
|
+
const store = this.stores[from];
|
|
19931
|
+
if (!store) {
|
|
19932
|
+
this.warnOnceIfInstallAccident();
|
|
19933
|
+
return void 0;
|
|
19934
|
+
}
|
|
19935
|
+
const secret = await store.getSecret(entry.secretRef);
|
|
18934
19936
|
if (secret === void 0) return void 0;
|
|
18935
19937
|
if (from !== this.backend.kind) await this.migrateSecret(entry, from, secret);
|
|
18936
19938
|
return secret;
|
|
@@ -19651,7 +20653,7 @@ function nowIso() {
|
|
|
19651
20653
|
}
|
|
19652
20654
|
function warnReplacingAlias(existing, alias, revokedKeyId) {
|
|
19653
20655
|
if (!existing) return;
|
|
19654
|
-
const stillLive = existing.keyId !== void 0 && existing.keyId
|
|
20656
|
+
const stillLive = existing.keyId !== void 0 && !(revokedKeyId ?? []).includes(existing.keyId);
|
|
19655
20657
|
process.stdout.write(
|
|
19656
20658
|
`
|
|
19657
20659
|
\u26A0 Replacing keyring entry '${alias}' (was ${existing.principalId || "<unknown principal>"} in '${existing.context}' @ ${existing.env}, key ${existing.keyId}). Pass --alias to keep both.
|
|
@@ -19662,13 +20664,14 @@ function warnReplacingAlias(existing, alias, revokedKeyId) {
|
|
|
19662
20664
|
}
|
|
19663
20665
|
async function warnOtherAliasesNaming(keyring, revokedKeyId, exceptAlias) {
|
|
19664
20666
|
const affected = (await keyring.list()).filter(
|
|
19665
|
-
(e) => e.alias !== exceptAlias && e.keyId
|
|
20667
|
+
(e) => e.alias !== exceptAlias && e.keyId !== void 0 && revokedKeyId.includes(e.keyId)
|
|
19666
20668
|
);
|
|
19667
20669
|
if (affected.length === 0) return;
|
|
19668
20670
|
const names = affected.map((e) => `'${e.alias}'`).join(", ");
|
|
20671
|
+
const revokedDisplay = revokedKeyId.length === 1 ? `Key ${revokedKeyId[0]}` : `Keys ${revokedKeyId.join(", ")}`;
|
|
19669
20672
|
process.stderr.write(
|
|
19670
20673
|
`
|
|
19671
|
-
\u26A0
|
|
20674
|
+
\u26A0 ${revokedDisplay} ${revokedKeyId.length === 1 ? "was" : "were"} just revoked, but ${affected.length === 1 ? "entry" : "entries"} ${names} still
|
|
19672
20675
|
${affected.length === 1 ? "names" : "name"} it. That secret is now dead \u2014 it keeps reading fine locally and
|
|
19673
20676
|
will simply stop authenticating. Re-point ${affected.length === 1 ? "it" : "them"} with
|
|
19674
20677
|
\`vectros key rotate --to-keyring --alias <name>\`, or drop with \`vectros keyring remove <name>\`.
|
|
@@ -19698,7 +20701,7 @@ async function storeMintedKey(keyring, binding, secret, opts = {}) {
|
|
|
19698
20701
|
was kept too` : `the ${noun} did not
|
|
19699
20702
|
confirm the delete`;
|
|
19700
20703
|
const tidyUp = vetoNoun !== void 0 ? `Clear the copies by hand from your ${vetoNoun} and your ${noun}` : `Clear it by hand from your ${noun}`;
|
|
19701
|
-
if (opts.revokedKeyId
|
|
20704
|
+
if (opts.revokedKeyId?.includes(stranded.keyId)) {
|
|
19702
20705
|
process.stderr.write(
|
|
19703
20706
|
`
|
|
19704
20707
|
\u26A0 The previous key for '${entry.alias}' (${stranded.keyId}) was revoked and replaced, but
|
|
@@ -19769,6 +20772,8 @@ function parseBootstrapArgs(argv) {
|
|
|
19769
20772
|
blankSlate: false,
|
|
19770
20773
|
noSeed: false,
|
|
19771
20774
|
allowTenantWideNamespaces: false,
|
|
20775
|
+
allowScriptAdoption: false,
|
|
20776
|
+
requireVerifiedIssuers: false,
|
|
19772
20777
|
client: "desktop",
|
|
19773
20778
|
tenant: "live",
|
|
19774
20779
|
help: false
|
|
@@ -19833,6 +20838,12 @@ function parseBootstrapArgs(argv) {
|
|
|
19833
20838
|
case "--allow-tenant-wide-namespaces":
|
|
19834
20839
|
args.allowTenantWideNamespaces = true;
|
|
19835
20840
|
break;
|
|
20841
|
+
case "--allow-script-adoption":
|
|
20842
|
+
args.allowScriptAdoption = true;
|
|
20843
|
+
break;
|
|
20844
|
+
case "--require-verified-issuers":
|
|
20845
|
+
args.requireVerifiedIssuers = true;
|
|
20846
|
+
break;
|
|
19836
20847
|
case "--tenant": {
|
|
19837
20848
|
const v = argv[++i];
|
|
19838
20849
|
if (v !== "test" && v !== "live") {
|
|
@@ -19861,7 +20872,7 @@ function parseBootstrapArgs(argv) {
|
|
|
19861
20872
|
return args;
|
|
19862
20873
|
}
|
|
19863
20874
|
function shouldOfferSeedOptOut(args, blueprint, isTty) {
|
|
19864
|
-
return !args.noSeed && (blueprint.seed?.length ?? 0) > 0 && !args.yes &&
|
|
20875
|
+
return !args.noSeed && (blueprint.seed?.length ?? 0) > 0 && !args.yes && isTty;
|
|
19865
20876
|
}
|
|
19866
20877
|
function seedDeclined(answer) {
|
|
19867
20878
|
const a = answer.trim().toLowerCase();
|
|
@@ -19905,6 +20916,31 @@ function seedIndexWarningBlock(failures) {
|
|
|
19905
20916
|
` + lines.join("") + ` They're stored and retrievable by id, but won't appear in search until the issue above is resolved.
|
|
19906
20917
|
`;
|
|
19907
20918
|
}
|
|
20919
|
+
var REQUIRE_VERIFIED_EXIT = 4;
|
|
20920
|
+
function issuerTally(items) {
|
|
20921
|
+
const base = reconcileTally(items, "issuer");
|
|
20922
|
+
if (!base) return null;
|
|
20923
|
+
const pending = items.filter(isPendingVerification).length;
|
|
20924
|
+
return pending > 0 ? base.replace(/\)$/, `; ${pending} pending verification)`) : base;
|
|
20925
|
+
}
|
|
20926
|
+
function pendingIssuerBlock(actions, ctx, now, rerun = "then re-run this `vectros bootstrap` command") {
|
|
20927
|
+
return actions.filter(isPendingVerification).map((a) => challengeExpired(a, now) ? renderExpiredBlock(a, ctx, rerun) : renderChallengeBlock(a, ctx, now)).join("\n");
|
|
20928
|
+
}
|
|
20929
|
+
function finishIssuers(actions, ctx, requireVerified, now, rerun) {
|
|
20930
|
+
const block = pendingIssuerBlock(actions, ctx, now, rerun);
|
|
20931
|
+
if (block) process.stderr.write(`
|
|
20932
|
+
${block}`);
|
|
20933
|
+
const pending = actions.filter(isPendingVerification).length;
|
|
20934
|
+
if (requireVerified && pending > 0) {
|
|
20935
|
+
process.stderr.write(
|
|
20936
|
+
`
|
|
20937
|
+
\u2716 --require-verified-issuers: ${pending} issuer${pending === 1 ? " is" : "s are"} still ${PENDING_VERIFICATION.replace("_", " ")} (exit ${REQUIRE_VERIFIED_EXIT}). The apply itself completed.
|
|
20938
|
+
`
|
|
20939
|
+
);
|
|
20940
|
+
return REQUIRE_VERIFIED_EXIT;
|
|
20941
|
+
}
|
|
20942
|
+
return 0;
|
|
20943
|
+
}
|
|
19908
20944
|
function reconcileTally(items, noun) {
|
|
19909
20945
|
if (items.length === 0) return null;
|
|
19910
20946
|
const created = items.filter((i) => i.action === "created").length;
|
|
@@ -19927,22 +20963,26 @@ function scriptTally(items) {
|
|
|
19927
20963
|
if (items.length === 0) return null;
|
|
19928
20964
|
const created = items.filter((i) => i.action === "created");
|
|
19929
20965
|
const superseded = items.filter((i) => i.action === "superseded");
|
|
19930
|
-
const
|
|
20966
|
+
const adopted = items.filter((i) => i.action === "adopted");
|
|
20967
|
+
const unchanged = items.length - created.length - superseded.length - adopted.length;
|
|
19931
20968
|
const seg = [];
|
|
19932
20969
|
if (created.length) seg.push(`${created.length} first version${created.length === 1 ? "" : "s"} pushed`);
|
|
19933
20970
|
if (superseded.length) {
|
|
19934
20971
|
seg.push(`${superseded.map((i) => `'${i.name}'`).join(", ")} superseded (a new version now current)`);
|
|
19935
20972
|
}
|
|
20973
|
+
if (adopted.length) {
|
|
20974
|
+
seg.push(`${adopted.map((i) => `'${i.name}'`).join(", ")} adopted (previously unmarked, now managed by this blueprint)`);
|
|
20975
|
+
}
|
|
19936
20976
|
if (unchanged) seg.push(`${unchanged} already current \u2014 nothing written`);
|
|
19937
20977
|
return `${items.length} script${items.length === 1 ? "" : "s"} (${seg.join("; ")})`;
|
|
19938
20978
|
}
|
|
19939
|
-
function applyOutcomeSummary(result) {
|
|
20979
|
+
function applyOutcomeSummary(result, opts = {}) {
|
|
19940
20980
|
const parts = [
|
|
19941
20981
|
reconcileTally(result.schemaActions, "schema"),
|
|
19942
20982
|
reconcileTally(result.roleActions, "role"),
|
|
19943
20983
|
// The platform's issuer partial-update endpoint makes an issuer's
|
|
19944
20984
|
// 'reconciled' mean the same "updated to match" as every other kind here.
|
|
19945
|
-
|
|
20985
|
+
issuerTally(result.issuerActions),
|
|
19946
20986
|
namespaceTally(result.namespaceActions),
|
|
19947
20987
|
// Scripts — reported BEFORE triggers, mirroring the order they are provisioned in, and with
|
|
19948
20988
|
// their own wording because "reconciled" would be a lie: a script version is immutable.
|
|
@@ -19955,7 +20995,7 @@ function applyOutcomeSummary(result) {
|
|
|
19955
20995
|
`profile ${result.profileAction}`
|
|
19956
20996
|
].filter((p) => p !== null);
|
|
19957
20997
|
const detail = parts.length ? ` \u2014 ${parts.join("; ")}` : "";
|
|
19958
|
-
const principalNote = result.userAction === "reconciled" ? ` \u26A0 service principal already existed in this tenant before this run (not newly created)
|
|
20998
|
+
const principalNote = (opts.principalNote ?? true) && result.userAction === "reconciled" ? ` \u26A0 service principal already existed in this tenant before this run (not newly created)
|
|
19959
20999
|
` : "";
|
|
19960
21000
|
const pruneNotes = result.triggerPrunes.map(
|
|
19961
21001
|
(p) => ` \u26A0 trigger '${p.name}' removed from the blueprint \u2014 de-provisioned (rule + access profile deleted)
|
|
@@ -19966,13 +21006,31 @@ function applyOutcomeSummary(result) {
|
|
|
19966
21006
|
`)
|
|
19967
21007
|
).join("");
|
|
19968
21008
|
const pruneSkippedNotes = result.triggerPruneSkipped.map(
|
|
19969
|
-
(s) => s.name ? ` \
|
|
21009
|
+
(s) => s.failed === false ? s.name ? ` \u2139 trigger '${s.name}' is not declared here and was left in place \u2014 ${s.reason}.
|
|
21010
|
+
` : ` \u2139 a trigger rule was left in place \u2014 ${s.reason}.
|
|
21011
|
+
` : s.name ? ` \u26A0 trigger '${s.name}' is no longer declared but could NOT be removed \u2014 ${s.reason}. It is still live.
|
|
19970
21012
|
` : ` \u26A0 triggers could not be reconciled this run \u2014 ${s.reason}. A trigger removed from this blueprint may still be live.
|
|
19971
21013
|
`
|
|
19972
21014
|
).join("");
|
|
19973
21015
|
return ` \u2714 context '${result.contextId}' ${result.contextAction}${detail}
|
|
19974
21016
|
${principalNote}${pruneNotes}${pruneSkippedNotes}`;
|
|
19975
21017
|
}
|
|
21018
|
+
function gateSummary(blueprint) {
|
|
21019
|
+
const roleNote = blueprint.accessProfile.roleIds ? ` via role(s) [${blueprint.accessProfile.roleIds.join(", ")}]` : "";
|
|
21020
|
+
const acceptedActions = effectiveAccessProfileActions(blueprint);
|
|
21021
|
+
const allDataPlane = !acceptedActions.some(
|
|
21022
|
+
(a) => KNOWN_CONTROL_PLANE_RESOURCES.includes(a.split(":", 1)[0])
|
|
21023
|
+
);
|
|
21024
|
+
let out = `
|
|
21025
|
+
Blueprint: ${blueprint.name}
|
|
21026
|
+
\u2714 Scope gate: [${acceptedActions.join(", ")}]${roleNote}` + (allDataPlane ? " \u2014 all data-plane \u2713\n" : " \u2014 includes a narrow, allowlisted control-plane exception (see above)\n");
|
|
21027
|
+
if (blueprint.accessProfile.capabilities?.length) {
|
|
21028
|
+
const capGate = evaluateCapabilityGate(blueprint.accessProfile.capabilities);
|
|
21029
|
+
out += ` \u2714 Capability gate: [${capGate.accepted.join(", ")}] \u2014 granted \u2713
|
|
21030
|
+
`;
|
|
21031
|
+
}
|
|
21032
|
+
return out;
|
|
21033
|
+
}
|
|
19976
21034
|
var BOOTSTRAP_HELP = `vectros bootstrap \u2014 provision a least-privilege MCP credential.
|
|
19977
21035
|
|
|
19978
21036
|
Usage:
|
|
@@ -19980,69 +21038,28 @@ Usage:
|
|
|
19980
21038
|
[--set name=value ...] [--values <file>]
|
|
19981
21039
|
[--tenant test|live | --test] [--client desktop|code]
|
|
19982
21040
|
[--yes] [--no-merge] [--rotate] [--confirm-existing-principal]
|
|
21041
|
+
[--require-verified-issuers]
|
|
19983
21042
|
[--token <t> | env VECTROS_BOOTSTRAP_TOKEN]
|
|
19984
21043
|
[--base-url <url>] [--use-scoped-token]
|
|
19985
21044
|
|
|
21045
|
+
To re-apply a blueprint into an existing context WITHOUT minting a key (CI/GitOps), use
|
|
21046
|
+
'vectros blueprint apply' instead.
|
|
21047
|
+
|
|
19986
21048
|
Options:
|
|
19987
|
-
|
|
19988
|
-
|
|
19989
|
-
|
|
19990
|
-
--set name=value Set an install-time input value (repeatable) for a file blueprint's
|
|
19991
|
-
'inputs:' block. Beats --values + the declared default. (Same flag as
|
|
19992
|
-
'vectros blueprint validate/plan'.)
|
|
19993
|
-
--values <file> A YAML/JSON map of input values. (File blueprints only.)
|
|
19994
|
-
--blank Credential only (read-only data-plane profile, no schemas).
|
|
19995
|
-
--no-seed Provision the blueprint's schemas, profile and key but SKIP its
|
|
19996
|
-
sample seed records \u2014 the right choice for a production context
|
|
19997
|
-
(a blueprint's seeds are illustrative, not real data). Unlike
|
|
19998
|
-
--blank, the schemas are still created.
|
|
19999
|
-
--allow-tenant-wide-namespaces
|
|
20000
|
-
Required to apply a blueprint that declares a namespace with
|
|
20001
|
-
tenantWide: true \u2014 visible to every context in the account, not
|
|
20002
|
-
just this one. Without it, bootstrap refuses before provisioning
|
|
20003
|
-
anything, naming the namespace(s). Only an account OWNER's
|
|
20004
|
-
credential can actually complete the registration either way \u2014
|
|
20005
|
-
this flag doesn't change who's allowed, only whether this run
|
|
20006
|
-
tries. Never needed for a blueprint with no tenantWide namespace.
|
|
20007
|
-
--tenant test|live Which tenant the minted key scopes to (default: live).
|
|
20008
|
-
'test' provisions into your isolated test tenant \u2014 a
|
|
20009
|
-
dry-run before pointing real data at the same blueprint.
|
|
20010
|
-
The tenant rides the key (ssk_test_* vs ssk_live_*), so
|
|
20011
|
-
the MCP/client config is otherwise identical.
|
|
20012
|
-
--test Shorthand for --tenant test.
|
|
20013
|
-
--activate Force the freshly-stored key to become your active identity, even
|
|
20014
|
-
if that would switch away from a currently-active LIVE credential
|
|
20015
|
-
(the one case --tenant test does NOT do this automatically).
|
|
20016
|
-
--no-activate Store the key without activating it, regardless of tenant.
|
|
20017
|
-
--base-url <url> Override the API base URL (beats VECTROS_API_BASE_URL).
|
|
20018
|
-
--client <desktop|code> MCP host to configure (default: desktop). 'desktop' merges
|
|
20019
|
-
claude_desktop_config.json (also works for Cursor/Cline);
|
|
20020
|
-
'code' targets Claude Code's project .mcp.json and prints
|
|
20021
|
-
the equivalent 'claude mcp add' command.
|
|
20022
|
-
--token <t> Cognito bridge token; or set VECTROS_BOOTSTRAP_TOKEN.
|
|
20023
|
-
Falls back to your stored \`vectros login\` session
|
|
20024
|
-
(same env) when neither is set \u2014 login once, then
|
|
20025
|
-
bootstrap unattended.
|
|
20026
|
-
--yes, -y Skip confirmations (agent / CI path).
|
|
20027
|
-
--no-merge Still mints/reconciles \u2014 this only changes where the result goes:
|
|
20028
|
-
print the config snippet instead of merging it into your client's
|
|
20029
|
-
config file. Not a dry run; see 'blueprint plan' for that.
|
|
20030
|
-
--rotate Revoke + re-mint THIS machine's key.
|
|
20031
|
-
--confirm-existing-principal
|
|
20032
|
-
Required (with --yes, or with no TTY) to mint a NEW key bound to a
|
|
20033
|
-
service principal that already existed in your tenant before this run
|
|
20034
|
-
\u2014 e.g. a deliberate second machine for an integration you already own.
|
|
20035
|
-
Refused by default so an unexpected collision halts a CI run loudly
|
|
20036
|
-
instead of silently minting a credential bound to the wrong identity.
|
|
20037
|
-
Never needed for --rotate or an ordinary same-machine re-apply.
|
|
20038
|
-
--use-scoped-token Legacy fallback.
|
|
20039
|
-
NOTE: it mints a token WITHOUT the provisioning
|
|
20040
|
-
capability, so it cannot create a blueprint's
|
|
20041
|
-
app-context \u2014 use only with the 'default' context
|
|
20042
|
-
(the dedicated endpoint is required otherwise).
|
|
20043
|
-
--help, -h Show this help.
|
|
21049
|
+
${renderOptionHelp("bootstrap", BLUEPRINT_NAMES)}
|
|
21050
|
+
|
|
21051
|
+
No --blueprint/--blank in a TTY \u2192 interactive menu.
|
|
20044
21052
|
|
|
20045
|
-
|
|
21053
|
+
Exit codes:
|
|
21054
|
+
0 applied; also --help, and answering no at "Apply this plan?" ("Aborted - nothing minted")
|
|
21055
|
+
1 failed: a bad flag, an unknown or malformed blueprint, a missing or invalid --set input, a
|
|
21056
|
+
declared issuer scope the platform did not apply, or any error while applying (the message says
|
|
21057
|
+
what did and did not run)
|
|
21058
|
+
2 refused by a gate: the scope gate or the capability gate rejected the blueprint, an identity it
|
|
21059
|
+
references could not be resolved, or it declares a tenant-wide namespace without the opt-in (the
|
|
21060
|
+
message says what, if anything, was already provisioned)
|
|
21061
|
+
3 needs a confirmation it did not get (an existing service principal, a key that cannot be recovered)
|
|
21062
|
+
4 applied in full, but --require-verified-issuers was given and an issuer is pending verification`;
|
|
20046
21063
|
async function resolveBlueprint(args, prompt, isTty) {
|
|
20047
21064
|
if (args.blankSlate) {
|
|
20048
21065
|
if (args.set.length > 0 || args.values) {
|
|
@@ -20144,7 +21161,7 @@ async function runBootstrapCli(argv, deps = {}) {
|
|
|
20144
21161
|
try {
|
|
20145
21162
|
args = parseBootstrapArgs(argv);
|
|
20146
21163
|
} catch (err) {
|
|
20147
|
-
process.stderr.write(`${err instanceof Error ? err.message : String(err)}
|
|
21164
|
+
process.stderr.write(`${redactCredentials(err instanceof Error ? err.message : String(err))}
|
|
20148
21165
|
|
|
20149
21166
|
${BOOTSTRAP_HELP}
|
|
20150
21167
|
`);
|
|
@@ -20186,21 +21203,7 @@ ${BOOTSTRAP_HELP}
|
|
|
20186
21203
|
}
|
|
20187
21204
|
throw err;
|
|
20188
21205
|
}
|
|
20189
|
-
|
|
20190
|
-
const acceptedActions = effectiveAccessProfileActions(blueprint);
|
|
20191
|
-
const allDataPlane = !acceptedActions.some(
|
|
20192
|
-
(a) => KNOWN_CONTROL_PLANE_RESOURCES.includes(a.split(":", 1)[0])
|
|
20193
|
-
);
|
|
20194
|
-
process.stdout.write(
|
|
20195
|
-
`
|
|
20196
|
-
Blueprint: ${blueprint.name}
|
|
20197
|
-
\u2714 Scope gate: [${acceptedActions.join(", ")}]${roleNote}` + (allDataPlane ? " \u2014 all data-plane \u2713\n" : " \u2014 includes a narrow, allowlisted control-plane exception (see above)\n")
|
|
20198
|
-
);
|
|
20199
|
-
if (blueprint.accessProfile.capabilities?.length) {
|
|
20200
|
-
const capGate = evaluateCapabilityGate(blueprint.accessProfile.capabilities);
|
|
20201
|
-
process.stdout.write(` \u2714 Capability gate: [${capGate.accepted.join(", ")}] \u2014 granted \u2713
|
|
20202
|
-
`);
|
|
20203
|
-
}
|
|
21206
|
+
process.stdout.write(gateSummary(blueprint));
|
|
20204
21207
|
let noSeed = args.noSeed;
|
|
20205
21208
|
let seedDeclinedInteractively = false;
|
|
20206
21209
|
if (shouldOfferSeedOptOut(args, blueprint, isTty)) {
|
|
@@ -20226,7 +21229,7 @@ ${renderPlan(blueprint, planSteps)}
|
|
|
20226
21229
|
`);
|
|
20227
21230
|
}
|
|
20228
21231
|
}
|
|
20229
|
-
if (!args.yes &&
|
|
21232
|
+
if (!args.yes && isTty) {
|
|
20230
21233
|
const ans = (await promptCtx.prompt("\n Apply this plan? [y/N]: ")).trim().toLowerCase();
|
|
20231
21234
|
if (ans !== "y" && ans !== "yes") {
|
|
20232
21235
|
process.stdout.write(" Aborted \u2014 nothing minted.\n");
|
|
@@ -20360,6 +21363,7 @@ ${renderPlan(blueprint, planSteps)}
|
|
|
20360
21363
|
machineId,
|
|
20361
21364
|
rotate: args.rotate,
|
|
20362
21365
|
skipSeed: noSeed,
|
|
21366
|
+
allowScriptAdoption: args.allowScriptAdoption,
|
|
20363
21367
|
readExistingKey,
|
|
20364
21368
|
persistMintedKey,
|
|
20365
21369
|
provisioned,
|
|
@@ -20372,6 +21376,12 @@ ${renderPlan(blueprint, planSteps)}
|
|
|
20372
21376
|
}),
|
|
20373
21377
|
log
|
|
20374
21378
|
});
|
|
21379
|
+
const finishApply = () => finishIssuers(
|
|
21380
|
+
result.issuerActions,
|
|
21381
|
+
{ context: blueprint.contextId, tenant: args.tenant, env: args.env, baseUrl: args.baseUrl },
|
|
21382
|
+
args.requireVerifiedIssuers,
|
|
21383
|
+
(deps.now ?? Date.now)()
|
|
21384
|
+
);
|
|
20375
21385
|
process.stdout.write(
|
|
20376
21386
|
applyOutcomeSummary(result) + ` \u2714 ${args.tenant} key (${result.keyAction}) \u2192 ${result.keyName}
|
|
20377
21387
|
` + seedSummaryLine({
|
|
@@ -20407,7 +21417,7 @@ ${renderPlan(blueprint, planSteps)}
|
|
|
20407
21417
|
|
|
20408
21418
|
${snippet}
|
|
20409
21419
|
${codeAddHint}`);
|
|
20410
|
-
return
|
|
21420
|
+
return finishApply();
|
|
20411
21421
|
}
|
|
20412
21422
|
let doMerge = args.yes;
|
|
20413
21423
|
if (!doMerge && isTty) {
|
|
@@ -20421,7 +21431,7 @@ ${codeAddHint}`);
|
|
|
20421
21431
|
|
|
20422
21432
|
${snippet}
|
|
20423
21433
|
${codeAddHint}`);
|
|
20424
|
-
return
|
|
21434
|
+
return finishApply();
|
|
20425
21435
|
}
|
|
20426
21436
|
try {
|
|
20427
21437
|
const merged = await safeMergeConfigFile({ configPath, entry });
|
|
@@ -20442,7 +21452,7 @@ ${codeAddHint}`);
|
|
|
20442
21452
|
${snippet}
|
|
20443
21453
|
${codeAddHint}`);
|
|
20444
21454
|
}
|
|
20445
|
-
return
|
|
21455
|
+
return finishApply();
|
|
20446
21456
|
} catch (err) {
|
|
20447
21457
|
if (err instanceof LostKeyError) {
|
|
20448
21458
|
process.stderr.write(`
|
|
@@ -20471,6 +21481,15 @@ ${codeAddHint}`);
|
|
|
20471
21481
|
);
|
|
20472
21482
|
return 2;
|
|
20473
21483
|
}
|
|
21484
|
+
if (err instanceof IssuerScopeNotAppliedError) {
|
|
21485
|
+
process.stderr.write(
|
|
21486
|
+
`
|
|
21487
|
+
\u2716 ${err.message}
|
|
21488
|
+
The app-context, service principal, and any declared namespaces were already provisioned. No schemas, profile, roles, key, or seed records were.
|
|
21489
|
+
`
|
|
21490
|
+
);
|
|
21491
|
+
return 1;
|
|
21492
|
+
}
|
|
20474
21493
|
if (err instanceof TenantWideNamespacesNotAllowedError) {
|
|
20475
21494
|
process.stderr.write(`
|
|
20476
21495
|
\u2716 ${err.message}
|
|
@@ -20493,126 +21512,614 @@ ${formatCliError(err, { action: "Bootstrap failed" })}`);
|
|
|
20493
21512
|
// src/cli-blueprint.ts
|
|
20494
21513
|
var import_promises3 = require("fs/promises");
|
|
20495
21514
|
|
|
20496
|
-
// src/
|
|
20497
|
-
var
|
|
20498
|
-
|
|
20499
|
-
|
|
20500
|
-
if (typeof lf === "string") return [lf];
|
|
20501
|
-
if (lf.fieldNames !== void 0) return lf.fieldNames;
|
|
20502
|
-
return lf.fieldName !== void 0 ? [lf.fieldName] : [];
|
|
20503
|
-
}
|
|
20504
|
-
function compositeLegFindings(schema, lf, fieldsById, rangeEnabledFieldNames) {
|
|
20505
|
-
const findings = [];
|
|
20506
|
-
const id = `[${lf.fieldNames.join(", ")}]`;
|
|
20507
|
-
const seenLegs = /* @__PURE__ */ new Set();
|
|
20508
|
-
for (const leg of lf.fieldNames) {
|
|
20509
|
-
if (seenLegs.has(leg)) {
|
|
20510
|
-
findings.push({
|
|
20511
|
-
severity: "error",
|
|
20512
|
-
rule: "lookup-composite-repeated-leg",
|
|
20513
|
-
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.`
|
|
20514
|
-
});
|
|
20515
|
-
}
|
|
20516
|
-
seenLegs.add(leg);
|
|
20517
|
-
const field = fieldsById.get(leg);
|
|
20518
|
-
if (!field) continue;
|
|
20519
|
-
if (field.fieldType === "array" || field.fieldType === "object") {
|
|
20520
|
-
findings.push({
|
|
20521
|
-
severity: "error",
|
|
20522
|
-
rule: "lookup-composite-leg-multi-valued",
|
|
20523
|
-
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.`
|
|
20524
|
-
});
|
|
20525
|
-
}
|
|
20526
|
-
if (field.fieldType === "reference" && field.cardinality === "many") {
|
|
20527
|
-
findings.push({
|
|
20528
|
-
severity: "error",
|
|
20529
|
-
rule: "lookup-composite-leg-multi-valued",
|
|
20530
|
-
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.`
|
|
20531
|
-
});
|
|
20532
|
-
}
|
|
20533
|
-
if (rangeEnabledFieldNames.has(leg)) {
|
|
20534
|
-
findings.push({
|
|
20535
|
-
severity: "error",
|
|
20536
|
-
rule: "lookup-composite-leg-range-enabled",
|
|
20537
|
-
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.`
|
|
20538
|
-
});
|
|
20539
|
-
}
|
|
20540
|
-
if (lf.sortBy !== void 0 && leg === lf.sortBy) {
|
|
20541
|
-
findings.push({
|
|
20542
|
-
severity: "error",
|
|
20543
|
-
rule: "lookup-composite-leg-is-sortby",
|
|
20544
|
-
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'.`
|
|
20545
|
-
});
|
|
20546
|
-
}
|
|
20547
|
-
}
|
|
20548
|
-
return findings;
|
|
21515
|
+
// src/cli-apply.ts
|
|
21516
|
+
var readline3 = __toESM(require("readline/promises"));
|
|
21517
|
+
function bootstrapOnly(spelling) {
|
|
21518
|
+
return spellingsFor("bootstrap").includes(spelling) && !spellingsFor("apply").includes(spelling);
|
|
20549
21519
|
}
|
|
20550
|
-
function
|
|
20551
|
-
const
|
|
20552
|
-
|
|
20553
|
-
|
|
20554
|
-
|
|
20555
|
-
|
|
20556
|
-
|
|
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
|
-
for (const lf of schema.lookupFields ?? []) {
|
|
20586
|
-
for (const name of lookupFieldNames(lf)) {
|
|
20587
|
-
if (!fieldIds.has(name)) {
|
|
20588
|
-
findings.push({
|
|
20589
|
-
severity: "error",
|
|
20590
|
-
rule: "lookup-field-unknown",
|
|
20591
|
-
message: `schema '${schema.typeName}': lookupField '${name}' is not a declared field.`
|
|
20592
|
-
});
|
|
21520
|
+
function parseApplyArgs(argv) {
|
|
21521
|
+
const args = {
|
|
21522
|
+
set: [],
|
|
21523
|
+
env: "production",
|
|
21524
|
+
yes: false,
|
|
21525
|
+
seed: false,
|
|
21526
|
+
allowTenantWideNamespaces: false,
|
|
21527
|
+
allowScriptAdoption: false,
|
|
21528
|
+
requireVerifiedIssuers: false,
|
|
21529
|
+
confirmExistingPrincipal: false,
|
|
21530
|
+
prune: false,
|
|
21531
|
+
help: false
|
|
21532
|
+
};
|
|
21533
|
+
const value = (flag, i) => {
|
|
21534
|
+
const v = argv[i + 1];
|
|
21535
|
+
if (v === void 0 || v.startsWith("-") && v !== "-") throw new Error(`${flag} needs a value`);
|
|
21536
|
+
if (v === "") throw new Error(`${flag} needs a non-empty value`);
|
|
21537
|
+
return v;
|
|
21538
|
+
};
|
|
21539
|
+
for (let i = 0; i < argv.length; i++) {
|
|
21540
|
+
const a = argv[i];
|
|
21541
|
+
switch (a) {
|
|
21542
|
+
case "--set":
|
|
21543
|
+
args.set.push(value(a, i));
|
|
21544
|
+
i++;
|
|
21545
|
+
break;
|
|
21546
|
+
case "--values":
|
|
21547
|
+
args.values = value(a, i);
|
|
21548
|
+
i++;
|
|
21549
|
+
break;
|
|
21550
|
+
case "--env": {
|
|
21551
|
+
const v = value(a, i);
|
|
21552
|
+
i++;
|
|
21553
|
+
if (v !== "staging" && v !== "production") {
|
|
21554
|
+
throw new Error(`--env must be 'staging' or 'production' (got '${v}')`);
|
|
20593
21555
|
}
|
|
21556
|
+
args.env = v;
|
|
21557
|
+
break;
|
|
20594
21558
|
}
|
|
20595
|
-
|
|
20596
|
-
|
|
20597
|
-
|
|
20598
|
-
|
|
20599
|
-
|
|
20600
|
-
|
|
20601
|
-
|
|
20602
|
-
|
|
20603
|
-
|
|
20604
|
-
|
|
20605
|
-
|
|
20606
|
-
|
|
20607
|
-
|
|
21559
|
+
case "--base-url":
|
|
21560
|
+
args.baseUrl = value(a, i);
|
|
21561
|
+
i++;
|
|
21562
|
+
break;
|
|
21563
|
+
case "--token":
|
|
21564
|
+
args.token = value(a, i);
|
|
21565
|
+
i++;
|
|
21566
|
+
break;
|
|
21567
|
+
case "--tenant": {
|
|
21568
|
+
const v = value(a, i);
|
|
21569
|
+
i++;
|
|
21570
|
+
if (v !== "test" && v !== "live") throw new Error(`--tenant must be 'test' or 'live' (got '${v}')`);
|
|
21571
|
+
if (args.tenant !== void 0) throw new Error("--tenant was given more than once");
|
|
21572
|
+
args.tenant = v;
|
|
21573
|
+
break;
|
|
20608
21574
|
}
|
|
20609
|
-
|
|
20610
|
-
|
|
20611
|
-
|
|
20612
|
-
|
|
20613
|
-
|
|
20614
|
-
|
|
20615
|
-
|
|
21575
|
+
case "--yes":
|
|
21576
|
+
case "-y":
|
|
21577
|
+
args.yes = true;
|
|
21578
|
+
break;
|
|
21579
|
+
case "--seed":
|
|
21580
|
+
args.seed = true;
|
|
21581
|
+
break;
|
|
21582
|
+
case "--allow-tenant-wide-namespaces":
|
|
21583
|
+
args.allowTenantWideNamespaces = true;
|
|
21584
|
+
break;
|
|
21585
|
+
case "--allow-script-adoption":
|
|
21586
|
+
args.allowScriptAdoption = true;
|
|
21587
|
+
break;
|
|
21588
|
+
case "--require-verified-issuers":
|
|
21589
|
+
args.requireVerifiedIssuers = true;
|
|
21590
|
+
break;
|
|
21591
|
+
case "--confirm-existing-principal":
|
|
21592
|
+
args.confirmExistingPrincipal = true;
|
|
21593
|
+
break;
|
|
21594
|
+
case "--prune":
|
|
21595
|
+
args.prune = true;
|
|
21596
|
+
break;
|
|
21597
|
+
case "--help":
|
|
21598
|
+
case "-h":
|
|
21599
|
+
args.help = true;
|
|
21600
|
+
break;
|
|
21601
|
+
default:
|
|
21602
|
+
if (a === "--test") {
|
|
21603
|
+
throw new Error("apply has no --test shorthand: write --tenant test (there is no default tenant)");
|
|
21604
|
+
}
|
|
21605
|
+
if (a === "--no-seed") {
|
|
21606
|
+
throw new Error("apply does not seed by default: pass --seed to write the sample records");
|
|
21607
|
+
}
|
|
21608
|
+
if (a === "--blueprint") {
|
|
21609
|
+
throw new Error("apply takes the blueprint as an argument, not a flag: vectros blueprint apply <file|name> --tenant test|live");
|
|
21610
|
+
}
|
|
21611
|
+
if (a === "--blank" || a === "--blank-slate") {
|
|
21612
|
+
throw new Error("apply has no blank slate: it needs a blueprint (vectros blueprint apply <file|name> --tenant test|live)");
|
|
21613
|
+
}
|
|
21614
|
+
if (bootstrapOnly(a)) {
|
|
21615
|
+
throw new Error(
|
|
21616
|
+
`${a} belongs to 'vectros bootstrap': apply mints no key and touches no client configuration`
|
|
21617
|
+
);
|
|
21618
|
+
}
|
|
21619
|
+
if (a.startsWith("-")) {
|
|
21620
|
+
const name = a.split("=")[0];
|
|
21621
|
+
throw new Error(
|
|
21622
|
+
`Unknown apply flag: ${name}` + (a.includes("=") ? " (a flag's value is a separate argument, not name=value)" : "")
|
|
21623
|
+
);
|
|
21624
|
+
}
|
|
21625
|
+
if (args.blueprint !== void 0) throw new Error("Unexpected extra argument: apply takes one blueprint");
|
|
21626
|
+
args.blueprint = a;
|
|
21627
|
+
}
|
|
21628
|
+
}
|
|
21629
|
+
return args;
|
|
21630
|
+
}
|
|
21631
|
+
function applySeedNotice(blueprint, seed) {
|
|
21632
|
+
const declared = blueprint.seed?.length ?? 0;
|
|
21633
|
+
if (declared === 0) {
|
|
21634
|
+
return seed ? "\n Sample records: --seed was passed, but this blueprint declares none.\n" : "";
|
|
21635
|
+
}
|
|
21636
|
+
return seed ? `
|
|
21637
|
+
Sample records: --seed was passed, so the ${declared} sample record(s) in the plan above WILL be written.
|
|
21638
|
+
` : `
|
|
21639
|
+
Sample records: this blueprint declares ${declared}; they will NOT be written (apply seeds only with --seed).
|
|
21640
|
+
`;
|
|
21641
|
+
}
|
|
21642
|
+
function applySeedSummary(blueprint, seed, seededCount) {
|
|
21643
|
+
const declared = blueprint.seed?.length ?? 0;
|
|
21644
|
+
if (declared === 0) return seed ? " \u2714 Sample records: none written \u2014 this blueprint declares none\n" : "";
|
|
21645
|
+
if (seed) {
|
|
21646
|
+
const shortfall = seededCount < declared ? " \u2014 the rest were skipped (this blueprint declares no schema for their type)" : "";
|
|
21647
|
+
return ` \u2714 Sample records: seeded ${seededCount} of ${declared} (--seed)${shortfall}
|
|
21648
|
+
`;
|
|
21649
|
+
}
|
|
21650
|
+
return ` \u2714 Sample records: none written \u2014 this blueprint declares ${declared}; pass --seed to write them
|
|
21651
|
+
`;
|
|
21652
|
+
}
|
|
21653
|
+
var APPLY_HELP = `vectros blueprint apply \u2014 provision a blueprint into a tenant, minting no key.
|
|
21654
|
+
|
|
21655
|
+
Usage:
|
|
21656
|
+
vectros blueprint apply <file|name> --tenant test|live [--yes] [--prune] [--env staging|production]
|
|
21657
|
+
[--set name=value ...] [--values <file>] [--seed]
|
|
21658
|
+
[--confirm-existing-principal] [--require-verified-issuers]
|
|
21659
|
+
[--allow-tenant-wide-namespaces] [--allow-script-adoption]
|
|
21660
|
+
[--token <t> | env VECTROS_BOOTSTRAP_TOKEN] [--base-url <url>]
|
|
21661
|
+
|
|
21662
|
+
<file|name> is a path to your own blueprint (YAML/JSON) or the name of a bundled one
|
|
21663
|
+
(${BLUEPRINT_NAMES.join(", ")}). Everything it declares is created or reconciled \u2014 the context, its
|
|
21664
|
+
service principal and access profile, schemas, roles, issuers, triggers and scripts (and, only with --seed, its
|
|
21665
|
+
sample seed records) \u2014 but NO scoped key is minted, nothing is written to a keyring and no MCP client config is
|
|
21666
|
+
touched, so a CI runner can re-apply a changed blueprint. To also get a credential, use 'vectros bootstrap'. It
|
|
21667
|
+
authenticates the same way bootstrap does and refuses on the same gates (scope, capability, tenant-wide namespaces)
|
|
21668
|
+
before anything changes. (A script pushed onto an existing hand-authored name is checked when the scripts are
|
|
21669
|
+
pushed, partway through: --allow-script-adoption.)
|
|
21670
|
+
|
|
21671
|
+
Before it changes anything it prints the account, tenant, context and API it is pointed at, then the plan, and
|
|
21672
|
+
asks to proceed unless --yes is given. With no terminal --yes is required.
|
|
21673
|
+
|
|
21674
|
+
Options:
|
|
21675
|
+
${renderOptionHelp("apply", BLUEPRINT_NAMES)}
|
|
21676
|
+
|
|
21677
|
+
Deletion: apply deletes nothing unless --prune is passed. With --prune it removes each trigger in the context
|
|
21678
|
+
whose stored 'provisionedBy' marker equals this blueprint's name and whose principal's external id is the one this
|
|
21679
|
+
blueprint would derive, and which the file no longer declares. Both are stored markers, not verified identities,
|
|
21680
|
+
and the name comes from the blueprint FILE (after any \${{ inputs.* }} substitution, so --set and --values can change
|
|
21681
|
+
it): a blueprint that reuses another one's name and omits some of its triggers will remove them. A trigger counts
|
|
21682
|
+
as matched when it carries this blueprint's marker and the file no longer declares it; if such a row cannot be
|
|
21683
|
+
removed (no id or principal to remove it by, or a failed read or delete) apply exits 1. Rows with no marker are left
|
|
21684
|
+
alone and reported; rows marked by another blueprint are left alone. Apply still replaces an access profile's scopes
|
|
21685
|
+
and a role's rules in place, as bootstrap does.
|
|
21686
|
+
|
|
21687
|
+
Seed records: unlike bootstrap, apply writes NO sample seed records unless --seed is passed, in either tenant (a
|
|
21688
|
+
blueprint's seeds are illustrative, not real data, and a re-apply from a pipeline must not write them by omission).
|
|
21689
|
+
The plan and the summary say plainly whether they will be or were written. --no-seed is refused here: apply already
|
|
21690
|
+
does not seed, so the flag would do nothing.
|
|
21691
|
+
|
|
21692
|
+
Exit codes:
|
|
21693
|
+
0 applied; also --help, and answering no at "Apply this plan?" (nothing was changed)
|
|
21694
|
+
1 failed while applying, or --prune could not remove a trigger it matched (steps earlier in the plan may
|
|
21695
|
+
already have been applied; a re-run converges)
|
|
21696
|
+
2 refused, before anything was changed: a usage error (no --tenant, no --yes without a terminal, no
|
|
21697
|
+
credential, an unknown flag, an unknown or malformed blueprint, a missing --set input, an invalid base
|
|
21698
|
+
URL), the scope or capability gate, or a tenant-wide namespace without the opt-in. Also 2, but AFTER the
|
|
21699
|
+
context and the service principal were provisioned: an identity the blueprint references could not be
|
|
21700
|
+
resolved (the message says so)
|
|
21701
|
+
3 needs a confirmation it did not get (an existing service principal: --confirm-existing-principal)
|
|
21702
|
+
4 applied in full, but --require-verified-issuers was given and an issuer is pending verification`;
|
|
21703
|
+
function blueprintRefusal(err) {
|
|
21704
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
21705
|
+
if (message.startsWith("Unknown blueprint ")) {
|
|
21706
|
+
return `No bundled blueprint matches the argument you gave. Bundled blueprints: ${BLUEPRINT_NAMES.join(", ")} (or pass a path like ./my.yaml).`;
|
|
21707
|
+
}
|
|
21708
|
+
return message;
|
|
21709
|
+
}
|
|
21710
|
+
var refuse = (message) => {
|
|
21711
|
+
process.stderr.write(`
|
|
21712
|
+
\u2716 ${redactCredentials(message)}
|
|
21713
|
+
Nothing was changed.
|
|
21714
|
+
`);
|
|
21715
|
+
return 2;
|
|
21716
|
+
};
|
|
21717
|
+
async function runApplyCli(argv, deps = {}) {
|
|
21718
|
+
const log = deps.log ?? createLogger2();
|
|
21719
|
+
let args;
|
|
21720
|
+
try {
|
|
21721
|
+
args = parseApplyArgs(argv);
|
|
21722
|
+
} catch (err) {
|
|
21723
|
+
process.stderr.write(
|
|
21724
|
+
`
|
|
21725
|
+
\u2716 ${redactCredentials(err instanceof Error ? err.message : String(err))}
|
|
21726
|
+
See 'vectros blueprint apply --help'. Nothing was changed.
|
|
21727
|
+
`
|
|
21728
|
+
);
|
|
21729
|
+
return 2;
|
|
21730
|
+
}
|
|
21731
|
+
if (args.help) {
|
|
21732
|
+
process.stdout.write(`${APPLY_HELP}
|
|
21733
|
+
`);
|
|
21734
|
+
return 0;
|
|
21735
|
+
}
|
|
21736
|
+
if (!args.blueprint) {
|
|
21737
|
+
return refuse("apply needs a blueprint: vectros blueprint apply <file|name> --tenant test|live");
|
|
21738
|
+
}
|
|
21739
|
+
if (!args.tenant) {
|
|
21740
|
+
return refuse(
|
|
21741
|
+
"apply needs --tenant test|live. There is no default, so a run never lands on 'live' by omission."
|
|
21742
|
+
);
|
|
21743
|
+
}
|
|
21744
|
+
const tenant = args.tenant;
|
|
21745
|
+
const isTty = deps.isTty ?? Boolean(process.stdin.isTTY);
|
|
21746
|
+
if (!args.yes && !isTty) {
|
|
21747
|
+
return refuse(
|
|
21748
|
+
"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."
|
|
21749
|
+
);
|
|
21750
|
+
}
|
|
21751
|
+
let baseUrl;
|
|
21752
|
+
try {
|
|
21753
|
+
baseUrl = envToBaseUrl(args.env, args.baseUrl ?? process.env.VECTROS_API_BASE_URL);
|
|
21754
|
+
} catch (err) {
|
|
21755
|
+
return refuse(err instanceof Error ? err.message : String(err));
|
|
21756
|
+
}
|
|
21757
|
+
const promptCtx = deps.createPrompt ? deps.createPrompt() : (() => {
|
|
21758
|
+
const rl = readline3.createInterface({ input: process.stdin, output: process.stdout });
|
|
21759
|
+
return { prompt: (q) => rl.question(q), close: () => rl.close() };
|
|
21760
|
+
})();
|
|
21761
|
+
try {
|
|
21762
|
+
let blueprint;
|
|
21763
|
+
try {
|
|
21764
|
+
blueprint = await resolveBlueprint(
|
|
21765
|
+
{ blueprint: args.blueprint, set: args.set, values: args.values, blankSlate: false },
|
|
21766
|
+
promptCtx.prompt,
|
|
21767
|
+
isTty
|
|
21768
|
+
);
|
|
21769
|
+
validateBlueprint(blueprint);
|
|
21770
|
+
assertTenantWideNamespacesAllowed(blueprint, args.allowTenantWideNamespaces);
|
|
21771
|
+
} catch (err) {
|
|
21772
|
+
if (err instanceof ScopeGateError || err instanceof CapabilityGateError) {
|
|
21773
|
+
process.stderr.write(
|
|
21774
|
+
`
|
|
21775
|
+
\u2716 ${err instanceof ScopeGateError ? "Scope" : "Capability"} gate refused blueprint:
|
|
21776
|
+
`
|
|
21777
|
+
);
|
|
21778
|
+
for (const r of err.rejected) process.stderr.write(` \u2022 ${formatRejection(r)}
|
|
21779
|
+
`);
|
|
21780
|
+
process.stderr.write(" Nothing was changed.\n");
|
|
21781
|
+
return 2;
|
|
21782
|
+
}
|
|
21783
|
+
return refuse(blueprintRefusal(err));
|
|
21784
|
+
}
|
|
21785
|
+
process.stdout.write(gateSummary(blueprint));
|
|
21786
|
+
const noSeed = !args.seed;
|
|
21787
|
+
let cognitoToken;
|
|
21788
|
+
try {
|
|
21789
|
+
cognitoToken = await resolveBridgeCognitoToken(args, promptCtx.prompt, isTty, deps.resolveCognitoToken);
|
|
21790
|
+
} catch (err) {
|
|
21791
|
+
return refuse(err instanceof Error ? err.message : String(err));
|
|
21792
|
+
}
|
|
21793
|
+
const mintBridge = deps.mintBridgeToken ?? mintBridgeToken;
|
|
21794
|
+
const mintContext = deps.mintContextToken ?? mintContextToken;
|
|
21795
|
+
let client;
|
|
21796
|
+
let tenantId;
|
|
21797
|
+
try {
|
|
21798
|
+
const bridge = await mintBridge({ cognitoToken, apiBaseUrl: baseUrl, tenant });
|
|
21799
|
+
client = new VectrosClient({ token: bridge.token, environment: baseUrl });
|
|
21800
|
+
const identity = await (deps.resolveIdentity ?? resolveIdentity)({ log, apiKey: bridge.token, environment: baseUrl });
|
|
21801
|
+
if (!identity.tenantId || typeof identity.tenantId !== "string") {
|
|
21802
|
+
throw new Error("the extended ping (tenant_id) must be deployed for apply to show which account it is pointed at");
|
|
21803
|
+
}
|
|
21804
|
+
tenantId = identity.tenantId;
|
|
21805
|
+
} catch (err) {
|
|
21806
|
+
process.stderr.write(`
|
|
21807
|
+
${formatCliError(err, { action: "Could not authenticate or resolve the account" })} Nothing was changed.
|
|
21808
|
+
`);
|
|
21809
|
+
return 1;
|
|
21810
|
+
}
|
|
21811
|
+
process.stdout.write(
|
|
21812
|
+
`
|
|
21813
|
+
Target
|
|
21814
|
+
account ${tenantId}
|
|
21815
|
+
tenant ${tenant}
|
|
21816
|
+
context ${blueprint.contextId}
|
|
21817
|
+
api ${baseUrl}
|
|
21818
|
+
`
|
|
21819
|
+
);
|
|
21820
|
+
process.stdout.write(
|
|
21821
|
+
`
|
|
21822
|
+
${renderPlan(blueprint, buildPlan(blueprint, { noSeed, noKey: true }), { prune: args.prune })}
|
|
21823
|
+
`
|
|
21824
|
+
);
|
|
21825
|
+
process.stdout.write(applySeedNotice(blueprint, args.seed));
|
|
21826
|
+
const declaredIdentities = Object.entries(blueprint.identities ?? {});
|
|
21827
|
+
if (declaredIdentities.length > 0) {
|
|
21828
|
+
process.stdout.write("\n Identities to ensure (tenant-wide, idempotent by external id):\n");
|
|
21829
|
+
for (const [name, decl] of declaredIdentities) {
|
|
21830
|
+
process.stdout.write(` ~ ${name} (${decl.kind}) \u2014 external id '${decl.externalId}'
|
|
21831
|
+
`);
|
|
21832
|
+
}
|
|
21833
|
+
}
|
|
21834
|
+
if (args.prune) {
|
|
21835
|
+
process.stdout.write(
|
|
21836
|
+
`
|
|
21837
|
+
\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.
|
|
21838
|
+
`
|
|
21839
|
+
);
|
|
21840
|
+
}
|
|
21841
|
+
if (!args.yes) {
|
|
21842
|
+
const ans = (await promptCtx.prompt("\n Apply this plan? [y/N]: ")).trim().toLowerCase();
|
|
21843
|
+
if (ans !== "y" && ans !== "yes") {
|
|
21844
|
+
process.stdout.write(" Aborted \u2014 nothing was changed.\n");
|
|
21845
|
+
return 0;
|
|
21846
|
+
}
|
|
21847
|
+
try {
|
|
21848
|
+
client = new VectrosClient({
|
|
21849
|
+
token: (await mintBridge({ cognitoToken, apiBaseUrl: baseUrl, tenant })).token,
|
|
21850
|
+
environment: baseUrl
|
|
21851
|
+
});
|
|
21852
|
+
} catch (err) {
|
|
21853
|
+
process.stderr.write(
|
|
21854
|
+
`
|
|
21855
|
+
${formatCliError(err, { action: "Could not mint a fresh bootstrap token after your confirmation" })} Nothing was changed.
|
|
21856
|
+
`
|
|
21857
|
+
);
|
|
21858
|
+
return 1;
|
|
21859
|
+
}
|
|
21860
|
+
}
|
|
21861
|
+
let confirmedBy;
|
|
21862
|
+
const confirmExistingPrincipal = async (d) => {
|
|
21863
|
+
if (!args.yes && isTty) {
|
|
21864
|
+
const ans = await promptCtx.prompt(
|
|
21865
|
+
`
|
|
21866
|
+
\u26A0 '${d.externalId}' already exists in your tenant (context '${d.contextId}') \u2014 this apply will REWRITE that
|
|
21867
|
+
existing identity's access profile to the blueprint's grants. Expected when you re-apply a blueprint you
|
|
21868
|
+
already manage. If you don't recognize this identity, do NOT proceed: it may be a name collision with
|
|
21869
|
+
something you don't control.
|
|
21870
|
+
Proceed and rewrite its access profile? [y/N]: `
|
|
21871
|
+
);
|
|
21872
|
+
const ok = existingPrincipalConfirmed(ans);
|
|
21873
|
+
if (ok) confirmedBy = "prompt";
|
|
21874
|
+
return ok;
|
|
21875
|
+
}
|
|
21876
|
+
if (args.confirmExistingPrincipal) confirmedBy = "flag";
|
|
21877
|
+
return args.confirmExistingPrincipal;
|
|
21878
|
+
};
|
|
21879
|
+
const remintBootstrapForContext = buildRemintBootstrapForContext({
|
|
21880
|
+
cognitoToken,
|
|
21881
|
+
apiBaseUrl: baseUrl,
|
|
21882
|
+
tenant,
|
|
21883
|
+
mintBridge
|
|
21884
|
+
});
|
|
21885
|
+
const provisioned = await (deps.provisionContext ?? provisionContext)(client, {
|
|
21886
|
+
blueprint,
|
|
21887
|
+
allowTenantWideNamespaces: args.allowTenantWideNamespaces,
|
|
21888
|
+
remintBootstrapForContext,
|
|
21889
|
+
// No key is minted or read here, so there is never a local record: the check fires for any principal
|
|
21890
|
+
// that already existed.
|
|
21891
|
+
readExistingKey: async () => void 0,
|
|
21892
|
+
confirmExistingPrincipal,
|
|
21893
|
+
skipKey: true,
|
|
21894
|
+
log
|
|
21895
|
+
});
|
|
21896
|
+
if (provisioned.userAction === "reconciled") {
|
|
21897
|
+
process.stdout.write(
|
|
21898
|
+
` \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")
|
|
21899
|
+
);
|
|
21900
|
+
}
|
|
21901
|
+
const resolvedBlueprint = await (deps.resolveApplyTimeIdentities ?? resolveApplyTimeIdentities)(
|
|
21902
|
+
blueprint,
|
|
21903
|
+
provisioned,
|
|
21904
|
+
client,
|
|
21905
|
+
log
|
|
21906
|
+
);
|
|
21907
|
+
const result = await (deps.runLoader ?? runLoader)(client, {
|
|
21908
|
+
blueprint: resolvedBlueprint,
|
|
21909
|
+
tenantId,
|
|
21910
|
+
machineId: "apply",
|
|
21911
|
+
skipKey: true,
|
|
21912
|
+
skipSeed: noSeed,
|
|
21913
|
+
// Off unless asked for: apply never deletes on its own. `blueprint-test` sets the same switch off.
|
|
21914
|
+
pruneTriggers: args.prune,
|
|
21915
|
+
allowScriptAdoption: args.allowScriptAdoption,
|
|
21916
|
+
readExistingKey: async () => void 0,
|
|
21917
|
+
persistMintedKey: async () => {
|
|
21918
|
+
},
|
|
21919
|
+
provisioned,
|
|
21920
|
+
remintBootstrapForContext,
|
|
21921
|
+
remintForContext: buildRemintForContext({ cognitoToken, apiBaseUrl: baseUrl, tenant, mintContext }),
|
|
21922
|
+
log
|
|
21923
|
+
});
|
|
21924
|
+
process.stdout.write(
|
|
21925
|
+
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.
|
|
21926
|
+
` + 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.
|
|
21927
|
+
`)
|
|
21928
|
+
);
|
|
21929
|
+
const issuerCode = finishIssuers(
|
|
21930
|
+
result.issuerActions,
|
|
21931
|
+
{ context: blueprint.contextId, tenant, env: args.env, baseUrl: args.baseUrl },
|
|
21932
|
+
args.requireVerifiedIssuers,
|
|
21933
|
+
(deps.now ?? Date.now)(),
|
|
21934
|
+
"then re-run this `vectros blueprint apply` command"
|
|
21935
|
+
);
|
|
21936
|
+
const pruneFailures = result.triggerPruneSkipped.filter((s) => s.failed !== false);
|
|
21937
|
+
if (args.prune && pruneFailures.length > 0) {
|
|
21938
|
+
process.stderr.write(
|
|
21939
|
+
`
|
|
21940
|
+
\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.
|
|
21941
|
+
`
|
|
21942
|
+
);
|
|
21943
|
+
return 1;
|
|
21944
|
+
}
|
|
21945
|
+
return issuerCode;
|
|
21946
|
+
} catch (err) {
|
|
21947
|
+
return reportApplyError(err);
|
|
21948
|
+
} finally {
|
|
21949
|
+
promptCtx.close();
|
|
21950
|
+
}
|
|
21951
|
+
}
|
|
21952
|
+
function reportApplyError(err) {
|
|
21953
|
+
if (err instanceof ExistingPrincipalNotConfirmedError) {
|
|
21954
|
+
process.stderr.write(
|
|
21955
|
+
`
|
|
21956
|
+
\u2716 ${err.message}
|
|
21957
|
+
|
|
21958
|
+
If this is expected (a re-apply of a blueprint you manage), re-run with --confirm-existing-principal
|
|
21959
|
+
(with --yes in a pipeline, where it is standing consent), or confirm at the prompt.
|
|
21960
|
+
If you don't recognize this identity, investigate the blueprint's servicePrincipal.externalId first.
|
|
21961
|
+
Only the app-context and the principal lookup had run; no grant or anything else was changed.
|
|
21962
|
+
`
|
|
21963
|
+
);
|
|
21964
|
+
return 3;
|
|
21965
|
+
}
|
|
21966
|
+
if (err instanceof BlueprintIdentityError || err instanceof UnresolvedIdentitiesError) {
|
|
21967
|
+
process.stderr.write(
|
|
21968
|
+
`
|
|
21969
|
+
\u2716 ${err.message}
|
|
21970
|
+
The app-context, service principal, and any declared namespaces were already provisioned. No schemas, profile, roles or seed records were.
|
|
21971
|
+
`
|
|
21972
|
+
);
|
|
21973
|
+
return 2;
|
|
21974
|
+
}
|
|
21975
|
+
if (err instanceof IssuerScopeNotAppliedError) {
|
|
21976
|
+
process.stderr.write(
|
|
21977
|
+
`
|
|
21978
|
+
\u2716 ${err.message}
|
|
21979
|
+
The app-context, service principal, and any declared namespaces were already provisioned. No schemas, profile, roles or seed records were.
|
|
21980
|
+
`
|
|
21981
|
+
);
|
|
21982
|
+
return 1;
|
|
21983
|
+
}
|
|
21984
|
+
if (err instanceof TenantWideNamespacesNotAllowedError) {
|
|
21985
|
+
process.stderr.write(`
|
|
21986
|
+
\u2716 ${err.message}
|
|
21987
|
+
Nothing was changed.
|
|
21988
|
+
`);
|
|
21989
|
+
return 2;
|
|
21990
|
+
}
|
|
21991
|
+
process.stderr.write(
|
|
21992
|
+
`
|
|
21993
|
+
${formatCliError(err, { action: "Apply failed" })} The apply stopped part-way: steps earlier in the plan may already have been applied. Re-running converges.
|
|
21994
|
+
`
|
|
21995
|
+
);
|
|
21996
|
+
if (process.env.VECTROS_MCP_DEBUG && err instanceof Error && err.stack) {
|
|
21997
|
+
process.stderr.write(`${err.stack}
|
|
21998
|
+
`);
|
|
21999
|
+
}
|
|
22000
|
+
return 1;
|
|
22001
|
+
}
|
|
22002
|
+
|
|
22003
|
+
// src/lint.ts
|
|
22004
|
+
var ENUM_COMPATIBLE_TYPES = /* @__PURE__ */ new Set(["enum", "string", "string[]"]);
|
|
22005
|
+
var BUILT_IN_SORT_BY = /* @__PURE__ */ new Set(["createdAt", "lastUpdated"]);
|
|
22006
|
+
function lookupFieldNames(lf) {
|
|
22007
|
+
if (typeof lf === "string") return [lf];
|
|
22008
|
+
if (lf.fieldNames !== void 0) return lf.fieldNames;
|
|
22009
|
+
return lf.fieldName !== void 0 ? [lf.fieldName] : [];
|
|
22010
|
+
}
|
|
22011
|
+
function compositeLegFindings(schema, lf, fieldsById, rangeEnabledFieldNames) {
|
|
22012
|
+
const findings = [];
|
|
22013
|
+
const id = `[${lf.fieldNames.join(", ")}]`;
|
|
22014
|
+
const seenLegs = /* @__PURE__ */ new Set();
|
|
22015
|
+
for (const leg of lf.fieldNames) {
|
|
22016
|
+
if (seenLegs.has(leg)) {
|
|
22017
|
+
findings.push({
|
|
22018
|
+
severity: "error",
|
|
22019
|
+
rule: "lookup-composite-repeated-leg",
|
|
22020
|
+
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.`
|
|
22021
|
+
});
|
|
22022
|
+
}
|
|
22023
|
+
seenLegs.add(leg);
|
|
22024
|
+
const field = fieldsById.get(leg);
|
|
22025
|
+
if (!field) continue;
|
|
22026
|
+
if (field.fieldType === "array" || field.fieldType === "object") {
|
|
22027
|
+
findings.push({
|
|
22028
|
+
severity: "error",
|
|
22029
|
+
rule: "lookup-composite-leg-multi-valued",
|
|
22030
|
+
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.`
|
|
22031
|
+
});
|
|
22032
|
+
}
|
|
22033
|
+
if (field.fieldType === "reference" && field.cardinality === "many") {
|
|
22034
|
+
findings.push({
|
|
22035
|
+
severity: "error",
|
|
22036
|
+
rule: "lookup-composite-leg-multi-valued",
|
|
22037
|
+
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.`
|
|
22038
|
+
});
|
|
22039
|
+
}
|
|
22040
|
+
if (rangeEnabledFieldNames.has(leg)) {
|
|
22041
|
+
findings.push({
|
|
22042
|
+
severity: "error",
|
|
22043
|
+
rule: "lookup-composite-leg-range-enabled",
|
|
22044
|
+
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.`
|
|
22045
|
+
});
|
|
22046
|
+
}
|
|
22047
|
+
if (lf.sortBy !== void 0 && leg === lf.sortBy) {
|
|
22048
|
+
findings.push({
|
|
22049
|
+
severity: "error",
|
|
22050
|
+
rule: "lookup-composite-leg-is-sortby",
|
|
22051
|
+
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'.`
|
|
22052
|
+
});
|
|
22053
|
+
}
|
|
22054
|
+
}
|
|
22055
|
+
return findings;
|
|
22056
|
+
}
|
|
22057
|
+
function lintBlueprint(bp) {
|
|
22058
|
+
const findings = [];
|
|
22059
|
+
const typeNames = /* @__PURE__ */ new Set();
|
|
22060
|
+
const expectedDimsByTypeName = /* @__PURE__ */ new Map();
|
|
22061
|
+
for (const schema of bp.schemas) {
|
|
22062
|
+
typeNames.add(schema.typeName);
|
|
22063
|
+
if (schema.expectedScopeDims && schema.expectedScopeDims.length > 0) {
|
|
22064
|
+
expectedDimsByTypeName.set(schema.typeName, schema.expectedScopeDims);
|
|
22065
|
+
}
|
|
22066
|
+
const fieldIds = /* @__PURE__ */ new Set();
|
|
22067
|
+
const fieldsById = /* @__PURE__ */ new Map();
|
|
22068
|
+
for (const f of schema.fields) {
|
|
22069
|
+
if (fieldIds.has(f.fieldId)) {
|
|
22070
|
+
findings.push({
|
|
22071
|
+
severity: "error",
|
|
22072
|
+
rule: "duplicate-field-id",
|
|
22073
|
+
message: `schema '${schema.typeName}': duplicate fieldId '${f.fieldId}'.`
|
|
22074
|
+
});
|
|
22075
|
+
}
|
|
22076
|
+
fieldIds.add(f.fieldId);
|
|
22077
|
+
fieldsById.set(f.fieldId, f);
|
|
22078
|
+
if (f.enumValues && f.enumValues.length > 0 && !ENUM_COMPATIBLE_TYPES.has(f.fieldType)) {
|
|
22079
|
+
findings.push({
|
|
22080
|
+
severity: "warning",
|
|
22081
|
+
rule: "enum-values-on-incompatible-type",
|
|
22082
|
+
message: `schema '${schema.typeName}', field '${f.fieldId}': enumValues set on fieldType '${f.fieldType}' (expected enum/string). The values will likely be ignored.`
|
|
22083
|
+
});
|
|
22084
|
+
}
|
|
22085
|
+
}
|
|
22086
|
+
const rangeEnabledFieldNames = /* @__PURE__ */ new Set();
|
|
22087
|
+
for (const lf of schema.lookupFields ?? []) {
|
|
22088
|
+
if (typeof lf !== "string" && lf.fieldName !== void 0 && lf.rangeEnabled) {
|
|
22089
|
+
rangeEnabledFieldNames.add(lf.fieldName);
|
|
22090
|
+
}
|
|
22091
|
+
}
|
|
22092
|
+
for (const lf of schema.lookupFields ?? []) {
|
|
22093
|
+
for (const name of lookupFieldNames(lf)) {
|
|
22094
|
+
if (!fieldIds.has(name)) {
|
|
22095
|
+
findings.push({
|
|
22096
|
+
severity: "error",
|
|
22097
|
+
rule: "lookup-field-unknown",
|
|
22098
|
+
message: `schema '${schema.typeName}': lookupField '${name}' is not a declared field.`
|
|
22099
|
+
});
|
|
22100
|
+
}
|
|
22101
|
+
}
|
|
22102
|
+
if (typeof lf !== "string" && lf.fieldNames !== void 0) {
|
|
22103
|
+
findings.push(
|
|
22104
|
+
...compositeLegFindings(schema, { fieldNames: lf.fieldNames, sortBy: lf.sortBy }, fieldsById, rangeEnabledFieldNames)
|
|
22105
|
+
);
|
|
22106
|
+
}
|
|
22107
|
+
if (typeof lf !== "string" && lf.sortBy !== void 0) {
|
|
22108
|
+
if (!BUILT_IN_SORT_BY.has(lf.sortBy) && !fieldIds.has(lf.sortBy)) {
|
|
22109
|
+
findings.push({
|
|
22110
|
+
severity: "warning",
|
|
22111
|
+
rule: "lookup-sortby-unknown-field",
|
|
22112
|
+
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.`
|
|
22113
|
+
});
|
|
22114
|
+
}
|
|
22115
|
+
}
|
|
22116
|
+
}
|
|
22117
|
+
}
|
|
22118
|
+
for (const rec of bp.seed ?? []) {
|
|
22119
|
+
if (!typeNames.has(rec.typeName)) {
|
|
22120
|
+
findings.push({
|
|
22121
|
+
severity: "error",
|
|
22122
|
+
rule: "seed-unknown-record-type",
|
|
20616
22123
|
message: `seed '${rec.externalId}': typeName '${rec.typeName}' has no matching schema (the loader would skip it).`
|
|
20617
22124
|
});
|
|
20618
22125
|
}
|
|
@@ -20774,7 +22281,10 @@ function defaultScaffold(name) {
|
|
|
20774
22281
|
return `# ${name}.blueprint.yaml \u2014 a Vectros blueprint.
|
|
20775
22282
|
# Edit, then check it with: vectros blueprint validate ./${name}.blueprint.yaml
|
|
20776
22283
|
# Preview what it provisions: vectros blueprint plan ./${name}.blueprint.yaml
|
|
20777
|
-
# Apply it:
|
|
22284
|
+
# Apply it (no key): vectros blueprint apply ./${name}.blueprint.yaml --tenant test
|
|
22285
|
+
# (writes the sample seed records below only with --seed)
|
|
22286
|
+
# Apply it and mint a key: vectros bootstrap --blueprint ./${name}.blueprint.yaml
|
|
22287
|
+
# (writes them unless you pass --no-seed)
|
|
20778
22288
|
|
|
20779
22289
|
name: ${name} # stable id + idempotency key. Re-running NEVER duplicates.
|
|
20780
22290
|
version: 1.0.0
|
|
@@ -20793,9 +22303,9 @@ schemas:
|
|
|
20793
22303
|
fieldType: string # string | number | boolean | date | enum | array | object
|
|
20794
22304
|
required: true
|
|
20795
22305
|
searchable: true # contributes to full-text + semantic search
|
|
20796
|
-
|
|
20797
|
-
|
|
20798
|
-
|
|
22306
|
+
# Do NOT declare an externalId field here: it is a first-class identifier the
|
|
22307
|
+
# platform reserves (a seed's externalId is set at the TOP LEVEL of the seed, below),
|
|
22308
|
+
# and it has its own built-in finder, so it is not listed under lookupFields either.
|
|
20799
22309
|
lookupFields: [title] # extra O(1) exact-match lookup indexes (max 10)
|
|
20800
22310
|
|
|
20801
22311
|
accessProfile:
|
|
@@ -20831,7 +22341,8 @@ servicePrincipal:
|
|
|
20831
22341
|
seed:
|
|
20832
22342
|
- surface: record # 'record' (createRecord) or 'document' (text-ingest: needs title + text)
|
|
20833
22343
|
typeName: thing
|
|
20834
|
-
externalId: seed-welcome # stable \u2192 idempotent; re-apply converges
|
|
22344
|
+
externalId: seed-welcome # stable \u2192 idempotent; re-apply converges. Top level ONLY:
|
|
22345
|
+
# never inside 'fields', never as a schema field.
|
|
20835
22346
|
fields: { title: "Hello from ${name}" }
|
|
20836
22347
|
`;
|
|
20837
22348
|
}
|
|
@@ -20899,7 +22410,10 @@ Usage:
|
|
|
20899
22410
|
vectros blueprint init <name> [--from <bundled>] [--out <path>] [--force]
|
|
20900
22411
|
Scaffold a new blueprint YAML to edit (no creds).
|
|
20901
22412
|
vectros blueprint validate <file|name> Structural + scope-gate + lint checks (no creds).
|
|
20902
|
-
vectros blueprint plan <file|name> Validate, then preview what bootstrap WOULD provision.
|
|
22413
|
+
vectros blueprint plan <file|name> Validate, then preview what bootstrap WOULD provision (apply: same, minus the sample seeds unless --seed).
|
|
22414
|
+
vectros blueprint apply <file|name> --tenant test|live
|
|
22415
|
+
Provision it into a tenant, minting NO key (needs credentials;
|
|
22416
|
+
'vectros blueprint apply --help' for its flags and exit codes).
|
|
20903
22417
|
vectros blueprint list List the bundled blueprint library.
|
|
20904
22418
|
|
|
20905
22419
|
<file|name> is a path to your own blueprint (YAML .yaml/.yml or JSON .json) OR
|
|
@@ -21024,10 +22538,17 @@ ${renderPlan(blueprint, steps)}
|
|
|
21024
22538
|
}
|
|
21025
22539
|
}
|
|
21026
22540
|
const setEcho = [...args.set].join(" --set ");
|
|
22541
|
+
const inputs = (args.set.length ? ` --set ${setEcho}` : "") + (args.values ? ` --values ${args.values}` : "");
|
|
21027
22542
|
process.stdout.write(
|
|
21028
22543
|
`
|
|
21029
22544
|
${steps.length} step(s). This is a preview \u2014 nothing was minted.
|
|
21030
|
-
Apply with: vectros
|
|
22545
|
+
Apply with: vectros blueprint apply ${args.file} --tenant test${inputs}
|
|
22546
|
+
(no scoped key is minted; write --tenant live for your live tenant), or
|
|
22547
|
+
vectros bootstrap --blueprint ${args.file}${inputs}
|
|
22548
|
+
(which also mints a key for your MCP client).
|
|
22549
|
+
` + (blueprint.seed?.length ? ` The plan above includes the sample seed records: bootstrap writes them unless --no-seed;
|
|
22550
|
+
blueprint apply writes them only when you pass --seed.
|
|
22551
|
+
` : "")
|
|
21031
22552
|
);
|
|
21032
22553
|
return 0;
|
|
21033
22554
|
}
|
|
@@ -21091,7 +22612,8 @@ function runList() {
|
|
|
21091
22612
|
}
|
|
21092
22613
|
process.stdout.write(
|
|
21093
22614
|
`
|
|
21094
|
-
Apply one with: vectros
|
|
22615
|
+
Apply one with: vectros blueprint apply <name> --tenant test (no key)
|
|
22616
|
+
vectros bootstrap --blueprint <name> (also mints a key)
|
|
21095
22617
|
Scaffold your own: vectros blueprint init my-thing
|
|
21096
22618
|
Validate it: vectros blueprint validate ./my-thing.blueprint.yaml
|
|
21097
22619
|
`
|
|
@@ -21099,7 +22621,7 @@ function runList() {
|
|
|
21099
22621
|
return 0;
|
|
21100
22622
|
}
|
|
21101
22623
|
async function runBlueprintCli(argv, deps = {}) {
|
|
21102
|
-
|
|
22624
|
+
if (argv[0] === "apply") return runApplyCli(argv.slice(1), deps);
|
|
21103
22625
|
let args;
|
|
21104
22626
|
try {
|
|
21105
22627
|
args = parseBlueprintArgs(argv);
|
|
@@ -21573,7 +23095,7 @@ ${list}
|
|
|
21573
23095
|
return storeAndReport(dest, key, auth, {
|
|
21574
23096
|
...opts,
|
|
21575
23097
|
principalId: `usr_${userId}`,
|
|
21576
|
-
revokedKeyId: existing?.keyId
|
|
23098
|
+
revokedKeyId: existing?.keyId ? [existing.keyId] : void 0
|
|
21577
23099
|
});
|
|
21578
23100
|
emitMintedKey(key, opts.format);
|
|
21579
23101
|
return 0;
|
|
@@ -22589,7 +24111,7 @@ async function runRoleDelete(opts, deps = {}) {
|
|
|
22589
24111
|
}
|
|
22590
24112
|
|
|
22591
24113
|
// src/commands/context.ts
|
|
22592
|
-
var
|
|
24114
|
+
var readline4 = __toESM(require("readline/promises"));
|
|
22593
24115
|
function renderContext(c) {
|
|
22594
24116
|
return formatKeyValues([
|
|
22595
24117
|
["context", c.contextId],
|
|
@@ -22670,7 +24192,7 @@ async function runContextDestroy(opts, deps = {}) {
|
|
|
22670
24192
|
return 2;
|
|
22671
24193
|
}
|
|
22672
24194
|
const promptCtx = deps.createPrompt ? deps.createPrompt() : (() => {
|
|
22673
|
-
const rl =
|
|
24195
|
+
const rl = readline4.createInterface({ input: process.stdin, output: process.stdout });
|
|
22674
24196
|
return { prompt: (q) => rl.question(q), close: () => rl.close() };
|
|
22675
24197
|
})();
|
|
22676
24198
|
try {
|
|
@@ -22741,7 +24263,8 @@ ${renderContext(ctx)}
|
|
|
22741
24263
|
}
|
|
22742
24264
|
|
|
22743
24265
|
// src/commands/issuers.ts
|
|
22744
|
-
var
|
|
24266
|
+
var readline5 = __toESM(require("readline/promises"));
|
|
24267
|
+
var import_node_fs4 = require("fs");
|
|
22745
24268
|
function parseSelfSignupPolicies(raw) {
|
|
22746
24269
|
if (!raw || raw.length === 0) return void 0;
|
|
22747
24270
|
return raw.map((entry) => {
|
|
@@ -22780,6 +24303,24 @@ function parseCapturedClaims(raw) {
|
|
|
22780
24303
|
}
|
|
22781
24304
|
return [...raw];
|
|
22782
24305
|
}
|
|
24306
|
+
function parseRestrictedToDomain(raw) {
|
|
24307
|
+
if (raw === void 0) return void 0;
|
|
24308
|
+
const domain = raw.trim();
|
|
24309
|
+
if (domain.length === 0) {
|
|
24310
|
+
throw new Error(
|
|
24311
|
+
"--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."
|
|
24312
|
+
);
|
|
24313
|
+
}
|
|
24314
|
+
if (!isRestrictedToDomainName(domain.toLowerCase())) {
|
|
24315
|
+
throw new Error(
|
|
24316
|
+
`--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.`
|
|
24317
|
+
);
|
|
24318
|
+
}
|
|
24319
|
+
return domain;
|
|
24320
|
+
}
|
|
24321
|
+
function renderRestrictedToDomain(domain) {
|
|
24322
|
+
return domain ? domain : "\u2014";
|
|
24323
|
+
}
|
|
22783
24324
|
function renderCapturedClaims(claims) {
|
|
22784
24325
|
if (!claims || claims.length === 0) return "\u2014";
|
|
22785
24326
|
return claims.join(", ");
|
|
@@ -22789,6 +24330,7 @@ function renderSelfSignup(policies) {
|
|
|
22789
24330
|
return policies.map((p) => `${p.signup_type} \u2192 ${p.role_id}`).join(", ");
|
|
22790
24331
|
}
|
|
22791
24332
|
function renderIssuer(i) {
|
|
24333
|
+
const pending = isPendingVerification(i);
|
|
22792
24334
|
return formatKeyValues([
|
|
22793
24335
|
["issuerId", i.issuerId],
|
|
22794
24336
|
["issuer", i.issuer],
|
|
@@ -22801,9 +24343,22 @@ function renderIssuer(i) {
|
|
|
22801
24343
|
["status", i.status],
|
|
22802
24344
|
["createdAt", i.createdAt],
|
|
22803
24345
|
["selfSignup", renderSelfSignup(i.selfSignupPolicies)],
|
|
22804
|
-
["capturedClaims", renderCapturedClaims(i.capturedClaims)]
|
|
24346
|
+
["capturedClaims", renderCapturedClaims(i.capturedClaims)],
|
|
24347
|
+
["restrictedToDomain", renderRestrictedToDomain(i.restrictedToDomain)],
|
|
24348
|
+
...pending ? [
|
|
24349
|
+
["verificationClaim", displayChallengeValue(i.verificationClaim)],
|
|
24350
|
+
["verificationNonce", displayChallengeValue(i.verificationNonce)],
|
|
24351
|
+
["verificationExpiresAt", displayChallengeValue(i.verificationExpiresAt)]
|
|
24352
|
+
] : []
|
|
22805
24353
|
]);
|
|
22806
24354
|
}
|
|
24355
|
+
function challengeContext(opts) {
|
|
24356
|
+
return { context: opts.context, tenant: opts.tenant, env: opts.env, baseUrl: opts.baseUrl };
|
|
24357
|
+
}
|
|
24358
|
+
function renderPendingNotes(i, ctx, now) {
|
|
24359
|
+
if (!isPendingVerification(i)) return "";
|
|
24360
|
+
return challengeExpired(i, now) ? renderExpiredBlock(i, ctx, "then register it again with the same arguments") : renderChallengeBlock(i, ctx, now);
|
|
24361
|
+
}
|
|
22807
24362
|
async function runIssuersList(opts, deps = {}) {
|
|
22808
24363
|
return withAuth({ ...opts, provisioning: true, context: opts.context }, deps, async (auth) => {
|
|
22809
24364
|
const client = auth.client;
|
|
@@ -22823,7 +24378,8 @@ async function runIssuersList(opts, deps = {}) {
|
|
|
22823
24378
|
{ header: "audience", get: (i) => i.audience },
|
|
22824
24379
|
{ header: "status", get: (i) => i.status },
|
|
22825
24380
|
{ header: "self-signup", get: (i) => renderSelfSignup(i.selfSignupPolicies) },
|
|
22826
|
-
{ header: "captured claims", get: (i) => renderCapturedClaims(i.capturedClaims) }
|
|
24381
|
+
{ header: "captured claims", get: (i) => renderCapturedClaims(i.capturedClaims) },
|
|
24382
|
+
{ header: "restricted to domain", get: (i) => renderRestrictedToDomain(i.restrictedToDomain) }
|
|
22827
24383
|
]) + "\n"
|
|
22828
24384
|
);
|
|
22829
24385
|
return 0;
|
|
@@ -22843,9 +24399,11 @@ async function runIssuersGet(opts, deps = {}) {
|
|
|
22843
24399
|
`);
|
|
22844
24400
|
return 0;
|
|
22845
24401
|
}
|
|
24402
|
+
const notes = renderPendingNotes(issuer, challengeContext(opts), (deps.now ?? Date.now)());
|
|
22846
24403
|
process.stdout.write(`
|
|
22847
24404
|
${renderIssuer(issuer)}
|
|
22848
|
-
`
|
|
24405
|
+
${notes ? `
|
|
24406
|
+
${notes}` : ""}`);
|
|
22849
24407
|
return 0;
|
|
22850
24408
|
} catch (err) {
|
|
22851
24409
|
process.stderr.write(formatCliError(err, { action: "issuers get failed" }));
|
|
@@ -22856,9 +24414,11 @@ ${renderIssuer(issuer)}
|
|
|
22856
24414
|
async function runIssuersRegister(opts, deps = {}) {
|
|
22857
24415
|
let selfSignupPolicies;
|
|
22858
24416
|
let capturedClaims;
|
|
24417
|
+
let restrictedToDomain;
|
|
22859
24418
|
try {
|
|
22860
24419
|
selfSignupPolicies = parseSelfSignupPolicies(opts.selfSignupPolicy);
|
|
22861
24420
|
capturedClaims = parseCapturedClaims(opts.capturedClaim);
|
|
24421
|
+
restrictedToDomain = parseRestrictedToDomain(opts.restrictedToDomain);
|
|
22862
24422
|
} catch (err) {
|
|
22863
24423
|
process.stderr.write(` \u2716 ${formatError(err)}
|
|
22864
24424
|
`);
|
|
@@ -22877,18 +24437,45 @@ async function runIssuersRegister(opts, deps = {}) {
|
|
|
22877
24437
|
emailClaim: opts.emailClaim,
|
|
22878
24438
|
userinfoUri: opts.userinfoUri,
|
|
22879
24439
|
selfSignupPolicies,
|
|
22880
|
-
capturedClaims
|
|
24440
|
+
capturedClaims,
|
|
24441
|
+
restrictedToDomain
|
|
22881
24442
|
});
|
|
24443
|
+
if (restrictedToDomain !== void 0 && (issuer.restrictedToDomain ?? "").toLowerCase() !== restrictedToDomain.toLowerCase()) {
|
|
24444
|
+
if (isPendingVerification(issuer)) {
|
|
24445
|
+
const ctx = challengeContext(opts);
|
|
24446
|
+
process.stderr.write(
|
|
24447
|
+
` \u2716 issuer '${opts.issuerId}' is ${issuer.created === false ? "already registered and " : ""}still pending verification, so the requested
|
|
24448
|
+
--restricted-to-domain '${restrictedToDomain}' was NOT applied: register never changes an existing registration, and a pending
|
|
24449
|
+
registration cannot be re-scoped. Either replace it with a scoped one (a domain-scoped issuer needs no login):
|
|
24450
|
+
${deleteCommand(opts.issuerId, ctx)}
|
|
24451
|
+
and register it again with the scope, or verify it first, which makes it active and scopable with issuers update:
|
|
24452
|
+
${verifyCommand(opts.issuerId, ctx)}
|
|
24453
|
+
`
|
|
24454
|
+
);
|
|
24455
|
+
return 1;
|
|
24456
|
+
}
|
|
24457
|
+
const stored = issuer.restrictedToDomain ? `restricted to '${issuer.restrictedToDomain}'` : "with no domain scope";
|
|
24458
|
+
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`;
|
|
24459
|
+
process.stderr.write(
|
|
24460
|
+
` \u2716 issuer '${opts.issuerId}' ${why} \u2014 the requested
|
|
24461
|
+
--restricted-to-domain '${restrictedToDomain}' was NOT applied. Run:
|
|
24462
|
+
vectros issuers update ${shellQuote(opts.issuerId)} --context ${shellQuote(opts.context)}${targetFlags(challengeContext(opts))} --restricted-to-domain ${shellQuote(restrictedToDomain)}
|
|
24463
|
+
`
|
|
24464
|
+
);
|
|
24465
|
+
return 1;
|
|
24466
|
+
}
|
|
22882
24467
|
if (opts.json) {
|
|
22883
24468
|
process.stdout.write(`${formatJson(issuer)}
|
|
22884
24469
|
`);
|
|
22885
24470
|
return 0;
|
|
22886
24471
|
}
|
|
22887
24472
|
const verb = issuer.created === false ? "already registered \u2014 returned unchanged" : "registered";
|
|
24473
|
+
const notes = renderPendingNotes(issuer, challengeContext(opts), (deps.now ?? Date.now)());
|
|
22888
24474
|
process.stdout.write(`
|
|
22889
24475
|
\u2714 issuer '${opts.issuerId}' ${verb}
|
|
22890
24476
|
${renderIssuer(issuer)}
|
|
22891
|
-
`
|
|
24477
|
+
${notes ? `
|
|
24478
|
+
${notes}` : ""}`);
|
|
22892
24479
|
return 0;
|
|
22893
24480
|
} catch (err) {
|
|
22894
24481
|
process.stderr.write(formatCliError(err, { action: "issuers register failed" }));
|
|
@@ -22899,14 +24486,22 @@ ${renderIssuer(issuer)}
|
|
|
22899
24486
|
async function runIssuersUpdate(opts, deps = {}) {
|
|
22900
24487
|
let selfSignupPolicies;
|
|
22901
24488
|
let capturedClaims;
|
|
24489
|
+
let restrictedToDomain;
|
|
22902
24490
|
try {
|
|
22903
24491
|
selfSignupPolicies = parseSelfSignupPolicies(opts.selfSignupPolicy);
|
|
22904
24492
|
capturedClaims = parseCapturedClaims(opts.capturedClaim);
|
|
24493
|
+
restrictedToDomain = parseRestrictedToDomain(opts.restrictedToDomain);
|
|
22905
24494
|
} catch (err) {
|
|
22906
24495
|
process.stderr.write(` \u2716 ${formatError(err)}
|
|
22907
24496
|
`);
|
|
22908
24497
|
return 2;
|
|
22909
24498
|
}
|
|
24499
|
+
if (opts.clearRestrictedToDomain && restrictedToDomain !== void 0) {
|
|
24500
|
+
process.stderr.write(
|
|
24501
|
+
" \u2716 --clear-restricted-to-domain and --restricted-to-domain are mutually exclusive.\n"
|
|
24502
|
+
);
|
|
24503
|
+
return 2;
|
|
24504
|
+
}
|
|
22910
24505
|
if (opts.clearSelfSignupPolicies && selfSignupPolicies && selfSignupPolicies.length > 0) {
|
|
22911
24506
|
process.stderr.write(
|
|
22912
24507
|
" \u2716 --clear-self-signup-policies and --self-signup-policy are mutually exclusive.\n"
|
|
@@ -22929,7 +24524,8 @@ async function runIssuersUpdate(opts, deps = {}) {
|
|
|
22929
24524
|
userinfoUri: opts.userinfoUri,
|
|
22930
24525
|
status: opts.status,
|
|
22931
24526
|
selfSignupPolicies: opts.clearSelfSignupPolicies ? [] : selfSignupPolicies,
|
|
22932
|
-
capturedClaims: opts.clearCapturedClaims ? [] : capturedClaims
|
|
24527
|
+
capturedClaims: opts.clearCapturedClaims ? [] : capturedClaims,
|
|
24528
|
+
restrictedToDomain: opts.clearRestrictedToDomain ? "" : restrictedToDomain
|
|
22933
24529
|
});
|
|
22934
24530
|
if (opts.json) {
|
|
22935
24531
|
process.stdout.write(`${formatJson(issuer)}
|
|
@@ -22960,7 +24556,7 @@ async function runIssuersDelete(opts, deps = {}) {
|
|
|
22960
24556
|
return 2;
|
|
22961
24557
|
}
|
|
22962
24558
|
const promptCtx = deps.createPrompt ? deps.createPrompt() : (() => {
|
|
22963
|
-
const rl =
|
|
24559
|
+
const rl = readline5.createInterface({ input: process.stdin, output: process.stdout });
|
|
22964
24560
|
return { prompt: (q) => rl.question(q), close: () => rl.close() };
|
|
22965
24561
|
})();
|
|
22966
24562
|
try {
|
|
@@ -23002,6 +24598,173 @@ async function runIssuersDelete(opts, deps = {}) {
|
|
|
23002
24598
|
}
|
|
23003
24599
|
});
|
|
23004
24600
|
}
|
|
24601
|
+
var JWT_SHAPE = /^[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]*$/;
|
|
24602
|
+
function decodeTokenBytes(bytes) {
|
|
24603
|
+
if (bytes.length >= 2 && bytes[0] === 255 && bytes[1] === 254) return bytes.subarray(2).toString("utf16le");
|
|
24604
|
+
if (bytes.length >= 2 && bytes[0] === 254 && bytes[1] === 255) {
|
|
24605
|
+
const swapped = Buffer.from(bytes.subarray(2));
|
|
24606
|
+
swapped.swap16();
|
|
24607
|
+
return swapped.toString("utf16le");
|
|
24608
|
+
}
|
|
24609
|
+
if (bytes.length >= 3 && bytes[0] === 239 && bytes[1] === 187 && bytes[2] === 191) {
|
|
24610
|
+
return bytes.subarray(3).toString("utf8");
|
|
24611
|
+
}
|
|
24612
|
+
return bytes.toString("utf8");
|
|
24613
|
+
}
|
|
24614
|
+
var MAX_TOKEN_BYTES = 64 * 1024;
|
|
24615
|
+
var TokenTooLargeError = class extends Error {
|
|
24616
|
+
constructor() {
|
|
24617
|
+
super(`the input is larger than ${MAX_TOKEN_BYTES / 1024} KB, which no token is; nothing was read further and nothing was sent.`);
|
|
24618
|
+
this.name = "TokenTooLargeError";
|
|
24619
|
+
}
|
|
24620
|
+
};
|
|
24621
|
+
async function readAllFrom(stream, maxBytes = MAX_TOKEN_BYTES) {
|
|
24622
|
+
const chunks = [];
|
|
24623
|
+
let total = 0;
|
|
24624
|
+
for await (const chunk of stream) {
|
|
24625
|
+
const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
24626
|
+
total += buf.length;
|
|
24627
|
+
if (total > maxBytes) throw new TokenTooLargeError();
|
|
24628
|
+
chunks.push(buf);
|
|
24629
|
+
}
|
|
24630
|
+
return decodeTokenBytes(Buffer.concat(chunks));
|
|
24631
|
+
}
|
|
24632
|
+
var drainStdin = () => readAllFrom(process.stdin);
|
|
24633
|
+
var readTokenFile = (path4) => readAllFrom((0, import_node_fs4.createReadStream)(path4));
|
|
24634
|
+
function fileFailureReason(err) {
|
|
24635
|
+
if (err instanceof TokenTooLargeError) return err.message;
|
|
24636
|
+
switch (err?.code) {
|
|
24637
|
+
case "ENOENT":
|
|
24638
|
+
return "no such file.";
|
|
24639
|
+
case "EISDIR":
|
|
24640
|
+
return "that is a directory.";
|
|
24641
|
+
case "EACCES":
|
|
24642
|
+
case "EPERM":
|
|
24643
|
+
return "permission denied.";
|
|
24644
|
+
default:
|
|
24645
|
+
return "it could not be read.";
|
|
24646
|
+
}
|
|
24647
|
+
}
|
|
24648
|
+
async function resolveIdpToken(opts, deps) {
|
|
24649
|
+
if (opts.idpToken !== void 0 && opts.idpTokenFile !== void 0) {
|
|
24650
|
+
throw new Error("--idp-token and --idp-token-file are mutually exclusive.");
|
|
24651
|
+
}
|
|
24652
|
+
const readStdin = deps.readStdin ?? drainStdin;
|
|
24653
|
+
const readTextFile = deps.readTextFile ?? readTokenFile;
|
|
24654
|
+
let raw;
|
|
24655
|
+
if (opts.idpToken !== void 0) {
|
|
24656
|
+
raw = opts.idpToken;
|
|
24657
|
+
} else if (opts.idpTokenFile !== void 0 && opts.idpTokenFile !== "-") {
|
|
24658
|
+
if (looksLikeCredential(opts.idpTokenFile)) {
|
|
24659
|
+
throw new Error(
|
|
24660
|
+
"--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."
|
|
24661
|
+
);
|
|
24662
|
+
}
|
|
24663
|
+
try {
|
|
24664
|
+
raw = await readTextFile(opts.idpTokenFile);
|
|
24665
|
+
} catch (err) {
|
|
24666
|
+
const name = safeBaseName(opts.idpTokenFile);
|
|
24667
|
+
throw new Error(`could not read the file given to --idp-token-file${name ? ` (${name})` : ""}: ${fileFailureReason(err)}`);
|
|
24668
|
+
}
|
|
24669
|
+
} else if (opts.idpTokenFile === "-" || !(deps.isTty ?? Boolean(process.stdin.isTTY))) {
|
|
24670
|
+
if (opts.idpTokenFile === "-" && (deps.isTty ?? Boolean(process.stdin.isTTY))) {
|
|
24671
|
+
throw new Error(
|
|
24672
|
+
"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>."
|
|
24673
|
+
);
|
|
24674
|
+
}
|
|
24675
|
+
if (opts.idpTokenFile !== "-") {
|
|
24676
|
+
process.stderr.write(" \u2026 no token flag given: reading the token from stdin (pass --idp-token-file <path> to avoid this).\n");
|
|
24677
|
+
}
|
|
24678
|
+
raw = await readStdin();
|
|
24679
|
+
} else {
|
|
24680
|
+
throw new Error(
|
|
24681
|
+
"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.)"
|
|
24682
|
+
);
|
|
24683
|
+
}
|
|
24684
|
+
if (raw.length > MAX_TOKEN_BYTES) throw new TokenTooLargeError();
|
|
24685
|
+
const token = raw.trim().replace(/^bearer\s+/i, "");
|
|
24686
|
+
if (token.length === 0) throw new Error("the token is empty.");
|
|
24687
|
+
if (!JWT_SHAPE.test(token)) {
|
|
24688
|
+
throw new Error(
|
|
24689
|
+
"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."
|
|
24690
|
+
);
|
|
24691
|
+
}
|
|
24692
|
+
return token;
|
|
24693
|
+
}
|
|
24694
|
+
async function runIssuersVerify(opts, deps = {}) {
|
|
24695
|
+
let token;
|
|
24696
|
+
try {
|
|
24697
|
+
token = await resolveIdpToken(opts, deps);
|
|
24698
|
+
} catch (err) {
|
|
24699
|
+
process.stderr.write(` \u2716 ${formatError(err)}
|
|
24700
|
+
`);
|
|
24701
|
+
return 2;
|
|
24702
|
+
}
|
|
24703
|
+
if (opts.idpToken !== void 0) {
|
|
24704
|
+
process.stderr.write(
|
|
24705
|
+
" \u26A0 --idp-token puts the token in your shell history and the process list; prefer --idp-token-file or stdin.\n"
|
|
24706
|
+
);
|
|
24707
|
+
}
|
|
24708
|
+
return withAuth({ ...opts, provisioning: true, context: opts.context }, deps, async (auth) => {
|
|
24709
|
+
const client = auth.client;
|
|
24710
|
+
try {
|
|
24711
|
+
const issuer = await client.auth.verifyIssuer({ issuerId: opts.issuerId, token });
|
|
24712
|
+
const active = issuer.status === "active";
|
|
24713
|
+
if (opts.json) {
|
|
24714
|
+
process.stdout.write(`${formatJson(issuer)}
|
|
24715
|
+
`);
|
|
24716
|
+
return active ? 0 : 1;
|
|
24717
|
+
}
|
|
24718
|
+
if (!active) {
|
|
24719
|
+
process.stderr.write(
|
|
24720
|
+
` \u2716 the platform accepted the call but issuer '${opts.issuerId}' reports status '${issuer.status ?? "(none)"}', not 'active'. Run: ${getCommand(opts.issuerId, challengeContext(opts))}
|
|
24721
|
+
`
|
|
24722
|
+
);
|
|
24723
|
+
return 1;
|
|
24724
|
+
}
|
|
24725
|
+
process.stdout.write(
|
|
24726
|
+
`
|
|
24727
|
+
\u2714 issuer '${opts.issuerId}' verified: it is now active and accepts tokens from this provider.
|
|
24728
|
+
${renderIssuer(issuer)}
|
|
24729
|
+
|
|
24730
|
+
The token was checked once and discarded. You can remove the rule you added at the provider.
|
|
24731
|
+
`
|
|
24732
|
+
);
|
|
24733
|
+
return 0;
|
|
24734
|
+
} catch (err) {
|
|
24735
|
+
const info = extractServerError(err);
|
|
24736
|
+
process.stderr.write(formatCliError(err, { action: "issuers verify failed" }));
|
|
24737
|
+
if (info.statusCode === 409) {
|
|
24738
|
+
process.stderr.write(
|
|
24739
|
+
` The registration changed or was removed while it was being verified, and this request activated
|
|
24740
|
+
nothing. Run: ${getCommand(opts.issuerId, challengeContext(opts))}
|
|
24741
|
+
(another verification may already have activated it); otherwise retry.
|
|
24742
|
+
`
|
|
24743
|
+
);
|
|
24744
|
+
} else if (info.statusCode === 400) {
|
|
24745
|
+
process.stderr.write(
|
|
24746
|
+
` This did not activate the registration. The message above says why; to see where it stands, run:
|
|
24747
|
+
${getCommand(opts.issuerId, challengeContext(opts))}
|
|
24748
|
+
`
|
|
24749
|
+
);
|
|
24750
|
+
} else if (info.statusCode === 404) {
|
|
24751
|
+
process.stderr.write(
|
|
24752
|
+
` No registration with this id is visible to you in this context. A pending registration whose challenge
|
|
24753
|
+
has expired is removed by the platform. Run: ${getCommand(opts.issuerId, challengeContext(opts))}
|
|
24754
|
+
`
|
|
24755
|
+
);
|
|
24756
|
+
} else {
|
|
24757
|
+
process.stderr.write(
|
|
24758
|
+
` This request did not report success, and the platform may still have finished it (a timeout or a
|
|
24759
|
+
dropped connection can leave the registration verified). Before retrying, run:
|
|
24760
|
+
${getCommand(opts.issuerId, challengeContext(opts))}
|
|
24761
|
+
`
|
|
24762
|
+
);
|
|
24763
|
+
}
|
|
24764
|
+
return 1;
|
|
24765
|
+
}
|
|
24766
|
+
});
|
|
24767
|
+
}
|
|
23005
24768
|
|
|
23006
24769
|
// src/commands/namespace.ts
|
|
23007
24770
|
function renderNamespace(n) {
|
|
@@ -23107,7 +24870,7 @@ ${renderNamespace(ns)}
|
|
|
23107
24870
|
|
|
23108
24871
|
// src/commands/scripts.ts
|
|
23109
24872
|
var import_promises4 = require("fs/promises");
|
|
23110
|
-
var
|
|
24873
|
+
var readline6 = __toESM(require("readline/promises"));
|
|
23111
24874
|
var SOURCE_MAX_BYTES = 3e5;
|
|
23112
24875
|
var INPUT_CONTRACT_MAX_BYTES = 5e4;
|
|
23113
24876
|
var utf8Bytes2 = (s) => new TextEncoder().encode(s).length;
|
|
@@ -23245,6 +25008,7 @@ async function runScriptsList(opts, deps = {}) {
|
|
|
23245
25008
|
const rows = await drainPages(
|
|
23246
25009
|
(startFrom) => client.scripts.listScripts({
|
|
23247
25010
|
limit: PAGE_LIMIT,
|
|
25011
|
+
includeSource: true,
|
|
23248
25012
|
...opts.name ? { name: opts.name } : {},
|
|
23249
25013
|
...startFrom ? { startFrom } : {}
|
|
23250
25014
|
})
|
|
@@ -23305,10 +25069,9 @@ async function runScriptsDelete(opts, deps = {}) {
|
|
|
23305
25069
|
`
|
|
23306
25070
|
\u26A0 Deleting script version '${opts.id}' is permanent \u2014 the stored source is the only copy.
|
|
23307
25071
|
Other versions of the same name are unaffected.
|
|
23308
|
-
|
|
23309
|
-
|
|
23310
|
-
|
|
23311
|
-
against the API or the portal before removing a version a rule may still use.
|
|
25072
|
+
The platform refuses this delete if a trigger rule still references this version \u2014
|
|
25073
|
+
directly, or via 'latest' if this is the name's current newest version \u2014 and names the
|
|
25074
|
+
offending rule instead of deleting. If no rule references it, the delete proceeds.
|
|
23312
25075
|
And if this is the name's LATEST version, its identity is REUSED: the next push of that
|
|
23313
25076
|
name recomputes the same version number and the same id, so anything holding a pinned
|
|
23314
25077
|
'${opts.id}' or a name@version reference will silently resolve to different source.
|
|
@@ -23326,7 +25089,7 @@ async function runScriptsDelete(opts, deps = {}) {
|
|
|
23326
25089
|
return 2;
|
|
23327
25090
|
}
|
|
23328
25091
|
const promptCtx = deps.createPrompt ? deps.createPrompt() : (() => {
|
|
23329
|
-
const rl =
|
|
25092
|
+
const rl = readline6.createInterface({ input: process.stdin, output: process.stdout });
|
|
23330
25093
|
return { prompt: (q) => rl.question(q), close: () => rl.close() };
|
|
23331
25094
|
})();
|
|
23332
25095
|
try {
|
|
@@ -23441,16 +25204,36 @@ function defaultOpenBrowser(url) {
|
|
|
23441
25204
|
void (0, import_open.default)(url).catch(() => {
|
|
23442
25205
|
});
|
|
23443
25206
|
}
|
|
23444
|
-
|
|
23445
|
-
|
|
23446
|
-
|
|
23447
|
-
|
|
23448
|
-
|
|
23449
|
-
|
|
23450
|
-
|
|
23451
|
-
|
|
23452
|
-
|
|
23453
|
-
|
|
25207
|
+
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>';
|
|
25208
|
+
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>';
|
|
25209
|
+
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.";
|
|
25210
|
+
var REQUEST_TIMEOUT_MS = 1e4;
|
|
25211
|
+
async function startLoopback(ports, expectedState, onCode, onIgnored) {
|
|
25212
|
+
const server = (0, import_node_http.createServer)(
|
|
25213
|
+
{
|
|
25214
|
+
headersTimeout: REQUEST_TIMEOUT_MS,
|
|
25215
|
+
requestTimeout: REQUEST_TIMEOUT_MS,
|
|
25216
|
+
keepAliveTimeout: 1e3,
|
|
25217
|
+
connectionsCheckingInterval: 1e3
|
|
25218
|
+
},
|
|
25219
|
+
(req, res) => {
|
|
25220
|
+
const classified = classifyCallback(req.method, req.url, expectedState);
|
|
25221
|
+
if (classified.kind === "accepted") {
|
|
25222
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
25223
|
+
res.end(classified.params.error ? PAGE_SIGN_IN_FAILED : PAGE_SIGNED_IN);
|
|
25224
|
+
onCode(classified.params);
|
|
25225
|
+
return;
|
|
25226
|
+
}
|
|
25227
|
+
if (classified.kind === "wrong-state") {
|
|
25228
|
+
res.writeHead(400, { "Content-Type": "text/plain" });
|
|
25229
|
+
res.end(TEXT_NOT_THIS_SIGN_IN);
|
|
25230
|
+
onIgnored();
|
|
25231
|
+
return;
|
|
25232
|
+
}
|
|
25233
|
+
res.writeHead(404, { "Content-Type": "text/html" });
|
|
25234
|
+
res.end("<html><body>Not found</body></html>");
|
|
25235
|
+
}
|
|
25236
|
+
);
|
|
23454
25237
|
for (const port of ports) {
|
|
23455
25238
|
const ok = await new Promise((resolve) => {
|
|
23456
25239
|
const onErr = () => {
|
|
@@ -23463,7 +25246,10 @@ async function startLoopback(ports, onCode) {
|
|
|
23463
25246
|
resolve(true);
|
|
23464
25247
|
});
|
|
23465
25248
|
});
|
|
23466
|
-
if (ok)
|
|
25249
|
+
if (ok) {
|
|
25250
|
+
const address = server.address();
|
|
25251
|
+
return { server, port: typeof address === "object" && address ? address.port : port };
|
|
25252
|
+
}
|
|
23467
25253
|
}
|
|
23468
25254
|
throw new Error(`No free loopback port among ${ports.join(", ")} \u2014 close whatever is using them and retry.`);
|
|
23469
25255
|
}
|
|
@@ -23477,7 +25263,7 @@ async function runLogin(opts, deps = {}) {
|
|
|
23477
25263
|
try {
|
|
23478
25264
|
config = resolveCognitoConfig(opts.env);
|
|
23479
25265
|
} catch (err) {
|
|
23480
|
-
if (err instanceof MissingCognitoClientIdError) {
|
|
25266
|
+
if (err instanceof MissingCognitoClientIdError || err instanceof InvalidBaseUrlError || err instanceof InvalidCognitoRegionError) {
|
|
23481
25267
|
process.stderr.write(` \u2716 ${err.message}
|
|
23482
25268
|
`);
|
|
23483
25269
|
return 2;
|
|
@@ -23492,9 +25278,26 @@ async function runLogin(opts, deps = {}) {
|
|
|
23492
25278
|
const codePromise = new Promise((resolve) => {
|
|
23493
25279
|
resolveCode = resolve;
|
|
23494
25280
|
});
|
|
25281
|
+
let warnedIgnored = false;
|
|
25282
|
+
let settled = false;
|
|
25283
|
+
const warnIgnored = () => {
|
|
25284
|
+
if (settled || warnedIgnored) return;
|
|
25285
|
+
warnedIgnored = true;
|
|
25286
|
+
process.stderr.write(
|
|
25287
|
+
" ! 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"
|
|
25288
|
+
);
|
|
25289
|
+
};
|
|
23495
25290
|
let loopback;
|
|
23496
25291
|
try {
|
|
23497
|
-
loopback = await startLoopback(
|
|
25292
|
+
loopback = await startLoopback(
|
|
25293
|
+
ports,
|
|
25294
|
+
state,
|
|
25295
|
+
(params) => {
|
|
25296
|
+
settled = true;
|
|
25297
|
+
resolveCode(params);
|
|
25298
|
+
},
|
|
25299
|
+
warnIgnored
|
|
25300
|
+
);
|
|
23498
25301
|
} catch (err) {
|
|
23499
25302
|
process.stderr.write(` \u2716 ${formatError(err)}
|
|
23500
25303
|
`);
|
|
@@ -23524,10 +25327,7 @@ async function runLogin(opts, deps = {}) {
|
|
|
23524
25327
|
}
|
|
23525
25328
|
const params = await withTimeout(codePromise, timeoutMs);
|
|
23526
25329
|
if (params.error) {
|
|
23527
|
-
throw new Error(`authorization failed: ${params.error
|
|
23528
|
-
}
|
|
23529
|
-
if (params.state !== state) {
|
|
23530
|
-
throw new Error("state mismatch on the callback \u2014 aborting (possible CSRF).");
|
|
25330
|
+
throw new Error(`authorization failed: ${describeAuthorizationError(params.error, params.errorDescription)}`);
|
|
23531
25331
|
}
|
|
23532
25332
|
if (!params.code) {
|
|
23533
25333
|
throw new Error("no authorization code on the callback.");
|
|
@@ -23548,13 +25348,20 @@ async function runLogin(opts, deps = {}) {
|
|
|
23548
25348
|
return 0;
|
|
23549
25349
|
} catch (err) {
|
|
23550
25350
|
process.stderr.write(`
|
|
23551
|
-
\u2716 login failed: ${formatError(err)}
|
|
25351
|
+
\u2716 login failed: ${stripControlCharacters(formatError(err), 1e3)}
|
|
23552
25352
|
`);
|
|
23553
25353
|
return 1;
|
|
23554
25354
|
} finally {
|
|
23555
25355
|
loopback.server.close();
|
|
25356
|
+
loopback.server.closeAllConnections();
|
|
23556
25357
|
}
|
|
23557
25358
|
}
|
|
25359
|
+
var OAUTH_ERROR_CODE = /^[A-Za-z0-9_.-]{1,64}$/;
|
|
25360
|
+
function describeAuthorizationError(error, description) {
|
|
25361
|
+
const code = OAUTH_ERROR_CODE.test(error) ? error : "an unrecognised error";
|
|
25362
|
+
const text = description ? stripControlCharacters(description) : "";
|
|
25363
|
+
return text ? `${code} \u2014 ${text}` : code;
|
|
25364
|
+
}
|
|
23558
25365
|
async function runLogout(_opts, deps = {}) {
|
|
23559
25366
|
const removed = await clearSession(deps.baseDir);
|
|
23560
25367
|
process.stdout.write(removed ? " \u2714 Logged out (session cleared).\n" : " No stored session.\n");
|
|
@@ -23755,11 +25562,17 @@ async function runAssertions(client, bootstrapClient, deps, blueprint, tenantId,
|
|
|
23755
25562
|
for (const { issuerId } of result.issuerActions) {
|
|
23756
25563
|
try {
|
|
23757
25564
|
const issuer = await bootstrapClient.auth.getIssuer({ issuerId });
|
|
23758
|
-
const
|
|
25565
|
+
const declaredScope = blueprint.issuers?.find((i) => i.issuerId === issuerId)?.restrictedToDomain;
|
|
25566
|
+
const scopeOk = declaredScope === void 0 || (issuer.restrictedToDomain ?? "").toLowerCase() === declaredScope.toLowerCase();
|
|
25567
|
+
const ok = issuer.issuerId === issuerId && scopeOk;
|
|
23759
25568
|
assertions.push({
|
|
23760
25569
|
name: `issuer:${issuerId}`,
|
|
23761
25570
|
ok,
|
|
23762
|
-
detail:
|
|
25571
|
+
detail: !scopeOk ? `getIssuer(${issuerId}) reports restrictedToDomain '${issuer.restrictedToDomain ?? "(none)"}', the blueprint declares '${declaredScope}'` : ok ? (
|
|
25572
|
+
// A pending issuer is a PASS here: this harness cannot perform the identity-provider login that
|
|
25573
|
+
// verification needs, and registration is what the blueprint declares. Say so rather than hide it.
|
|
25574
|
+
`issuer '${issuerId}' registered + readable` + (issuer.status === "pending_verification" ? " (pending verification: it accepts no token until a login at its identity provider proves control)" : "")
|
|
25575
|
+
) : `getIssuer(${issuerId}) returned issuerId '${issuer.issuerId ?? "(none)"}'`
|
|
23763
25576
|
});
|
|
23764
25577
|
} catch (e) {
|
|
23765
25578
|
assertions.push({ name: `issuer:${issuerId}`, ok: false, detail: `getIssuer(${issuerId}) failed: ${errMsg(e)}` });
|
|
@@ -24070,6 +25883,11 @@ async function runBlueprintHarness(client, input, deps) {
|
|
|
24070
25883
|
// recording that it happened and nothing able to restore it. Opt out rather than grow the
|
|
24071
25884
|
// snapshot a leg teardown could not honour anyway.
|
|
24072
25885
|
pruneTriggers: false
|
|
25886
|
+
// `allowScriptAdoption` is left at its default `false` — the SAME created-only contract above
|
|
25887
|
+
// applies here: this harness may run against a shared tenant, and adopting a pre-existing,
|
|
25888
|
+
// unmarked script name would start this run's teardown treating (and, worse, later deleting)
|
|
25889
|
+
// versions on a name it does not own. A test blueprint that needs `scripts:` should ship a
|
|
25890
|
+
// fresh, collision-free name, exactly as the harness already expects for schemas/roles/issuers.
|
|
24073
25891
|
});
|
|
24074
25892
|
log.debug({ component: "harness", contextId: result.contextId, keyName: result.keyName }, "applied");
|
|
24075
25893
|
assertions = await runAssertions(
|
|
@@ -24283,7 +26101,7 @@ async function issueOrRotateKey(client, tenantId, context, userId, keyName) {
|
|
|
24283
26101
|
"The key already exists and its secret could not be re-minted (the prior key may not have cleared yet). Retry shortly."
|
|
24284
26102
|
);
|
|
24285
26103
|
}
|
|
24286
|
-
return { key: rotated, revokedKeyId: existing?.keyId };
|
|
26104
|
+
return { key: rotated, revokedKeyId: existing?.keyId ? [existing.keyId] : void 0 };
|
|
24287
26105
|
}
|
|
24288
26106
|
async function runJoin(opts, deps = {}) {
|
|
24289
26107
|
const hasRole = Boolean(opts.role?.length);
|
|
@@ -24430,9 +26248,9 @@ function emitJoined(opts) {
|
|
|
24430
26248
|
|
|
24431
26249
|
// src/build-info.ts
|
|
24432
26250
|
var BUILD_INFO = {
|
|
24433
|
-
cli: true ? "0.
|
|
24434
|
-
sdk: true ? "0.
|
|
24435
|
-
blueprints: true ? "0.
|
|
26251
|
+
cli: true ? "0.23.0" : "dev",
|
|
26252
|
+
sdk: true ? "0.45.0" : "dev",
|
|
26253
|
+
blueprints: true ? "0.20.0" : "dev"
|
|
24436
26254
|
};
|
|
24437
26255
|
function formatBuildInfo() {
|
|
24438
26256
|
return `vectros ${BUILD_INFO.cli} (sdk ${BUILD_INFO.sdk}, blueprints ${BUILD_INFO.blueprints})`;
|
|
@@ -24459,11 +26277,13 @@ function addCommonAuthOptions(cmd) {
|
|
|
24459
26277
|
"emit machine-readable JSON instead of the human-readable view"
|
|
24460
26278
|
);
|
|
24461
26279
|
}
|
|
26280
|
+
var TENANT_FLAG_INVALID = /* @__PURE__ */ Symbol("parseTenantFlag:invalid");
|
|
24462
26281
|
function parseTenantFlag(raw) {
|
|
24463
26282
|
if (raw !== void 0 && raw !== "test" && raw !== "live") {
|
|
24464
26283
|
process.stderr.write(` \u2716 --tenant must be 'test' or 'live' (got '${raw}')
|
|
24465
26284
|
`);
|
|
24466
|
-
process.
|
|
26285
|
+
process.exitCode = 2;
|
|
26286
|
+
return TENANT_FLAG_INVALID;
|
|
24467
26287
|
}
|
|
24468
26288
|
return raw;
|
|
24469
26289
|
}
|
|
@@ -24478,20 +26298,20 @@ function toCommonOptions(flags) {
|
|
|
24478
26298
|
}
|
|
24479
26299
|
function buildProgram(deps = {}) {
|
|
24480
26300
|
const program = new import_commander.Command();
|
|
24481
|
-
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(
|
|
26301
|
+
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(
|
|
24482
26302
|
"after",
|
|
24483
|
-
"\nProvisioning + authoring (own help):\n bootstrap One-shot least-privilege MCP credential. vectros bootstrap --help\n blueprint Author + inspect blueprints.
|
|
26303
|
+
"\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"
|
|
24484
26304
|
);
|
|
24485
26305
|
const login = program.command("login").description("Sign in via your browser (Cognito PKCE) and store a session.").addOption(new import_commander.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)");
|
|
24486
26306
|
login.action(async (flags) => {
|
|
24487
|
-
process.
|
|
26307
|
+
process.exitCode = await runLogin({ env: flags.env, json: false, noBrowser: flags.browser === false });
|
|
24488
26308
|
});
|
|
24489
26309
|
program.command("logout").description("Clear the stored login session.").action(async () => {
|
|
24490
|
-
process.
|
|
26310
|
+
process.exitCode = await runLogout({ env: "production", json: false });
|
|
24491
26311
|
});
|
|
24492
26312
|
const whoami = program.command("whoami").description("Show the authenticated session (tenant, env, principal).");
|
|
24493
26313
|
addCommonAuthOptions(whoami).action(async (flags) => {
|
|
24494
|
-
process.
|
|
26314
|
+
process.exitCode = await runWhoami(toCommonOptions(flags), deps);
|
|
24495
26315
|
});
|
|
24496
26316
|
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)");
|
|
24497
26317
|
addCommonAuthOptions(blueprintTest).action(
|
|
@@ -24499,14 +26319,13 @@ function buildProgram(deps = {}) {
|
|
|
24499
26319
|
if (flags.tenant !== void 0 && flags.tenant !== "test" && flags.tenant !== "live") {
|
|
24500
26320
|
process.stderr.write(` \u2716 --tenant must be 'test' or 'live' (got '${flags.tenant}')
|
|
24501
26321
|
`);
|
|
24502
|
-
process.
|
|
24503
|
-
|
|
24504
|
-
|
|
24505
|
-
|
|
24506
|
-
|
|
24507
|
-
|
|
24508
|
-
|
|
24509
|
-
)
|
|
26322
|
+
process.exitCode = 2;
|
|
26323
|
+
return;
|
|
26324
|
+
}
|
|
26325
|
+
process.exitCode = await runBlueprintTest(
|
|
26326
|
+
file,
|
|
26327
|
+
{ ...toCommonOptions(flags), keep: Boolean(flags.keep), tenant: flags.tenant },
|
|
26328
|
+
deps
|
|
24510
26329
|
);
|
|
24511
26330
|
}
|
|
24512
26331
|
);
|
|
@@ -24527,9 +26346,7 @@ function registerAssumeCommand(program, deps) {
|
|
|
24527
26346
|
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, []);
|
|
24528
26347
|
addCommonAuthOptions(assume).action(
|
|
24529
26348
|
async (flags) => {
|
|
24530
|
-
process.
|
|
24531
|
-
await runAssume({ ...toCommonOptions(flags), context: flags.context, scope: flags.scope ?? [] }, deps)
|
|
24532
|
-
);
|
|
26349
|
+
process.exitCode = await runAssume({ ...toCommonOptions(flags), context: flags.context, scope: flags.scope ?? [] }, deps);
|
|
24533
26350
|
}
|
|
24534
26351
|
);
|
|
24535
26352
|
}
|
|
@@ -24543,23 +26360,22 @@ function registerJoinCommand(program, deps) {
|
|
|
24543
26360
|
if (flags.tenant !== void 0 && flags.tenant !== "test" && flags.tenant !== "live") {
|
|
24544
26361
|
process.stderr.write(` \u2716 --tenant must be 'test' or 'live' (got '${flags.tenant}')
|
|
24545
26362
|
`);
|
|
24546
|
-
process.
|
|
26363
|
+
process.exitCode = 2;
|
|
26364
|
+
return;
|
|
24547
26365
|
}
|
|
24548
|
-
process.
|
|
24549
|
-
|
|
24550
|
-
|
|
24551
|
-
|
|
24552
|
-
|
|
24553
|
-
|
|
24554
|
-
|
|
24555
|
-
|
|
24556
|
-
|
|
24557
|
-
|
|
24558
|
-
|
|
24559
|
-
|
|
24560
|
-
|
|
24561
|
-
deps
|
|
24562
|
-
)
|
|
26366
|
+
process.exitCode = await runJoin(
|
|
26367
|
+
{
|
|
26368
|
+
...toCommonOptions(flags),
|
|
26369
|
+
contextArg,
|
|
26370
|
+
role: flags.role,
|
|
26371
|
+
actions: flags.actions,
|
|
26372
|
+
as: flags.as,
|
|
26373
|
+
scope: flags.scope,
|
|
26374
|
+
alias: flags.alias,
|
|
26375
|
+
tenant: flags.tenant,
|
|
26376
|
+
activate: flags.activate
|
|
26377
|
+
},
|
|
26378
|
+
deps
|
|
24563
26379
|
);
|
|
24564
26380
|
}
|
|
24565
26381
|
);
|
|
@@ -24568,29 +26384,27 @@ function registerKeyringCommands(program) {
|
|
|
24568
26384
|
const KEYRING_SHOW_FORMATS = ["human", "env", "raw"];
|
|
24569
26385
|
const keyring = program.command("keyring").description("Inspect and manage the local multi-principal keyring.");
|
|
24570
26386
|
keyring.command("list").description("List keyring entries and which one is active.").option("--json", "emit machine-readable JSON").action(async (flags) => {
|
|
24571
|
-
process.
|
|
26387
|
+
process.exitCode = await runKeyringList({ json: Boolean(flags.json) });
|
|
24572
26388
|
});
|
|
24573
26389
|
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) => {
|
|
24574
|
-
process.
|
|
26390
|
+
process.exitCode = await runKeyringUse(alias, { json: Boolean(flags.json) });
|
|
24575
26391
|
});
|
|
24576
26392
|
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 import_commander.Option("--format <fmt>", "how to emit the key").choices(KEYRING_SHOW_FORMATS).default("human")).action(async (flags) => {
|
|
24577
|
-
process.
|
|
26393
|
+
process.exitCode = await runKeyringShow({ alias: flags.alias, format: flags.format });
|
|
24578
26394
|
});
|
|
24579
26395
|
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) => {
|
|
24580
|
-
process.
|
|
26396
|
+
process.exitCode = await runKeyringDoctor({ json: Boolean(flags.json) });
|
|
24581
26397
|
});
|
|
24582
26398
|
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) => {
|
|
24583
|
-
process.
|
|
24584
|
-
await runKeyringPrune({ dryRun: Boolean(flags.dryRun), force: Boolean(flags.force), json: Boolean(flags.json) })
|
|
24585
|
-
);
|
|
26399
|
+
process.exitCode = await runKeyringPrune({ dryRun: Boolean(flags.dryRun), force: Boolean(flags.force), json: Boolean(flags.json) });
|
|
24586
26400
|
});
|
|
24587
26401
|
keyring.command("remove").description(
|
|
24588
26402
|
"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."
|
|
24589
26403
|
).argument("<alias>", "the keyring entry to remove").option("--json", "emit machine-readable JSON").action(async (alias, flags) => {
|
|
24590
|
-
process.
|
|
26404
|
+
process.exitCode = await runKeyringRemove(alias, { json: Boolean(flags.json) });
|
|
24591
26405
|
});
|
|
24592
26406
|
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) => {
|
|
24593
|
-
process.
|
|
26407
|
+
process.exitCode = await runKeyringUse(alias, { json: Boolean(flags.json) });
|
|
24594
26408
|
});
|
|
24595
26409
|
}
|
|
24596
26410
|
function registerContextCommands(program, deps) {
|
|
@@ -24601,18 +26415,17 @@ function registerContextCommands(program, deps) {
|
|
|
24601
26415
|
if (flags.tenant !== void 0 && flags.tenant !== "test" && flags.tenant !== "live") {
|
|
24602
26416
|
process.stderr.write(` \u2716 --tenant must be 'test' or 'live' (got '${flags.tenant}')
|
|
24603
26417
|
`);
|
|
24604
|
-
process.
|
|
26418
|
+
process.exitCode = 2;
|
|
26419
|
+
return;
|
|
24605
26420
|
}
|
|
24606
|
-
process.
|
|
24607
|
-
|
|
24608
|
-
|
|
24609
|
-
|
|
24610
|
-
|
|
24611
|
-
|
|
24612
|
-
|
|
24613
|
-
|
|
24614
|
-
deps
|
|
24615
|
-
)
|
|
26421
|
+
process.exitCode = await runContextCreate(
|
|
26422
|
+
{
|
|
26423
|
+
...toCommonOptions(flags),
|
|
26424
|
+
contextId,
|
|
26425
|
+
name: flags.name,
|
|
26426
|
+
tenant: flags.tenant
|
|
26427
|
+
},
|
|
26428
|
+
deps
|
|
24616
26429
|
);
|
|
24617
26430
|
}
|
|
24618
26431
|
);
|
|
@@ -24621,13 +26434,12 @@ function registerContextCommands(program, deps) {
|
|
|
24621
26434
|
if (flags.tenant !== void 0 && flags.tenant !== "test" && flags.tenant !== "live") {
|
|
24622
26435
|
process.stderr.write(` \u2716 --tenant must be 'test' or 'live' (got '${flags.tenant}')
|
|
24623
26436
|
`);
|
|
24624
|
-
process.
|
|
26437
|
+
process.exitCode = 2;
|
|
26438
|
+
return;
|
|
24625
26439
|
}
|
|
24626
|
-
process.
|
|
24627
|
-
|
|
24628
|
-
|
|
24629
|
-
deps
|
|
24630
|
-
)
|
|
26440
|
+
process.exitCode = await runContextList(
|
|
26441
|
+
{ ...toCommonOptions(flags), mine: Boolean(flags.mine), tenant: flags.tenant },
|
|
26442
|
+
deps
|
|
24631
26443
|
);
|
|
24632
26444
|
});
|
|
24633
26445
|
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)");
|
|
@@ -24635,13 +26447,12 @@ function registerContextCommands(program, deps) {
|
|
|
24635
26447
|
if (flags.tenant !== void 0 && flags.tenant !== "test" && flags.tenant !== "live") {
|
|
24636
26448
|
process.stderr.write(` \u2716 --tenant must be 'test' or 'live' (got '${flags.tenant}')
|
|
24637
26449
|
`);
|
|
24638
|
-
process.
|
|
26450
|
+
process.exitCode = 2;
|
|
26451
|
+
return;
|
|
24639
26452
|
}
|
|
24640
|
-
process.
|
|
24641
|
-
|
|
24642
|
-
|
|
24643
|
-
deps
|
|
24644
|
-
)
|
|
26453
|
+
process.exitCode = await runContextGet(
|
|
26454
|
+
{ ...toCommonOptions(flags), contextId, tenant: flags.tenant },
|
|
26455
|
+
deps
|
|
24645
26456
|
);
|
|
24646
26457
|
});
|
|
24647
26458
|
const destroy = context.command("destroy").description(
|
|
@@ -24652,18 +26463,17 @@ function registerContextCommands(program, deps) {
|
|
|
24652
26463
|
if (flags.tenant !== void 0 && flags.tenant !== "test" && flags.tenant !== "live") {
|
|
24653
26464
|
process.stderr.write(` \u2716 --tenant must be 'test' or 'live' (got '${flags.tenant}')
|
|
24654
26465
|
`);
|
|
24655
|
-
process.
|
|
26466
|
+
process.exitCode = 2;
|
|
26467
|
+
return;
|
|
24656
26468
|
}
|
|
24657
|
-
process.
|
|
24658
|
-
|
|
24659
|
-
|
|
24660
|
-
|
|
24661
|
-
|
|
24662
|
-
|
|
24663
|
-
|
|
24664
|
-
|
|
24665
|
-
deps
|
|
24666
|
-
)
|
|
26469
|
+
process.exitCode = await runContextDestroy(
|
|
26470
|
+
{
|
|
26471
|
+
...toCommonOptions(flags),
|
|
26472
|
+
contextId,
|
|
26473
|
+
force: Boolean(flags.force),
|
|
26474
|
+
tenant: flags.tenant
|
|
26475
|
+
},
|
|
26476
|
+
deps
|
|
24667
26477
|
);
|
|
24668
26478
|
}
|
|
24669
26479
|
);
|
|
@@ -24674,18 +26484,16 @@ function registerIssuersCommands(program, deps) {
|
|
|
24674
26484
|
addCommonAuthOptions(list).action(
|
|
24675
26485
|
async (flags) => {
|
|
24676
26486
|
const tenant = parseTenantFlag(flags.tenant);
|
|
24677
|
-
|
|
24678
|
-
|
|
24679
|
-
);
|
|
26487
|
+
if (tenant === TENANT_FLAG_INVALID) return;
|
|
26488
|
+
process.exitCode = await runIssuersList({ ...toCommonOptions(flags), context: flags.context, tenant }, deps);
|
|
24680
26489
|
}
|
|
24681
26490
|
);
|
|
24682
26491
|
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)");
|
|
24683
26492
|
addCommonAuthOptions(get).action(
|
|
24684
26493
|
async (issuerId, flags) => {
|
|
24685
26494
|
const tenant = parseTenantFlag(flags.tenant);
|
|
24686
|
-
|
|
24687
|
-
|
|
24688
|
-
);
|
|
26495
|
+
if (tenant === TENANT_FLAG_INVALID) return;
|
|
26496
|
+
process.exitCode = await runIssuersGet({ ...toCommonOptions(flags), context: flags.context, issuerId, tenant }, deps);
|
|
24689
26497
|
}
|
|
24690
26498
|
);
|
|
24691
26499
|
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(
|
|
@@ -24697,32 +26505,35 @@ function registerIssuersCommands(program, deps) {
|
|
|
24697
26505
|
"--captured-claim <name>",
|
|
24698
26506
|
"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",
|
|
24699
26507
|
collectOptional
|
|
26508
|
+
).option(
|
|
26509
|
+
"--restricted-to-domain <domain>",
|
|
26510
|
+
"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)"
|
|
24700
26511
|
).option("--tenant <tenant>", "tenant the context lives in: 'test' or 'live' (default live)");
|
|
24701
26512
|
addCommonAuthOptions(register).action(
|
|
24702
26513
|
async (flags) => {
|
|
24703
26514
|
const tenant = parseTenantFlag(flags.tenant);
|
|
24704
|
-
|
|
24705
|
-
|
|
24706
|
-
|
|
24707
|
-
|
|
24708
|
-
|
|
24709
|
-
|
|
24710
|
-
|
|
24711
|
-
|
|
24712
|
-
|
|
24713
|
-
|
|
24714
|
-
|
|
24715
|
-
|
|
24716
|
-
|
|
24717
|
-
|
|
24718
|
-
|
|
24719
|
-
|
|
24720
|
-
|
|
24721
|
-
|
|
26515
|
+
if (tenant === TENANT_FLAG_INVALID) return;
|
|
26516
|
+
process.exitCode = await runIssuersRegister(
|
|
26517
|
+
{
|
|
26518
|
+
...toCommonOptions(flags),
|
|
26519
|
+
context: flags.context,
|
|
26520
|
+
issuerId: flags.issuerId,
|
|
26521
|
+
issuer: flags.issuer,
|
|
26522
|
+
jwksUri: flags.jwksUri,
|
|
26523
|
+
audience: flags.audience,
|
|
26524
|
+
subClaim: flags.subClaim,
|
|
26525
|
+
emailClaim: flags.emailClaim,
|
|
26526
|
+
userinfoUri: flags.userinfoUri,
|
|
26527
|
+
selfSignupPolicy: flags.selfSignupPolicy,
|
|
26528
|
+
capturedClaim: flags.capturedClaim,
|
|
26529
|
+
restrictedToDomain: flags.restrictedToDomain,
|
|
26530
|
+
tenant
|
|
26531
|
+
},
|
|
26532
|
+
deps
|
|
24722
26533
|
);
|
|
24723
26534
|
}
|
|
24724
26535
|
);
|
|
24725
|
-
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(
|
|
26536
|
+
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(
|
|
24726
26537
|
"--self-signup-policy <type:roleId>",
|
|
24727
26538
|
"replace self-signup policies, repeatable (e.g. default:member) \u2014 omit entirely to leave unchanged",
|
|
24728
26539
|
collect,
|
|
@@ -24731,28 +26542,32 @@ function registerIssuersCommands(program, deps) {
|
|
|
24731
26542
|
"--captured-claim <name>",
|
|
24732
26543
|
"replace the additional OIDC claims captured beyond the email claim, repeatable \u2014 omit entirely to leave unchanged",
|
|
24733
26544
|
collectOptional
|
|
24734
|
-
).option("--clear-captured-claims", "capture nothing beyond the email claim (sends an empty list; XOR --captured-claim)").option(
|
|
26545
|
+
).option("--clear-captured-claims", "capture nothing beyond the email claim (sends an empty list; XOR --captured-claim)").option(
|
|
26546
|
+
"--restricted-to-domain <domain>",
|
|
26547
|
+
"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"
|
|
26548
|
+
).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)");
|
|
24735
26549
|
addCommonAuthOptions(update).action(
|
|
24736
26550
|
async (issuerId, flags) => {
|
|
24737
26551
|
const tenant = parseTenantFlag(flags.tenant);
|
|
24738
|
-
|
|
24739
|
-
|
|
24740
|
-
|
|
24741
|
-
|
|
24742
|
-
|
|
24743
|
-
|
|
24744
|
-
|
|
24745
|
-
|
|
24746
|
-
|
|
24747
|
-
|
|
24748
|
-
|
|
24749
|
-
|
|
24750
|
-
|
|
24751
|
-
|
|
24752
|
-
|
|
24753
|
-
|
|
24754
|
-
|
|
24755
|
-
|
|
26552
|
+
if (tenant === TENANT_FLAG_INVALID) return;
|
|
26553
|
+
process.exitCode = await runIssuersUpdate(
|
|
26554
|
+
{
|
|
26555
|
+
...toCommonOptions(flags),
|
|
26556
|
+
context: flags.context,
|
|
26557
|
+
issuerId,
|
|
26558
|
+
subClaim: flags.subClaim,
|
|
26559
|
+
emailClaim: flags.emailClaim,
|
|
26560
|
+
userinfoUri: flags.userinfoUri,
|
|
26561
|
+
status: flags.status,
|
|
26562
|
+
selfSignupPolicy: flags.selfSignupPolicy,
|
|
26563
|
+
clearSelfSignupPolicies: Boolean(flags.clearSelfSignupPolicies),
|
|
26564
|
+
capturedClaim: flags.capturedClaim,
|
|
26565
|
+
clearCapturedClaims: Boolean(flags.clearCapturedClaims),
|
|
26566
|
+
restrictedToDomain: flags.restrictedToDomain,
|
|
26567
|
+
clearRestrictedToDomain: Boolean(flags.clearRestrictedToDomain),
|
|
26568
|
+
tenant
|
|
26569
|
+
},
|
|
26570
|
+
deps
|
|
24756
26571
|
);
|
|
24757
26572
|
}
|
|
24758
26573
|
);
|
|
@@ -24760,11 +26575,33 @@ function registerIssuersCommands(program, deps) {
|
|
|
24760
26575
|
addCommonAuthOptions(del).action(
|
|
24761
26576
|
async (issuerId, flags) => {
|
|
24762
26577
|
const tenant = parseTenantFlag(flags.tenant);
|
|
24763
|
-
|
|
24764
|
-
|
|
24765
|
-
|
|
24766
|
-
|
|
24767
|
-
|
|
26578
|
+
if (tenant === TENANT_FLAG_INVALID) return;
|
|
26579
|
+
process.exitCode = await runIssuersDelete(
|
|
26580
|
+
{ ...toCommonOptions(flags), context: flags.context, issuerId, force: Boolean(flags.force), tenant },
|
|
26581
|
+
deps
|
|
26582
|
+
);
|
|
26583
|
+
}
|
|
26584
|
+
);
|
|
26585
|
+
const verify = issuers.command("verify").description(
|
|
26586
|
+
"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)."
|
|
26587
|
+
).argument("<issuerId>").requiredOption("--context <c>", "app context the issuer is registered under").option(
|
|
26588
|
+
"--idp-token <jwt>",
|
|
26589
|
+
"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"
|
|
26590
|
+
).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)");
|
|
26591
|
+
addCommonAuthOptions(verify).action(
|
|
26592
|
+
async (issuerId, flags) => {
|
|
26593
|
+
const tenant = parseTenantFlag(flags.tenant);
|
|
26594
|
+
if (tenant === TENANT_FLAG_INVALID) return;
|
|
26595
|
+
process.exitCode = await runIssuersVerify(
|
|
26596
|
+
{
|
|
26597
|
+
...toCommonOptions(flags),
|
|
26598
|
+
context: flags.context,
|
|
26599
|
+
issuerId,
|
|
26600
|
+
idpToken: flags.idpToken,
|
|
26601
|
+
idpTokenFile: flags.idpTokenFile,
|
|
26602
|
+
tenant
|
|
26603
|
+
},
|
|
26604
|
+
deps
|
|
24768
26605
|
);
|
|
24769
26606
|
}
|
|
24770
26607
|
);
|
|
@@ -24780,33 +26617,30 @@ function registerNamespaceCommands(program, deps) {
|
|
|
24780
26617
|
` \u2716 --specificity-rank must be an integer between 0 and 1000000 (got '${flags.specificityRank}').
|
|
24781
26618
|
`
|
|
24782
26619
|
);
|
|
24783
|
-
process.
|
|
26620
|
+
process.exitCode = 2;
|
|
26621
|
+
return;
|
|
24784
26622
|
}
|
|
24785
|
-
process.
|
|
24786
|
-
|
|
24787
|
-
|
|
24788
|
-
|
|
24789
|
-
|
|
24790
|
-
|
|
24791
|
-
|
|
24792
|
-
|
|
24793
|
-
|
|
24794
|
-
|
|
24795
|
-
deps
|
|
24796
|
-
)
|
|
26623
|
+
process.exitCode = await runNamespaceRegister(
|
|
26624
|
+
{
|
|
26625
|
+
...toCommonOptions(flags),
|
|
26626
|
+
namespace: name,
|
|
26627
|
+
context: flags.context,
|
|
26628
|
+
specificityRank,
|
|
26629
|
+
entityBacked: Boolean(flags.entityBacked),
|
|
26630
|
+
defaultSchemaId: flags.defaultSchemaId
|
|
26631
|
+
},
|
|
26632
|
+
deps
|
|
24797
26633
|
);
|
|
24798
26634
|
}
|
|
24799
26635
|
);
|
|
24800
26636
|
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");
|
|
24801
26637
|
addCommonAuthOptions(list).action(async (flags) => {
|
|
24802
|
-
process.
|
|
26638
|
+
process.exitCode = await runNamespaceList({ ...toCommonOptions(flags), context: flags.context }, deps);
|
|
24803
26639
|
});
|
|
24804
26640
|
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");
|
|
24805
26641
|
addCommonAuthOptions(get).action(
|
|
24806
26642
|
async (name, flags) => {
|
|
24807
|
-
process.
|
|
24808
|
-
await runNamespaceGet({ ...toCommonOptions(flags), namespace: name, context: flags.context }, deps)
|
|
24809
|
-
);
|
|
26643
|
+
process.exitCode = await runNamespaceGet({ ...toCommonOptions(flags), namespace: name, context: flags.context }, deps);
|
|
24810
26644
|
}
|
|
24811
26645
|
);
|
|
24812
26646
|
}
|
|
@@ -24815,59 +26649,59 @@ function registerScriptsCommands(program, deps) {
|
|
|
24815
26649
|
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)");
|
|
24816
26650
|
addCommonAuthOptions(push).action(
|
|
24817
26651
|
async (name, flags) => {
|
|
24818
|
-
|
|
24819
|
-
|
|
24820
|
-
|
|
24821
|
-
|
|
24822
|
-
|
|
24823
|
-
|
|
24824
|
-
|
|
24825
|
-
|
|
24826
|
-
|
|
24827
|
-
|
|
24828
|
-
|
|
24829
|
-
|
|
24830
|
-
|
|
24831
|
-
|
|
26652
|
+
const tenant = parseTenantFlag(flags.tenant);
|
|
26653
|
+
if (tenant === TENANT_FLAG_INVALID) return;
|
|
26654
|
+
process.exitCode = await runScriptsPush(
|
|
26655
|
+
{
|
|
26656
|
+
...toCommonOptions(flags),
|
|
26657
|
+
name,
|
|
26658
|
+
context: flags.context,
|
|
26659
|
+
sourceFile: flags.sourceFile,
|
|
26660
|
+
inputContract: flags.inputContract,
|
|
26661
|
+
inputContractFile: flags.inputContractFile,
|
|
26662
|
+
ifChanged: Boolean(flags.ifChanged),
|
|
26663
|
+
tenant
|
|
26664
|
+
},
|
|
26665
|
+
deps
|
|
24832
26666
|
);
|
|
24833
26667
|
}
|
|
24834
26668
|
);
|
|
24835
26669
|
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)");
|
|
24836
26670
|
addCommonAuthOptions(list).action(
|
|
24837
26671
|
async (flags) => {
|
|
24838
|
-
|
|
24839
|
-
|
|
24840
|
-
|
|
24841
|
-
|
|
24842
|
-
|
|
26672
|
+
const tenant = parseTenantFlag(flags.tenant);
|
|
26673
|
+
if (tenant === TENANT_FLAG_INVALID) return;
|
|
26674
|
+
process.exitCode = await runScriptsList(
|
|
26675
|
+
{ ...toCommonOptions(flags), context: flags.context, name: flags.name, tenant },
|
|
26676
|
+
deps
|
|
24843
26677
|
);
|
|
24844
26678
|
}
|
|
24845
26679
|
);
|
|
24846
26680
|
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)");
|
|
24847
26681
|
addCommonAuthOptions(get).action(
|
|
24848
26682
|
async (id, flags) => {
|
|
24849
|
-
|
|
24850
|
-
|
|
24851
|
-
|
|
24852
|
-
|
|
24853
|
-
|
|
26683
|
+
const tenant = parseTenantFlag(flags.tenant);
|
|
26684
|
+
if (tenant === TENANT_FLAG_INVALID) return;
|
|
26685
|
+
process.exitCode = await runScriptsGet(
|
|
26686
|
+
{ ...toCommonOptions(flags), id, context: flags.context, tenant },
|
|
26687
|
+
deps
|
|
24854
26688
|
);
|
|
24855
26689
|
}
|
|
24856
26690
|
);
|
|
24857
26691
|
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)");
|
|
24858
26692
|
addCommonAuthOptions(del).action(
|
|
24859
26693
|
async (id, flags) => {
|
|
24860
|
-
|
|
24861
|
-
|
|
24862
|
-
|
|
24863
|
-
|
|
24864
|
-
|
|
24865
|
-
|
|
24866
|
-
|
|
24867
|
-
|
|
24868
|
-
|
|
24869
|
-
|
|
24870
|
-
|
|
26694
|
+
const tenant = parseTenantFlag(flags.tenant);
|
|
26695
|
+
if (tenant === TENANT_FLAG_INVALID) return;
|
|
26696
|
+
process.exitCode = await runScriptsDelete(
|
|
26697
|
+
{
|
|
26698
|
+
...toCommonOptions(flags),
|
|
26699
|
+
id,
|
|
26700
|
+
context: flags.context,
|
|
26701
|
+
tenant,
|
|
26702
|
+
force: Boolean(flags.force)
|
|
26703
|
+
},
|
|
26704
|
+
deps
|
|
24871
26705
|
);
|
|
24872
26706
|
}
|
|
24873
26707
|
);
|
|
@@ -24887,9 +26721,11 @@ function registerRoleCommands(program, deps) {
|
|
|
24887
26721
|
).option("--description <text>", "free-text description").option("--tenant <tenant>", "tenant to target: 'test' or 'live' (default live)");
|
|
24888
26722
|
addCommonAuthOptions(create).action(
|
|
24889
26723
|
async (flags) => {
|
|
26724
|
+
const tenant = parseTenantFlag(flags.tenant);
|
|
26725
|
+
if (tenant === TENANT_FLAG_INVALID) return;
|
|
24890
26726
|
process.exitCode = await runRoleCreate({
|
|
24891
26727
|
...toCommonOptions(flags),
|
|
24892
|
-
tenant
|
|
26728
|
+
tenant,
|
|
24893
26729
|
context: flags.context,
|
|
24894
26730
|
roleId: flags.roleId,
|
|
24895
26731
|
name: flags.name,
|
|
@@ -24903,16 +26739,20 @@ function registerRoleCommands(program, deps) {
|
|
|
24903
26739
|
);
|
|
24904
26740
|
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)");
|
|
24905
26741
|
addCommonAuthOptions(list).action(async (flags) => {
|
|
26742
|
+
const tenant = parseTenantFlag(flags.tenant);
|
|
26743
|
+
if (tenant === TENANT_FLAG_INVALID) return;
|
|
24906
26744
|
process.exitCode = await runRoleList(
|
|
24907
|
-
{ ...toCommonOptions(flags), tenant
|
|
26745
|
+
{ ...toCommonOptions(flags), tenant, context: flags.context },
|
|
24908
26746
|
deps
|
|
24909
26747
|
);
|
|
24910
26748
|
});
|
|
24911
26749
|
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)");
|
|
24912
26750
|
addCommonAuthOptions(get).action(
|
|
24913
26751
|
async (flags) => {
|
|
26752
|
+
const tenant = parseTenantFlag(flags.tenant);
|
|
26753
|
+
if (tenant === TENANT_FLAG_INVALID) return;
|
|
24914
26754
|
process.exitCode = await runRoleGet(
|
|
24915
|
-
{ ...toCommonOptions(flags), tenant
|
|
26755
|
+
{ ...toCommonOptions(flags), tenant, context: flags.context, roleId: flags.roleId },
|
|
24916
26756
|
deps
|
|
24917
26757
|
);
|
|
24918
26758
|
}
|
|
@@ -24920,8 +26760,10 @@ function registerRoleCommands(program, deps) {
|
|
|
24920
26760
|
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)");
|
|
24921
26761
|
addAuthOptions(del).action(
|
|
24922
26762
|
async (flags) => {
|
|
26763
|
+
const tenant = parseTenantFlag(flags.tenant);
|
|
26764
|
+
if (tenant === TENANT_FLAG_INVALID) return;
|
|
24923
26765
|
process.exitCode = await runRoleDelete(
|
|
24924
|
-
{ ...toCommonOptions(flags), tenant
|
|
26766
|
+
{ ...toCommonOptions(flags), tenant, context: flags.context, roleId: flags.roleId },
|
|
24925
26767
|
deps
|
|
24926
26768
|
);
|
|
24927
26769
|
}
|
|
@@ -24932,9 +26774,11 @@ function registerAccessCommands(program, deps) {
|
|
|
24932
26774
|
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)");
|
|
24933
26775
|
addCommonAuthOptions(grant).action(
|
|
24934
26776
|
async (flags) => {
|
|
26777
|
+
const tenant = parseTenantFlag(flags.tenant);
|
|
26778
|
+
if (tenant === TENANT_FLAG_INVALID) return;
|
|
24935
26779
|
process.exitCode = await runAccessGrant({
|
|
24936
26780
|
...toCommonOptions(flags),
|
|
24937
|
-
tenant
|
|
26781
|
+
tenant,
|
|
24938
26782
|
principal: flags.principal,
|
|
24939
26783
|
context: flags.context,
|
|
24940
26784
|
role: flags.role,
|
|
@@ -24947,8 +26791,10 @@ function registerAccessCommands(program, deps) {
|
|
|
24947
26791
|
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)");
|
|
24948
26792
|
addAuthOptions(revoke).action(
|
|
24949
26793
|
async (flags) => {
|
|
26794
|
+
const tenant = parseTenantFlag(flags.tenant);
|
|
26795
|
+
if (tenant === TENANT_FLAG_INVALID) return;
|
|
24950
26796
|
process.exitCode = await runAccessRevoke(
|
|
24951
|
-
{ ...toCommonOptions(flags), tenant
|
|
26797
|
+
{ ...toCommonOptions(flags), tenant, principal: flags.principal, context: flags.context },
|
|
24952
26798
|
deps
|
|
24953
26799
|
);
|
|
24954
26800
|
}
|
|
@@ -24956,8 +26802,10 @@ function registerAccessCommands(program, deps) {
|
|
|
24956
26802
|
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)");
|
|
24957
26803
|
addCommonAuthOptions(list).action(
|
|
24958
26804
|
async (flags) => {
|
|
26805
|
+
const tenant = parseTenantFlag(flags.tenant);
|
|
26806
|
+
if (tenant === TENANT_FLAG_INVALID) return;
|
|
24959
26807
|
process.exitCode = await runAccessList(
|
|
24960
|
-
{ ...toCommonOptions(flags), tenant
|
|
26808
|
+
{ ...toCommonOptions(flags), tenant, context: flags.context, principal: flags.principal },
|
|
24961
26809
|
deps
|
|
24962
26810
|
);
|
|
24963
26811
|
}
|
|
@@ -24965,8 +26813,10 @@ function registerAccessCommands(program, deps) {
|
|
|
24965
26813
|
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)");
|
|
24966
26814
|
addCommonAuthOptions(get).action(
|
|
24967
26815
|
async (flags) => {
|
|
26816
|
+
const tenant = parseTenantFlag(flags.tenant);
|
|
26817
|
+
if (tenant === TENANT_FLAG_INVALID) return;
|
|
24968
26818
|
process.exitCode = await runAccessGet(
|
|
24969
|
-
{ ...toCommonOptions(flags), tenant
|
|
26819
|
+
{ ...toCommonOptions(flags), tenant, principal: flags.principal, context: flags.context },
|
|
24970
26820
|
deps
|
|
24971
26821
|
);
|
|
24972
26822
|
}
|
|
@@ -24974,8 +26824,10 @@ function registerAccessCommands(program, deps) {
|
|
|
24974
26824
|
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)");
|
|
24975
26825
|
addCommonAuthOptions(explain).action(
|
|
24976
26826
|
async (flags) => {
|
|
26827
|
+
const tenant = parseTenantFlag(flags.tenant);
|
|
26828
|
+
if (tenant === TENANT_FLAG_INVALID) return;
|
|
24977
26829
|
process.exitCode = await runAccessExplain(
|
|
24978
|
-
{ ...toCommonOptions(flags), tenant
|
|
26830
|
+
{ ...toCommonOptions(flags), tenant, principal: flags.principal, context: flags.context },
|
|
24979
26831
|
deps
|
|
24980
26832
|
);
|
|
24981
26833
|
}
|
|
@@ -24992,31 +26844,27 @@ function registerIdentityCommands(program, deps) {
|
|
|
24992
26844
|
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");
|
|
24993
26845
|
addCommonAuthOptions(create).action(
|
|
24994
26846
|
async (flags) => {
|
|
24995
|
-
process.
|
|
24996
|
-
|
|
24997
|
-
|
|
24998
|
-
|
|
24999
|
-
|
|
25000
|
-
|
|
25001
|
-
|
|
25002
|
-
|
|
25003
|
-
|
|
25004
|
-
|
|
25005
|
-
}, deps)
|
|
25006
|
-
);
|
|
26847
|
+
process.exitCode = await runIdentityCreate({
|
|
26848
|
+
...toCommonOptions(flags),
|
|
26849
|
+
type: flags.type,
|
|
26850
|
+
externalId: flags.externalId,
|
|
26851
|
+
name: flags.name,
|
|
26852
|
+
email: flags.email,
|
|
26853
|
+
service: flags.service,
|
|
26854
|
+
scope: flags.scope,
|
|
26855
|
+
metadata: flags.metadata
|
|
26856
|
+
}, deps);
|
|
25007
26857
|
}
|
|
25008
26858
|
);
|
|
25009
26859
|
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));
|
|
25010
26860
|
addCommonAuthOptions(list).action(
|
|
25011
26861
|
async (flags) => {
|
|
25012
|
-
process.
|
|
25013
|
-
|
|
25014
|
-
|
|
25015
|
-
|
|
25016
|
-
|
|
25017
|
-
|
|
25018
|
-
}, deps)
|
|
25019
|
-
);
|
|
26862
|
+
process.exitCode = await runIdentityList({
|
|
26863
|
+
...toCommonOptions(flags),
|
|
26864
|
+
type: flags.type,
|
|
26865
|
+
externalId: flags.externalId,
|
|
26866
|
+
limit: flags.limit
|
|
26867
|
+
}, deps);
|
|
25020
26868
|
}
|
|
25021
26869
|
);
|
|
25022
26870
|
const listAll = identity.command("list-all").description(
|
|
@@ -25025,38 +26873,35 @@ function registerIdentityCommands(program, deps) {
|
|
|
25025
26873
|
addCommonAuthOptions(listAll).action(
|
|
25026
26874
|
async (flags) => {
|
|
25027
26875
|
const tenant = parseTenantFlag(flags.tenant);
|
|
25028
|
-
|
|
25029
|
-
|
|
25030
|
-
|
|
25031
|
-
|
|
25032
|
-
)
|
|
26876
|
+
if (tenant === TENANT_FLAG_INVALID) return;
|
|
26877
|
+
process.exitCode = await runIdentityListAll(
|
|
26878
|
+
{ ...toCommonOptions(flags), externalId: flags.externalId, limit: flags.limit, tenant },
|
|
26879
|
+
deps
|
|
25033
26880
|
);
|
|
25034
26881
|
}
|
|
25035
26882
|
);
|
|
25036
26883
|
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)");
|
|
25037
26884
|
addCommonAuthOptions(get).action(async (flags) => {
|
|
25038
|
-
process.
|
|
26885
|
+
process.exitCode = await runIdentityGet({ ...toCommonOptions(flags), type: flags.type, id: flags.id }, deps);
|
|
25039
26886
|
});
|
|
25040
26887
|
const del = identity.command("delete").description("Delete a principal by its Vectros id.").addOption(dimensionOption()).requiredOption("--id <vectrosId>", "the Vectros-assigned id");
|
|
25041
26888
|
addAuthOptions(del).action(async (flags) => {
|
|
25042
|
-
process.
|
|
26889
|
+
process.exitCode = await runIdentityDelete({ ...toCommonOptions(flags), type: flags.type, id: flags.id }, deps);
|
|
25043
26890
|
});
|
|
25044
26891
|
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");
|
|
25045
26892
|
addCommonAuthOptions(invite).action(
|
|
25046
26893
|
async (flags) => {
|
|
25047
|
-
process.
|
|
25048
|
-
|
|
25049
|
-
|
|
25050
|
-
|
|
25051
|
-
|
|
25052
|
-
|
|
25053
|
-
|
|
25054
|
-
|
|
25055
|
-
|
|
25056
|
-
|
|
25057
|
-
|
|
25058
|
-
deps
|
|
25059
|
-
)
|
|
26894
|
+
process.exitCode = await runIdentityInvite(
|
|
26895
|
+
{
|
|
26896
|
+
...toCommonOptions(flags),
|
|
26897
|
+
context: flags.context,
|
|
26898
|
+
role: flags.role,
|
|
26899
|
+
email: flags.email,
|
|
26900
|
+
firstName: flags.firstName,
|
|
26901
|
+
acceptUrl: flags.acceptUrl,
|
|
26902
|
+
noSendEmail: flags.sendEmail === false
|
|
26903
|
+
},
|
|
26904
|
+
deps
|
|
25060
26905
|
);
|
|
25061
26906
|
}
|
|
25062
26907
|
);
|
|
@@ -25082,17 +26927,16 @@ function registerKeyCommands(program, deps) {
|
|
|
25082
26927
|
);
|
|
25083
26928
|
addAuthOptions(issue).action(async (flags) => {
|
|
25084
26929
|
const tenant = parseTenantFlag(flags.tenant);
|
|
25085
|
-
|
|
25086
|
-
|
|
25087
|
-
|
|
25088
|
-
|
|
25089
|
-
|
|
25090
|
-
|
|
25091
|
-
|
|
25092
|
-
|
|
25093
|
-
|
|
25094
|
-
|
|
25095
|
-
);
|
|
26930
|
+
if (tenant === TENANT_FLAG_INVALID) return;
|
|
26931
|
+
process.exitCode = await runKeyIssue({
|
|
26932
|
+
...toCommonOptions(flags),
|
|
26933
|
+
principal: flags.principal,
|
|
26934
|
+
context: flags.context,
|
|
26935
|
+
name: flags.name,
|
|
26936
|
+
label: flags.label,
|
|
26937
|
+
tenant,
|
|
26938
|
+
...toKeyDestination(flags)
|
|
26939
|
+
}, deps);
|
|
25096
26940
|
});
|
|
25097
26941
|
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(
|
|
25098
26942
|
"--all-contexts",
|
|
@@ -25102,20 +26946,20 @@ function registerKeyCommands(program, deps) {
|
|
|
25102
26946
|
async (flags) => {
|
|
25103
26947
|
if (flags.allContexts && flags.tenant !== void 0) {
|
|
25104
26948
|
process.stderr.write(" \u2716 --tenant is not combinable with --all-contexts (which already spans both tenants).\n");
|
|
25105
|
-
process.
|
|
26949
|
+
process.exitCode = 2;
|
|
26950
|
+
return;
|
|
25106
26951
|
}
|
|
25107
26952
|
const tenant = parseTenantFlag(flags.tenant);
|
|
25108
|
-
|
|
25109
|
-
|
|
25110
|
-
|
|
25111
|
-
|
|
25112
|
-
|
|
25113
|
-
|
|
25114
|
-
|
|
25115
|
-
|
|
25116
|
-
|
|
25117
|
-
|
|
25118
|
-
)
|
|
26953
|
+
if (tenant === TENANT_FLAG_INVALID) return;
|
|
26954
|
+
process.exitCode = await runKeyList(
|
|
26955
|
+
{
|
|
26956
|
+
...toCommonOptions(flags),
|
|
26957
|
+
principal: flags.principal,
|
|
26958
|
+
context: flags.context,
|
|
26959
|
+
allContexts: Boolean(flags.allContexts),
|
|
26960
|
+
tenant
|
|
26961
|
+
},
|
|
26962
|
+
deps
|
|
25119
26963
|
);
|
|
25120
26964
|
}
|
|
25121
26965
|
);
|
|
@@ -25123,14 +26967,16 @@ function registerKeyCommands(program, deps) {
|
|
|
25123
26967
|
addCommonAuthOptions(get).action(
|
|
25124
26968
|
async (keyId, flags) => {
|
|
25125
26969
|
const tenant = parseTenantFlag(flags.tenant);
|
|
25126
|
-
|
|
26970
|
+
if (tenant === TENANT_FLAG_INVALID) return;
|
|
26971
|
+
process.exitCode = await runKeyGet(keyId, { ...toCommonOptions(flags), context: flags.context, tenant }, deps);
|
|
25127
26972
|
}
|
|
25128
26973
|
);
|
|
25129
26974
|
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)");
|
|
25130
26975
|
addAuthOptions(revoke).action(
|
|
25131
26976
|
async (keyId, flags) => {
|
|
25132
26977
|
const tenant = parseTenantFlag(flags.tenant);
|
|
25133
|
-
|
|
26978
|
+
if (tenant === TENANT_FLAG_INVALID) return;
|
|
26979
|
+
process.exitCode = await runKeyRevoke(keyId, { ...toCommonOptions(flags), context: flags.context, tenant }, deps);
|
|
25134
26980
|
}
|
|
25135
26981
|
);
|
|
25136
26982
|
const rotate = addKeyDestinationOptions(
|
|
@@ -25141,17 +26987,16 @@ function registerKeyCommands(program, deps) {
|
|
|
25141
26987
|
);
|
|
25142
26988
|
addAuthOptions(rotate).action(async (flags) => {
|
|
25143
26989
|
const tenant = parseTenantFlag(flags.tenant);
|
|
25144
|
-
|
|
25145
|
-
|
|
25146
|
-
|
|
25147
|
-
|
|
25148
|
-
|
|
25149
|
-
|
|
25150
|
-
|
|
25151
|
-
|
|
25152
|
-
|
|
25153
|
-
|
|
25154
|
-
);
|
|
26990
|
+
if (tenant === TENANT_FLAG_INVALID) return;
|
|
26991
|
+
process.exitCode = await runKeyRotate({
|
|
26992
|
+
...toCommonOptions(flags),
|
|
26993
|
+
principal: flags.principal,
|
|
26994
|
+
context: flags.context,
|
|
26995
|
+
name: flags.name,
|
|
26996
|
+
forceNew: flags.forceNew,
|
|
26997
|
+
tenant,
|
|
26998
|
+
...toKeyDestination(flags)
|
|
26999
|
+
}, deps);
|
|
25155
27000
|
});
|
|
25156
27001
|
}
|
|
25157
27002
|
|
|
@@ -25164,7 +27009,8 @@ async function main() {
|
|
|
25164
27009
|
const sub = process.argv[2];
|
|
25165
27010
|
const passthrough = sub ? PASSTHROUGH_COMMANDS[sub] : void 0;
|
|
25166
27011
|
if (passthrough) {
|
|
25167
|
-
process.
|
|
27012
|
+
process.exitCode = await passthrough(process.argv.slice(3));
|
|
27013
|
+
return;
|
|
25168
27014
|
}
|
|
25169
27015
|
await buildProgram().parseAsync(process.argv);
|
|
25170
27016
|
}
|
|
@@ -25175,6 +27021,6 @@ main().catch((err) => {
|
|
|
25175
27021
|
process.stderr.write(`${err.stack}
|
|
25176
27022
|
`);
|
|
25177
27023
|
}
|
|
25178
|
-
process.
|
|
27024
|
+
process.exitCode = 2;
|
|
25179
27025
|
});
|
|
25180
27026
|
//# sourceMappingURL=cli.js.map
|