@vectros-ai/cli 0.19.0 → 0.20.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 +17 -6
- package/CHANGELOG.md +141 -0
- package/README.md +77 -13
- package/dist/cli.js +743 -36
- package/dist/cli.js.map +1 -1
- package/dist/cli.mjs +743 -36
- package/dist/cli.mjs.map +1 -1
- package/package.json +3 -3
package/dist/cli.js
CHANGED
|
@@ -6104,7 +6104,7 @@ var AuthClient = class {
|
|
|
6104
6104
|
return handleNonStatusCodeError(_response.error, _response.rawResponse, "POST", "/v1/auth/token");
|
|
6105
6105
|
}
|
|
6106
6106
|
/**
|
|
6107
|
-
* Invite a new member to one of your app contexts by email, OR grant an existing member access to an additional app context by inviting their same email again. Idempotent on the combination of context and email: re-inviting the same email into the SAME context rotates the token and resends the invitation rather than creating a duplicate — this requires the `users:r` and `users:u` scopes in addition to `users:c`, because resending rotates a credential on an existing invitation and invalidates any link already sent. Without them the collision returns 409 instead, with no invitation details and no change to the outstanding invitation. Inviting the SAME email into a DIFFERENT app context in this tenant, where that email already resolves to an existing member: if that member is active AND already has (or, once accepted, will have) a credential that works for the new context's own identity provider, this immediately grants them access to the new context (no email is sent — there is nothing to accept, `emailSent` is false) — this additionally requires the `users:r` scope (no `users:u`, since nothing is mutated), because the response names the existing member's userId, a fact about them your credential could not otherwise learn through this endpoint. If that active member's ONLY existing credential is for a DIFFERENT identity provider than the one the new context uses, a normal, independent invitation is created instead (its own new member id, a real token/accept link) — attaching them silently would leave no way for them to ever actually sign in to that context. If the existing member's original invitation is still pending, this attaches the new context's access to that same outstanding invitation and rotates its token (`users:r`+`users:u`, same as an ordinary resend — both the disclosure and the credential rotation apply here). A SUSPENDED member's email does not get new-context access this way — reactivate them explicitly first. Returns HTTP 201 in every one of those cases. Returns 409 if that email already belongs to an active or suspended member of THIS specific app context, already has a PENDING invitation for THIS specific app context, or resolves to an existing member elsewhere in the tenant and your token lacks the additional scope the grant/attach requires (`users:r`, or `users:r`+`users:u` for the still-pending case). An email that already has an identity in your OTHER tenant (test vs. live) is not a collision either — it creates an additional, independent membership in this tenant for that same identity. When `sendEmail` is false, the response includes the raw token and a ready-to-use accept link so you can deliver the invitation through your own email provider. Requires the `users:c` scope.
|
|
6107
|
+
* Invite a new member to one of your app contexts by email, OR grant an existing member access to an additional app context by inviting their same email again. Idempotent on the combination of context and email: re-inviting the same email into the SAME context rotates the token and resends the invitation rather than creating a duplicate — this requires the `users:r` and `users:u` scopes in addition to `users:c`, because resending rotates a credential on an existing invitation and invalidates any link already sent. Without them the collision returns 409 instead, with no invitation details and no change to the outstanding invitation. Inviting the SAME email into a DIFFERENT app context in this tenant, where that email already resolves to an existing member: if that member is active AND already has (or, once accepted, will have) a credential that works for the new context's own identity provider, this immediately grants them access to the new context (no email is sent — there is nothing to accept, `emailSent` is false) — this additionally requires the `users:r` scope (no `users:u`, since nothing is mutated), because the response names the existing member's userId, a fact about them your credential could not otherwise learn through this endpoint. If that active member's ONLY existing credential is for a DIFFERENT identity provider than the one the new context uses, a normal, independent invitation is created instead (its own new member id, a real token/accept link) — attaching them silently would leave no way for them to ever actually sign in to that context. If the existing member's original invitation is still pending, this attaches the new context's access to that same outstanding invitation and rotates its token (`users:r`+`users:u`, same as an ordinary resend — both the disclosure and the credential rotation apply here). A SUSPENDED member's email does not get new-context access this way — reactivate them explicitly first. Returns HTTP 201 in every one of those cases. Returns 409 if that email already belongs to an active or suspended member of THIS specific app context, already has a PENDING invitation for THIS specific app context, or resolves to an existing member elsewhere in the tenant and your token lacks the additional scope the grant/attach requires (`users:r`, or `users:r`+`users:u` for the still-pending case). An email that already has an identity in your OTHER tenant (test vs. live) is not a collision either — it creates an additional, independent membership in this tenant for that same identity. When `sendEmail` is false, the response includes the raw token and a ready-to-use accept link so you can deliver the invitation through your own email provider. Requires the `users:c` scope — or, for a credential scoped to a single app context, the `member-lifecycle` capability plus the matching `profiles:c`/`profiles:r`/`profiles:u` grant(s) in that context, as an equally sufficient alternative to every `users:c`/`users:r`/`users:u` requirement in this description.
|
|
6108
6108
|
*
|
|
6109
6109
|
* @param {Vectros.CreateInviteRequest} request
|
|
6110
6110
|
* @param {AuthClient.RequestOptions} requestOptions - Request-specific configuration.
|
|
@@ -6175,7 +6175,7 @@ var AuthClient = class {
|
|
|
6175
6175
|
return handleNonStatusCodeError(_response.error, _response.rawResponse, "POST", "/v1/users/invite");
|
|
6176
6176
|
}
|
|
6177
6177
|
/**
|
|
6178
|
-
* Resend an outstanding invitation, identified by its email and app context. Rotates the invitation token and extends its expiry, then (when `sendEmail` is true) re-delivers the email. Rotating the token invalidates any previously issued link for this invitation, so only the newest link works. The invitee's pending permissions are left unchanged. Because this rotates a credential on an existing invitation, it requires the `users:c`, `users:r` and `users:u` scopes.
|
|
6178
|
+
* Resend an outstanding invitation, identified by its email and app context. Rotates the invitation token and extends its expiry, then (when `sendEmail` is true) re-delivers the email. Rotating the token invalidates any previously issued link for this invitation, so only the newest link works. The invitee's pending permissions are left unchanged. Because this rotates a credential on an existing invitation, it requires the `users:c`, `users:r` and `users:u` scopes — or, for a credential scoped to a single app context, the `member-lifecycle` capability plus the matching `profiles:c`/`profiles:r`/`profiles:u` grant(s) in that context.
|
|
6179
6179
|
*
|
|
6180
6180
|
* @param {Vectros.CreateInviteRequest} request
|
|
6181
6181
|
* @param {AuthClient.RequestOptions} requestOptions - Request-specific configuration.
|
|
@@ -7048,7 +7048,21 @@ var DocumentsClient = class {
|
|
|
7048
7048
|
return HttpResponsePromise.fromPromise(this.__lookupDocuments(request, requestOptions));
|
|
7049
7049
|
}
|
|
7050
7050
|
async __lookupDocuments(request, requestOptions) {
|
|
7051
|
-
const {
|
|
7051
|
+
const {
|
|
7052
|
+
type: type_,
|
|
7053
|
+
field,
|
|
7054
|
+
value,
|
|
7055
|
+
from: from_,
|
|
7056
|
+
to,
|
|
7057
|
+
prefix,
|
|
7058
|
+
sortFrom,
|
|
7059
|
+
sortTo,
|
|
7060
|
+
startFrom,
|
|
7061
|
+
limit,
|
|
7062
|
+
order,
|
|
7063
|
+
userId,
|
|
7064
|
+
scope
|
|
7065
|
+
} = request;
|
|
7052
7066
|
const _queryParams = {
|
|
7053
7067
|
type: type_,
|
|
7054
7068
|
field,
|
|
@@ -7056,6 +7070,8 @@ var DocumentsClient = class {
|
|
|
7056
7070
|
from: from_,
|
|
7057
7071
|
to,
|
|
7058
7072
|
prefix,
|
|
7073
|
+
sortFrom,
|
|
7074
|
+
sortTo,
|
|
7059
7075
|
startFrom,
|
|
7060
7076
|
limit,
|
|
7061
7077
|
order: order != null ? order : void 0,
|
|
@@ -11027,7 +11043,21 @@ var BlueprintSchemaSchema = import_zod.z.object({
|
|
|
11027
11043
|
// (it becomes that name's shared base). Must point directly at the base (one
|
|
11028
11044
|
// hop); immutable once set. Mirrors the `basedOn` field on the platform's
|
|
11029
11045
|
// schema-create request.
|
|
11030
|
-
basedOn: import_zod.z.string().min(1).optional()
|
|
11046
|
+
basedOn: import_zod.z.string().min(1).optional(),
|
|
11047
|
+
// ADVISORY ONLY — not sent to the platform, never runtime-enforced. The
|
|
11048
|
+
// ownership dimensions this schema's records are typically scoped by
|
|
11049
|
+
// (e.g. ['org', 'client']), declared so the CLI's blueprint lint can warn
|
|
11050
|
+
// when a role clause grants r/u/d on this type but its `dataScope` names
|
|
11051
|
+
// only SOME of these dims — a schema author's easiest way to leave a
|
|
11052
|
+
// dimension unintentionally unconstrained, since reads (unlike creates)
|
|
11053
|
+
// never require full-dimension `dataScope` coverage — this helps an
|
|
11054
|
+
// author NOTICE that read/write asymmetry, not change it; the asymmetry
|
|
11055
|
+
// itself is a ratified, deliberate platform behavior.
|
|
11056
|
+
// Same grammar as a `dataScope` key with the `scope:` prefix stripped:
|
|
11057
|
+
// a bare namespace name, or 'userId' for the principal dimension.
|
|
11058
|
+
// Structural-only here (validated below by `lintExpectedScopeDims`); the
|
|
11059
|
+
// cross-referencing lint against role clauses lives in `packages/cli`.
|
|
11060
|
+
expectedScopeDims: import_zod.z.array(import_zod.z.string().min(1)).max(MAX_SCOPE_DIMENSIONS).optional()
|
|
11031
11061
|
}).strict().superRefine((schema, ctx) => {
|
|
11032
11062
|
const hasComposite = (schema.lookupFields ?? []).some(
|
|
11033
11063
|
(lf) => typeof lf === "object" && lf.fieldNames !== void 0
|
|
@@ -11078,8 +11108,8 @@ var BlueprintAccessProfileSchema = import_zod.z.object({
|
|
|
11078
11108
|
//
|
|
11079
11109
|
// Optional now: mutually exclusive with `roleIds` below (the
|
|
11080
11110
|
// `.superRefine` on this schema enforces the XOR, mirroring the platform's
|
|
11081
|
-
// own
|
|
11082
|
-
// authors this profile's grant.
|
|
11111
|
+
// own access-profile scopes/role-composition XOR check). Exactly one of
|
|
11112
|
+
// the two authors this profile's grant.
|
|
11083
11113
|
allowedActions: import_zod.z.array(import_zod.z.string().min(1)).min(1).optional(),
|
|
11084
11114
|
// Additive role composition: this profile's effective grant is
|
|
11085
11115
|
// each named role's own clauses, concatenated in the order listed, instead of
|
|
@@ -11682,6 +11712,35 @@ function lintDataScopeKeys(value, ctx) {
|
|
|
11682
11712
|
}
|
|
11683
11713
|
}
|
|
11684
11714
|
}
|
|
11715
|
+
function lintExpectedScopeDims(value, ctx) {
|
|
11716
|
+
const bp = value;
|
|
11717
|
+
(bp.schemas ?? []).forEach((schema, i) => {
|
|
11718
|
+
const dims = schema.expectedScopeDims;
|
|
11719
|
+
if (!dims) return;
|
|
11720
|
+
const seen = /* @__PURE__ */ new Set();
|
|
11721
|
+
dims.forEach((raw, j) => {
|
|
11722
|
+
const dim = String(raw);
|
|
11723
|
+
const path4 = ["schemas", i, "expectedScopeDims", j];
|
|
11724
|
+
if (seen.has(dim)) {
|
|
11725
|
+
ctx.addIssue({
|
|
11726
|
+
code: import_zod.z.ZodIssueCode.custom,
|
|
11727
|
+
path: path4,
|
|
11728
|
+
message: `schema '${schema.typeName}': expectedScopeDims lists '${dim}' more than once.`
|
|
11729
|
+
});
|
|
11730
|
+
}
|
|
11731
|
+
seen.add(dim);
|
|
11732
|
+
if (dim === "userId") return;
|
|
11733
|
+
const nsError = scopeNamespaceGrammarError(dim);
|
|
11734
|
+
if (nsError !== null) {
|
|
11735
|
+
ctx.addIssue({
|
|
11736
|
+
code: import_zod.z.ZodIssueCode.custom,
|
|
11737
|
+
path: path4,
|
|
11738
|
+
message: `schema '${schema.typeName}': expectedScopeDims['${dim}'] ${nsError}`
|
|
11739
|
+
});
|
|
11740
|
+
}
|
|
11741
|
+
});
|
|
11742
|
+
});
|
|
11743
|
+
}
|
|
11685
11744
|
function lintDataScopeRefs(value, ctx) {
|
|
11686
11745
|
const bp = value;
|
|
11687
11746
|
const declared = new Set(Object.keys(bp.fragments ?? {}));
|
|
@@ -11874,6 +11933,15 @@ var BlueprintSchema = import_zod.z.object({
|
|
|
11874
11933
|
}),
|
|
11875
11934
|
/** Human-readable app-context name; defaults to `MCP — <name>` (see {@link contextNameOf}) when absent. */
|
|
11876
11935
|
contextName: import_zod.z.string().min(1).optional(),
|
|
11936
|
+
/**
|
|
11937
|
+
* The deploying organization's own display name — distinct from {@link contextName}.
|
|
11938
|
+
* `contextName` is the blueprint author's fixed identity for the app (the same for every
|
|
11939
|
+
* deployer); `companyName` is meant to vary per install, typically templated from a
|
|
11940
|
+
* deployer-supplied `${{ inputs.x }}` value rather than hardcoded. Used for branding on
|
|
11941
|
+
* platform-sent correspondence (e.g. invite emails) alongside `contextName` — not a
|
|
11942
|
+
* replacement for it. See {@link companyNameOf}.
|
|
11943
|
+
*/
|
|
11944
|
+
companyName: import_zod.z.string().min(1).optional(),
|
|
11877
11945
|
schemas: import_zod.z.array(BlueprintSchemaSchema).default([]),
|
|
11878
11946
|
accessProfile: BlueprintAccessProfileSchema,
|
|
11879
11947
|
servicePrincipal: BlueprintServicePrincipalSchema,
|
|
@@ -11922,6 +11990,7 @@ var BlueprintSchema = import_zod.z.object({
|
|
|
11922
11990
|
lintIdentityOverrides(bp, ctx);
|
|
11923
11991
|
lintNamespacedScopeArrays(bp, ctx);
|
|
11924
11992
|
lintDataScopeKeys(bp, ctx);
|
|
11993
|
+
lintExpectedScopeDims(bp, ctx);
|
|
11925
11994
|
lintDataScopeRefs(bp, ctx);
|
|
11926
11995
|
lintRoleAssumable(bp, ctx);
|
|
11927
11996
|
lintAccessProfileRoleIds(bp, ctx);
|
|
@@ -14104,6 +14173,7 @@ var ENV_BASE_URLS = {
|
|
|
14104
14173
|
// `--base-url` or VECTROS_API_BASE_URL; verify at smoke time.
|
|
14105
14174
|
staging: "https://api.staging.vectros.ai"
|
|
14106
14175
|
};
|
|
14176
|
+
var AUTH_FLAG_NAMES = ["--env", "--base-url", "--token", "--use-scoped-token"];
|
|
14107
14177
|
function envToBaseUrl(env, override) {
|
|
14108
14178
|
return validateBaseUrl(override ?? ENV_BASE_URLS[env]);
|
|
14109
14179
|
}
|
|
@@ -14610,7 +14680,7 @@ ${summary}`
|
|
|
14610
14680
|
this.rejected = rejected;
|
|
14611
14681
|
}
|
|
14612
14682
|
};
|
|
14613
|
-
var GRANTED_CAPABILITIES_ALLOWLIST = ["member-lifecycle"];
|
|
14683
|
+
var GRANTED_CAPABILITIES_ALLOWLIST = ["member-lifecycle", "delegate-principal-stamp"];
|
|
14614
14684
|
var GRANTED_CAPABILITIES_SET = new Set(GRANTED_CAPABILITIES_ALLOWLIST);
|
|
14615
14685
|
var CAPABILITIES_HINT = GRANTED_CAPABILITIES_ALLOWLIST.join("/");
|
|
14616
14686
|
var CapabilityGateError = class extends Error {
|
|
@@ -14887,7 +14957,8 @@ function makeIdentityResolver(client, log) {
|
|
|
14887
14957
|
return id;
|
|
14888
14958
|
};
|
|
14889
14959
|
}
|
|
14890
|
-
async function resolveApplyTimeIdentities(blueprint,
|
|
14960
|
+
async function resolveApplyTimeIdentities(blueprint, provisioned, bootstrapClient, log) {
|
|
14961
|
+
const client = provisioned.ctxBootstrapClient ?? bootstrapClient;
|
|
14891
14962
|
const resolver = makeIdentityResolver(client, log);
|
|
14892
14963
|
const resolved = await resolveBlueprintIdentities(blueprint, resolver);
|
|
14893
14964
|
return parseBlueprint(resolved);
|
|
@@ -15155,21 +15226,13 @@ async function runLoader(client, input) {
|
|
|
15155
15226
|
machineId,
|
|
15156
15227
|
rotate = false,
|
|
15157
15228
|
skipSeed = false,
|
|
15158
|
-
|
|
15229
|
+
seedIndexPoll,
|
|
15230
|
+
provisioned,
|
|
15159
15231
|
readExistingKey,
|
|
15160
15232
|
persistMintedKey,
|
|
15161
|
-
confirmExistingPrincipal,
|
|
15162
15233
|
log
|
|
15163
15234
|
} = input;
|
|
15164
15235
|
assertNoUnresolvedIdentities(blueprint);
|
|
15165
|
-
const provisioned = input.provisioned ?? await provisionContext(client, {
|
|
15166
|
-
blueprint,
|
|
15167
|
-
allowTenantWideNamespaces,
|
|
15168
|
-
remintBootstrapForContext: input.remintBootstrapForContext,
|
|
15169
|
-
readExistingKey,
|
|
15170
|
-
confirmExistingPrincipal,
|
|
15171
|
-
log
|
|
15172
|
-
});
|
|
15173
15236
|
const { contextAction, servicePrincipalUserId: userId, userAction, namespaceActions } = provisioned;
|
|
15174
15237
|
assertBlueprintGates(blueprint);
|
|
15175
15238
|
const needsContextPinnedBootstrapForIssuers = (blueprint.issuers ?? []).length > 0;
|
|
@@ -15348,6 +15411,7 @@ async function runLoader(client, input) {
|
|
|
15348
15411
|
const seededRecordIds = [];
|
|
15349
15412
|
const seededDocumentIds = [];
|
|
15350
15413
|
const seedIndexFailures = [];
|
|
15414
|
+
const pendingIndexSeeds = [];
|
|
15351
15415
|
const noteIfIndexFailed = (created, rec, surface) => {
|
|
15352
15416
|
if (created.indexStatus !== "FAILED") return;
|
|
15353
15417
|
seedIndexFailures.push({
|
|
@@ -15382,6 +15446,9 @@ async function runLoader(client, input) {
|
|
|
15382
15446
|
}
|
|
15383
15447
|
});
|
|
15384
15448
|
if (created.id) seededDocumentIds.push(created.id);
|
|
15449
|
+
if (created.id && created.indexStatus === "PENDING_INDEX") {
|
|
15450
|
+
pendingIndexSeeds.push({ id: created.id, externalId: rec.externalId, typeName: rec.typeName, surface: "document" });
|
|
15451
|
+
}
|
|
15385
15452
|
noteIfIndexFailed(created, rec, "document");
|
|
15386
15453
|
} else {
|
|
15387
15454
|
const created = await ctxClient.records.createRecord({
|
|
@@ -15394,10 +15461,30 @@ async function runLoader(client, input) {
|
|
|
15394
15461
|
}
|
|
15395
15462
|
});
|
|
15396
15463
|
if (created.id) seededRecordIds.push(created.id);
|
|
15464
|
+
if (created.id && created.indexStatus === "PENDING_INDEX") {
|
|
15465
|
+
pendingIndexSeeds.push({ id: created.id, externalId: rec.externalId, typeName: rec.typeName, surface: "record" });
|
|
15466
|
+
}
|
|
15397
15467
|
noteIfIndexFailed(created, rec, "record");
|
|
15398
15468
|
}
|
|
15399
15469
|
seededCount++;
|
|
15400
15470
|
}
|
|
15471
|
+
const pollAttempts = seedIndexPoll?.attempts ?? 3;
|
|
15472
|
+
const pollDelayMs = seedIndexPoll?.delayMs ?? 500;
|
|
15473
|
+
if (pollAttempts > 0 && pendingIndexSeeds.length > 0) {
|
|
15474
|
+
await Promise.all(
|
|
15475
|
+
pendingIndexSeeds.map(async (seed) => {
|
|
15476
|
+
for (let attempt = 0; attempt < pollAttempts; attempt++) {
|
|
15477
|
+
await new Promise((r) => setTimeout(r, pollDelayMs));
|
|
15478
|
+
const readBack = seed.surface === "document" ? await ctxClient.documents.getDocument({ id: seed.id }) : await ctxClient.records.getRecord({ id: seed.id });
|
|
15479
|
+
if (readBack.indexStatus === "FAILED") {
|
|
15480
|
+
noteIfIndexFailed(readBack, seed, seed.surface);
|
|
15481
|
+
return;
|
|
15482
|
+
}
|
|
15483
|
+
if (readBack.indexStatus !== "PENDING_INDEX") return;
|
|
15484
|
+
}
|
|
15485
|
+
})
|
|
15486
|
+
);
|
|
15487
|
+
}
|
|
15401
15488
|
return {
|
|
15402
15489
|
contextId: blueprint.contextId,
|
|
15403
15490
|
servicePrincipalUserId: userId,
|
|
@@ -15577,6 +15664,11 @@ function buildClaudeCodeAddCommand(entry, serverKey = "vectros") {
|
|
|
15577
15664
|
const envFlags = Object.entries(entry.env ?? {}).map(([k, v]) => `-e ${k}=${v}`);
|
|
15578
15665
|
return ["claude", "mcp", "add", serverKey, ...envFlags, "--", entry.command, ...entry.args].join(" ");
|
|
15579
15666
|
}
|
|
15667
|
+
function buildRestartHint(blueprintName, isCode) {
|
|
15668
|
+
const target = isCode ? "Reopen Claude Code in this project" : "Restart your MCP client";
|
|
15669
|
+
return ` ${target}, then ask Claude about your '${blueprintName}' data.
|
|
15670
|
+
`;
|
|
15671
|
+
}
|
|
15580
15672
|
function mergeServerEntry(existing, entry, serverKey = "vectros") {
|
|
15581
15673
|
const base = existing ? { ...existing } : {};
|
|
15582
15674
|
const servers = { ...base.mcpServers ?? {} };
|
|
@@ -15916,11 +16008,19 @@ var CryptoFileSecretStore = class {
|
|
|
15916
16008
|
* sub-millisecond in practice; the budget is generous for a loaded machine)
|
|
15917
16009
|
* before deciding it is a crash remnant. A SHORT-but-nonempty winner is never
|
|
15918
16010
|
* a write in flight — the 16 salt bytes land in one write — so it is refused
|
|
15919
|
-
* immediately, exactly like the top-of-function read's twin check.
|
|
16011
|
+
* immediately, exactly like the top-of-function read's twin check. A winner
|
|
16012
|
+
* that goes MISSING mid-poll (ENOENT) restarts creation from the top rather
|
|
16013
|
+
* than surfacing the raw ENOENT — see the catch below.
|
|
15920
16014
|
*/
|
|
15921
16015
|
async adoptSalt(saltPath) {
|
|
15922
16016
|
for (let attempt = 0; ; attempt++) {
|
|
15923
|
-
|
|
16017
|
+
let winner;
|
|
16018
|
+
try {
|
|
16019
|
+
winner = await import_node_fs3.promises.readFile(saltPath);
|
|
16020
|
+
} catch (err) {
|
|
16021
|
+
if (err.code === "ENOENT") return this.readOrCreateSalt();
|
|
16022
|
+
throw err;
|
|
16023
|
+
}
|
|
15924
16024
|
if (winner.length >= 16) return winner;
|
|
15925
16025
|
if (winner.length > 0) throw corruptSaltError(saltPath, winner.length);
|
|
15926
16026
|
if (attempt >= 24) break;
|
|
@@ -17759,7 +17859,7 @@ function applyOutcomeSummary(result) {
|
|
|
17759
17859
|
const parts = [
|
|
17760
17860
|
reconcileTally(result.schemaActions, "schema"),
|
|
17761
17861
|
reconcileTally(result.roleActions, "role"),
|
|
17762
|
-
//
|
|
17862
|
+
// The platform's issuer partial-update endpoint makes an issuer's
|
|
17763
17863
|
// 'reconciled' mean the same "updated to match" as every other kind here.
|
|
17764
17864
|
reconcileTally(result.issuerActions, "issuer"),
|
|
17765
17865
|
namespaceTally(result.namespaceActions),
|
|
@@ -18150,11 +18250,7 @@ ${renderPlan(blueprint, planSteps)}
|
|
|
18150
18250
|
confirmExistingPrincipal,
|
|
18151
18251
|
log
|
|
18152
18252
|
});
|
|
18153
|
-
const resolvedBlueprint = await resolveIdentities(
|
|
18154
|
-
blueprint,
|
|
18155
|
-
provisioned.ctxBootstrapClient ?? client,
|
|
18156
|
-
log
|
|
18157
|
-
);
|
|
18253
|
+
const resolvedBlueprint = await resolveIdentities(blueprint, provisioned, client, log);
|
|
18158
18254
|
const machineId = await resolveMachineId();
|
|
18159
18255
|
const result = await loader(client, {
|
|
18160
18256
|
blueprint: resolvedBlueprint,
|
|
@@ -18162,7 +18258,6 @@ ${renderPlan(blueprint, planSteps)}
|
|
|
18162
18258
|
machineId,
|
|
18163
18259
|
rotate: args.rotate,
|
|
18164
18260
|
skipSeed: noSeed,
|
|
18165
|
-
allowTenantWideNamespaces: args.allowTenantWideNamespaces,
|
|
18166
18261
|
readExistingKey,
|
|
18167
18262
|
persistMintedKey,
|
|
18168
18263
|
provisioned,
|
|
@@ -18173,7 +18268,6 @@ ${renderPlan(blueprint, planSteps)}
|
|
|
18173
18268
|
tenant: args.tenant,
|
|
18174
18269
|
mintContext
|
|
18175
18270
|
}),
|
|
18176
|
-
confirmExistingPrincipal,
|
|
18177
18271
|
log
|
|
18178
18272
|
});
|
|
18179
18273
|
process.stdout.write(
|
|
@@ -18229,10 +18323,13 @@ ${codeAddHint}`);
|
|
|
18229
18323
|
}
|
|
18230
18324
|
try {
|
|
18231
18325
|
const merged = await safeMergeConfigFile({ configPath, entry });
|
|
18232
|
-
const restartHint =
|
|
18326
|
+
const restartHint = buildRestartHint(blueprint.name, isCode);
|
|
18233
18327
|
process.stdout.write(
|
|
18234
18328
|
` \u2714 Config ${merged.action}${merged.backupPath ? ` (backup: ${merged.backupPath})` : ""}
|
|
18235
|
-
` + restartHint
|
|
18329
|
+
` + restartHint + // The `claude mcp add` line is otherwise only shown on --no-merge / a
|
|
18330
|
+
// declined / a failed merge — surface it here too so a successful
|
|
18331
|
+
// auto-merge doesn't hide the idiomatic install command entirely.
|
|
18332
|
+
codeAddHint
|
|
18236
18333
|
);
|
|
18237
18334
|
} catch (err) {
|
|
18238
18335
|
process.stderr.write(` \u26A0 Could not merge config (${err instanceof Error ? err.message : String(err)}).
|
|
@@ -18416,8 +18513,12 @@ function compositeLegFindings(schema, lf, fieldsById, rangeEnabledFieldNames) {
|
|
|
18416
18513
|
function lintBlueprint(bp) {
|
|
18417
18514
|
const findings = [];
|
|
18418
18515
|
const typeNames = /* @__PURE__ */ new Set();
|
|
18516
|
+
const expectedDimsByTypeName = /* @__PURE__ */ new Map();
|
|
18419
18517
|
for (const schema of bp.schemas) {
|
|
18420
18518
|
typeNames.add(schema.typeName);
|
|
18519
|
+
if (schema.expectedScopeDims && schema.expectedScopeDims.length > 0) {
|
|
18520
|
+
expectedDimsByTypeName.set(schema.typeName, schema.expectedScopeDims);
|
|
18521
|
+
}
|
|
18421
18522
|
const fieldIds = /* @__PURE__ */ new Set();
|
|
18422
18523
|
const fieldsById = /* @__PURE__ */ new Map();
|
|
18423
18524
|
for (const f of schema.fields) {
|
|
@@ -18498,6 +18599,9 @@ function lintBlueprint(bp) {
|
|
|
18498
18599
|
if (bp.accessProfile.allowedActions !== void 0) {
|
|
18499
18600
|
const reached = ownerScopedReachWithoutDataScope(bp.accessProfile.allowedActions, ds);
|
|
18500
18601
|
if (reached.length > 0) findings.push(unconstrainedReachFinding("accessProfile", reached));
|
|
18602
|
+
findings.push(
|
|
18603
|
+
...partialCoverageFindings("accessProfile", bp.accessProfile.allowedActions, ds, expectedDimsByTypeName)
|
|
18604
|
+
);
|
|
18501
18605
|
}
|
|
18502
18606
|
for (const roleId of Object.keys(bp.roles ?? {})) {
|
|
18503
18607
|
if (!ROLE_ID_RE.test(roleId)) {
|
|
@@ -18515,6 +18619,9 @@ function lintBlueprint(bp) {
|
|
|
18515
18619
|
if (reached.length > 0) {
|
|
18516
18620
|
findings.push(unconstrainedReachFinding(`role '${roleId}' clause ${i}`, reached));
|
|
18517
18621
|
}
|
|
18622
|
+
findings.push(
|
|
18623
|
+
...partialCoverageFindings(`role '${roleId}' clause ${i}`, clause.allowedActions, cds, expectedDimsByTypeName)
|
|
18624
|
+
);
|
|
18518
18625
|
if (!cds) return;
|
|
18519
18626
|
for (const [key, values] of Object.entries(cds)) {
|
|
18520
18627
|
for (const value of values) {
|
|
@@ -18591,6 +18698,32 @@ function unconstrainedReachFinding(where, resources) {
|
|
|
18591
18698
|
message: `${where}: no dataScope at all, but allowedActions grants r/u/d on ${resources.join("/")} \u2014 this reaches EVERY row of EVERY owner on ${resources.length > 1 ? "those resources" : "that resource"}, not just "your own org" or any other value you may have intended. If that's deliberate (a genuinely shared/tenant-wide resource), you can ignore this; otherwise add a dataScope entry.`
|
|
18592
18699
|
};
|
|
18593
18700
|
}
|
|
18701
|
+
function dimToDataScopeKey(dim) {
|
|
18702
|
+
return dim === "userId" ? "userId" : `scope:${dim}`;
|
|
18703
|
+
}
|
|
18704
|
+
function partialCoverageFindings(where, allowedActions, dataScope, expectedDimsByTypeName) {
|
|
18705
|
+
const findings = [];
|
|
18706
|
+
const dataScopeKeys = new Set(Object.keys(dataScope ?? {}));
|
|
18707
|
+
for (const action of allowedActions) {
|
|
18708
|
+
const parts = action.split(":");
|
|
18709
|
+
const [resource, ops] = parts;
|
|
18710
|
+
if (resource !== "records" && resource !== "documents" || !ops) continue;
|
|
18711
|
+
const qualifier = parts.length > 2 ? parts.slice(2).join(":") : void 0;
|
|
18712
|
+
if (qualifier === void 0) continue;
|
|
18713
|
+
const dims = expectedDimsByTypeName.get(qualifier);
|
|
18714
|
+
if (!dims || dims.length === 0) continue;
|
|
18715
|
+
if (!(ops === "*" || [...ops].some((op) => op === "r" || op === "u" || op === "d"))) continue;
|
|
18716
|
+
const covered = dims.filter((d) => dataScopeKeys.has(dimToDataScopeKey(d)));
|
|
18717
|
+
if (covered.length === 0 || covered.length === dims.length) continue;
|
|
18718
|
+
const missing = dims.filter((d) => !covered.includes(d));
|
|
18719
|
+
findings.push({
|
|
18720
|
+
severity: "warning",
|
|
18721
|
+
rule: "partial-datascope-dimension-coverage",
|
|
18722
|
+
message: `${where}: ${action} covers dataScope dim${covered.length > 1 ? "s" : ""} [${covered.join(", ")}] of '${qualifier}'s declared expectedScopeDims [${dims.join(", ")}] \u2014 the ${missing.join("/")} dimension${missing.length > 1 ? "s are" : " is"} UNCONSTRAINED for this grant (every value reaches, not just the one(s) you may have intended). If that's deliberate, you can ignore this; otherwise add a dataScope entry for the missing dimension(s).`
|
|
18723
|
+
});
|
|
18724
|
+
}
|
|
18725
|
+
return findings;
|
|
18726
|
+
}
|
|
18594
18727
|
function lintPasses(findings) {
|
|
18595
18728
|
return !findings.some((f) => f.severity === "error");
|
|
18596
18729
|
}
|
|
@@ -18682,6 +18815,10 @@ function buildScaffold(name, from) {
|
|
|
18682
18815
|
}
|
|
18683
18816
|
|
|
18684
18817
|
// src/cli-blueprint.ts
|
|
18818
|
+
function noEffectOnBlueprint(flag, sub) {
|
|
18819
|
+
const cmd = sub ? `blueprint ${sub}` : "blueprint";
|
|
18820
|
+
return `'${flag}' has no effect on '${cmd}' \u2014 blueprint validate/plan/list/init are creds-free and never call the API, so there's no environment/credential to select. Did you mean 'vectros bootstrap'?`;
|
|
18821
|
+
}
|
|
18685
18822
|
function parseBlueprintArgs(argv) {
|
|
18686
18823
|
const args = { help: false, force: false, set: [] };
|
|
18687
18824
|
const positionals = [];
|
|
@@ -18708,6 +18845,9 @@ function parseBlueprintArgs(argv) {
|
|
|
18708
18845
|
args.values = argv[++i];
|
|
18709
18846
|
break;
|
|
18710
18847
|
default:
|
|
18848
|
+
if (AUTH_FLAG_NAMES.includes(a)) {
|
|
18849
|
+
throw new Error(noEffectOnBlueprint(a, positionals[0]));
|
|
18850
|
+
}
|
|
18711
18851
|
if (a.startsWith("-")) throw new Error(`Unknown blueprint flag: ${a}`);
|
|
18712
18852
|
positionals.push(a);
|
|
18713
18853
|
}
|
|
@@ -19500,6 +19640,51 @@ ${list}
|
|
|
19500
19640
|
});
|
|
19501
19641
|
}
|
|
19502
19642
|
|
|
19643
|
+
// src/dev-api.ts
|
|
19644
|
+
var DeveloperApiError = class extends Error {
|
|
19645
|
+
statusCode;
|
|
19646
|
+
constructor(message, statusCode) {
|
|
19647
|
+
super(message);
|
|
19648
|
+
this.name = "DeveloperApiError";
|
|
19649
|
+
this.statusCode = statusCode;
|
|
19650
|
+
}
|
|
19651
|
+
};
|
|
19652
|
+
async function fetchDeveloperUsersPage(opts) {
|
|
19653
|
+
const { cognitoToken, apiBaseUrl, tenant, startFrom, limit, fetchImpl = fetch } = opts;
|
|
19654
|
+
const base = apiBaseUrl.replace(/\/$/, "");
|
|
19655
|
+
let url = `${base}/developer/users?tenant=${encodeURIComponent(tenant)}`;
|
|
19656
|
+
if (startFrom) url += `&startFrom=${encodeURIComponent(startFrom)}`;
|
|
19657
|
+
if (limit !== void 0) url += `&limit=${encodeURIComponent(String(limit))}`;
|
|
19658
|
+
let res;
|
|
19659
|
+
try {
|
|
19660
|
+
res = await fetchImpl(url, {
|
|
19661
|
+
method: "GET",
|
|
19662
|
+
headers: { Authorization: `Bearer ${cognitoToken}` }
|
|
19663
|
+
});
|
|
19664
|
+
} catch (err) {
|
|
19665
|
+
throw new DeveloperApiError(
|
|
19666
|
+
`Request to /developer/users failed: ${err instanceof Error ? err.message : String(err)}`
|
|
19667
|
+
);
|
|
19668
|
+
}
|
|
19669
|
+
if (!res.ok) {
|
|
19670
|
+
const body = await res.text().catch(() => "");
|
|
19671
|
+
const hint = res.status === 403 ? " (this command requires an account owner login \u2014 sign in as the owner and retry)" : "";
|
|
19672
|
+
throw new DeveloperApiError(
|
|
19673
|
+
`GET /developer/users failed: HTTP ${res.status}${hint}${body ? ` \u2014 ${body}` : ""}`,
|
|
19674
|
+
res.status
|
|
19675
|
+
);
|
|
19676
|
+
}
|
|
19677
|
+
let parsed;
|
|
19678
|
+
try {
|
|
19679
|
+
parsed = await res.json();
|
|
19680
|
+
} catch {
|
|
19681
|
+
throw new DeveloperApiError("GET /developer/users returned a non-JSON body");
|
|
19682
|
+
}
|
|
19683
|
+
const data = Array.isArray(parsed.data) ? parsed.data : [];
|
|
19684
|
+
const nextCursor = typeof parsed.nextCursor === "string" ? parsed.nextCursor : null;
|
|
19685
|
+
return { data, nextCursor };
|
|
19686
|
+
}
|
|
19687
|
+
|
|
19503
19688
|
// src/commands/identity.ts
|
|
19504
19689
|
function isUserDimension(type) {
|
|
19505
19690
|
return type === "user";
|
|
@@ -19654,6 +19839,57 @@ async function runIdentityList(opts, deps = {}) {
|
|
|
19654
19839
|
}
|
|
19655
19840
|
});
|
|
19656
19841
|
}
|
|
19842
|
+
function toIdentityView(row) {
|
|
19843
|
+
const str = (v) => typeof v === "string" ? v : void 0;
|
|
19844
|
+
return {
|
|
19845
|
+
id: str(row.id),
|
|
19846
|
+
externalId: str(row.externalId),
|
|
19847
|
+
email: str(row.email),
|
|
19848
|
+
status: str(row.status),
|
|
19849
|
+
type: str(row.type),
|
|
19850
|
+
createdAt: str(row.createdAt)
|
|
19851
|
+
};
|
|
19852
|
+
}
|
|
19853
|
+
async function runIdentityListAll(opts, deps = {}) {
|
|
19854
|
+
const fetchImpl = deps.fetchImpl ?? fetch;
|
|
19855
|
+
return withAuth({ ...opts, provisioning: true }, deps, async (auth) => {
|
|
19856
|
+
try {
|
|
19857
|
+
const tenant = opts.tenant ?? "live";
|
|
19858
|
+
const fetchPage = async (startFrom) => {
|
|
19859
|
+
const page = await fetchDeveloperUsersPage({
|
|
19860
|
+
cognitoToken: auth.cognitoToken,
|
|
19861
|
+
apiBaseUrl: auth.baseUrl,
|
|
19862
|
+
tenant,
|
|
19863
|
+
startFrom,
|
|
19864
|
+
limit: opts.limit,
|
|
19865
|
+
fetchImpl
|
|
19866
|
+
});
|
|
19867
|
+
return { data: page.data.map(toIdentityView), nextCursor: page.nextCursor };
|
|
19868
|
+
};
|
|
19869
|
+
let rows = opts.limit !== void 0 ? (await fetchPage()).data ?? [] : await drainPages(fetchPage);
|
|
19870
|
+
if (opts.externalId) rows = rows.filter((r) => r.externalId === opts.externalId);
|
|
19871
|
+
if (opts.json) {
|
|
19872
|
+
process.stdout.write(`${formatJson(rows)}
|
|
19873
|
+
`);
|
|
19874
|
+
return 0;
|
|
19875
|
+
}
|
|
19876
|
+
process.stdout.write(
|
|
19877
|
+
"\n" + formatTable(rows, [
|
|
19878
|
+
{ header: "id", get: (r) => r.id },
|
|
19879
|
+
{ header: "external id", get: (r) => r.externalId },
|
|
19880
|
+
{ header: "email", get: (r) => r.email },
|
|
19881
|
+
{ header: "kind", get: (r) => r.type },
|
|
19882
|
+
{ header: "status", get: (r) => r.status }
|
|
19883
|
+
]) + "\n"
|
|
19884
|
+
);
|
|
19885
|
+
return 0;
|
|
19886
|
+
} catch (err) {
|
|
19887
|
+
process.stderr.write(` \u2716 identity list-all failed: ${formatError(err)}
|
|
19888
|
+
`);
|
|
19889
|
+
return 1;
|
|
19890
|
+
}
|
|
19891
|
+
});
|
|
19892
|
+
}
|
|
19657
19893
|
async function runIdentityGet(opts, deps = {}) {
|
|
19658
19894
|
return withAuth(opts, deps, async (auth) => {
|
|
19659
19895
|
const client = auth.client;
|
|
@@ -20257,6 +20493,16 @@ async function runRoleCreate(opts, deps = {}) {
|
|
|
20257
20493
|
dataScope = parseJsonObjectFlag("--data-scope", opts.dataScope);
|
|
20258
20494
|
} catch (err) {
|
|
20259
20495
|
process.stderr.write(` \u2716 ${formatError(err)}
|
|
20496
|
+
`);
|
|
20497
|
+
return 2;
|
|
20498
|
+
}
|
|
20499
|
+
}
|
|
20500
|
+
let assumable;
|
|
20501
|
+
if (opts.assumable !== void 0) {
|
|
20502
|
+
try {
|
|
20503
|
+
assumable = parseJsonObjectFlag("--assumable", opts.assumable);
|
|
20504
|
+
} catch (err) {
|
|
20505
|
+
process.stderr.write(` \u2716 ${formatError(err)}
|
|
20260
20506
|
`);
|
|
20261
20507
|
return 2;
|
|
20262
20508
|
}
|
|
@@ -20274,7 +20520,12 @@ async function runRoleCreate(opts, deps = {}) {
|
|
|
20274
20520
|
// stays byte-identical for unscoped roles.
|
|
20275
20521
|
scopes: [
|
|
20276
20522
|
dataScope ? { allowed_actions: actions, data_scope: dataScope } : { allowed_actions: actions }
|
|
20277
|
-
]
|
|
20523
|
+
],
|
|
20524
|
+
// Included only when supplied — omitting it (rather than sending an
|
|
20525
|
+
// empty object) keeps "no assume grant" and "explicitly empty grant"
|
|
20526
|
+
// indistinguishable on the wire, matching the platform's own
|
|
20527
|
+
// "omit for the safe default" contract.
|
|
20528
|
+
assumable
|
|
20278
20529
|
}
|
|
20279
20530
|
});
|
|
20280
20531
|
if (opts.json) {
|
|
@@ -20514,6 +20765,19 @@ ${renderContext(ctx)}
|
|
|
20514
20765
|
}
|
|
20515
20766
|
|
|
20516
20767
|
// src/commands/issuers.ts
|
|
20768
|
+
var readline4 = __toESM(require("readline/promises"));
|
|
20769
|
+
function parseSelfSignupPolicies(raw) {
|
|
20770
|
+
if (!raw || raw.length === 0) return void 0;
|
|
20771
|
+
return raw.map((entry) => {
|
|
20772
|
+
const sep = entry.indexOf(":");
|
|
20773
|
+
if (sep <= 0 || sep === entry.length - 1) {
|
|
20774
|
+
throw new Error(
|
|
20775
|
+
`--self-signup-policy must be '<signup_type>:<roleId>' (e.g. 'default:member'); got '${entry}'.`
|
|
20776
|
+
);
|
|
20777
|
+
}
|
|
20778
|
+
return { signup_type: entry.slice(0, sep), role_id: entry.slice(sep + 1) };
|
|
20779
|
+
});
|
|
20780
|
+
}
|
|
20517
20781
|
function renderSelfSignup(policies) {
|
|
20518
20782
|
if (!policies || policies.length === 0) return "\u2014";
|
|
20519
20783
|
return policies.map((p) => `${p.signup_type} \u2192 ${p.role_id}`).join(", ");
|
|
@@ -20581,6 +20845,308 @@ ${renderIssuer(issuer)}
|
|
|
20581
20845
|
}
|
|
20582
20846
|
});
|
|
20583
20847
|
}
|
|
20848
|
+
async function runIssuersRegister(opts, deps = {}) {
|
|
20849
|
+
let selfSignupPolicies;
|
|
20850
|
+
try {
|
|
20851
|
+
selfSignupPolicies = parseSelfSignupPolicies(opts.selfSignupPolicy);
|
|
20852
|
+
} catch (err) {
|
|
20853
|
+
process.stderr.write(` \u2716 ${formatError(err)}
|
|
20854
|
+
`);
|
|
20855
|
+
return 2;
|
|
20856
|
+
}
|
|
20857
|
+
return withAuth({ ...opts, provisioning: true, context: opts.context }, deps, async (auth) => {
|
|
20858
|
+
const client = auth.client;
|
|
20859
|
+
try {
|
|
20860
|
+
const issuer = await client.auth.registerIssuer({
|
|
20861
|
+
issuerId: opts.issuerId,
|
|
20862
|
+
issuer: opts.issuer,
|
|
20863
|
+
jwksUri: opts.jwksUri,
|
|
20864
|
+
audience: opts.audience,
|
|
20865
|
+
contextId: opts.context,
|
|
20866
|
+
subClaim: opts.subClaim,
|
|
20867
|
+
emailClaim: opts.emailClaim,
|
|
20868
|
+
userinfoUri: opts.userinfoUri,
|
|
20869
|
+
selfSignupPolicies
|
|
20870
|
+
});
|
|
20871
|
+
if (opts.json) {
|
|
20872
|
+
process.stdout.write(`${formatJson(issuer)}
|
|
20873
|
+
`);
|
|
20874
|
+
return 0;
|
|
20875
|
+
}
|
|
20876
|
+
const verb = issuer.created === false ? "already registered \u2014 returned unchanged" : "registered";
|
|
20877
|
+
process.stdout.write(`
|
|
20878
|
+
\u2714 issuer '${opts.issuerId}' ${verb}
|
|
20879
|
+
${renderIssuer(issuer)}
|
|
20880
|
+
`);
|
|
20881
|
+
return 0;
|
|
20882
|
+
} catch (err) {
|
|
20883
|
+
process.stderr.write(formatCliError(err, { action: "issuers register failed" }));
|
|
20884
|
+
return 1;
|
|
20885
|
+
}
|
|
20886
|
+
});
|
|
20887
|
+
}
|
|
20888
|
+
async function runIssuersUpdate(opts, deps = {}) {
|
|
20889
|
+
let selfSignupPolicies;
|
|
20890
|
+
try {
|
|
20891
|
+
selfSignupPolicies = parseSelfSignupPolicies(opts.selfSignupPolicy);
|
|
20892
|
+
} catch (err) {
|
|
20893
|
+
process.stderr.write(` \u2716 ${formatError(err)}
|
|
20894
|
+
`);
|
|
20895
|
+
return 2;
|
|
20896
|
+
}
|
|
20897
|
+
if (opts.clearSelfSignupPolicies && selfSignupPolicies && selfSignupPolicies.length > 0) {
|
|
20898
|
+
process.stderr.write(
|
|
20899
|
+
" \u2716 --clear-self-signup-policies and --self-signup-policy are mutually exclusive.\n"
|
|
20900
|
+
);
|
|
20901
|
+
return 2;
|
|
20902
|
+
}
|
|
20903
|
+
return withAuth({ ...opts, provisioning: true, context: opts.context }, deps, async (auth) => {
|
|
20904
|
+
const client = auth.client;
|
|
20905
|
+
try {
|
|
20906
|
+
const issuer = await client.auth.updateIssuer({
|
|
20907
|
+
issuerId: opts.issuerId,
|
|
20908
|
+
subClaim: opts.subClaim,
|
|
20909
|
+
emailClaim: opts.emailClaim,
|
|
20910
|
+
userinfoUri: opts.userinfoUri,
|
|
20911
|
+
status: opts.status,
|
|
20912
|
+
selfSignupPolicies: opts.clearSelfSignupPolicies ? [] : selfSignupPolicies
|
|
20913
|
+
});
|
|
20914
|
+
if (opts.json) {
|
|
20915
|
+
process.stdout.write(`${formatJson(issuer)}
|
|
20916
|
+
`);
|
|
20917
|
+
return 0;
|
|
20918
|
+
}
|
|
20919
|
+
process.stdout.write(`
|
|
20920
|
+
\u2714 issuer '${opts.issuerId}' updated
|
|
20921
|
+
${renderIssuer(issuer)}
|
|
20922
|
+
`);
|
|
20923
|
+
return 0;
|
|
20924
|
+
} catch (err) {
|
|
20925
|
+
process.stderr.write(formatCliError(err, { action: "issuers update failed" }));
|
|
20926
|
+
return 1;
|
|
20927
|
+
}
|
|
20928
|
+
});
|
|
20929
|
+
}
|
|
20930
|
+
async function runIssuersDelete(opts, deps = {}) {
|
|
20931
|
+
const { issuerId, context } = opts;
|
|
20932
|
+
if (!opts.force) {
|
|
20933
|
+
const isTty = deps.isTty ?? Boolean(process.stdin.isTTY);
|
|
20934
|
+
if (!isTty) {
|
|
20935
|
+
process.stderr.write(
|
|
20936
|
+
` \u2716 issuers delete is destructive and needs an interactive confirmation.
|
|
20937
|
+
Re-run with --force to delete '${issuerId}' without a prompt.
|
|
20938
|
+
`
|
|
20939
|
+
);
|
|
20940
|
+
return 2;
|
|
20941
|
+
}
|
|
20942
|
+
const promptCtx = deps.createPrompt ? deps.createPrompt() : (() => {
|
|
20943
|
+
const rl = readline4.createInterface({ input: process.stdin, output: process.stdout });
|
|
20944
|
+
return { prompt: (q) => rl.question(q), close: () => rl.close() };
|
|
20945
|
+
})();
|
|
20946
|
+
try {
|
|
20947
|
+
process.stdout.write(
|
|
20948
|
+
`
|
|
20949
|
+
\u26A0 Deregistering issuer '${issuerId}' from context '${context}' removes it as a token-exchange
|
|
20950
|
+
trust anchor. The platform refuses this while any user was ever created or matched
|
|
20951
|
+
through it \u2014 deactivate those users or register a replacement issuer first if you
|
|
20952
|
+
intend to cut off access.
|
|
20953
|
+
|
|
20954
|
+
`
|
|
20955
|
+
);
|
|
20956
|
+
const typed = await promptCtx.prompt(` Type the issuer id ('${issuerId}') to confirm: `);
|
|
20957
|
+
if (typed.trim() !== issuerId) {
|
|
20958
|
+
process.stderr.write(` \u2716 Confirmation did not match \u2014 nothing deleted.
|
|
20959
|
+
`);
|
|
20960
|
+
return 1;
|
|
20961
|
+
}
|
|
20962
|
+
} finally {
|
|
20963
|
+
promptCtx.close();
|
|
20964
|
+
}
|
|
20965
|
+
}
|
|
20966
|
+
return withAuth({ ...opts, provisioning: true, context }, deps, async (auth) => {
|
|
20967
|
+
const client = auth.client;
|
|
20968
|
+
try {
|
|
20969
|
+
await client.auth.deleteIssuer({ issuerId });
|
|
20970
|
+
if (opts.json) {
|
|
20971
|
+
process.stdout.write(`${formatJson({ issuerId, deleted: true })}
|
|
20972
|
+
`);
|
|
20973
|
+
return 0;
|
|
20974
|
+
}
|
|
20975
|
+
process.stdout.write(`
|
|
20976
|
+
\u2714 issuer '${issuerId}' deregistered from '${context}'
|
|
20977
|
+
`);
|
|
20978
|
+
return 0;
|
|
20979
|
+
} catch (err) {
|
|
20980
|
+
process.stderr.write(formatCliError(err, { action: "issuers delete failed" }));
|
|
20981
|
+
return 1;
|
|
20982
|
+
}
|
|
20983
|
+
});
|
|
20984
|
+
}
|
|
20985
|
+
|
|
20986
|
+
// src/commands/namespace.ts
|
|
20987
|
+
function renderNamespace(n) {
|
|
20988
|
+
return formatKeyValues([
|
|
20989
|
+
["namespace", n.namespace],
|
|
20990
|
+
["contextId", n.contextId ?? "(tenant-wide)"],
|
|
20991
|
+
["entityBacked", n.entityBacked === void 0 ? void 0 : String(n.entityBacked)],
|
|
20992
|
+
["reserved", n.reserved === void 0 ? void 0 : String(n.reserved)],
|
|
20993
|
+
["specificityRank", n.specificityRank === void 0 ? void 0 : String(n.specificityRank)],
|
|
20994
|
+
["defaultSchemaId", n.defaultSchemaId],
|
|
20995
|
+
["createdAt", n.createdAt]
|
|
20996
|
+
]);
|
|
20997
|
+
}
|
|
20998
|
+
async function runNamespaceList(opts, deps = {}) {
|
|
20999
|
+
return withAuth({ ...opts, context: opts.context }, deps, async (auth) => {
|
|
21000
|
+
const client = auth.client;
|
|
21001
|
+
try {
|
|
21002
|
+
const rows = await drainPages(
|
|
21003
|
+
(startFrom) => client.identity.listNamespaces({
|
|
21004
|
+
limit: PAGE_LIMIT,
|
|
21005
|
+
...opts.context ? { contextId: opts.context } : {},
|
|
21006
|
+
...startFrom ? { startFrom } : {}
|
|
21007
|
+
})
|
|
21008
|
+
);
|
|
21009
|
+
if (opts.json) {
|
|
21010
|
+
process.stdout.write(`${formatJson(rows)}
|
|
21011
|
+
`);
|
|
21012
|
+
return 0;
|
|
21013
|
+
}
|
|
21014
|
+
process.stdout.write(
|
|
21015
|
+
"\n" + formatTable(rows, [
|
|
21016
|
+
{ header: "namespace", get: (n) => n.namespace },
|
|
21017
|
+
{ header: "contextId", get: (n) => n.contextId ?? "(tenant-wide)" },
|
|
21018
|
+
{
|
|
21019
|
+
header: "entityBacked",
|
|
21020
|
+
get: (n) => n.entityBacked === void 0 ? void 0 : String(n.entityBacked)
|
|
21021
|
+
},
|
|
21022
|
+
{
|
|
21023
|
+
header: "specificityRank",
|
|
21024
|
+
get: (n) => n.specificityRank === void 0 ? void 0 : String(n.specificityRank)
|
|
21025
|
+
}
|
|
21026
|
+
]) + "\n"
|
|
21027
|
+
);
|
|
21028
|
+
return 0;
|
|
21029
|
+
} catch (err) {
|
|
21030
|
+
process.stderr.write(formatCliError(err, { action: "namespace list failed" }));
|
|
21031
|
+
return 1;
|
|
21032
|
+
}
|
|
21033
|
+
});
|
|
21034
|
+
}
|
|
21035
|
+
async function runNamespaceGet(opts, deps = {}) {
|
|
21036
|
+
return withAuth({ ...opts, context: opts.context }, deps, async (auth) => {
|
|
21037
|
+
const client = auth.client;
|
|
21038
|
+
try {
|
|
21039
|
+
const ns = await client.identity.getNamespace({
|
|
21040
|
+
namespace: opts.namespace,
|
|
21041
|
+
...opts.context ? { contextId: opts.context } : {}
|
|
21042
|
+
});
|
|
21043
|
+
if (opts.json) {
|
|
21044
|
+
process.stdout.write(`${formatJson(ns)}
|
|
21045
|
+
`);
|
|
21046
|
+
return 0;
|
|
21047
|
+
}
|
|
21048
|
+
process.stdout.write(`
|
|
21049
|
+
${renderNamespace(ns)}
|
|
21050
|
+
`);
|
|
21051
|
+
return 0;
|
|
21052
|
+
} catch (err) {
|
|
21053
|
+
process.stderr.write(formatCliError(err, { action: "namespace get failed" }));
|
|
21054
|
+
return 1;
|
|
21055
|
+
}
|
|
21056
|
+
});
|
|
21057
|
+
}
|
|
21058
|
+
async function runNamespaceRegister(opts, deps = {}) {
|
|
21059
|
+
return withAuth({ ...opts, provisioning: true, context: opts.context }, deps, async (auth) => {
|
|
21060
|
+
const client = auth.client;
|
|
21061
|
+
try {
|
|
21062
|
+
const ns = await client.identity.registerNamespace({
|
|
21063
|
+
contextId: opts.context,
|
|
21064
|
+
body: {
|
|
21065
|
+
namespace: opts.namespace,
|
|
21066
|
+
specificityRank: opts.specificityRank,
|
|
21067
|
+
entityBacked: opts.entityBacked,
|
|
21068
|
+
defaultSchemaId: opts.defaultSchemaId
|
|
21069
|
+
}
|
|
21070
|
+
});
|
|
21071
|
+
if (opts.json) {
|
|
21072
|
+
process.stdout.write(`${formatJson(ns)}
|
|
21073
|
+
`);
|
|
21074
|
+
return 0;
|
|
21075
|
+
}
|
|
21076
|
+
process.stdout.write(`
|
|
21077
|
+
\u2714 namespace '${opts.namespace}' registered
|
|
21078
|
+
${renderNamespace(ns)}
|
|
21079
|
+
`);
|
|
21080
|
+
return 0;
|
|
21081
|
+
} catch (err) {
|
|
21082
|
+
process.stderr.write(formatCliError(err, { action: "namespace register failed" }));
|
|
21083
|
+
return 1;
|
|
21084
|
+
}
|
|
21085
|
+
});
|
|
21086
|
+
}
|
|
21087
|
+
|
|
21088
|
+
// src/commands/assume.ts
|
|
21089
|
+
function parseAssumeScopes(raw) {
|
|
21090
|
+
const out = {};
|
|
21091
|
+
for (const entry of raw) {
|
|
21092
|
+
const sep = entry.indexOf(":");
|
|
21093
|
+
if (sep <= 0 || sep === entry.length - 1) {
|
|
21094
|
+
throw new Error(`--scope must be '<namespace>:<value>' (e.g. 'org:orgB'); got '${entry}'.`);
|
|
21095
|
+
}
|
|
21096
|
+
const ns = entry.slice(0, sep);
|
|
21097
|
+
const value = entry.slice(sep + 1);
|
|
21098
|
+
const valueError = valueGrammarError(value);
|
|
21099
|
+
if (valueError) throw new Error(`--scope value ${valueError}`);
|
|
21100
|
+
const key = `scope:${ns}`;
|
|
21101
|
+
if (Object.prototype.hasOwnProperty.call(out, key)) {
|
|
21102
|
+
throw new Error(`--scope names namespace '${ns}' more than once \u2014 pass one value per namespace.`);
|
|
21103
|
+
}
|
|
21104
|
+
out[key] = value;
|
|
21105
|
+
}
|
|
21106
|
+
return out;
|
|
21107
|
+
}
|
|
21108
|
+
function renderAssumeToken(t) {
|
|
21109
|
+
return formatKeyValues([
|
|
21110
|
+
["access_token", t.access_token],
|
|
21111
|
+
["token_type", t.token_type],
|
|
21112
|
+
["expires_in", t.expires_in === void 0 ? void 0 : `${t.expires_in}s`]
|
|
21113
|
+
]);
|
|
21114
|
+
}
|
|
21115
|
+
async function runAssume(opts, deps = {}) {
|
|
21116
|
+
if (!opts.scope || opts.scope.length === 0) {
|
|
21117
|
+
process.stderr.write(" \u2716 --scope must be given at least once (e.g. --scope org:orgB).\n");
|
|
21118
|
+
return 2;
|
|
21119
|
+
}
|
|
21120
|
+
let scopes;
|
|
21121
|
+
try {
|
|
21122
|
+
scopes = parseAssumeScopes(opts.scope);
|
|
21123
|
+
} catch (err) {
|
|
21124
|
+
process.stderr.write(` \u2716 ${formatError(err)}
|
|
21125
|
+
`);
|
|
21126
|
+
return 2;
|
|
21127
|
+
}
|
|
21128
|
+
return withAuth({ ...opts, context: opts.context }, deps, async (auth) => {
|
|
21129
|
+
const client = auth.client;
|
|
21130
|
+
try {
|
|
21131
|
+
const result = await client.auth.assumeToken(scopes);
|
|
21132
|
+
if (opts.json) {
|
|
21133
|
+
process.stdout.write(`${formatJson(result)}
|
|
21134
|
+
`);
|
|
21135
|
+
return 0;
|
|
21136
|
+
}
|
|
21137
|
+
process.stdout.write(
|
|
21138
|
+
`
|
|
21139
|
+
\u2714 assumed ${Object.keys(scopes).join(", ")}
|
|
21140
|
+
${renderAssumeToken(result)}
|
|
21141
|
+
`
|
|
21142
|
+
);
|
|
21143
|
+
return 0;
|
|
21144
|
+
} catch (err) {
|
|
21145
|
+
process.stderr.write(formatCliError(err, { action: "assume failed" }));
|
|
21146
|
+
return 1;
|
|
21147
|
+
}
|
|
21148
|
+
});
|
|
21149
|
+
}
|
|
20584
21150
|
|
|
20585
21151
|
// src/commands/login.ts
|
|
20586
21152
|
var import_node_http = require("http");
|
|
@@ -21045,7 +21611,8 @@ async function runBlueprintHarness(client, input, deps) {
|
|
|
21045
21611
|
});
|
|
21046
21612
|
const blueprint = await resolveApplyTimeIdentities(
|
|
21047
21613
|
input.blueprint,
|
|
21048
|
-
provisioned
|
|
21614
|
+
provisioned,
|
|
21615
|
+
client,
|
|
21049
21616
|
log
|
|
21050
21617
|
);
|
|
21051
21618
|
const remint = remintForContext ?? (async () => client);
|
|
@@ -21458,9 +22025,9 @@ function emitJoined(opts) {
|
|
|
21458
22025
|
|
|
21459
22026
|
// src/build-info.ts
|
|
21460
22027
|
var BUILD_INFO = {
|
|
21461
|
-
cli: true ? "0.
|
|
21462
|
-
sdk: true ? "0.
|
|
21463
|
-
blueprints: true ? "0.
|
|
22028
|
+
cli: true ? "0.20.0" : "dev",
|
|
22029
|
+
sdk: true ? "0.42.0" : "dev",
|
|
22030
|
+
blueprints: true ? "0.17.0" : "dev"
|
|
21464
22031
|
};
|
|
21465
22032
|
function formatBuildInfo() {
|
|
21466
22033
|
return `vectros ${BUILD_INFO.cli} (sdk ${BUILD_INFO.sdk}, blueprints ${BUILD_INFO.blueprints})`;
|
|
@@ -21541,10 +22108,22 @@ function buildProgram(deps = {}) {
|
|
|
21541
22108
|
registerRoleCommands(program, deps);
|
|
21542
22109
|
registerContextCommands(program, deps);
|
|
21543
22110
|
registerIssuersCommands(program, deps);
|
|
22111
|
+
registerNamespaceCommands(program, deps);
|
|
22112
|
+
registerAssumeCommand(program, deps);
|
|
21544
22113
|
registerJoinCommand(program, deps);
|
|
21545
22114
|
registerKeyringCommands(program);
|
|
21546
22115
|
return program;
|
|
21547
22116
|
}
|
|
22117
|
+
function registerAssumeCommand(program, deps) {
|
|
22118
|
+
const assume = program.command("assume").description("Re-mint your own verb token with one or more identity.<namespace> values switched (needs a role's assumable grant).").requiredOption("--context <c>", "app context to operate in").option("--scope <ns:value>", "namespace value to assume, repeatable (e.g. org:orgB)", collect, []);
|
|
22119
|
+
addCommonAuthOptions(assume).action(
|
|
22120
|
+
async (flags) => {
|
|
22121
|
+
process.exit(
|
|
22122
|
+
await runAssume({ ...toCommonOptions(flags), context: flags.context, scope: flags.scope ?? [] }, deps)
|
|
22123
|
+
);
|
|
22124
|
+
}
|
|
22125
|
+
);
|
|
22126
|
+
}
|
|
21548
22127
|
function registerJoinCommand(program, deps) {
|
|
21549
22128
|
const join3 = program.command("join").description("Enroll a principal in a context (grant + key) and keep the key in the keyring.").argument("<context>", "the app context to join").option("--role <roleId>", "reference a Role to grant (repeatable \u2014 2+ compose additively; XOR --actions)", collect, []).option("--actions <csv>", "inline single-clause actions to grant, comma-separated (XOR --role)").option("--as <principal>", "who to enroll: 'me' (default) or an explicit 'usr_<id>'", "me").option("--scope <ns:value>", "identity override: a scope the member acts under (repeatable, e.g. org:<id>)", collect, []).option("--alias <name>", "keyring entry name (defaults to the context id)").option("--tenant <tenant>", "target tenant: 'test' or 'live' (default live)").option(
|
|
21550
22129
|
"--activate",
|
|
@@ -21681,7 +22260,7 @@ function registerContextCommands(program, deps) {
|
|
|
21681
22260
|
);
|
|
21682
22261
|
}
|
|
21683
22262
|
function registerIssuersCommands(program, deps) {
|
|
21684
|
-
const issuers = program.command("issuers").description("
|
|
22263
|
+
const issuers = program.command("issuers").description("Manage registered third-party IdP issuers (token-exchange trust anchors).");
|
|
21685
22264
|
const list = issuers.command("list").description("List issuers registered in one app context.").requiredOption("--context <c>", "app context the issuer(s) are registered under").option("--tenant <tenant>", "tenant to read: 'test' or 'live' (default live)");
|
|
21686
22265
|
addCommonAuthOptions(list).action(
|
|
21687
22266
|
async (flags) => {
|
|
@@ -21700,12 +22279,125 @@ function registerIssuersCommands(program, deps) {
|
|
|
21700
22279
|
);
|
|
21701
22280
|
}
|
|
21702
22281
|
);
|
|
22282
|
+
const register = issuers.command("register").description("Register (idempotent by issuerId) a trusted third-party IdP issuer.").requiredOption("--context <c>", "app context an exchanged token targets").requiredOption("--issuer-id <id>", "short slug for this issuer (3-31 chars; lowercase letter + lowercase/digit/dash)").requiredOption("--issuer <url>", "the IdP's iss claim value").requiredOption("--jwks-uri <url>", "the IdP's remote JWKS endpoint").requiredOption("--audience <aud>", "the required aud claim value (globally unique with --issuer)").option("--sub-claim <claim>", "claim carrying the subject identifier (default 'sub')").option("--email-claim <claim>", "claim carrying the subject's email (default 'email')").option("--userinfo-uri <url>", "OIDC userinfo endpoint, used as an email-resolution fallback").option(
|
|
22283
|
+
"--self-signup-policy <type:roleId>",
|
|
22284
|
+
"opt-in self-service signup entry, repeatable (e.g. default:member) \u2014 no entry may target an elevated role",
|
|
22285
|
+
collect,
|
|
22286
|
+
[]
|
|
22287
|
+
).option("--tenant <tenant>", "tenant the context lives in: 'test' or 'live' (default live)");
|
|
22288
|
+
addCommonAuthOptions(register).action(
|
|
22289
|
+
async (flags) => {
|
|
22290
|
+
const tenant = parseTenantFlag(flags.tenant);
|
|
22291
|
+
process.exit(
|
|
22292
|
+
await runIssuersRegister(
|
|
22293
|
+
{
|
|
22294
|
+
...toCommonOptions(flags),
|
|
22295
|
+
context: flags.context,
|
|
22296
|
+
issuerId: flags.issuerId,
|
|
22297
|
+
issuer: flags.issuer,
|
|
22298
|
+
jwksUri: flags.jwksUri,
|
|
22299
|
+
audience: flags.audience,
|
|
22300
|
+
subClaim: flags.subClaim,
|
|
22301
|
+
emailClaim: flags.emailClaim,
|
|
22302
|
+
userinfoUri: flags.userinfoUri,
|
|
22303
|
+
selfSignupPolicy: flags.selfSignupPolicy,
|
|
22304
|
+
tenant
|
|
22305
|
+
},
|
|
22306
|
+
deps
|
|
22307
|
+
)
|
|
22308
|
+
);
|
|
22309
|
+
}
|
|
22310
|
+
);
|
|
22311
|
+
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(
|
|
22312
|
+
"--self-signup-policy <type:roleId>",
|
|
22313
|
+
"replace self-signup policies, repeatable (e.g. default:member) \u2014 omit entirely to leave unchanged",
|
|
22314
|
+
collect,
|
|
22315
|
+
[]
|
|
22316
|
+
).option("--clear-self-signup-policies", "disable self-signup entirely (sends an empty list; XOR --self-signup-policy)").option("--tenant <tenant>", "tenant the context lives in: 'test' or 'live' (default live)");
|
|
22317
|
+
addCommonAuthOptions(update).action(
|
|
22318
|
+
async (issuerId, flags) => {
|
|
22319
|
+
const tenant = parseTenantFlag(flags.tenant);
|
|
22320
|
+
process.exit(
|
|
22321
|
+
await runIssuersUpdate(
|
|
22322
|
+
{
|
|
22323
|
+
...toCommonOptions(flags),
|
|
22324
|
+
context: flags.context,
|
|
22325
|
+
issuerId,
|
|
22326
|
+
subClaim: flags.subClaim,
|
|
22327
|
+
emailClaim: flags.emailClaim,
|
|
22328
|
+
userinfoUri: flags.userinfoUri,
|
|
22329
|
+
status: flags.status,
|
|
22330
|
+
selfSignupPolicy: flags.selfSignupPolicy,
|
|
22331
|
+
clearSelfSignupPolicies: Boolean(flags.clearSelfSignupPolicies),
|
|
22332
|
+
tenant
|
|
22333
|
+
},
|
|
22334
|
+
deps
|
|
22335
|
+
)
|
|
22336
|
+
);
|
|
22337
|
+
}
|
|
22338
|
+
);
|
|
22339
|
+
const del = issuers.command("delete").description("Deregister a trusted third-party IdP issuer (refused while any user is bound through it).").argument("<issuerId>").requiredOption("--context <c>", "app context the issuer is registered under").option("--force", "skip the interactive confirmation (required when stdin is not a TTY)").option("--tenant <tenant>", "tenant the context lives in: 'test' or 'live' (default live)");
|
|
22340
|
+
addCommonAuthOptions(del).action(
|
|
22341
|
+
async (issuerId, flags) => {
|
|
22342
|
+
const tenant = parseTenantFlag(flags.tenant);
|
|
22343
|
+
process.exit(
|
|
22344
|
+
await runIssuersDelete(
|
|
22345
|
+
{ ...toCommonOptions(flags), context: flags.context, issuerId, force: Boolean(flags.force), tenant },
|
|
22346
|
+
deps
|
|
22347
|
+
)
|
|
22348
|
+
);
|
|
22349
|
+
}
|
|
22350
|
+
);
|
|
22351
|
+
}
|
|
22352
|
+
function registerNamespaceCommands(program, deps) {
|
|
22353
|
+
const namespace = program.command("namespace").description("Manage scope-namespace registrations (whether scope:<namespace> values resolve to identity entities).");
|
|
22354
|
+
const register = namespace.command("register").description("Register a new scope namespace (NOT idempotent \u2014 a repeat name 400s).").argument("<name>", "namespace name: 2-32 chars, a lowercase letter first, then lowercase/digit/dash/underscore").requiredOption("--context <c>", "app context to own this registration").requiredOption("--specificity-rank <n>", "position in the account specificity order (integer 0-1000000, unique)").option("--entity-backed", "values must resolve to an identity entity (create via `identity create --type <name>`)").option("--default-schema-id <id>", "default record schema bound to entities in this namespace");
|
|
22355
|
+
addCommonAuthOptions(register).action(
|
|
22356
|
+
async (name, flags) => {
|
|
22357
|
+
const specificityRank = Number(flags.specificityRank);
|
|
22358
|
+
if (!Number.isInteger(specificityRank) || specificityRank < 0 || specificityRank > 1e6) {
|
|
22359
|
+
process.stderr.write(
|
|
22360
|
+
` \u2716 --specificity-rank must be an integer between 0 and 1000000 (got '${flags.specificityRank}').
|
|
22361
|
+
`
|
|
22362
|
+
);
|
|
22363
|
+
process.exit(2);
|
|
22364
|
+
}
|
|
22365
|
+
process.exit(
|
|
22366
|
+
await runNamespaceRegister(
|
|
22367
|
+
{
|
|
22368
|
+
...toCommonOptions(flags),
|
|
22369
|
+
namespace: name,
|
|
22370
|
+
context: flags.context,
|
|
22371
|
+
specificityRank,
|
|
22372
|
+
entityBacked: Boolean(flags.entityBacked),
|
|
22373
|
+
defaultSchemaId: flags.defaultSchemaId
|
|
22374
|
+
},
|
|
22375
|
+
deps
|
|
22376
|
+
)
|
|
22377
|
+
);
|
|
22378
|
+
}
|
|
22379
|
+
);
|
|
22380
|
+
const list = namespace.command("list").description("List scope-namespace registrations \u2014 tenant-wide, or one context\u2019s own.").option("--context <c>", "list one app context\u2019s own registrations instead of the tenant-wide set");
|
|
22381
|
+
addCommonAuthOptions(list).action(async (flags) => {
|
|
22382
|
+
process.exit(await runNamespaceList({ ...toCommonOptions(flags), context: flags.context }, deps));
|
|
22383
|
+
});
|
|
22384
|
+
const get = namespace.command("get").description("Show one namespace registration \u2014 tenant-wide, or one context\u2019s own.").argument("<name>").option("--context <c>", "resolve for one app context\u2019s own registration instead of the tenant-wide one");
|
|
22385
|
+
addCommonAuthOptions(get).action(
|
|
22386
|
+
async (name, flags) => {
|
|
22387
|
+
process.exit(
|
|
22388
|
+
await runNamespaceGet({ ...toCommonOptions(flags), namespace: name, context: flags.context }, deps)
|
|
22389
|
+
);
|
|
22390
|
+
}
|
|
22391
|
+
);
|
|
21703
22392
|
}
|
|
21704
22393
|
function registerRoleCommands(program, deps) {
|
|
21705
22394
|
const role = program.command("role").description("Manage context-scoped roles (reusable scope rules; bind via `access grant --role`).");
|
|
21706
22395
|
const create = role.command("create").description("Create (idempotent by roleId) a single-clause role from --actions.").requiredOption("--context <c>", "app context").requiredOption("--role-id <id>", "role id (3-31 chars; lowercase letter + lowercase/digit/dash)").requiredOption("--name <name>", "human-readable name").requiredOption("--actions <csv>", "allowed actions, comma-separated (e.g. records:cru,search:r)").option(
|
|
21707
22396
|
"--data-scope <json>",
|
|
21708
22397
|
`confine the clause to owned data \u2014 JSON map of dimension \u2192 allowed values, e.g. '{"userId":["\${{ self.userId }}"]}' or '{"scope:org":["<orgId>", null]}'`
|
|
22398
|
+
).option(
|
|
22399
|
+
"--assumable <json>",
|
|
22400
|
+
`role-level \`vectros assume\` grant \u2014 JSON map of scope:<namespace> \u2192 allowed values (e.g. '{"scope:org":["orgB"]}'); independent of --data-scope, never data_scope's grammar (userId cannot be named)`
|
|
21709
22401
|
).option("--description <text>", "free-text description");
|
|
21710
22402
|
addCommonAuthOptions(create).action(
|
|
21711
22403
|
async (flags) => {
|
|
@@ -21717,6 +22409,7 @@ function registerRoleCommands(program, deps) {
|
|
|
21717
22409
|
name: flags.name,
|
|
21718
22410
|
actions: flags.actions,
|
|
21719
22411
|
dataScope: flags.dataScope,
|
|
22412
|
+
assumable: flags.assumable,
|
|
21720
22413
|
description: flags.description
|
|
21721
22414
|
}, deps)
|
|
21722
22415
|
);
|
|
@@ -21810,6 +22503,20 @@ function registerIdentityCommands(program, deps) {
|
|
|
21810
22503
|
);
|
|
21811
22504
|
}
|
|
21812
22505
|
);
|
|
22506
|
+
const listAll = identity.command("list-all").description(
|
|
22507
|
+
"List every user in the tenant (owner login required), bypassing the app-context confinement 'identity list' is subject to \u2014 for recovering a user created but never granted access anywhere, which 'identity list' can never show."
|
|
22508
|
+
).option("--external-id <id>", "filter to a stable id").option("--limit <n>", "page size (1\u2013100)", (v) => Number.parseInt(v, 10)).option("--tenant <tenant>", "tenant to list: 'test' or 'live' (default live)");
|
|
22509
|
+
addCommonAuthOptions(listAll).action(
|
|
22510
|
+
async (flags) => {
|
|
22511
|
+
const tenant = parseTenantFlag(flags.tenant);
|
|
22512
|
+
process.exit(
|
|
22513
|
+
await runIdentityListAll(
|
|
22514
|
+
{ ...toCommonOptions(flags), externalId: flags.externalId, limit: flags.limit, tenant },
|
|
22515
|
+
deps
|
|
22516
|
+
)
|
|
22517
|
+
);
|
|
22518
|
+
}
|
|
22519
|
+
);
|
|
21813
22520
|
const get = identity.command("get").description("Show one principal by its Vectros id.").addOption(dimensionOption()).requiredOption("--id <vectrosId>", "the Vectros-assigned id (from create/list)");
|
|
21814
22521
|
addCommonAuthOptions(get).action(async (flags) => {
|
|
21815
22522
|
process.exit(await runIdentityGet({ ...toCommonOptions(flags), type: flags.type, id: flags.id }, deps));
|