@vectros-ai/cli 0.22.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 +37 -16
- package/CHANGELOG.md +124 -1
- package/README.md +101 -10
- package/dist/cli.js +1742 -239
- package/dist/cli.js.map +1 -1
- package/dist/cli.mjs +1743 -240
- 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) {
|
|
@@ -1614,7 +1614,7 @@ var require_proper_lockfile = __commonJS({
|
|
|
1614
1614
|
// src/cli-bootstrap.ts
|
|
1615
1615
|
var readline2 = __toESM(require("readline/promises"));
|
|
1616
1616
|
|
|
1617
|
-
// node_modules/@vectros-ai/sdk/dist/index.mjs
|
|
1617
|
+
// ../../node_modules/@vectros-ai/sdk/dist/index.mjs
|
|
1618
1618
|
var __defProp2 = Object.defineProperty;
|
|
1619
1619
|
var __export = (target, all) => {
|
|
1620
1620
|
for (var name in all)
|
|
@@ -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`, `userinfoUri`, `capturedClaims`, `restrictedToDomain` (
|
|
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.
|
|
@@ -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 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
|
|
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.
|
|
@@ -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.
|
|
@@ -12379,6 +12458,10 @@ var BlueprintServicePrincipalSchema = import_zod.z.object({
|
|
|
12379
12458
|
displayName: safeDisplayString(import_zod.z.string().min(1))
|
|
12380
12459
|
}).strict();
|
|
12381
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
|
+
}
|
|
12382
12465
|
var MAX_CAPTURED_CLAIMS = 20;
|
|
12383
12466
|
var MAX_CAPTURED_CLAIM_NAME_LENGTH = 64;
|
|
12384
12467
|
function assertNoDuplicateClaimNames(field) {
|
|
@@ -12443,6 +12526,26 @@ var BlueprintIssuerSchema = import_zod.z.object({
|
|
|
12443
12526
|
// `safeUrlString` — see `issuer`'s own comment above for why these three trust-anchor
|
|
12444
12527
|
// fields get the stronger positive check instead of `safeDisplayString`'s blocklist.
|
|
12445
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(),
|
|
12446
12549
|
/**
|
|
12447
12550
|
* Opt-in self-service signup: a list of {signupType, roleId} pairs. When a first-time exchange
|
|
12448
12551
|
* caller presents no invite token but names a signupType matching one of these (or omits it and
|
|
@@ -15616,6 +15719,357 @@ function renderActionableError(info) {
|
|
|
15616
15719
|
return null;
|
|
15617
15720
|
}
|
|
15618
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
|
+
|
|
15619
16073
|
// src/base-url.ts
|
|
15620
16074
|
var ALLOWED_HOST_APEX = "vectros.ai";
|
|
15621
16075
|
var ALLOWED_HOST_SUFFIX = ".vectros.ai";
|
|
@@ -15861,6 +16315,13 @@ var import_promises = require("fs/promises");
|
|
|
15861
16315
|
var import_node_os = require("os");
|
|
15862
16316
|
var import_node_path = require("path");
|
|
15863
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
|
+
|
|
15864
16325
|
// src/pkce.ts
|
|
15865
16326
|
var import_node_crypto = require("crypto");
|
|
15866
16327
|
|
|
@@ -15969,6 +16430,24 @@ function parseCallback(requestUrl) {
|
|
|
15969
16430
|
errorDescription: url.searchParams.get("error_description") ?? void 0
|
|
15970
16431
|
};
|
|
15971
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
|
+
}
|
|
15972
16451
|
|
|
15973
16452
|
// src/auth-store.ts
|
|
15974
16453
|
var DIR_NAME = ".vectros";
|
|
@@ -16014,7 +16493,7 @@ async function postToken(config, body, fetchImpl) {
|
|
|
16014
16493
|
});
|
|
16015
16494
|
const text = await res.text().catch(() => "");
|
|
16016
16495
|
if (!res.ok) {
|
|
16017
|
-
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)}` : ""}`);
|
|
16018
16497
|
}
|
|
16019
16498
|
try {
|
|
16020
16499
|
return JSON.parse(text);
|
|
@@ -16783,6 +17262,12 @@ async function drainPages(fetch2) {
|
|
|
16783
17262
|
return out;
|
|
16784
17263
|
}
|
|
16785
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
|
+
|
|
16786
17271
|
// src/loader.ts
|
|
16787
17272
|
function actionOf(created) {
|
|
16788
17273
|
return created ? "created" : "reconciled";
|
|
@@ -16970,14 +17455,16 @@ async function pruneUndeclaredTriggers(args) {
|
|
|
16970
17455
|
{ component: "loader", err: extractServerError(err) },
|
|
16971
17456
|
"could not list trigger rules \u2014 skipping the trigger reconcile entirely; a trigger removed from this blueprint may still be live"
|
|
16972
17457
|
);
|
|
16973
|
-
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 }] };
|
|
16974
17459
|
}
|
|
16975
17460
|
for (const row of live) {
|
|
16976
17461
|
if (!row.name || !row.id || !row.principalId) {
|
|
16977
17462
|
if (!declaredNames.has(row.name ?? "")) {
|
|
17463
|
+
const ours = row.provisionedBy === blueprintName;
|
|
16978
17464
|
skipped.push({
|
|
16979
17465
|
name: row.name,
|
|
16980
|
-
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
|
|
16981
17468
|
});
|
|
16982
17469
|
}
|
|
16983
17470
|
continue;
|
|
@@ -16986,7 +17473,8 @@ async function pruneUndeclaredTriggers(args) {
|
|
|
16986
17473
|
if (!row.provisionedBy) {
|
|
16987
17474
|
skipped.push({
|
|
16988
17475
|
name: row.name,
|
|
16989
|
-
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
|
|
16990
17478
|
});
|
|
16991
17479
|
continue;
|
|
16992
17480
|
}
|
|
@@ -17007,7 +17495,8 @@ async function pruneUndeclaredTriggers(args) {
|
|
|
17007
17495
|
);
|
|
17008
17496
|
skipped.push({
|
|
17009
17497
|
name: row.name,
|
|
17010
|
-
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
|
|
17011
17500
|
});
|
|
17012
17501
|
continue;
|
|
17013
17502
|
}
|
|
@@ -17018,7 +17507,8 @@ async function pruneUndeclaredTriggers(args) {
|
|
|
17018
17507
|
);
|
|
17019
17508
|
skipped.push({
|
|
17020
17509
|
name: row.name,
|
|
17021
|
-
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
|
|
17022
17512
|
});
|
|
17023
17513
|
continue;
|
|
17024
17514
|
}
|
|
@@ -17031,7 +17521,7 @@ async function pruneUndeclaredTriggers(args) {
|
|
|
17031
17521
|
{ component: "loader", triggerName: row.name, principalId: row.principalId, err: extractServerError(err) },
|
|
17032
17522
|
"could not delete a removed trigger's access profile \u2014 leaving the trigger rule in place so the next apply retries"
|
|
17033
17523
|
);
|
|
17034
|
-
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 });
|
|
17035
17525
|
continue;
|
|
17036
17526
|
}
|
|
17037
17527
|
profileDeleted = false;
|
|
@@ -17043,7 +17533,7 @@ async function pruneUndeclaredTriggers(args) {
|
|
|
17043
17533
|
{ component: "loader", triggerName: row.name, err: extractServerError(err) },
|
|
17044
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"
|
|
17045
17535
|
);
|
|
17046
|
-
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 });
|
|
17047
17537
|
continue;
|
|
17048
17538
|
}
|
|
17049
17539
|
pruned.push({ name: row.name, principalId: row.principalId, profileDeleted });
|
|
@@ -17133,18 +17623,20 @@ var LostKeyError = class extends Error {
|
|
|
17133
17623
|
keyName;
|
|
17134
17624
|
};
|
|
17135
17625
|
var ExistingPrincipalNotConfirmedError = class extends Error {
|
|
17136
|
-
constructor(contextId, userId, externalId) {
|
|
17626
|
+
constructor(contextId, userId, externalId, mintsKey = true) {
|
|
17137
17627
|
super(
|
|
17138
|
-
`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.`
|
|
17139
17629
|
);
|
|
17140
17630
|
this.contextId = contextId;
|
|
17141
17631
|
this.userId = userId;
|
|
17142
17632
|
this.externalId = externalId;
|
|
17633
|
+
this.mintsKey = mintsKey;
|
|
17143
17634
|
this.name = "ExistingPrincipalNotConfirmedError";
|
|
17144
17635
|
}
|
|
17145
17636
|
contextId;
|
|
17146
17637
|
userId;
|
|
17147
17638
|
externalId;
|
|
17639
|
+
mintsKey;
|
|
17148
17640
|
};
|
|
17149
17641
|
function mapEnumValues(enumValues) {
|
|
17150
17642
|
if (!enumValues) return void 0;
|
|
@@ -17247,6 +17739,30 @@ function assertNoUnresolvedIdentities(blueprint) {
|
|
|
17247
17739
|
);
|
|
17248
17740
|
}
|
|
17249
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
|
+
}
|
|
17250
17766
|
var TenantWideNamespacesNotAllowedError = class extends Error {
|
|
17251
17767
|
constructor(message) {
|
|
17252
17768
|
super(message);
|
|
@@ -17302,7 +17818,12 @@ async function provisionContext(client, input) {
|
|
|
17302
17818
|
externalId: blueprint.servicePrincipal.externalId
|
|
17303
17819
|
}) : true;
|
|
17304
17820
|
if (!proceed) {
|
|
17305
|
-
throw new ExistingPrincipalNotConfirmedError(
|
|
17821
|
+
throw new ExistingPrincipalNotConfirmedError(
|
|
17822
|
+
blueprint.contextId,
|
|
17823
|
+
userId,
|
|
17824
|
+
blueprint.servicePrincipal.externalId,
|
|
17825
|
+
!input.skipKey
|
|
17826
|
+
);
|
|
17306
17827
|
}
|
|
17307
17828
|
log.debug(
|
|
17308
17829
|
{ component: "loader", contextId: blueprint.contextId, userId },
|
|
@@ -17320,7 +17841,14 @@ async function provisionContext(client, input) {
|
|
|
17320
17841
|
namespaceActions.push({ namespace: ns.namespace, action });
|
|
17321
17842
|
log.debug({ component: "loader", namespace: ns.namespace, contextId: blueprint.contextId, action }, "namespace ensured");
|
|
17322
17843
|
}
|
|
17323
|
-
return {
|
|
17844
|
+
return {
|
|
17845
|
+
contextAction,
|
|
17846
|
+
servicePrincipalUserId: userId,
|
|
17847
|
+
...user.createdAt === void 0 ? {} : { servicePrincipalCreatedAt: user.createdAt },
|
|
17848
|
+
userAction,
|
|
17849
|
+
namespaceActions,
|
|
17850
|
+
ctxBootstrapClient
|
|
17851
|
+
};
|
|
17324
17852
|
}
|
|
17325
17853
|
async function runLoader(client, input) {
|
|
17326
17854
|
const {
|
|
@@ -17328,6 +17856,7 @@ async function runLoader(client, input) {
|
|
|
17328
17856
|
tenantId,
|
|
17329
17857
|
machineId,
|
|
17330
17858
|
rotate = false,
|
|
17859
|
+
skipKey = false,
|
|
17331
17860
|
skipSeed = false,
|
|
17332
17861
|
seedIndexPoll,
|
|
17333
17862
|
provisioned,
|
|
@@ -17336,6 +17865,9 @@ async function runLoader(client, input) {
|
|
|
17336
17865
|
allowScriptAdoption = false,
|
|
17337
17866
|
log
|
|
17338
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
|
+
}
|
|
17339
17871
|
assertNoUnresolvedIdentities(blueprint);
|
|
17340
17872
|
const { contextAction, servicePrincipalUserId: userId, userAction, namespaceActions } = provisioned;
|
|
17341
17873
|
assertBlueprintGates(blueprint);
|
|
@@ -17359,13 +17891,15 @@ async function runLoader(client, input) {
|
|
|
17359
17891
|
subClaim: issuer.subClaim,
|
|
17360
17892
|
emailClaim: issuer.emailClaim,
|
|
17361
17893
|
userinfoUri: issuer.userinfoUri,
|
|
17894
|
+
restrictedToDomain: issuer.restrictedToDomain,
|
|
17362
17895
|
selfSignupPolicies,
|
|
17363
17896
|
capturedClaims: issuer.capturedClaims
|
|
17364
17897
|
});
|
|
17365
17898
|
const action = actionOf(issuerResp.created);
|
|
17366
|
-
|
|
17899
|
+
let observed = issuerResp;
|
|
17900
|
+
if (action === "created") assertIssuerScopeApplied(issuer, issuerResp.restrictedToDomain, "registered");
|
|
17367
17901
|
if (action === "reconciled") {
|
|
17368
|
-
await ctxBootstrapClient.auth.updateIssuer({
|
|
17902
|
+
const updated = await ctxBootstrapClient.auth.updateIssuer({
|
|
17369
17903
|
issuerId: issuer.issuerId,
|
|
17370
17904
|
// Trust-anchor/routing-pin fields — echoed back, never actually
|
|
17371
17905
|
// changed here (the platform rejects a differing value).
|
|
@@ -17376,13 +17910,35 @@ async function runLoader(client, input) {
|
|
|
17376
17910
|
subClaim: issuer.subClaim,
|
|
17377
17911
|
emailClaim: issuer.emailClaim,
|
|
17378
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,
|
|
17379
17918
|
selfSignupPolicies,
|
|
17380
17919
|
capturedClaims: issuer.capturedClaims
|
|
17381
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
|
+
}
|
|
17382
17930
|
log.debug({ component: "loader", issuerId: issuer.issuerId }, "issuer already registered \u2014 reconciled safe fields to the declared values");
|
|
17383
17931
|
} else {
|
|
17384
17932
|
log.debug({ component: "loader", issuerId: issuer.issuerId, action }, "issuer registered");
|
|
17385
17933
|
}
|
|
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
|
+
});
|
|
17386
17942
|
}
|
|
17387
17943
|
const ctxClient = input.remintForContext ? await input.remintForContext(blueprint.contextId) : client;
|
|
17388
17944
|
log.debug({ component: "loader", contextId: blueprint.contextId }, "re-minted per-context client");
|
|
@@ -17583,7 +18139,9 @@ async function runLoader(client, input) {
|
|
|
17583
18139
|
{ component: "loader", contextId: blueprint.contextId },
|
|
17584
18140
|
"trigger reconcile-by-absence skipped \u2014 no per-context client is wired, so a removed trigger may still be live"
|
|
17585
18141
|
);
|
|
17586
|
-
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
|
+
];
|
|
17587
18145
|
} else {
|
|
17588
18146
|
({ pruned: triggerPrunes, skipped: triggerPruneSkipped } = await pruneUndeclaredTriggers({
|
|
17589
18147
|
ctxClient,
|
|
@@ -17593,74 +18151,76 @@ async function runLoader(client, input) {
|
|
|
17593
18151
|
log
|
|
17594
18152
|
}));
|
|
17595
18153
|
}
|
|
17596
|
-
const keyName = keyNameFor(blueprint.name, machineId);
|
|
17597
|
-
let keyAction = "created";
|
|
18154
|
+
const keyName = skipKey ? "" : keyNameFor(blueprint.name, machineId);
|
|
18155
|
+
let keyAction = skipKey ? "skipped" : "created";
|
|
17598
18156
|
let revokedKeyId;
|
|
17599
|
-
|
|
17600
|
-
|
|
17601
|
-
|
|
17602
|
-
|
|
17603
|
-
|
|
17604
|
-
|
|
17605
|
-
|
|
17606
|
-
|
|
17607
|
-
|
|
17608
|
-
activeKeys = await drainPages(() => ctxClient.auth.listScopedKeys());
|
|
17609
|
-
} catch (err) {
|
|
17610
|
-
throw new Error(
|
|
17611
|
-
`--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.`,
|
|
17612
|
-
{ cause: err }
|
|
17613
|
-
);
|
|
17614
|
-
}
|
|
17615
|
-
const matches = activeKeys.filter(
|
|
17616
|
-
(k) => k.contextId === blueprint.contextId && k.userId === userId && k.keyName === keyName && k.status === "active" && k.keyId
|
|
17617
|
-
);
|
|
17618
|
-
if (matches.length > 1) {
|
|
17619
|
-
log.warn(
|
|
17620
|
-
{ component: "loader", keyName, contextId: blueprint.contextId, count: matches.length },
|
|
17621
|
-
"rotate: more than one active key matched this context/user/name tuple server-side \u2014 revoking all of them"
|
|
17622
|
-
);
|
|
17623
|
-
}
|
|
17624
|
-
if (matches.length > 0) {
|
|
17625
|
-
const revokedIds = [];
|
|
17626
|
-
for (const match of matches) {
|
|
17627
|
-
await ctxClient.auth.revokeScopedKey({ keyId: match.keyId });
|
|
17628
|
-
revokedIds.push(match.keyId);
|
|
17629
|
-
}
|
|
17630
|
-
log.debug(
|
|
17631
|
-
{ component: "loader", keyIds: revokedIds },
|
|
17632
|
-
"revoked prior key(s) for rotate \u2014 resolved server-side, no local record existed"
|
|
17633
|
-
);
|
|
17634
|
-
revokedKeyId = revokedIds;
|
|
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];
|
|
17635
18166
|
} else {
|
|
17636
|
-
|
|
17637
|
-
|
|
17638
|
-
|
|
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
|
|
17639
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
|
+
}
|
|
17640
18202
|
}
|
|
18203
|
+
if (revokedKeyId) keyAction = "rotated";
|
|
17641
18204
|
}
|
|
17642
|
-
|
|
17643
|
-
|
|
17644
|
-
|
|
17645
|
-
|
|
17646
|
-
|
|
17647
|
-
|
|
17648
|
-
|
|
17649
|
-
|
|
17650
|
-
|
|
17651
|
-
await
|
|
17652
|
-
|
|
17653
|
-
|
|
17654
|
-
|
|
17655
|
-
|
|
17656
|
-
|
|
17657
|
-
|
|
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";
|
|
17658
18221
|
}
|
|
17659
|
-
|
|
17660
|
-
keyId = existing.keyId;
|
|
17661
|
-
keyAction = "reused";
|
|
18222
|
+
log.debug({ component: "loader", keyName, keyAction }, "scoped key resolved");
|
|
17662
18223
|
}
|
|
17663
|
-
log.debug({ component: "loader", keyName, keyAction }, "scoped key resolved");
|
|
17664
18224
|
let seededCount = 0;
|
|
17665
18225
|
const seededRecordIds = [];
|
|
17666
18226
|
const seededDocumentIds = [];
|
|
@@ -17768,8 +18328,7 @@ async function runLoader(client, input) {
|
|
|
17768
18328
|
};
|
|
17769
18329
|
}
|
|
17770
18330
|
function sanitizeForPlanDisplay(value, maxLen = 200) {
|
|
17771
|
-
|
|
17772
|
-
return stripped.length > maxLen ? `${stripped.slice(0, maxLen)}\u2026` : stripped;
|
|
18331
|
+
return stripControlCharacters(value, maxLen);
|
|
17773
18332
|
}
|
|
17774
18333
|
function renderPlanDataScope(ds) {
|
|
17775
18334
|
const entries = Object.entries(ds ?? {});
|
|
@@ -17800,11 +18359,12 @@ function buildPlan(blueprint, opts = {}) {
|
|
|
17800
18359
|
const issuerUrl = sanitizeForPlanDisplay(iss.issuer);
|
|
17801
18360
|
const jwksNote = `, jwksUri: ${sanitizeForPlanDisplay(iss.jwksUri)}`;
|
|
17802
18361
|
const userinfoNote = iss.userinfoUri ? `, userinfoUri: ${sanitizeForPlanDisplay(iss.userinfoUri)}` : "";
|
|
18362
|
+
const domainNote = iss.restrictedToDomain ? `, restricted to domain: ${sanitizeForPlanDisplay(iss.restrictedToDomain)}` : "";
|
|
17803
18363
|
const selfSignup = iss.selfSignupPolicies?.length ? `, self-signup: [${iss.selfSignupPolicies.map((p) => `${p.signupType}\u2192${p.roleId}`).join(", ")}]` : "";
|
|
17804
18364
|
const captured = iss.capturedClaims?.length ? `, captured claims: [${iss.capturedClaims.map((c) => sanitizeForPlanDisplay(c)).join(", ")}]` : "";
|
|
17805
18365
|
steps.push({
|
|
17806
18366
|
kind: "issuer",
|
|
17807
|
-
summary: `issuer '${iss.issuerId}' (${issuerUrl} \u2192 context '${iss.contextId}'${jwksNote}${userinfoNote}${selfSignup}${captured}) [${BLUEPRINT_FIELD_PHASES.issuers}-token phase]`
|
|
18367
|
+
summary: `issuer '${iss.issuerId}' (${issuerUrl} \u2192 context '${iss.contextId}'${jwksNote}${userinfoNote}${domainNote}${selfSignup}${captured}) [${BLUEPRINT_FIELD_PHASES.issuers}-token phase]`
|
|
17808
18368
|
});
|
|
17809
18369
|
}
|
|
17810
18370
|
for (const s of blueprint.schemas) {
|
|
@@ -17894,7 +18454,9 @@ function buildPlan(blueprint, opts = {}) {
|
|
|
17894
18454
|
});
|
|
17895
18455
|
}
|
|
17896
18456
|
}
|
|
17897
|
-
|
|
18457
|
+
if (!opts.noKey) {
|
|
18458
|
+
steps.push({ kind: "key", summary: `key ${blueprint.name}-<machine> (mint 1 scoped ssk_*)` });
|
|
18459
|
+
}
|
|
17898
18460
|
const seeds = blueprint.seed ?? [];
|
|
17899
18461
|
if (seeds.length > 0 && !opts.noSeed) {
|
|
17900
18462
|
const docs = seeds.filter((s) => s.surface === "document").length;
|
|
@@ -17906,20 +18468,32 @@ function buildPlan(blueprint, opts = {}) {
|
|
|
17906
18468
|
}
|
|
17907
18469
|
return steps;
|
|
17908
18470
|
}
|
|
17909
|
-
function renderPlan(blueprint, steps) {
|
|
18471
|
+
function renderPlan(blueprint, steps, opts = {}) {
|
|
17910
18472
|
const header = ` Plan for blueprint '${blueprint.name}' (v${blueprint.version}) \u2192 context '${blueprint.contextId}':`;
|
|
17911
18473
|
const legendLines = [
|
|
17912
18474
|
` Apply is a reconcile \u2014 each item is created if absent, else updated in place to`,
|
|
17913
18475
|
` match. Nothing is recreated, and nothing is dropped except as noted below.`
|
|
17914
18476
|
];
|
|
17915
|
-
|
|
17916
|
-
|
|
17917
|
-
|
|
17918
|
-
|
|
17919
|
-
|
|
17920
|
-
|
|
17921
|
-
|
|
17922
|
-
|
|
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
|
+
}
|
|
17923
18497
|
legendLines.push(
|
|
17924
18498
|
` Counter-exception: a script is never updated in place \u2014 a changed source ADDS a new`,
|
|
17925
18499
|
` immutable version and every earlier one persists, which is also how a 'latest' rule`,
|
|
@@ -17932,12 +18506,19 @@ function renderPlan(blueprint, steps) {
|
|
|
17932
18506
|
` and would break any trigger still referencing it. Remove one deliberately with`,
|
|
17933
18507
|
` 'vectros scripts delete', after 'vectros scripts list' shows you what is there.`
|
|
17934
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
|
+
}
|
|
17935
18516
|
if (steps.some((s) => s.kind === "issuer")) {
|
|
17936
18517
|
legendLines.push(
|
|
17937
18518
|
` Exception: an issuer's trust-anchor fields (issuer, jwksUri, audience,`,
|
|
17938
18519
|
` contextId) are immutable once registered \u2014 only subClaim/emailClaim/`,
|
|
17939
|
-
` userinfoUri/selfSignupPolicies actually reconcile
|
|
17940
|
-
` 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).`
|
|
17941
18522
|
);
|
|
17942
18523
|
}
|
|
17943
18524
|
if (steps.some((s) => s.kind === "namespace")) {
|
|
@@ -17958,6 +18539,104 @@ function renderPlan(blueprint, steps) {
|
|
|
17958
18539
|
return [header, legend, "", ...lines].join("\n");
|
|
17959
18540
|
}
|
|
17960
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
|
+
|
|
17961
18640
|
// src/config-merge.ts
|
|
17962
18641
|
var import_node_fs2 = require("fs");
|
|
17963
18642
|
var import_node_os3 = __toESM(require("os"));
|
|
@@ -20094,6 +20773,7 @@ function parseBootstrapArgs(argv) {
|
|
|
20094
20773
|
noSeed: false,
|
|
20095
20774
|
allowTenantWideNamespaces: false,
|
|
20096
20775
|
allowScriptAdoption: false,
|
|
20776
|
+
requireVerifiedIssuers: false,
|
|
20097
20777
|
client: "desktop",
|
|
20098
20778
|
tenant: "live",
|
|
20099
20779
|
help: false
|
|
@@ -20161,6 +20841,9 @@ function parseBootstrapArgs(argv) {
|
|
|
20161
20841
|
case "--allow-script-adoption":
|
|
20162
20842
|
args.allowScriptAdoption = true;
|
|
20163
20843
|
break;
|
|
20844
|
+
case "--require-verified-issuers":
|
|
20845
|
+
args.requireVerifiedIssuers = true;
|
|
20846
|
+
break;
|
|
20164
20847
|
case "--tenant": {
|
|
20165
20848
|
const v = argv[++i];
|
|
20166
20849
|
if (v !== "test" && v !== "live") {
|
|
@@ -20233,6 +20916,31 @@ function seedIndexWarningBlock(failures) {
|
|
|
20233
20916
|
` + lines.join("") + ` They're stored and retrievable by id, but won't appear in search until the issue above is resolved.
|
|
20234
20917
|
`;
|
|
20235
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
|
+
}
|
|
20236
20944
|
function reconcileTally(items, noun) {
|
|
20237
20945
|
if (items.length === 0) return null;
|
|
20238
20946
|
const created = items.filter((i) => i.action === "created").length;
|
|
@@ -20268,13 +20976,13 @@ function scriptTally(items) {
|
|
|
20268
20976
|
if (unchanged) seg.push(`${unchanged} already current \u2014 nothing written`);
|
|
20269
20977
|
return `${items.length} script${items.length === 1 ? "" : "s"} (${seg.join("; ")})`;
|
|
20270
20978
|
}
|
|
20271
|
-
function applyOutcomeSummary(result) {
|
|
20979
|
+
function applyOutcomeSummary(result, opts = {}) {
|
|
20272
20980
|
const parts = [
|
|
20273
20981
|
reconcileTally(result.schemaActions, "schema"),
|
|
20274
20982
|
reconcileTally(result.roleActions, "role"),
|
|
20275
20983
|
// The platform's issuer partial-update endpoint makes an issuer's
|
|
20276
20984
|
// 'reconciled' mean the same "updated to match" as every other kind here.
|
|
20277
|
-
|
|
20985
|
+
issuerTally(result.issuerActions),
|
|
20278
20986
|
namespaceTally(result.namespaceActions),
|
|
20279
20987
|
// Scripts — reported BEFORE triggers, mirroring the order they are provisioned in, and with
|
|
20280
20988
|
// their own wording because "reconciled" would be a lie: a script version is immutable.
|
|
@@ -20287,7 +20995,7 @@ function applyOutcomeSummary(result) {
|
|
|
20287
20995
|
`profile ${result.profileAction}`
|
|
20288
20996
|
].filter((p) => p !== null);
|
|
20289
20997
|
const detail = parts.length ? ` \u2014 ${parts.join("; ")}` : "";
|
|
20290
|
-
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)
|
|
20291
20999
|
` : "";
|
|
20292
21000
|
const pruneNotes = result.triggerPrunes.map(
|
|
20293
21001
|
(p) => ` \u26A0 trigger '${p.name}' removed from the blueprint \u2014 de-provisioned (rule + access profile deleted)
|
|
@@ -20298,13 +21006,31 @@ function applyOutcomeSummary(result) {
|
|
|
20298
21006
|
`)
|
|
20299
21007
|
).join("");
|
|
20300
21008
|
const pruneSkippedNotes = result.triggerPruneSkipped.map(
|
|
20301
|
-
(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.
|
|
20302
21012
|
` : ` \u26A0 triggers could not be reconciled this run \u2014 ${s.reason}. A trigger removed from this blueprint may still be live.
|
|
20303
21013
|
`
|
|
20304
21014
|
).join("");
|
|
20305
21015
|
return ` \u2714 context '${result.contextId}' ${result.contextAction}${detail}
|
|
20306
21016
|
${principalNote}${pruneNotes}${pruneSkippedNotes}`;
|
|
20307
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
|
+
}
|
|
20308
21034
|
var BOOTSTRAP_HELP = `vectros bootstrap \u2014 provision a least-privilege MCP credential.
|
|
20309
21035
|
|
|
20310
21036
|
Usage:
|
|
@@ -20312,78 +21038,28 @@ Usage:
|
|
|
20312
21038
|
[--set name=value ...] [--values <file>]
|
|
20313
21039
|
[--tenant test|live | --test] [--client desktop|code]
|
|
20314
21040
|
[--yes] [--no-merge] [--rotate] [--confirm-existing-principal]
|
|
21041
|
+
[--require-verified-issuers]
|
|
20315
21042
|
[--token <t> | env VECTROS_BOOTSTRAP_TOKEN]
|
|
20316
21043
|
[--base-url <url>] [--use-scoped-token]
|
|
20317
21044
|
|
|
21045
|
+
To re-apply a blueprint into an existing context WITHOUT minting a key (CI/GitOps), use
|
|
21046
|
+
'vectros blueprint apply' instead.
|
|
21047
|
+
|
|
20318
21048
|
Options:
|
|
20319
|
-
|
|
20320
|
-
OR a path to your own blueprint YAML/JSON (./my.yaml) \u2014 the "copy &
|
|
20321
|
-
own" path. File blueprints get the identical scope gate (no override).
|
|
20322
|
-
--set name=value Set an install-time input value (repeatable) for a file blueprint's
|
|
20323
|
-
'inputs:' block. Beats --values + the declared default. (Same flag as
|
|
20324
|
-
'vectros blueprint validate/plan'.)
|
|
20325
|
-
--values <file> A YAML/JSON map of input values. (File blueprints only.)
|
|
20326
|
-
--blank Credential only (read-only data-plane profile, no schemas).
|
|
20327
|
-
--no-seed Provision the blueprint's schemas, profile and key but SKIP its
|
|
20328
|
-
sample seed records \u2014 the right choice for a production context
|
|
20329
|
-
(a blueprint's seeds are illustrative, not real data). Unlike
|
|
20330
|
-
--blank, the schemas are still created.
|
|
20331
|
-
--allow-tenant-wide-namespaces
|
|
20332
|
-
Required to apply a blueprint that declares a namespace with
|
|
20333
|
-
tenantWide: true \u2014 visible to every context in the account, not
|
|
20334
|
-
just this one. Without it, bootstrap refuses before provisioning
|
|
20335
|
-
anything, naming the namespace(s). Only an account OWNER's
|
|
20336
|
-
credential can actually complete the registration either way \u2014
|
|
20337
|
-
this flag doesn't change who's allowed, only whether this run
|
|
20338
|
-
tries. Never needed for a blueprint with no tenantWide namespace.
|
|
20339
|
-
--allow-script-adoption
|
|
20340
|
-
Required to push a script version onto a name that already exists in
|
|
20341
|
-
this context but carries no blueprint provenance marker (hand-authored,
|
|
20342
|
-
or pushed before this marker existed) \u2014 without it, bootstrap refuses
|
|
20343
|
-
before minting anything for that name, naming it. Never needed for a
|
|
20344
|
-
brand-new script name, or for re-applying a blueprint that already owns
|
|
20345
|
-
the name it's pushing to. Does NOT help if the name is already owned by
|
|
20346
|
-
a DIFFERENT blueprint \u2014 that is refused unconditionally, no flag past it.
|
|
20347
|
-
--tenant test|live Which tenant the minted key scopes to (default: live).
|
|
20348
|
-
'test' provisions into your isolated test tenant \u2014 a
|
|
20349
|
-
dry-run before pointing real data at the same blueprint.
|
|
20350
|
-
The tenant rides the key (ssk_test_* vs ssk_live_*), so
|
|
20351
|
-
the MCP/client config is otherwise identical.
|
|
20352
|
-
--test Shorthand for --tenant test.
|
|
20353
|
-
--activate Force the freshly-stored key to become your active identity, even
|
|
20354
|
-
if that would switch away from a currently-active LIVE credential
|
|
20355
|
-
(the one case --tenant test does NOT do this automatically).
|
|
20356
|
-
--no-activate Store the key without activating it, regardless of tenant.
|
|
20357
|
-
--base-url <url> Override the API base URL (beats VECTROS_API_BASE_URL).
|
|
20358
|
-
--client <desktop|code> MCP host to configure (default: desktop). 'desktop' merges
|
|
20359
|
-
claude_desktop_config.json (also works for Cursor/Cline);
|
|
20360
|
-
'code' targets Claude Code's project .mcp.json and prints
|
|
20361
|
-
the equivalent 'claude mcp add' command.
|
|
20362
|
-
--token <t> Cognito bridge token; or set VECTROS_BOOTSTRAP_TOKEN.
|
|
20363
|
-
Falls back to your stored \`vectros login\` session
|
|
20364
|
-
(same env) when neither is set \u2014 login once, then
|
|
20365
|
-
bootstrap unattended.
|
|
20366
|
-
--yes, -y Skip confirmations (agent / CI path).
|
|
20367
|
-
--no-merge Still mints/reconciles \u2014 this only changes where the result goes:
|
|
20368
|
-
print the config snippet instead of merging it into your client's
|
|
20369
|
-
config file. Not a dry run; see 'blueprint plan' for that. Does NOT
|
|
20370
|
-
skip the 'Apply this plan?' confirmation \u2014 pass --yes for that.
|
|
20371
|
-
--rotate Revoke + re-mint THIS machine's key.
|
|
20372
|
-
--confirm-existing-principal
|
|
20373
|
-
Required (with --yes, or with no TTY) to mint a NEW key bound to a
|
|
20374
|
-
service principal that already existed in your tenant before this run
|
|
20375
|
-
\u2014 e.g. a deliberate second machine for an integration you already own.
|
|
20376
|
-
Refused by default so an unexpected collision halts a CI run loudly
|
|
20377
|
-
instead of silently minting a credential bound to the wrong identity.
|
|
20378
|
-
Never needed for --rotate or an ordinary same-machine re-apply.
|
|
20379
|
-
--use-scoped-token Legacy fallback.
|
|
20380
|
-
NOTE: it mints a token WITHOUT the provisioning
|
|
20381
|
-
capability, so it cannot create a blueprint's
|
|
20382
|
-
app-context \u2014 use only with the 'default' context
|
|
20383
|
-
(the dedicated endpoint is required otherwise).
|
|
20384
|
-
--help, -h Show this help.
|
|
21049
|
+
${renderOptionHelp("bootstrap", BLUEPRINT_NAMES)}
|
|
20385
21050
|
|
|
20386
|
-
No --blueprint/--blank in a TTY \u2192 interactive menu
|
|
21051
|
+
No --blueprint/--blank in a TTY \u2192 interactive menu.
|
|
21052
|
+
|
|
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`;
|
|
20387
21063
|
async function resolveBlueprint(args, prompt, isTty) {
|
|
20388
21064
|
if (args.blankSlate) {
|
|
20389
21065
|
if (args.set.length > 0 || args.values) {
|
|
@@ -20485,7 +21161,7 @@ async function runBootstrapCli(argv, deps = {}) {
|
|
|
20485
21161
|
try {
|
|
20486
21162
|
args = parseBootstrapArgs(argv);
|
|
20487
21163
|
} catch (err) {
|
|
20488
|
-
process.stderr.write(`${err instanceof Error ? err.message : String(err)}
|
|
21164
|
+
process.stderr.write(`${redactCredentials(err instanceof Error ? err.message : String(err))}
|
|
20489
21165
|
|
|
20490
21166
|
${BOOTSTRAP_HELP}
|
|
20491
21167
|
`);
|
|
@@ -20527,21 +21203,7 @@ ${BOOTSTRAP_HELP}
|
|
|
20527
21203
|
}
|
|
20528
21204
|
throw err;
|
|
20529
21205
|
}
|
|
20530
|
-
|
|
20531
|
-
const acceptedActions = effectiveAccessProfileActions(blueprint);
|
|
20532
|
-
const allDataPlane = !acceptedActions.some(
|
|
20533
|
-
(a) => KNOWN_CONTROL_PLANE_RESOURCES.includes(a.split(":", 1)[0])
|
|
20534
|
-
);
|
|
20535
|
-
process.stdout.write(
|
|
20536
|
-
`
|
|
20537
|
-
Blueprint: ${blueprint.name}
|
|
20538
|
-
\u2714 Scope gate: [${acceptedActions.join(", ")}]${roleNote}` + (allDataPlane ? " \u2014 all data-plane \u2713\n" : " \u2014 includes a narrow, allowlisted control-plane exception (see above)\n")
|
|
20539
|
-
);
|
|
20540
|
-
if (blueprint.accessProfile.capabilities?.length) {
|
|
20541
|
-
const capGate = evaluateCapabilityGate(blueprint.accessProfile.capabilities);
|
|
20542
|
-
process.stdout.write(` \u2714 Capability gate: [${capGate.accepted.join(", ")}] \u2014 granted \u2713
|
|
20543
|
-
`);
|
|
20544
|
-
}
|
|
21206
|
+
process.stdout.write(gateSummary(blueprint));
|
|
20545
21207
|
let noSeed = args.noSeed;
|
|
20546
21208
|
let seedDeclinedInteractively = false;
|
|
20547
21209
|
if (shouldOfferSeedOptOut(args, blueprint, isTty)) {
|
|
@@ -20714,6 +21376,12 @@ ${renderPlan(blueprint, planSteps)}
|
|
|
20714
21376
|
}),
|
|
20715
21377
|
log
|
|
20716
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
|
+
);
|
|
20717
21385
|
process.stdout.write(
|
|
20718
21386
|
applyOutcomeSummary(result) + ` \u2714 ${args.tenant} key (${result.keyAction}) \u2192 ${result.keyName}
|
|
20719
21387
|
` + seedSummaryLine({
|
|
@@ -20749,7 +21417,7 @@ ${renderPlan(blueprint, planSteps)}
|
|
|
20749
21417
|
|
|
20750
21418
|
${snippet}
|
|
20751
21419
|
${codeAddHint}`);
|
|
20752
|
-
return
|
|
21420
|
+
return finishApply();
|
|
20753
21421
|
}
|
|
20754
21422
|
let doMerge = args.yes;
|
|
20755
21423
|
if (!doMerge && isTty) {
|
|
@@ -20763,7 +21431,7 @@ ${codeAddHint}`);
|
|
|
20763
21431
|
|
|
20764
21432
|
${snippet}
|
|
20765
21433
|
${codeAddHint}`);
|
|
20766
|
-
return
|
|
21434
|
+
return finishApply();
|
|
20767
21435
|
}
|
|
20768
21436
|
try {
|
|
20769
21437
|
const merged = await safeMergeConfigFile({ configPath, entry });
|
|
@@ -20784,7 +21452,7 @@ ${codeAddHint}`);
|
|
|
20784
21452
|
${snippet}
|
|
20785
21453
|
${codeAddHint}`);
|
|
20786
21454
|
}
|
|
20787
|
-
return
|
|
21455
|
+
return finishApply();
|
|
20788
21456
|
} catch (err) {
|
|
20789
21457
|
if (err instanceof LostKeyError) {
|
|
20790
21458
|
process.stderr.write(`
|
|
@@ -20813,8 +21481,17 @@ ${codeAddHint}`);
|
|
|
20813
21481
|
);
|
|
20814
21482
|
return 2;
|
|
20815
21483
|
}
|
|
20816
|
-
if (err instanceof
|
|
20817
|
-
process.stderr.write(
|
|
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
|
+
}
|
|
21493
|
+
if (err instanceof TenantWideNamespacesNotAllowedError) {
|
|
21494
|
+
process.stderr.write(`
|
|
20818
21495
|
\u2716 ${err.message}
|
|
20819
21496
|
Nothing was provisioned.
|
|
20820
21497
|
`);
|
|
@@ -20835,6 +21512,494 @@ ${formatCliError(err, { action: "Bootstrap failed" })}`);
|
|
|
20835
21512
|
// src/cli-blueprint.ts
|
|
20836
21513
|
var import_promises3 = require("fs/promises");
|
|
20837
21514
|
|
|
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);
|
|
21519
|
+
}
|
|
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}')`);
|
|
21555
|
+
}
|
|
21556
|
+
args.env = v;
|
|
21557
|
+
break;
|
|
21558
|
+
}
|
|
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;
|
|
21574
|
+
}
|
|
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
|
+
|
|
20838
22003
|
// src/lint.ts
|
|
20839
22004
|
var ENUM_COMPATIBLE_TYPES = /* @__PURE__ */ new Set(["enum", "string", "string[]"]);
|
|
20840
22005
|
var BUILT_IN_SORT_BY = /* @__PURE__ */ new Set(["createdAt", "lastUpdated"]);
|
|
@@ -21116,7 +22281,10 @@ function defaultScaffold(name) {
|
|
|
21116
22281
|
return `# ${name}.blueprint.yaml \u2014 a Vectros blueprint.
|
|
21117
22282
|
# Edit, then check it with: vectros blueprint validate ./${name}.blueprint.yaml
|
|
21118
22283
|
# Preview what it provisions: vectros blueprint plan ./${name}.blueprint.yaml
|
|
21119
|
-
# 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)
|
|
21120
22288
|
|
|
21121
22289
|
name: ${name} # stable id + idempotency key. Re-running NEVER duplicates.
|
|
21122
22290
|
version: 1.0.0
|
|
@@ -21135,9 +22303,9 @@ schemas:
|
|
|
21135
22303
|
fieldType: string # string | number | boolean | date | enum | array | object
|
|
21136
22304
|
required: true
|
|
21137
22305
|
searchable: true # contributes to full-text + semantic search
|
|
21138
|
-
|
|
21139
|
-
|
|
21140
|
-
|
|
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.
|
|
21141
22309
|
lookupFields: [title] # extra O(1) exact-match lookup indexes (max 10)
|
|
21142
22310
|
|
|
21143
22311
|
accessProfile:
|
|
@@ -21173,7 +22341,8 @@ servicePrincipal:
|
|
|
21173
22341
|
seed:
|
|
21174
22342
|
- surface: record # 'record' (createRecord) or 'document' (text-ingest: needs title + text)
|
|
21175
22343
|
typeName: thing
|
|
21176
|
-
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.
|
|
21177
22346
|
fields: { title: "Hello from ${name}" }
|
|
21178
22347
|
`;
|
|
21179
22348
|
}
|
|
@@ -21241,7 +22410,10 @@ Usage:
|
|
|
21241
22410
|
vectros blueprint init <name> [--from <bundled>] [--out <path>] [--force]
|
|
21242
22411
|
Scaffold a new blueprint YAML to edit (no creds).
|
|
21243
22412
|
vectros blueprint validate <file|name> Structural + scope-gate + lint checks (no creds).
|
|
21244
|
-
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).
|
|
21245
22417
|
vectros blueprint list List the bundled blueprint library.
|
|
21246
22418
|
|
|
21247
22419
|
<file|name> is a path to your own blueprint (YAML .yaml/.yml or JSON .json) OR
|
|
@@ -21366,10 +22538,17 @@ ${renderPlan(blueprint, steps)}
|
|
|
21366
22538
|
}
|
|
21367
22539
|
}
|
|
21368
22540
|
const setEcho = [...args.set].join(" --set ");
|
|
22541
|
+
const inputs = (args.set.length ? ` --set ${setEcho}` : "") + (args.values ? ` --values ${args.values}` : "");
|
|
21369
22542
|
process.stdout.write(
|
|
21370
22543
|
`
|
|
21371
22544
|
${steps.length} step(s). This is a preview \u2014 nothing was minted.
|
|
21372
|
-
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
|
+
` : "")
|
|
21373
22552
|
);
|
|
21374
22553
|
return 0;
|
|
21375
22554
|
}
|
|
@@ -21433,7 +22612,8 @@ function runList() {
|
|
|
21433
22612
|
}
|
|
21434
22613
|
process.stdout.write(
|
|
21435
22614
|
`
|
|
21436
|
-
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)
|
|
21437
22617
|
Scaffold your own: vectros blueprint init my-thing
|
|
21438
22618
|
Validate it: vectros blueprint validate ./my-thing.blueprint.yaml
|
|
21439
22619
|
`
|
|
@@ -21441,7 +22621,7 @@ function runList() {
|
|
|
21441
22621
|
return 0;
|
|
21442
22622
|
}
|
|
21443
22623
|
async function runBlueprintCli(argv, deps = {}) {
|
|
21444
|
-
|
|
22624
|
+
if (argv[0] === "apply") return runApplyCli(argv.slice(1), deps);
|
|
21445
22625
|
let args;
|
|
21446
22626
|
try {
|
|
21447
22627
|
args = parseBlueprintArgs(argv);
|
|
@@ -22931,7 +24111,7 @@ async function runRoleDelete(opts, deps = {}) {
|
|
|
22931
24111
|
}
|
|
22932
24112
|
|
|
22933
24113
|
// src/commands/context.ts
|
|
22934
|
-
var
|
|
24114
|
+
var readline4 = __toESM(require("readline/promises"));
|
|
22935
24115
|
function renderContext(c) {
|
|
22936
24116
|
return formatKeyValues([
|
|
22937
24117
|
["context", c.contextId],
|
|
@@ -23012,7 +24192,7 @@ async function runContextDestroy(opts, deps = {}) {
|
|
|
23012
24192
|
return 2;
|
|
23013
24193
|
}
|
|
23014
24194
|
const promptCtx = deps.createPrompt ? deps.createPrompt() : (() => {
|
|
23015
|
-
const rl =
|
|
24195
|
+
const rl = readline4.createInterface({ input: process.stdin, output: process.stdout });
|
|
23016
24196
|
return { prompt: (q) => rl.question(q), close: () => rl.close() };
|
|
23017
24197
|
})();
|
|
23018
24198
|
try {
|
|
@@ -23083,7 +24263,8 @@ ${renderContext(ctx)}
|
|
|
23083
24263
|
}
|
|
23084
24264
|
|
|
23085
24265
|
// src/commands/issuers.ts
|
|
23086
|
-
var
|
|
24266
|
+
var readline5 = __toESM(require("readline/promises"));
|
|
24267
|
+
var import_node_fs4 = require("fs");
|
|
23087
24268
|
function parseSelfSignupPolicies(raw) {
|
|
23088
24269
|
if (!raw || raw.length === 0) return void 0;
|
|
23089
24270
|
return raw.map((entry) => {
|
|
@@ -23122,6 +24303,24 @@ function parseCapturedClaims(raw) {
|
|
|
23122
24303
|
}
|
|
23123
24304
|
return [...raw];
|
|
23124
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
|
+
}
|
|
23125
24324
|
function renderCapturedClaims(claims) {
|
|
23126
24325
|
if (!claims || claims.length === 0) return "\u2014";
|
|
23127
24326
|
return claims.join(", ");
|
|
@@ -23131,6 +24330,7 @@ function renderSelfSignup(policies) {
|
|
|
23131
24330
|
return policies.map((p) => `${p.signup_type} \u2192 ${p.role_id}`).join(", ");
|
|
23132
24331
|
}
|
|
23133
24332
|
function renderIssuer(i) {
|
|
24333
|
+
const pending = isPendingVerification(i);
|
|
23134
24334
|
return formatKeyValues([
|
|
23135
24335
|
["issuerId", i.issuerId],
|
|
23136
24336
|
["issuer", i.issuer],
|
|
@@ -23143,9 +24343,22 @@ function renderIssuer(i) {
|
|
|
23143
24343
|
["status", i.status],
|
|
23144
24344
|
["createdAt", i.createdAt],
|
|
23145
24345
|
["selfSignup", renderSelfSignup(i.selfSignupPolicies)],
|
|
23146
|
-
["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
|
+
] : []
|
|
23147
24353
|
]);
|
|
23148
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
|
+
}
|
|
23149
24362
|
async function runIssuersList(opts, deps = {}) {
|
|
23150
24363
|
return withAuth({ ...opts, provisioning: true, context: opts.context }, deps, async (auth) => {
|
|
23151
24364
|
const client = auth.client;
|
|
@@ -23165,7 +24378,8 @@ async function runIssuersList(opts, deps = {}) {
|
|
|
23165
24378
|
{ header: "audience", get: (i) => i.audience },
|
|
23166
24379
|
{ header: "status", get: (i) => i.status },
|
|
23167
24380
|
{ header: "self-signup", get: (i) => renderSelfSignup(i.selfSignupPolicies) },
|
|
23168
|
-
{ 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) }
|
|
23169
24383
|
]) + "\n"
|
|
23170
24384
|
);
|
|
23171
24385
|
return 0;
|
|
@@ -23185,9 +24399,11 @@ async function runIssuersGet(opts, deps = {}) {
|
|
|
23185
24399
|
`);
|
|
23186
24400
|
return 0;
|
|
23187
24401
|
}
|
|
24402
|
+
const notes = renderPendingNotes(issuer, challengeContext(opts), (deps.now ?? Date.now)());
|
|
23188
24403
|
process.stdout.write(`
|
|
23189
24404
|
${renderIssuer(issuer)}
|
|
23190
|
-
`
|
|
24405
|
+
${notes ? `
|
|
24406
|
+
${notes}` : ""}`);
|
|
23191
24407
|
return 0;
|
|
23192
24408
|
} catch (err) {
|
|
23193
24409
|
process.stderr.write(formatCliError(err, { action: "issuers get failed" }));
|
|
@@ -23198,9 +24414,11 @@ ${renderIssuer(issuer)}
|
|
|
23198
24414
|
async function runIssuersRegister(opts, deps = {}) {
|
|
23199
24415
|
let selfSignupPolicies;
|
|
23200
24416
|
let capturedClaims;
|
|
24417
|
+
let restrictedToDomain;
|
|
23201
24418
|
try {
|
|
23202
24419
|
selfSignupPolicies = parseSelfSignupPolicies(opts.selfSignupPolicy);
|
|
23203
24420
|
capturedClaims = parseCapturedClaims(opts.capturedClaim);
|
|
24421
|
+
restrictedToDomain = parseRestrictedToDomain(opts.restrictedToDomain);
|
|
23204
24422
|
} catch (err) {
|
|
23205
24423
|
process.stderr.write(` \u2716 ${formatError(err)}
|
|
23206
24424
|
`);
|
|
@@ -23219,18 +24437,45 @@ async function runIssuersRegister(opts, deps = {}) {
|
|
|
23219
24437
|
emailClaim: opts.emailClaim,
|
|
23220
24438
|
userinfoUri: opts.userinfoUri,
|
|
23221
24439
|
selfSignupPolicies,
|
|
23222
|
-
capturedClaims
|
|
24440
|
+
capturedClaims,
|
|
24441
|
+
restrictedToDomain
|
|
23223
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
|
+
}
|
|
23224
24467
|
if (opts.json) {
|
|
23225
24468
|
process.stdout.write(`${formatJson(issuer)}
|
|
23226
24469
|
`);
|
|
23227
24470
|
return 0;
|
|
23228
24471
|
}
|
|
23229
24472
|
const verb = issuer.created === false ? "already registered \u2014 returned unchanged" : "registered";
|
|
24473
|
+
const notes = renderPendingNotes(issuer, challengeContext(opts), (deps.now ?? Date.now)());
|
|
23230
24474
|
process.stdout.write(`
|
|
23231
24475
|
\u2714 issuer '${opts.issuerId}' ${verb}
|
|
23232
24476
|
${renderIssuer(issuer)}
|
|
23233
|
-
`
|
|
24477
|
+
${notes ? `
|
|
24478
|
+
${notes}` : ""}`);
|
|
23234
24479
|
return 0;
|
|
23235
24480
|
} catch (err) {
|
|
23236
24481
|
process.stderr.write(formatCliError(err, { action: "issuers register failed" }));
|
|
@@ -23241,14 +24486,22 @@ ${renderIssuer(issuer)}
|
|
|
23241
24486
|
async function runIssuersUpdate(opts, deps = {}) {
|
|
23242
24487
|
let selfSignupPolicies;
|
|
23243
24488
|
let capturedClaims;
|
|
24489
|
+
let restrictedToDomain;
|
|
23244
24490
|
try {
|
|
23245
24491
|
selfSignupPolicies = parseSelfSignupPolicies(opts.selfSignupPolicy);
|
|
23246
24492
|
capturedClaims = parseCapturedClaims(opts.capturedClaim);
|
|
24493
|
+
restrictedToDomain = parseRestrictedToDomain(opts.restrictedToDomain);
|
|
23247
24494
|
} catch (err) {
|
|
23248
24495
|
process.stderr.write(` \u2716 ${formatError(err)}
|
|
23249
24496
|
`);
|
|
23250
24497
|
return 2;
|
|
23251
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
|
+
}
|
|
23252
24505
|
if (opts.clearSelfSignupPolicies && selfSignupPolicies && selfSignupPolicies.length > 0) {
|
|
23253
24506
|
process.stderr.write(
|
|
23254
24507
|
" \u2716 --clear-self-signup-policies and --self-signup-policy are mutually exclusive.\n"
|
|
@@ -23271,7 +24524,8 @@ async function runIssuersUpdate(opts, deps = {}) {
|
|
|
23271
24524
|
userinfoUri: opts.userinfoUri,
|
|
23272
24525
|
status: opts.status,
|
|
23273
24526
|
selfSignupPolicies: opts.clearSelfSignupPolicies ? [] : selfSignupPolicies,
|
|
23274
|
-
capturedClaims: opts.clearCapturedClaims ? [] : capturedClaims
|
|
24527
|
+
capturedClaims: opts.clearCapturedClaims ? [] : capturedClaims,
|
|
24528
|
+
restrictedToDomain: opts.clearRestrictedToDomain ? "" : restrictedToDomain
|
|
23275
24529
|
});
|
|
23276
24530
|
if (opts.json) {
|
|
23277
24531
|
process.stdout.write(`${formatJson(issuer)}
|
|
@@ -23302,7 +24556,7 @@ async function runIssuersDelete(opts, deps = {}) {
|
|
|
23302
24556
|
return 2;
|
|
23303
24557
|
}
|
|
23304
24558
|
const promptCtx = deps.createPrompt ? deps.createPrompt() : (() => {
|
|
23305
|
-
const rl =
|
|
24559
|
+
const rl = readline5.createInterface({ input: process.stdin, output: process.stdout });
|
|
23306
24560
|
return { prompt: (q) => rl.question(q), close: () => rl.close() };
|
|
23307
24561
|
})();
|
|
23308
24562
|
try {
|
|
@@ -23344,6 +24598,173 @@ async function runIssuersDelete(opts, deps = {}) {
|
|
|
23344
24598
|
}
|
|
23345
24599
|
});
|
|
23346
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
|
+
}
|
|
23347
24768
|
|
|
23348
24769
|
// src/commands/namespace.ts
|
|
23349
24770
|
function renderNamespace(n) {
|
|
@@ -23449,7 +24870,7 @@ ${renderNamespace(ns)}
|
|
|
23449
24870
|
|
|
23450
24871
|
// src/commands/scripts.ts
|
|
23451
24872
|
var import_promises4 = require("fs/promises");
|
|
23452
|
-
var
|
|
24873
|
+
var readline6 = __toESM(require("readline/promises"));
|
|
23453
24874
|
var SOURCE_MAX_BYTES = 3e5;
|
|
23454
24875
|
var INPUT_CONTRACT_MAX_BYTES = 5e4;
|
|
23455
24876
|
var utf8Bytes2 = (s) => new TextEncoder().encode(s).length;
|
|
@@ -23668,7 +25089,7 @@ async function runScriptsDelete(opts, deps = {}) {
|
|
|
23668
25089
|
return 2;
|
|
23669
25090
|
}
|
|
23670
25091
|
const promptCtx = deps.createPrompt ? deps.createPrompt() : (() => {
|
|
23671
|
-
const rl =
|
|
25092
|
+
const rl = readline6.createInterface({ input: process.stdin, output: process.stdout });
|
|
23672
25093
|
return { prompt: (q) => rl.question(q), close: () => rl.close() };
|
|
23673
25094
|
})();
|
|
23674
25095
|
try {
|
|
@@ -23783,16 +25204,36 @@ function defaultOpenBrowser(url) {
|
|
|
23783
25204
|
void (0, import_open.default)(url).catch(() => {
|
|
23784
25205
|
});
|
|
23785
25206
|
}
|
|
23786
|
-
|
|
23787
|
-
|
|
23788
|
-
|
|
23789
|
-
|
|
23790
|
-
|
|
23791
|
-
|
|
23792
|
-
|
|
23793
|
-
|
|
23794
|
-
|
|
23795
|
-
|
|
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
|
+
);
|
|
23796
25237
|
for (const port of ports) {
|
|
23797
25238
|
const ok = await new Promise((resolve) => {
|
|
23798
25239
|
const onErr = () => {
|
|
@@ -23805,7 +25246,10 @@ async function startLoopback(ports, onCode) {
|
|
|
23805
25246
|
resolve(true);
|
|
23806
25247
|
});
|
|
23807
25248
|
});
|
|
23808
|
-
if (ok)
|
|
25249
|
+
if (ok) {
|
|
25250
|
+
const address = server.address();
|
|
25251
|
+
return { server, port: typeof address === "object" && address ? address.port : port };
|
|
25252
|
+
}
|
|
23809
25253
|
}
|
|
23810
25254
|
throw new Error(`No free loopback port among ${ports.join(", ")} \u2014 close whatever is using them and retry.`);
|
|
23811
25255
|
}
|
|
@@ -23834,9 +25278,26 @@ async function runLogin(opts, deps = {}) {
|
|
|
23834
25278
|
const codePromise = new Promise((resolve) => {
|
|
23835
25279
|
resolveCode = resolve;
|
|
23836
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
|
+
};
|
|
23837
25290
|
let loopback;
|
|
23838
25291
|
try {
|
|
23839
|
-
loopback = await startLoopback(
|
|
25292
|
+
loopback = await startLoopback(
|
|
25293
|
+
ports,
|
|
25294
|
+
state,
|
|
25295
|
+
(params) => {
|
|
25296
|
+
settled = true;
|
|
25297
|
+
resolveCode(params);
|
|
25298
|
+
},
|
|
25299
|
+
warnIgnored
|
|
25300
|
+
);
|
|
23840
25301
|
} catch (err) {
|
|
23841
25302
|
process.stderr.write(` \u2716 ${formatError(err)}
|
|
23842
25303
|
`);
|
|
@@ -23866,10 +25327,7 @@ async function runLogin(opts, deps = {}) {
|
|
|
23866
25327
|
}
|
|
23867
25328
|
const params = await withTimeout(codePromise, timeoutMs);
|
|
23868
25329
|
if (params.error) {
|
|
23869
|
-
throw new Error(`authorization failed: ${params.error
|
|
23870
|
-
}
|
|
23871
|
-
if (params.state !== state) {
|
|
23872
|
-
throw new Error("state mismatch on the callback \u2014 aborting (possible CSRF).");
|
|
25330
|
+
throw new Error(`authorization failed: ${describeAuthorizationError(params.error, params.errorDescription)}`);
|
|
23873
25331
|
}
|
|
23874
25332
|
if (!params.code) {
|
|
23875
25333
|
throw new Error("no authorization code on the callback.");
|
|
@@ -23890,13 +25348,20 @@ async function runLogin(opts, deps = {}) {
|
|
|
23890
25348
|
return 0;
|
|
23891
25349
|
} catch (err) {
|
|
23892
25350
|
process.stderr.write(`
|
|
23893
|
-
\u2716 login failed: ${formatError(err)}
|
|
25351
|
+
\u2716 login failed: ${stripControlCharacters(formatError(err), 1e3)}
|
|
23894
25352
|
`);
|
|
23895
25353
|
return 1;
|
|
23896
25354
|
} finally {
|
|
23897
25355
|
loopback.server.close();
|
|
25356
|
+
loopback.server.closeAllConnections();
|
|
23898
25357
|
}
|
|
23899
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
|
+
}
|
|
23900
25365
|
async function runLogout(_opts, deps = {}) {
|
|
23901
25366
|
const removed = await clearSession(deps.baseDir);
|
|
23902
25367
|
process.stdout.write(removed ? " \u2714 Logged out (session cleared).\n" : " No stored session.\n");
|
|
@@ -24097,11 +25562,17 @@ async function runAssertions(client, bootstrapClient, deps, blueprint, tenantId,
|
|
|
24097
25562
|
for (const { issuerId } of result.issuerActions) {
|
|
24098
25563
|
try {
|
|
24099
25564
|
const issuer = await bootstrapClient.auth.getIssuer({ issuerId });
|
|
24100
|
-
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;
|
|
24101
25568
|
assertions.push({
|
|
24102
25569
|
name: `issuer:${issuerId}`,
|
|
24103
25570
|
ok,
|
|
24104
|
-
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)"}'`
|
|
24105
25576
|
});
|
|
24106
25577
|
} catch (e) {
|
|
24107
25578
|
assertions.push({ name: `issuer:${issuerId}`, ok: false, detail: `getIssuer(${issuerId}) failed: ${errMsg(e)}` });
|
|
@@ -24777,9 +26248,9 @@ function emitJoined(opts) {
|
|
|
24777
26248
|
|
|
24778
26249
|
// src/build-info.ts
|
|
24779
26250
|
var BUILD_INFO = {
|
|
24780
|
-
cli: true ? "0.
|
|
24781
|
-
sdk: true ? "0.
|
|
24782
|
-
blueprints: true ? "0.
|
|
26251
|
+
cli: true ? "0.23.0" : "dev",
|
|
26252
|
+
sdk: true ? "0.45.0" : "dev",
|
|
26253
|
+
blueprints: true ? "0.20.0" : "dev"
|
|
24783
26254
|
};
|
|
24784
26255
|
function formatBuildInfo() {
|
|
24785
26256
|
return `vectros ${BUILD_INFO.cli} (sdk ${BUILD_INFO.sdk}, blueprints ${BUILD_INFO.blueprints})`;
|
|
@@ -24827,9 +26298,9 @@ function toCommonOptions(flags) {
|
|
|
24827
26298
|
}
|
|
24828
26299
|
function buildProgram(deps = {}) {
|
|
24829
26300
|
const program = new import_commander.Command();
|
|
24830
|
-
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(
|
|
24831
26302
|
"after",
|
|
24832
|
-
"\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"
|
|
24833
26304
|
);
|
|
24834
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)");
|
|
24835
26306
|
login.action(async (flags) => {
|
|
@@ -25034,6 +26505,9 @@ function registerIssuersCommands(program, deps) {
|
|
|
25034
26505
|
"--captured-claim <name>",
|
|
25035
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",
|
|
25036
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)"
|
|
25037
26511
|
).option("--tenant <tenant>", "tenant the context lives in: 'test' or 'live' (default live)");
|
|
25038
26512
|
addCommonAuthOptions(register).action(
|
|
25039
26513
|
async (flags) => {
|
|
@@ -25052,13 +26526,14 @@ function registerIssuersCommands(program, deps) {
|
|
|
25052
26526
|
userinfoUri: flags.userinfoUri,
|
|
25053
26527
|
selfSignupPolicy: flags.selfSignupPolicy,
|
|
25054
26528
|
capturedClaim: flags.capturedClaim,
|
|
26529
|
+
restrictedToDomain: flags.restrictedToDomain,
|
|
25055
26530
|
tenant
|
|
25056
26531
|
},
|
|
25057
26532
|
deps
|
|
25058
26533
|
);
|
|
25059
26534
|
}
|
|
25060
26535
|
);
|
|
25061
|
-
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(
|
|
25062
26537
|
"--self-signup-policy <type:roleId>",
|
|
25063
26538
|
"replace self-signup policies, repeatable (e.g. default:member) \u2014 omit entirely to leave unchanged",
|
|
25064
26539
|
collect,
|
|
@@ -25067,7 +26542,10 @@ function registerIssuersCommands(program, deps) {
|
|
|
25067
26542
|
"--captured-claim <name>",
|
|
25068
26543
|
"replace the additional OIDC claims captured beyond the email claim, repeatable \u2014 omit entirely to leave unchanged",
|
|
25069
26544
|
collectOptional
|
|
25070
|
-
).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)");
|
|
25071
26549
|
addCommonAuthOptions(update).action(
|
|
25072
26550
|
async (issuerId, flags) => {
|
|
25073
26551
|
const tenant = parseTenantFlag(flags.tenant);
|
|
@@ -25085,6 +26563,8 @@ function registerIssuersCommands(program, deps) {
|
|
|
25085
26563
|
clearSelfSignupPolicies: Boolean(flags.clearSelfSignupPolicies),
|
|
25086
26564
|
capturedClaim: flags.capturedClaim,
|
|
25087
26565
|
clearCapturedClaims: Boolean(flags.clearCapturedClaims),
|
|
26566
|
+
restrictedToDomain: flags.restrictedToDomain,
|
|
26567
|
+
clearRestrictedToDomain: Boolean(flags.clearRestrictedToDomain),
|
|
25088
26568
|
tenant
|
|
25089
26569
|
},
|
|
25090
26570
|
deps
|
|
@@ -25102,6 +26582,29 @@ function registerIssuersCommands(program, deps) {
|
|
|
25102
26582
|
);
|
|
25103
26583
|
}
|
|
25104
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
|
|
26605
|
+
);
|
|
26606
|
+
}
|
|
26607
|
+
);
|
|
25105
26608
|
}
|
|
25106
26609
|
function registerNamespaceCommands(program, deps) {
|
|
25107
26610
|
const namespace = program.command("namespace").description("Manage scope-namespace registrations (whether scope:<namespace> values resolve to identity entities).");
|