@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.mjs
CHANGED
|
@@ -6109,7 +6109,7 @@ var AuthClient = class {
|
|
|
6109
6109
|
return handleNonStatusCodeError(_response.error, _response.rawResponse, "POST", "/v1/auth/token");
|
|
6110
6110
|
}
|
|
6111
6111
|
/**
|
|
6112
|
-
* 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.
|
|
6112
|
+
* 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.
|
|
6113
6113
|
*
|
|
6114
6114
|
* @param {Vectros.CreateInviteRequest} request
|
|
6115
6115
|
* @param {AuthClient.RequestOptions} requestOptions - Request-specific configuration.
|
|
@@ -6180,7 +6180,7 @@ var AuthClient = class {
|
|
|
6180
6180
|
return handleNonStatusCodeError(_response.error, _response.rawResponse, "POST", "/v1/users/invite");
|
|
6181
6181
|
}
|
|
6182
6182
|
/**
|
|
6183
|
-
* 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.
|
|
6183
|
+
* 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.
|
|
6184
6184
|
*
|
|
6185
6185
|
* @param {Vectros.CreateInviteRequest} request
|
|
6186
6186
|
* @param {AuthClient.RequestOptions} requestOptions - Request-specific configuration.
|
|
@@ -7053,7 +7053,21 @@ var DocumentsClient = class {
|
|
|
7053
7053
|
return HttpResponsePromise.fromPromise(this.__lookupDocuments(request, requestOptions));
|
|
7054
7054
|
}
|
|
7055
7055
|
async __lookupDocuments(request, requestOptions) {
|
|
7056
|
-
const {
|
|
7056
|
+
const {
|
|
7057
|
+
type: type_,
|
|
7058
|
+
field,
|
|
7059
|
+
value,
|
|
7060
|
+
from: from_,
|
|
7061
|
+
to,
|
|
7062
|
+
prefix,
|
|
7063
|
+
sortFrom,
|
|
7064
|
+
sortTo,
|
|
7065
|
+
startFrom,
|
|
7066
|
+
limit,
|
|
7067
|
+
order,
|
|
7068
|
+
userId,
|
|
7069
|
+
scope
|
|
7070
|
+
} = request;
|
|
7057
7071
|
const _queryParams = {
|
|
7058
7072
|
type: type_,
|
|
7059
7073
|
field,
|
|
@@ -7061,6 +7075,8 @@ var DocumentsClient = class {
|
|
|
7061
7075
|
from: from_,
|
|
7062
7076
|
to,
|
|
7063
7077
|
prefix,
|
|
7078
|
+
sortFrom,
|
|
7079
|
+
sortTo,
|
|
7064
7080
|
startFrom,
|
|
7065
7081
|
limit,
|
|
7066
7082
|
order: order != null ? order : void 0,
|
|
@@ -11032,7 +11048,21 @@ var BlueprintSchemaSchema = z.object({
|
|
|
11032
11048
|
// (it becomes that name's shared base). Must point directly at the base (one
|
|
11033
11049
|
// hop); immutable once set. Mirrors the `basedOn` field on the platform's
|
|
11034
11050
|
// schema-create request.
|
|
11035
|
-
basedOn: z.string().min(1).optional()
|
|
11051
|
+
basedOn: z.string().min(1).optional(),
|
|
11052
|
+
// ADVISORY ONLY — not sent to the platform, never runtime-enforced. The
|
|
11053
|
+
// ownership dimensions this schema's records are typically scoped by
|
|
11054
|
+
// (e.g. ['org', 'client']), declared so the CLI's blueprint lint can warn
|
|
11055
|
+
// when a role clause grants r/u/d on this type but its `dataScope` names
|
|
11056
|
+
// only SOME of these dims — a schema author's easiest way to leave a
|
|
11057
|
+
// dimension unintentionally unconstrained, since reads (unlike creates)
|
|
11058
|
+
// never require full-dimension `dataScope` coverage — this helps an
|
|
11059
|
+
// author NOTICE that read/write asymmetry, not change it; the asymmetry
|
|
11060
|
+
// itself is a ratified, deliberate platform behavior.
|
|
11061
|
+
// Same grammar as a `dataScope` key with the `scope:` prefix stripped:
|
|
11062
|
+
// a bare namespace name, or 'userId' for the principal dimension.
|
|
11063
|
+
// Structural-only here (validated below by `lintExpectedScopeDims`); the
|
|
11064
|
+
// cross-referencing lint against role clauses lives in `packages/cli`.
|
|
11065
|
+
expectedScopeDims: z.array(z.string().min(1)).max(MAX_SCOPE_DIMENSIONS).optional()
|
|
11036
11066
|
}).strict().superRefine((schema, ctx) => {
|
|
11037
11067
|
const hasComposite = (schema.lookupFields ?? []).some(
|
|
11038
11068
|
(lf) => typeof lf === "object" && lf.fieldNames !== void 0
|
|
@@ -11083,8 +11113,8 @@ var BlueprintAccessProfileSchema = z.object({
|
|
|
11083
11113
|
//
|
|
11084
11114
|
// Optional now: mutually exclusive with `roleIds` below (the
|
|
11085
11115
|
// `.superRefine` on this schema enforces the XOR, mirroring the platform's
|
|
11086
|
-
// own
|
|
11087
|
-
// authors this profile's grant.
|
|
11116
|
+
// own access-profile scopes/role-composition XOR check). Exactly one of
|
|
11117
|
+
// the two authors this profile's grant.
|
|
11088
11118
|
allowedActions: z.array(z.string().min(1)).min(1).optional(),
|
|
11089
11119
|
// Additive role composition: this profile's effective grant is
|
|
11090
11120
|
// each named role's own clauses, concatenated in the order listed, instead of
|
|
@@ -11687,6 +11717,35 @@ function lintDataScopeKeys(value, ctx) {
|
|
|
11687
11717
|
}
|
|
11688
11718
|
}
|
|
11689
11719
|
}
|
|
11720
|
+
function lintExpectedScopeDims(value, ctx) {
|
|
11721
|
+
const bp = value;
|
|
11722
|
+
(bp.schemas ?? []).forEach((schema, i) => {
|
|
11723
|
+
const dims = schema.expectedScopeDims;
|
|
11724
|
+
if (!dims) return;
|
|
11725
|
+
const seen = /* @__PURE__ */ new Set();
|
|
11726
|
+
dims.forEach((raw, j) => {
|
|
11727
|
+
const dim = String(raw);
|
|
11728
|
+
const path4 = ["schemas", i, "expectedScopeDims", j];
|
|
11729
|
+
if (seen.has(dim)) {
|
|
11730
|
+
ctx.addIssue({
|
|
11731
|
+
code: z.ZodIssueCode.custom,
|
|
11732
|
+
path: path4,
|
|
11733
|
+
message: `schema '${schema.typeName}': expectedScopeDims lists '${dim}' more than once.`
|
|
11734
|
+
});
|
|
11735
|
+
}
|
|
11736
|
+
seen.add(dim);
|
|
11737
|
+
if (dim === "userId") return;
|
|
11738
|
+
const nsError = scopeNamespaceGrammarError(dim);
|
|
11739
|
+
if (nsError !== null) {
|
|
11740
|
+
ctx.addIssue({
|
|
11741
|
+
code: z.ZodIssueCode.custom,
|
|
11742
|
+
path: path4,
|
|
11743
|
+
message: `schema '${schema.typeName}': expectedScopeDims['${dim}'] ${nsError}`
|
|
11744
|
+
});
|
|
11745
|
+
}
|
|
11746
|
+
});
|
|
11747
|
+
});
|
|
11748
|
+
}
|
|
11690
11749
|
function lintDataScopeRefs(value, ctx) {
|
|
11691
11750
|
const bp = value;
|
|
11692
11751
|
const declared = new Set(Object.keys(bp.fragments ?? {}));
|
|
@@ -11879,6 +11938,15 @@ var BlueprintSchema = z.object({
|
|
|
11879
11938
|
}),
|
|
11880
11939
|
/** Human-readable app-context name; defaults to `MCP — <name>` (see {@link contextNameOf}) when absent. */
|
|
11881
11940
|
contextName: z.string().min(1).optional(),
|
|
11941
|
+
/**
|
|
11942
|
+
* The deploying organization's own display name — distinct from {@link contextName}.
|
|
11943
|
+
* `contextName` is the blueprint author's fixed identity for the app (the same for every
|
|
11944
|
+
* deployer); `companyName` is meant to vary per install, typically templated from a
|
|
11945
|
+
* deployer-supplied `${{ inputs.x }}` value rather than hardcoded. Used for branding on
|
|
11946
|
+
* platform-sent correspondence (e.g. invite emails) alongside `contextName` — not a
|
|
11947
|
+
* replacement for it. See {@link companyNameOf}.
|
|
11948
|
+
*/
|
|
11949
|
+
companyName: z.string().min(1).optional(),
|
|
11882
11950
|
schemas: z.array(BlueprintSchemaSchema).default([]),
|
|
11883
11951
|
accessProfile: BlueprintAccessProfileSchema,
|
|
11884
11952
|
servicePrincipal: BlueprintServicePrincipalSchema,
|
|
@@ -11927,6 +11995,7 @@ var BlueprintSchema = z.object({
|
|
|
11927
11995
|
lintIdentityOverrides(bp, ctx);
|
|
11928
11996
|
lintNamespacedScopeArrays(bp, ctx);
|
|
11929
11997
|
lintDataScopeKeys(bp, ctx);
|
|
11998
|
+
lintExpectedScopeDims(bp, ctx);
|
|
11930
11999
|
lintDataScopeRefs(bp, ctx);
|
|
11931
12000
|
lintRoleAssumable(bp, ctx);
|
|
11932
12001
|
lintAccessProfileRoleIds(bp, ctx);
|
|
@@ -14109,6 +14178,7 @@ var ENV_BASE_URLS = {
|
|
|
14109
14178
|
// `--base-url` or VECTROS_API_BASE_URL; verify at smoke time.
|
|
14110
14179
|
staging: "https://api.staging.vectros.ai"
|
|
14111
14180
|
};
|
|
14181
|
+
var AUTH_FLAG_NAMES = ["--env", "--base-url", "--token", "--use-scoped-token"];
|
|
14112
14182
|
function envToBaseUrl(env, override) {
|
|
14113
14183
|
return validateBaseUrl(override ?? ENV_BASE_URLS[env]);
|
|
14114
14184
|
}
|
|
@@ -14615,7 +14685,7 @@ ${summary}`
|
|
|
14615
14685
|
this.rejected = rejected;
|
|
14616
14686
|
}
|
|
14617
14687
|
};
|
|
14618
|
-
var GRANTED_CAPABILITIES_ALLOWLIST = ["member-lifecycle"];
|
|
14688
|
+
var GRANTED_CAPABILITIES_ALLOWLIST = ["member-lifecycle", "delegate-principal-stamp"];
|
|
14619
14689
|
var GRANTED_CAPABILITIES_SET = new Set(GRANTED_CAPABILITIES_ALLOWLIST);
|
|
14620
14690
|
var CAPABILITIES_HINT = GRANTED_CAPABILITIES_ALLOWLIST.join("/");
|
|
14621
14691
|
var CapabilityGateError = class extends Error {
|
|
@@ -14892,7 +14962,8 @@ function makeIdentityResolver(client, log) {
|
|
|
14892
14962
|
return id;
|
|
14893
14963
|
};
|
|
14894
14964
|
}
|
|
14895
|
-
async function resolveApplyTimeIdentities(blueprint,
|
|
14965
|
+
async function resolveApplyTimeIdentities(blueprint, provisioned, bootstrapClient, log) {
|
|
14966
|
+
const client = provisioned.ctxBootstrapClient ?? bootstrapClient;
|
|
14896
14967
|
const resolver = makeIdentityResolver(client, log);
|
|
14897
14968
|
const resolved = await resolveBlueprintIdentities(blueprint, resolver);
|
|
14898
14969
|
return parseBlueprint(resolved);
|
|
@@ -15160,21 +15231,13 @@ async function runLoader(client, input) {
|
|
|
15160
15231
|
machineId,
|
|
15161
15232
|
rotate = false,
|
|
15162
15233
|
skipSeed = false,
|
|
15163
|
-
|
|
15234
|
+
seedIndexPoll,
|
|
15235
|
+
provisioned,
|
|
15164
15236
|
readExistingKey,
|
|
15165
15237
|
persistMintedKey,
|
|
15166
|
-
confirmExistingPrincipal,
|
|
15167
15238
|
log
|
|
15168
15239
|
} = input;
|
|
15169
15240
|
assertNoUnresolvedIdentities(blueprint);
|
|
15170
|
-
const provisioned = input.provisioned ?? await provisionContext(client, {
|
|
15171
|
-
blueprint,
|
|
15172
|
-
allowTenantWideNamespaces,
|
|
15173
|
-
remintBootstrapForContext: input.remintBootstrapForContext,
|
|
15174
|
-
readExistingKey,
|
|
15175
|
-
confirmExistingPrincipal,
|
|
15176
|
-
log
|
|
15177
|
-
});
|
|
15178
15241
|
const { contextAction, servicePrincipalUserId: userId, userAction, namespaceActions } = provisioned;
|
|
15179
15242
|
assertBlueprintGates(blueprint);
|
|
15180
15243
|
const needsContextPinnedBootstrapForIssuers = (blueprint.issuers ?? []).length > 0;
|
|
@@ -15353,6 +15416,7 @@ async function runLoader(client, input) {
|
|
|
15353
15416
|
const seededRecordIds = [];
|
|
15354
15417
|
const seededDocumentIds = [];
|
|
15355
15418
|
const seedIndexFailures = [];
|
|
15419
|
+
const pendingIndexSeeds = [];
|
|
15356
15420
|
const noteIfIndexFailed = (created, rec, surface) => {
|
|
15357
15421
|
if (created.indexStatus !== "FAILED") return;
|
|
15358
15422
|
seedIndexFailures.push({
|
|
@@ -15387,6 +15451,9 @@ async function runLoader(client, input) {
|
|
|
15387
15451
|
}
|
|
15388
15452
|
});
|
|
15389
15453
|
if (created.id) seededDocumentIds.push(created.id);
|
|
15454
|
+
if (created.id && created.indexStatus === "PENDING_INDEX") {
|
|
15455
|
+
pendingIndexSeeds.push({ id: created.id, externalId: rec.externalId, typeName: rec.typeName, surface: "document" });
|
|
15456
|
+
}
|
|
15390
15457
|
noteIfIndexFailed(created, rec, "document");
|
|
15391
15458
|
} else {
|
|
15392
15459
|
const created = await ctxClient.records.createRecord({
|
|
@@ -15399,10 +15466,30 @@ async function runLoader(client, input) {
|
|
|
15399
15466
|
}
|
|
15400
15467
|
});
|
|
15401
15468
|
if (created.id) seededRecordIds.push(created.id);
|
|
15469
|
+
if (created.id && created.indexStatus === "PENDING_INDEX") {
|
|
15470
|
+
pendingIndexSeeds.push({ id: created.id, externalId: rec.externalId, typeName: rec.typeName, surface: "record" });
|
|
15471
|
+
}
|
|
15402
15472
|
noteIfIndexFailed(created, rec, "record");
|
|
15403
15473
|
}
|
|
15404
15474
|
seededCount++;
|
|
15405
15475
|
}
|
|
15476
|
+
const pollAttempts = seedIndexPoll?.attempts ?? 3;
|
|
15477
|
+
const pollDelayMs = seedIndexPoll?.delayMs ?? 500;
|
|
15478
|
+
if (pollAttempts > 0 && pendingIndexSeeds.length > 0) {
|
|
15479
|
+
await Promise.all(
|
|
15480
|
+
pendingIndexSeeds.map(async (seed) => {
|
|
15481
|
+
for (let attempt = 0; attempt < pollAttempts; attempt++) {
|
|
15482
|
+
await new Promise((r) => setTimeout(r, pollDelayMs));
|
|
15483
|
+
const readBack = seed.surface === "document" ? await ctxClient.documents.getDocument({ id: seed.id }) : await ctxClient.records.getRecord({ id: seed.id });
|
|
15484
|
+
if (readBack.indexStatus === "FAILED") {
|
|
15485
|
+
noteIfIndexFailed(readBack, seed, seed.surface);
|
|
15486
|
+
return;
|
|
15487
|
+
}
|
|
15488
|
+
if (readBack.indexStatus !== "PENDING_INDEX") return;
|
|
15489
|
+
}
|
|
15490
|
+
})
|
|
15491
|
+
);
|
|
15492
|
+
}
|
|
15406
15493
|
return {
|
|
15407
15494
|
contextId: blueprint.contextId,
|
|
15408
15495
|
servicePrincipalUserId: userId,
|
|
@@ -15582,6 +15669,11 @@ function buildClaudeCodeAddCommand(entry, serverKey = "vectros") {
|
|
|
15582
15669
|
const envFlags = Object.entries(entry.env ?? {}).map(([k, v]) => `-e ${k}=${v}`);
|
|
15583
15670
|
return ["claude", "mcp", "add", serverKey, ...envFlags, "--", entry.command, ...entry.args].join(" ");
|
|
15584
15671
|
}
|
|
15672
|
+
function buildRestartHint(blueprintName, isCode) {
|
|
15673
|
+
const target = isCode ? "Reopen Claude Code in this project" : "Restart your MCP client";
|
|
15674
|
+
return ` ${target}, then ask Claude about your '${blueprintName}' data.
|
|
15675
|
+
`;
|
|
15676
|
+
}
|
|
15585
15677
|
function mergeServerEntry(existing, entry, serverKey = "vectros") {
|
|
15586
15678
|
const base = existing ? { ...existing } : {};
|
|
15587
15679
|
const servers = { ...base.mcpServers ?? {} };
|
|
@@ -15921,11 +16013,19 @@ var CryptoFileSecretStore = class {
|
|
|
15921
16013
|
* sub-millisecond in practice; the budget is generous for a loaded machine)
|
|
15922
16014
|
* before deciding it is a crash remnant. A SHORT-but-nonempty winner is never
|
|
15923
16015
|
* a write in flight — the 16 salt bytes land in one write — so it is refused
|
|
15924
|
-
* immediately, exactly like the top-of-function read's twin check.
|
|
16016
|
+
* immediately, exactly like the top-of-function read's twin check. A winner
|
|
16017
|
+
* that goes MISSING mid-poll (ENOENT) restarts creation from the top rather
|
|
16018
|
+
* than surfacing the raw ENOENT — see the catch below.
|
|
15925
16019
|
*/
|
|
15926
16020
|
async adoptSalt(saltPath) {
|
|
15927
16021
|
for (let attempt = 0; ; attempt++) {
|
|
15928
|
-
|
|
16022
|
+
let winner;
|
|
16023
|
+
try {
|
|
16024
|
+
winner = await fs3.readFile(saltPath);
|
|
16025
|
+
} catch (err) {
|
|
16026
|
+
if (err.code === "ENOENT") return this.readOrCreateSalt();
|
|
16027
|
+
throw err;
|
|
16028
|
+
}
|
|
15929
16029
|
if (winner.length >= 16) return winner;
|
|
15930
16030
|
if (winner.length > 0) throw corruptSaltError(saltPath, winner.length);
|
|
15931
16031
|
if (attempt >= 24) break;
|
|
@@ -17764,7 +17864,7 @@ function applyOutcomeSummary(result) {
|
|
|
17764
17864
|
const parts = [
|
|
17765
17865
|
reconcileTally(result.schemaActions, "schema"),
|
|
17766
17866
|
reconcileTally(result.roleActions, "role"),
|
|
17767
|
-
//
|
|
17867
|
+
// The platform's issuer partial-update endpoint makes an issuer's
|
|
17768
17868
|
// 'reconciled' mean the same "updated to match" as every other kind here.
|
|
17769
17869
|
reconcileTally(result.issuerActions, "issuer"),
|
|
17770
17870
|
namespaceTally(result.namespaceActions),
|
|
@@ -18155,11 +18255,7 @@ ${renderPlan(blueprint, planSteps)}
|
|
|
18155
18255
|
confirmExistingPrincipal,
|
|
18156
18256
|
log
|
|
18157
18257
|
});
|
|
18158
|
-
const resolvedBlueprint = await resolveIdentities(
|
|
18159
|
-
blueprint,
|
|
18160
|
-
provisioned.ctxBootstrapClient ?? client,
|
|
18161
|
-
log
|
|
18162
|
-
);
|
|
18258
|
+
const resolvedBlueprint = await resolveIdentities(blueprint, provisioned, client, log);
|
|
18163
18259
|
const machineId = await resolveMachineId();
|
|
18164
18260
|
const result = await loader(client, {
|
|
18165
18261
|
blueprint: resolvedBlueprint,
|
|
@@ -18167,7 +18263,6 @@ ${renderPlan(blueprint, planSteps)}
|
|
|
18167
18263
|
machineId,
|
|
18168
18264
|
rotate: args.rotate,
|
|
18169
18265
|
skipSeed: noSeed,
|
|
18170
|
-
allowTenantWideNamespaces: args.allowTenantWideNamespaces,
|
|
18171
18266
|
readExistingKey,
|
|
18172
18267
|
persistMintedKey,
|
|
18173
18268
|
provisioned,
|
|
@@ -18178,7 +18273,6 @@ ${renderPlan(blueprint, planSteps)}
|
|
|
18178
18273
|
tenant: args.tenant,
|
|
18179
18274
|
mintContext
|
|
18180
18275
|
}),
|
|
18181
|
-
confirmExistingPrincipal,
|
|
18182
18276
|
log
|
|
18183
18277
|
});
|
|
18184
18278
|
process.stdout.write(
|
|
@@ -18234,10 +18328,13 @@ ${codeAddHint}`);
|
|
|
18234
18328
|
}
|
|
18235
18329
|
try {
|
|
18236
18330
|
const merged = await safeMergeConfigFile({ configPath, entry });
|
|
18237
|
-
const restartHint =
|
|
18331
|
+
const restartHint = buildRestartHint(blueprint.name, isCode);
|
|
18238
18332
|
process.stdout.write(
|
|
18239
18333
|
` \u2714 Config ${merged.action}${merged.backupPath ? ` (backup: ${merged.backupPath})` : ""}
|
|
18240
|
-
` + restartHint
|
|
18334
|
+
` + restartHint + // The `claude mcp add` line is otherwise only shown on --no-merge / a
|
|
18335
|
+
// declined / a failed merge — surface it here too so a successful
|
|
18336
|
+
// auto-merge doesn't hide the idiomatic install command entirely.
|
|
18337
|
+
codeAddHint
|
|
18241
18338
|
);
|
|
18242
18339
|
} catch (err) {
|
|
18243
18340
|
process.stderr.write(` \u26A0 Could not merge config (${err instanceof Error ? err.message : String(err)}).
|
|
@@ -18421,8 +18518,12 @@ function compositeLegFindings(schema, lf, fieldsById, rangeEnabledFieldNames) {
|
|
|
18421
18518
|
function lintBlueprint(bp) {
|
|
18422
18519
|
const findings = [];
|
|
18423
18520
|
const typeNames = /* @__PURE__ */ new Set();
|
|
18521
|
+
const expectedDimsByTypeName = /* @__PURE__ */ new Map();
|
|
18424
18522
|
for (const schema of bp.schemas) {
|
|
18425
18523
|
typeNames.add(schema.typeName);
|
|
18524
|
+
if (schema.expectedScopeDims && schema.expectedScopeDims.length > 0) {
|
|
18525
|
+
expectedDimsByTypeName.set(schema.typeName, schema.expectedScopeDims);
|
|
18526
|
+
}
|
|
18426
18527
|
const fieldIds = /* @__PURE__ */ new Set();
|
|
18427
18528
|
const fieldsById = /* @__PURE__ */ new Map();
|
|
18428
18529
|
for (const f of schema.fields) {
|
|
@@ -18503,6 +18604,9 @@ function lintBlueprint(bp) {
|
|
|
18503
18604
|
if (bp.accessProfile.allowedActions !== void 0) {
|
|
18504
18605
|
const reached = ownerScopedReachWithoutDataScope(bp.accessProfile.allowedActions, ds);
|
|
18505
18606
|
if (reached.length > 0) findings.push(unconstrainedReachFinding("accessProfile", reached));
|
|
18607
|
+
findings.push(
|
|
18608
|
+
...partialCoverageFindings("accessProfile", bp.accessProfile.allowedActions, ds, expectedDimsByTypeName)
|
|
18609
|
+
);
|
|
18506
18610
|
}
|
|
18507
18611
|
for (const roleId of Object.keys(bp.roles ?? {})) {
|
|
18508
18612
|
if (!ROLE_ID_RE.test(roleId)) {
|
|
@@ -18520,6 +18624,9 @@ function lintBlueprint(bp) {
|
|
|
18520
18624
|
if (reached.length > 0) {
|
|
18521
18625
|
findings.push(unconstrainedReachFinding(`role '${roleId}' clause ${i}`, reached));
|
|
18522
18626
|
}
|
|
18627
|
+
findings.push(
|
|
18628
|
+
...partialCoverageFindings(`role '${roleId}' clause ${i}`, clause.allowedActions, cds, expectedDimsByTypeName)
|
|
18629
|
+
);
|
|
18523
18630
|
if (!cds) return;
|
|
18524
18631
|
for (const [key, values] of Object.entries(cds)) {
|
|
18525
18632
|
for (const value of values) {
|
|
@@ -18596,6 +18703,32 @@ function unconstrainedReachFinding(where, resources) {
|
|
|
18596
18703
|
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.`
|
|
18597
18704
|
};
|
|
18598
18705
|
}
|
|
18706
|
+
function dimToDataScopeKey(dim) {
|
|
18707
|
+
return dim === "userId" ? "userId" : `scope:${dim}`;
|
|
18708
|
+
}
|
|
18709
|
+
function partialCoverageFindings(where, allowedActions, dataScope, expectedDimsByTypeName) {
|
|
18710
|
+
const findings = [];
|
|
18711
|
+
const dataScopeKeys = new Set(Object.keys(dataScope ?? {}));
|
|
18712
|
+
for (const action of allowedActions) {
|
|
18713
|
+
const parts = action.split(":");
|
|
18714
|
+
const [resource, ops] = parts;
|
|
18715
|
+
if (resource !== "records" && resource !== "documents" || !ops) continue;
|
|
18716
|
+
const qualifier = parts.length > 2 ? parts.slice(2).join(":") : void 0;
|
|
18717
|
+
if (qualifier === void 0) continue;
|
|
18718
|
+
const dims = expectedDimsByTypeName.get(qualifier);
|
|
18719
|
+
if (!dims || dims.length === 0) continue;
|
|
18720
|
+
if (!(ops === "*" || [...ops].some((op) => op === "r" || op === "u" || op === "d"))) continue;
|
|
18721
|
+
const covered = dims.filter((d) => dataScopeKeys.has(dimToDataScopeKey(d)));
|
|
18722
|
+
if (covered.length === 0 || covered.length === dims.length) continue;
|
|
18723
|
+
const missing = dims.filter((d) => !covered.includes(d));
|
|
18724
|
+
findings.push({
|
|
18725
|
+
severity: "warning",
|
|
18726
|
+
rule: "partial-datascope-dimension-coverage",
|
|
18727
|
+
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).`
|
|
18728
|
+
});
|
|
18729
|
+
}
|
|
18730
|
+
return findings;
|
|
18731
|
+
}
|
|
18599
18732
|
function lintPasses(findings) {
|
|
18600
18733
|
return !findings.some((f) => f.severity === "error");
|
|
18601
18734
|
}
|
|
@@ -18687,6 +18820,10 @@ function buildScaffold(name, from) {
|
|
|
18687
18820
|
}
|
|
18688
18821
|
|
|
18689
18822
|
// src/cli-blueprint.ts
|
|
18823
|
+
function noEffectOnBlueprint(flag, sub) {
|
|
18824
|
+
const cmd = sub ? `blueprint ${sub}` : "blueprint";
|
|
18825
|
+
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'?`;
|
|
18826
|
+
}
|
|
18690
18827
|
function parseBlueprintArgs(argv) {
|
|
18691
18828
|
const args = { help: false, force: false, set: [] };
|
|
18692
18829
|
const positionals = [];
|
|
@@ -18713,6 +18850,9 @@ function parseBlueprintArgs(argv) {
|
|
|
18713
18850
|
args.values = argv[++i];
|
|
18714
18851
|
break;
|
|
18715
18852
|
default:
|
|
18853
|
+
if (AUTH_FLAG_NAMES.includes(a)) {
|
|
18854
|
+
throw new Error(noEffectOnBlueprint(a, positionals[0]));
|
|
18855
|
+
}
|
|
18716
18856
|
if (a.startsWith("-")) throw new Error(`Unknown blueprint flag: ${a}`);
|
|
18717
18857
|
positionals.push(a);
|
|
18718
18858
|
}
|
|
@@ -19505,6 +19645,51 @@ ${list}
|
|
|
19505
19645
|
});
|
|
19506
19646
|
}
|
|
19507
19647
|
|
|
19648
|
+
// src/dev-api.ts
|
|
19649
|
+
var DeveloperApiError = class extends Error {
|
|
19650
|
+
statusCode;
|
|
19651
|
+
constructor(message, statusCode) {
|
|
19652
|
+
super(message);
|
|
19653
|
+
this.name = "DeveloperApiError";
|
|
19654
|
+
this.statusCode = statusCode;
|
|
19655
|
+
}
|
|
19656
|
+
};
|
|
19657
|
+
async function fetchDeveloperUsersPage(opts) {
|
|
19658
|
+
const { cognitoToken, apiBaseUrl, tenant, startFrom, limit, fetchImpl = fetch } = opts;
|
|
19659
|
+
const base = apiBaseUrl.replace(/\/$/, "");
|
|
19660
|
+
let url = `${base}/developer/users?tenant=${encodeURIComponent(tenant)}`;
|
|
19661
|
+
if (startFrom) url += `&startFrom=${encodeURIComponent(startFrom)}`;
|
|
19662
|
+
if (limit !== void 0) url += `&limit=${encodeURIComponent(String(limit))}`;
|
|
19663
|
+
let res;
|
|
19664
|
+
try {
|
|
19665
|
+
res = await fetchImpl(url, {
|
|
19666
|
+
method: "GET",
|
|
19667
|
+
headers: { Authorization: `Bearer ${cognitoToken}` }
|
|
19668
|
+
});
|
|
19669
|
+
} catch (err) {
|
|
19670
|
+
throw new DeveloperApiError(
|
|
19671
|
+
`Request to /developer/users failed: ${err instanceof Error ? err.message : String(err)}`
|
|
19672
|
+
);
|
|
19673
|
+
}
|
|
19674
|
+
if (!res.ok) {
|
|
19675
|
+
const body = await res.text().catch(() => "");
|
|
19676
|
+
const hint = res.status === 403 ? " (this command requires an account owner login \u2014 sign in as the owner and retry)" : "";
|
|
19677
|
+
throw new DeveloperApiError(
|
|
19678
|
+
`GET /developer/users failed: HTTP ${res.status}${hint}${body ? ` \u2014 ${body}` : ""}`,
|
|
19679
|
+
res.status
|
|
19680
|
+
);
|
|
19681
|
+
}
|
|
19682
|
+
let parsed;
|
|
19683
|
+
try {
|
|
19684
|
+
parsed = await res.json();
|
|
19685
|
+
} catch {
|
|
19686
|
+
throw new DeveloperApiError("GET /developer/users returned a non-JSON body");
|
|
19687
|
+
}
|
|
19688
|
+
const data = Array.isArray(parsed.data) ? parsed.data : [];
|
|
19689
|
+
const nextCursor = typeof parsed.nextCursor === "string" ? parsed.nextCursor : null;
|
|
19690
|
+
return { data, nextCursor };
|
|
19691
|
+
}
|
|
19692
|
+
|
|
19508
19693
|
// src/commands/identity.ts
|
|
19509
19694
|
function isUserDimension(type) {
|
|
19510
19695
|
return type === "user";
|
|
@@ -19659,6 +19844,57 @@ async function runIdentityList(opts, deps = {}) {
|
|
|
19659
19844
|
}
|
|
19660
19845
|
});
|
|
19661
19846
|
}
|
|
19847
|
+
function toIdentityView(row) {
|
|
19848
|
+
const str = (v) => typeof v === "string" ? v : void 0;
|
|
19849
|
+
return {
|
|
19850
|
+
id: str(row.id),
|
|
19851
|
+
externalId: str(row.externalId),
|
|
19852
|
+
email: str(row.email),
|
|
19853
|
+
status: str(row.status),
|
|
19854
|
+
type: str(row.type),
|
|
19855
|
+
createdAt: str(row.createdAt)
|
|
19856
|
+
};
|
|
19857
|
+
}
|
|
19858
|
+
async function runIdentityListAll(opts, deps = {}) {
|
|
19859
|
+
const fetchImpl = deps.fetchImpl ?? fetch;
|
|
19860
|
+
return withAuth({ ...opts, provisioning: true }, deps, async (auth) => {
|
|
19861
|
+
try {
|
|
19862
|
+
const tenant = opts.tenant ?? "live";
|
|
19863
|
+
const fetchPage = async (startFrom) => {
|
|
19864
|
+
const page = await fetchDeveloperUsersPage({
|
|
19865
|
+
cognitoToken: auth.cognitoToken,
|
|
19866
|
+
apiBaseUrl: auth.baseUrl,
|
|
19867
|
+
tenant,
|
|
19868
|
+
startFrom,
|
|
19869
|
+
limit: opts.limit,
|
|
19870
|
+
fetchImpl
|
|
19871
|
+
});
|
|
19872
|
+
return { data: page.data.map(toIdentityView), nextCursor: page.nextCursor };
|
|
19873
|
+
};
|
|
19874
|
+
let rows = opts.limit !== void 0 ? (await fetchPage()).data ?? [] : await drainPages(fetchPage);
|
|
19875
|
+
if (opts.externalId) rows = rows.filter((r) => r.externalId === opts.externalId);
|
|
19876
|
+
if (opts.json) {
|
|
19877
|
+
process.stdout.write(`${formatJson(rows)}
|
|
19878
|
+
`);
|
|
19879
|
+
return 0;
|
|
19880
|
+
}
|
|
19881
|
+
process.stdout.write(
|
|
19882
|
+
"\n" + formatTable(rows, [
|
|
19883
|
+
{ header: "id", get: (r) => r.id },
|
|
19884
|
+
{ header: "external id", get: (r) => r.externalId },
|
|
19885
|
+
{ header: "email", get: (r) => r.email },
|
|
19886
|
+
{ header: "kind", get: (r) => r.type },
|
|
19887
|
+
{ header: "status", get: (r) => r.status }
|
|
19888
|
+
]) + "\n"
|
|
19889
|
+
);
|
|
19890
|
+
return 0;
|
|
19891
|
+
} catch (err) {
|
|
19892
|
+
process.stderr.write(` \u2716 identity list-all failed: ${formatError(err)}
|
|
19893
|
+
`);
|
|
19894
|
+
return 1;
|
|
19895
|
+
}
|
|
19896
|
+
});
|
|
19897
|
+
}
|
|
19662
19898
|
async function runIdentityGet(opts, deps = {}) {
|
|
19663
19899
|
return withAuth(opts, deps, async (auth) => {
|
|
19664
19900
|
const client = auth.client;
|
|
@@ -20262,6 +20498,16 @@ async function runRoleCreate(opts, deps = {}) {
|
|
|
20262
20498
|
dataScope = parseJsonObjectFlag("--data-scope", opts.dataScope);
|
|
20263
20499
|
} catch (err) {
|
|
20264
20500
|
process.stderr.write(` \u2716 ${formatError(err)}
|
|
20501
|
+
`);
|
|
20502
|
+
return 2;
|
|
20503
|
+
}
|
|
20504
|
+
}
|
|
20505
|
+
let assumable;
|
|
20506
|
+
if (opts.assumable !== void 0) {
|
|
20507
|
+
try {
|
|
20508
|
+
assumable = parseJsonObjectFlag("--assumable", opts.assumable);
|
|
20509
|
+
} catch (err) {
|
|
20510
|
+
process.stderr.write(` \u2716 ${formatError(err)}
|
|
20265
20511
|
`);
|
|
20266
20512
|
return 2;
|
|
20267
20513
|
}
|
|
@@ -20279,7 +20525,12 @@ async function runRoleCreate(opts, deps = {}) {
|
|
|
20279
20525
|
// stays byte-identical for unscoped roles.
|
|
20280
20526
|
scopes: [
|
|
20281
20527
|
dataScope ? { allowed_actions: actions, data_scope: dataScope } : { allowed_actions: actions }
|
|
20282
|
-
]
|
|
20528
|
+
],
|
|
20529
|
+
// Included only when supplied — omitting it (rather than sending an
|
|
20530
|
+
// empty object) keeps "no assume grant" and "explicitly empty grant"
|
|
20531
|
+
// indistinguishable on the wire, matching the platform's own
|
|
20532
|
+
// "omit for the safe default" contract.
|
|
20533
|
+
assumable
|
|
20283
20534
|
}
|
|
20284
20535
|
});
|
|
20285
20536
|
if (opts.json) {
|
|
@@ -20519,6 +20770,19 @@ ${renderContext(ctx)}
|
|
|
20519
20770
|
}
|
|
20520
20771
|
|
|
20521
20772
|
// src/commands/issuers.ts
|
|
20773
|
+
import * as readline4 from "readline/promises";
|
|
20774
|
+
function parseSelfSignupPolicies(raw) {
|
|
20775
|
+
if (!raw || raw.length === 0) return void 0;
|
|
20776
|
+
return raw.map((entry) => {
|
|
20777
|
+
const sep = entry.indexOf(":");
|
|
20778
|
+
if (sep <= 0 || sep === entry.length - 1) {
|
|
20779
|
+
throw new Error(
|
|
20780
|
+
`--self-signup-policy must be '<signup_type>:<roleId>' (e.g. 'default:member'); got '${entry}'.`
|
|
20781
|
+
);
|
|
20782
|
+
}
|
|
20783
|
+
return { signup_type: entry.slice(0, sep), role_id: entry.slice(sep + 1) };
|
|
20784
|
+
});
|
|
20785
|
+
}
|
|
20522
20786
|
function renderSelfSignup(policies) {
|
|
20523
20787
|
if (!policies || policies.length === 0) return "\u2014";
|
|
20524
20788
|
return policies.map((p) => `${p.signup_type} \u2192 ${p.role_id}`).join(", ");
|
|
@@ -20586,6 +20850,308 @@ ${renderIssuer(issuer)}
|
|
|
20586
20850
|
}
|
|
20587
20851
|
});
|
|
20588
20852
|
}
|
|
20853
|
+
async function runIssuersRegister(opts, deps = {}) {
|
|
20854
|
+
let selfSignupPolicies;
|
|
20855
|
+
try {
|
|
20856
|
+
selfSignupPolicies = parseSelfSignupPolicies(opts.selfSignupPolicy);
|
|
20857
|
+
} catch (err) {
|
|
20858
|
+
process.stderr.write(` \u2716 ${formatError(err)}
|
|
20859
|
+
`);
|
|
20860
|
+
return 2;
|
|
20861
|
+
}
|
|
20862
|
+
return withAuth({ ...opts, provisioning: true, context: opts.context }, deps, async (auth) => {
|
|
20863
|
+
const client = auth.client;
|
|
20864
|
+
try {
|
|
20865
|
+
const issuer = await client.auth.registerIssuer({
|
|
20866
|
+
issuerId: opts.issuerId,
|
|
20867
|
+
issuer: opts.issuer,
|
|
20868
|
+
jwksUri: opts.jwksUri,
|
|
20869
|
+
audience: opts.audience,
|
|
20870
|
+
contextId: opts.context,
|
|
20871
|
+
subClaim: opts.subClaim,
|
|
20872
|
+
emailClaim: opts.emailClaim,
|
|
20873
|
+
userinfoUri: opts.userinfoUri,
|
|
20874
|
+
selfSignupPolicies
|
|
20875
|
+
});
|
|
20876
|
+
if (opts.json) {
|
|
20877
|
+
process.stdout.write(`${formatJson(issuer)}
|
|
20878
|
+
`);
|
|
20879
|
+
return 0;
|
|
20880
|
+
}
|
|
20881
|
+
const verb = issuer.created === false ? "already registered \u2014 returned unchanged" : "registered";
|
|
20882
|
+
process.stdout.write(`
|
|
20883
|
+
\u2714 issuer '${opts.issuerId}' ${verb}
|
|
20884
|
+
${renderIssuer(issuer)}
|
|
20885
|
+
`);
|
|
20886
|
+
return 0;
|
|
20887
|
+
} catch (err) {
|
|
20888
|
+
process.stderr.write(formatCliError(err, { action: "issuers register failed" }));
|
|
20889
|
+
return 1;
|
|
20890
|
+
}
|
|
20891
|
+
});
|
|
20892
|
+
}
|
|
20893
|
+
async function runIssuersUpdate(opts, deps = {}) {
|
|
20894
|
+
let selfSignupPolicies;
|
|
20895
|
+
try {
|
|
20896
|
+
selfSignupPolicies = parseSelfSignupPolicies(opts.selfSignupPolicy);
|
|
20897
|
+
} catch (err) {
|
|
20898
|
+
process.stderr.write(` \u2716 ${formatError(err)}
|
|
20899
|
+
`);
|
|
20900
|
+
return 2;
|
|
20901
|
+
}
|
|
20902
|
+
if (opts.clearSelfSignupPolicies && selfSignupPolicies && selfSignupPolicies.length > 0) {
|
|
20903
|
+
process.stderr.write(
|
|
20904
|
+
" \u2716 --clear-self-signup-policies and --self-signup-policy are mutually exclusive.\n"
|
|
20905
|
+
);
|
|
20906
|
+
return 2;
|
|
20907
|
+
}
|
|
20908
|
+
return withAuth({ ...opts, provisioning: true, context: opts.context }, deps, async (auth) => {
|
|
20909
|
+
const client = auth.client;
|
|
20910
|
+
try {
|
|
20911
|
+
const issuer = await client.auth.updateIssuer({
|
|
20912
|
+
issuerId: opts.issuerId,
|
|
20913
|
+
subClaim: opts.subClaim,
|
|
20914
|
+
emailClaim: opts.emailClaim,
|
|
20915
|
+
userinfoUri: opts.userinfoUri,
|
|
20916
|
+
status: opts.status,
|
|
20917
|
+
selfSignupPolicies: opts.clearSelfSignupPolicies ? [] : selfSignupPolicies
|
|
20918
|
+
});
|
|
20919
|
+
if (opts.json) {
|
|
20920
|
+
process.stdout.write(`${formatJson(issuer)}
|
|
20921
|
+
`);
|
|
20922
|
+
return 0;
|
|
20923
|
+
}
|
|
20924
|
+
process.stdout.write(`
|
|
20925
|
+
\u2714 issuer '${opts.issuerId}' updated
|
|
20926
|
+
${renderIssuer(issuer)}
|
|
20927
|
+
`);
|
|
20928
|
+
return 0;
|
|
20929
|
+
} catch (err) {
|
|
20930
|
+
process.stderr.write(formatCliError(err, { action: "issuers update failed" }));
|
|
20931
|
+
return 1;
|
|
20932
|
+
}
|
|
20933
|
+
});
|
|
20934
|
+
}
|
|
20935
|
+
async function runIssuersDelete(opts, deps = {}) {
|
|
20936
|
+
const { issuerId, context } = opts;
|
|
20937
|
+
if (!opts.force) {
|
|
20938
|
+
const isTty = deps.isTty ?? Boolean(process.stdin.isTTY);
|
|
20939
|
+
if (!isTty) {
|
|
20940
|
+
process.stderr.write(
|
|
20941
|
+
` \u2716 issuers delete is destructive and needs an interactive confirmation.
|
|
20942
|
+
Re-run with --force to delete '${issuerId}' without a prompt.
|
|
20943
|
+
`
|
|
20944
|
+
);
|
|
20945
|
+
return 2;
|
|
20946
|
+
}
|
|
20947
|
+
const promptCtx = deps.createPrompt ? deps.createPrompt() : (() => {
|
|
20948
|
+
const rl = readline4.createInterface({ input: process.stdin, output: process.stdout });
|
|
20949
|
+
return { prompt: (q) => rl.question(q), close: () => rl.close() };
|
|
20950
|
+
})();
|
|
20951
|
+
try {
|
|
20952
|
+
process.stdout.write(
|
|
20953
|
+
`
|
|
20954
|
+
\u26A0 Deregistering issuer '${issuerId}' from context '${context}' removes it as a token-exchange
|
|
20955
|
+
trust anchor. The platform refuses this while any user was ever created or matched
|
|
20956
|
+
through it \u2014 deactivate those users or register a replacement issuer first if you
|
|
20957
|
+
intend to cut off access.
|
|
20958
|
+
|
|
20959
|
+
`
|
|
20960
|
+
);
|
|
20961
|
+
const typed = await promptCtx.prompt(` Type the issuer id ('${issuerId}') to confirm: `);
|
|
20962
|
+
if (typed.trim() !== issuerId) {
|
|
20963
|
+
process.stderr.write(` \u2716 Confirmation did not match \u2014 nothing deleted.
|
|
20964
|
+
`);
|
|
20965
|
+
return 1;
|
|
20966
|
+
}
|
|
20967
|
+
} finally {
|
|
20968
|
+
promptCtx.close();
|
|
20969
|
+
}
|
|
20970
|
+
}
|
|
20971
|
+
return withAuth({ ...opts, provisioning: true, context }, deps, async (auth) => {
|
|
20972
|
+
const client = auth.client;
|
|
20973
|
+
try {
|
|
20974
|
+
await client.auth.deleteIssuer({ issuerId });
|
|
20975
|
+
if (opts.json) {
|
|
20976
|
+
process.stdout.write(`${formatJson({ issuerId, deleted: true })}
|
|
20977
|
+
`);
|
|
20978
|
+
return 0;
|
|
20979
|
+
}
|
|
20980
|
+
process.stdout.write(`
|
|
20981
|
+
\u2714 issuer '${issuerId}' deregistered from '${context}'
|
|
20982
|
+
`);
|
|
20983
|
+
return 0;
|
|
20984
|
+
} catch (err) {
|
|
20985
|
+
process.stderr.write(formatCliError(err, { action: "issuers delete failed" }));
|
|
20986
|
+
return 1;
|
|
20987
|
+
}
|
|
20988
|
+
});
|
|
20989
|
+
}
|
|
20990
|
+
|
|
20991
|
+
// src/commands/namespace.ts
|
|
20992
|
+
function renderNamespace(n) {
|
|
20993
|
+
return formatKeyValues([
|
|
20994
|
+
["namespace", n.namespace],
|
|
20995
|
+
["contextId", n.contextId ?? "(tenant-wide)"],
|
|
20996
|
+
["entityBacked", n.entityBacked === void 0 ? void 0 : String(n.entityBacked)],
|
|
20997
|
+
["reserved", n.reserved === void 0 ? void 0 : String(n.reserved)],
|
|
20998
|
+
["specificityRank", n.specificityRank === void 0 ? void 0 : String(n.specificityRank)],
|
|
20999
|
+
["defaultSchemaId", n.defaultSchemaId],
|
|
21000
|
+
["createdAt", n.createdAt]
|
|
21001
|
+
]);
|
|
21002
|
+
}
|
|
21003
|
+
async function runNamespaceList(opts, deps = {}) {
|
|
21004
|
+
return withAuth({ ...opts, context: opts.context }, deps, async (auth) => {
|
|
21005
|
+
const client = auth.client;
|
|
21006
|
+
try {
|
|
21007
|
+
const rows = await drainPages(
|
|
21008
|
+
(startFrom) => client.identity.listNamespaces({
|
|
21009
|
+
limit: PAGE_LIMIT,
|
|
21010
|
+
...opts.context ? { contextId: opts.context } : {},
|
|
21011
|
+
...startFrom ? { startFrom } : {}
|
|
21012
|
+
})
|
|
21013
|
+
);
|
|
21014
|
+
if (opts.json) {
|
|
21015
|
+
process.stdout.write(`${formatJson(rows)}
|
|
21016
|
+
`);
|
|
21017
|
+
return 0;
|
|
21018
|
+
}
|
|
21019
|
+
process.stdout.write(
|
|
21020
|
+
"\n" + formatTable(rows, [
|
|
21021
|
+
{ header: "namespace", get: (n) => n.namespace },
|
|
21022
|
+
{ header: "contextId", get: (n) => n.contextId ?? "(tenant-wide)" },
|
|
21023
|
+
{
|
|
21024
|
+
header: "entityBacked",
|
|
21025
|
+
get: (n) => n.entityBacked === void 0 ? void 0 : String(n.entityBacked)
|
|
21026
|
+
},
|
|
21027
|
+
{
|
|
21028
|
+
header: "specificityRank",
|
|
21029
|
+
get: (n) => n.specificityRank === void 0 ? void 0 : String(n.specificityRank)
|
|
21030
|
+
}
|
|
21031
|
+
]) + "\n"
|
|
21032
|
+
);
|
|
21033
|
+
return 0;
|
|
21034
|
+
} catch (err) {
|
|
21035
|
+
process.stderr.write(formatCliError(err, { action: "namespace list failed" }));
|
|
21036
|
+
return 1;
|
|
21037
|
+
}
|
|
21038
|
+
});
|
|
21039
|
+
}
|
|
21040
|
+
async function runNamespaceGet(opts, deps = {}) {
|
|
21041
|
+
return withAuth({ ...opts, context: opts.context }, deps, async (auth) => {
|
|
21042
|
+
const client = auth.client;
|
|
21043
|
+
try {
|
|
21044
|
+
const ns = await client.identity.getNamespace({
|
|
21045
|
+
namespace: opts.namespace,
|
|
21046
|
+
...opts.context ? { contextId: opts.context } : {}
|
|
21047
|
+
});
|
|
21048
|
+
if (opts.json) {
|
|
21049
|
+
process.stdout.write(`${formatJson(ns)}
|
|
21050
|
+
`);
|
|
21051
|
+
return 0;
|
|
21052
|
+
}
|
|
21053
|
+
process.stdout.write(`
|
|
21054
|
+
${renderNamespace(ns)}
|
|
21055
|
+
`);
|
|
21056
|
+
return 0;
|
|
21057
|
+
} catch (err) {
|
|
21058
|
+
process.stderr.write(formatCliError(err, { action: "namespace get failed" }));
|
|
21059
|
+
return 1;
|
|
21060
|
+
}
|
|
21061
|
+
});
|
|
21062
|
+
}
|
|
21063
|
+
async function runNamespaceRegister(opts, deps = {}) {
|
|
21064
|
+
return withAuth({ ...opts, provisioning: true, context: opts.context }, deps, async (auth) => {
|
|
21065
|
+
const client = auth.client;
|
|
21066
|
+
try {
|
|
21067
|
+
const ns = await client.identity.registerNamespace({
|
|
21068
|
+
contextId: opts.context,
|
|
21069
|
+
body: {
|
|
21070
|
+
namespace: opts.namespace,
|
|
21071
|
+
specificityRank: opts.specificityRank,
|
|
21072
|
+
entityBacked: opts.entityBacked,
|
|
21073
|
+
defaultSchemaId: opts.defaultSchemaId
|
|
21074
|
+
}
|
|
21075
|
+
});
|
|
21076
|
+
if (opts.json) {
|
|
21077
|
+
process.stdout.write(`${formatJson(ns)}
|
|
21078
|
+
`);
|
|
21079
|
+
return 0;
|
|
21080
|
+
}
|
|
21081
|
+
process.stdout.write(`
|
|
21082
|
+
\u2714 namespace '${opts.namespace}' registered
|
|
21083
|
+
${renderNamespace(ns)}
|
|
21084
|
+
`);
|
|
21085
|
+
return 0;
|
|
21086
|
+
} catch (err) {
|
|
21087
|
+
process.stderr.write(formatCliError(err, { action: "namespace register failed" }));
|
|
21088
|
+
return 1;
|
|
21089
|
+
}
|
|
21090
|
+
});
|
|
21091
|
+
}
|
|
21092
|
+
|
|
21093
|
+
// src/commands/assume.ts
|
|
21094
|
+
function parseAssumeScopes(raw) {
|
|
21095
|
+
const out = {};
|
|
21096
|
+
for (const entry of raw) {
|
|
21097
|
+
const sep = entry.indexOf(":");
|
|
21098
|
+
if (sep <= 0 || sep === entry.length - 1) {
|
|
21099
|
+
throw new Error(`--scope must be '<namespace>:<value>' (e.g. 'org:orgB'); got '${entry}'.`);
|
|
21100
|
+
}
|
|
21101
|
+
const ns = entry.slice(0, sep);
|
|
21102
|
+
const value = entry.slice(sep + 1);
|
|
21103
|
+
const valueError = valueGrammarError(value);
|
|
21104
|
+
if (valueError) throw new Error(`--scope value ${valueError}`);
|
|
21105
|
+
const key = `scope:${ns}`;
|
|
21106
|
+
if (Object.prototype.hasOwnProperty.call(out, key)) {
|
|
21107
|
+
throw new Error(`--scope names namespace '${ns}' more than once \u2014 pass one value per namespace.`);
|
|
21108
|
+
}
|
|
21109
|
+
out[key] = value;
|
|
21110
|
+
}
|
|
21111
|
+
return out;
|
|
21112
|
+
}
|
|
21113
|
+
function renderAssumeToken(t) {
|
|
21114
|
+
return formatKeyValues([
|
|
21115
|
+
["access_token", t.access_token],
|
|
21116
|
+
["token_type", t.token_type],
|
|
21117
|
+
["expires_in", t.expires_in === void 0 ? void 0 : `${t.expires_in}s`]
|
|
21118
|
+
]);
|
|
21119
|
+
}
|
|
21120
|
+
async function runAssume(opts, deps = {}) {
|
|
21121
|
+
if (!opts.scope || opts.scope.length === 0) {
|
|
21122
|
+
process.stderr.write(" \u2716 --scope must be given at least once (e.g. --scope org:orgB).\n");
|
|
21123
|
+
return 2;
|
|
21124
|
+
}
|
|
21125
|
+
let scopes;
|
|
21126
|
+
try {
|
|
21127
|
+
scopes = parseAssumeScopes(opts.scope);
|
|
21128
|
+
} catch (err) {
|
|
21129
|
+
process.stderr.write(` \u2716 ${formatError(err)}
|
|
21130
|
+
`);
|
|
21131
|
+
return 2;
|
|
21132
|
+
}
|
|
21133
|
+
return withAuth({ ...opts, context: opts.context }, deps, async (auth) => {
|
|
21134
|
+
const client = auth.client;
|
|
21135
|
+
try {
|
|
21136
|
+
const result = await client.auth.assumeToken(scopes);
|
|
21137
|
+
if (opts.json) {
|
|
21138
|
+
process.stdout.write(`${formatJson(result)}
|
|
21139
|
+
`);
|
|
21140
|
+
return 0;
|
|
21141
|
+
}
|
|
21142
|
+
process.stdout.write(
|
|
21143
|
+
`
|
|
21144
|
+
\u2714 assumed ${Object.keys(scopes).join(", ")}
|
|
21145
|
+
${renderAssumeToken(result)}
|
|
21146
|
+
`
|
|
21147
|
+
);
|
|
21148
|
+
return 0;
|
|
21149
|
+
} catch (err) {
|
|
21150
|
+
process.stderr.write(formatCliError(err, { action: "assume failed" }));
|
|
21151
|
+
return 1;
|
|
21152
|
+
}
|
|
21153
|
+
});
|
|
21154
|
+
}
|
|
20589
21155
|
|
|
20590
21156
|
// src/commands/login.ts
|
|
20591
21157
|
import { createServer } from "http";
|
|
@@ -21050,7 +21616,8 @@ async function runBlueprintHarness(client, input, deps) {
|
|
|
21050
21616
|
});
|
|
21051
21617
|
const blueprint = await resolveApplyTimeIdentities(
|
|
21052
21618
|
input.blueprint,
|
|
21053
|
-
provisioned
|
|
21619
|
+
provisioned,
|
|
21620
|
+
client,
|
|
21054
21621
|
log
|
|
21055
21622
|
);
|
|
21056
21623
|
const remint = remintForContext ?? (async () => client);
|
|
@@ -21463,9 +22030,9 @@ function emitJoined(opts) {
|
|
|
21463
22030
|
|
|
21464
22031
|
// src/build-info.ts
|
|
21465
22032
|
var BUILD_INFO = {
|
|
21466
|
-
cli: true ? "0.
|
|
21467
|
-
sdk: true ? "0.
|
|
21468
|
-
blueprints: true ? "0.
|
|
22033
|
+
cli: true ? "0.20.0" : "dev",
|
|
22034
|
+
sdk: true ? "0.42.0" : "dev",
|
|
22035
|
+
blueprints: true ? "0.17.0" : "dev"
|
|
21469
22036
|
};
|
|
21470
22037
|
function formatBuildInfo() {
|
|
21471
22038
|
return `vectros ${BUILD_INFO.cli} (sdk ${BUILD_INFO.sdk}, blueprints ${BUILD_INFO.blueprints})`;
|
|
@@ -21546,10 +22113,22 @@ function buildProgram(deps = {}) {
|
|
|
21546
22113
|
registerRoleCommands(program, deps);
|
|
21547
22114
|
registerContextCommands(program, deps);
|
|
21548
22115
|
registerIssuersCommands(program, deps);
|
|
22116
|
+
registerNamespaceCommands(program, deps);
|
|
22117
|
+
registerAssumeCommand(program, deps);
|
|
21549
22118
|
registerJoinCommand(program, deps);
|
|
21550
22119
|
registerKeyringCommands(program);
|
|
21551
22120
|
return program;
|
|
21552
22121
|
}
|
|
22122
|
+
function registerAssumeCommand(program, deps) {
|
|
22123
|
+
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, []);
|
|
22124
|
+
addCommonAuthOptions(assume).action(
|
|
22125
|
+
async (flags) => {
|
|
22126
|
+
process.exit(
|
|
22127
|
+
await runAssume({ ...toCommonOptions(flags), context: flags.context, scope: flags.scope ?? [] }, deps)
|
|
22128
|
+
);
|
|
22129
|
+
}
|
|
22130
|
+
);
|
|
22131
|
+
}
|
|
21553
22132
|
function registerJoinCommand(program, deps) {
|
|
21554
22133
|
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(
|
|
21555
22134
|
"--activate",
|
|
@@ -21686,7 +22265,7 @@ function registerContextCommands(program, deps) {
|
|
|
21686
22265
|
);
|
|
21687
22266
|
}
|
|
21688
22267
|
function registerIssuersCommands(program, deps) {
|
|
21689
|
-
const issuers = program.command("issuers").description("
|
|
22268
|
+
const issuers = program.command("issuers").description("Manage registered third-party IdP issuers (token-exchange trust anchors).");
|
|
21690
22269
|
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)");
|
|
21691
22270
|
addCommonAuthOptions(list).action(
|
|
21692
22271
|
async (flags) => {
|
|
@@ -21705,12 +22284,125 @@ function registerIssuersCommands(program, deps) {
|
|
|
21705
22284
|
);
|
|
21706
22285
|
}
|
|
21707
22286
|
);
|
|
22287
|
+
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(
|
|
22288
|
+
"--self-signup-policy <type:roleId>",
|
|
22289
|
+
"opt-in self-service signup entry, repeatable (e.g. default:member) \u2014 no entry may target an elevated role",
|
|
22290
|
+
collect,
|
|
22291
|
+
[]
|
|
22292
|
+
).option("--tenant <tenant>", "tenant the context lives in: 'test' or 'live' (default live)");
|
|
22293
|
+
addCommonAuthOptions(register).action(
|
|
22294
|
+
async (flags) => {
|
|
22295
|
+
const tenant = parseTenantFlag(flags.tenant);
|
|
22296
|
+
process.exit(
|
|
22297
|
+
await runIssuersRegister(
|
|
22298
|
+
{
|
|
22299
|
+
...toCommonOptions(flags),
|
|
22300
|
+
context: flags.context,
|
|
22301
|
+
issuerId: flags.issuerId,
|
|
22302
|
+
issuer: flags.issuer,
|
|
22303
|
+
jwksUri: flags.jwksUri,
|
|
22304
|
+
audience: flags.audience,
|
|
22305
|
+
subClaim: flags.subClaim,
|
|
22306
|
+
emailClaim: flags.emailClaim,
|
|
22307
|
+
userinfoUri: flags.userinfoUri,
|
|
22308
|
+
selfSignupPolicy: flags.selfSignupPolicy,
|
|
22309
|
+
tenant
|
|
22310
|
+
},
|
|
22311
|
+
deps
|
|
22312
|
+
)
|
|
22313
|
+
);
|
|
22314
|
+
}
|
|
22315
|
+
);
|
|
22316
|
+
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(
|
|
22317
|
+
"--self-signup-policy <type:roleId>",
|
|
22318
|
+
"replace self-signup policies, repeatable (e.g. default:member) \u2014 omit entirely to leave unchanged",
|
|
22319
|
+
collect,
|
|
22320
|
+
[]
|
|
22321
|
+
).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)");
|
|
22322
|
+
addCommonAuthOptions(update).action(
|
|
22323
|
+
async (issuerId, flags) => {
|
|
22324
|
+
const tenant = parseTenantFlag(flags.tenant);
|
|
22325
|
+
process.exit(
|
|
22326
|
+
await runIssuersUpdate(
|
|
22327
|
+
{
|
|
22328
|
+
...toCommonOptions(flags),
|
|
22329
|
+
context: flags.context,
|
|
22330
|
+
issuerId,
|
|
22331
|
+
subClaim: flags.subClaim,
|
|
22332
|
+
emailClaim: flags.emailClaim,
|
|
22333
|
+
userinfoUri: flags.userinfoUri,
|
|
22334
|
+
status: flags.status,
|
|
22335
|
+
selfSignupPolicy: flags.selfSignupPolicy,
|
|
22336
|
+
clearSelfSignupPolicies: Boolean(flags.clearSelfSignupPolicies),
|
|
22337
|
+
tenant
|
|
22338
|
+
},
|
|
22339
|
+
deps
|
|
22340
|
+
)
|
|
22341
|
+
);
|
|
22342
|
+
}
|
|
22343
|
+
);
|
|
22344
|
+
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)");
|
|
22345
|
+
addCommonAuthOptions(del).action(
|
|
22346
|
+
async (issuerId, flags) => {
|
|
22347
|
+
const tenant = parseTenantFlag(flags.tenant);
|
|
22348
|
+
process.exit(
|
|
22349
|
+
await runIssuersDelete(
|
|
22350
|
+
{ ...toCommonOptions(flags), context: flags.context, issuerId, force: Boolean(flags.force), tenant },
|
|
22351
|
+
deps
|
|
22352
|
+
)
|
|
22353
|
+
);
|
|
22354
|
+
}
|
|
22355
|
+
);
|
|
22356
|
+
}
|
|
22357
|
+
function registerNamespaceCommands(program, deps) {
|
|
22358
|
+
const namespace = program.command("namespace").description("Manage scope-namespace registrations (whether scope:<namespace> values resolve to identity entities).");
|
|
22359
|
+
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");
|
|
22360
|
+
addCommonAuthOptions(register).action(
|
|
22361
|
+
async (name, flags) => {
|
|
22362
|
+
const specificityRank = Number(flags.specificityRank);
|
|
22363
|
+
if (!Number.isInteger(specificityRank) || specificityRank < 0 || specificityRank > 1e6) {
|
|
22364
|
+
process.stderr.write(
|
|
22365
|
+
` \u2716 --specificity-rank must be an integer between 0 and 1000000 (got '${flags.specificityRank}').
|
|
22366
|
+
`
|
|
22367
|
+
);
|
|
22368
|
+
process.exit(2);
|
|
22369
|
+
}
|
|
22370
|
+
process.exit(
|
|
22371
|
+
await runNamespaceRegister(
|
|
22372
|
+
{
|
|
22373
|
+
...toCommonOptions(flags),
|
|
22374
|
+
namespace: name,
|
|
22375
|
+
context: flags.context,
|
|
22376
|
+
specificityRank,
|
|
22377
|
+
entityBacked: Boolean(flags.entityBacked),
|
|
22378
|
+
defaultSchemaId: flags.defaultSchemaId
|
|
22379
|
+
},
|
|
22380
|
+
deps
|
|
22381
|
+
)
|
|
22382
|
+
);
|
|
22383
|
+
}
|
|
22384
|
+
);
|
|
22385
|
+
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");
|
|
22386
|
+
addCommonAuthOptions(list).action(async (flags) => {
|
|
22387
|
+
process.exit(await runNamespaceList({ ...toCommonOptions(flags), context: flags.context }, deps));
|
|
22388
|
+
});
|
|
22389
|
+
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");
|
|
22390
|
+
addCommonAuthOptions(get).action(
|
|
22391
|
+
async (name, flags) => {
|
|
22392
|
+
process.exit(
|
|
22393
|
+
await runNamespaceGet({ ...toCommonOptions(flags), namespace: name, context: flags.context }, deps)
|
|
22394
|
+
);
|
|
22395
|
+
}
|
|
22396
|
+
);
|
|
21708
22397
|
}
|
|
21709
22398
|
function registerRoleCommands(program, deps) {
|
|
21710
22399
|
const role = program.command("role").description("Manage context-scoped roles (reusable scope rules; bind via `access grant --role`).");
|
|
21711
22400
|
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(
|
|
21712
22401
|
"--data-scope <json>",
|
|
21713
22402
|
`confine the clause to owned data \u2014 JSON map of dimension \u2192 allowed values, e.g. '{"userId":["\${{ self.userId }}"]}' or '{"scope:org":["<orgId>", null]}'`
|
|
22403
|
+
).option(
|
|
22404
|
+
"--assumable <json>",
|
|
22405
|
+
`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)`
|
|
21714
22406
|
).option("--description <text>", "free-text description");
|
|
21715
22407
|
addCommonAuthOptions(create).action(
|
|
21716
22408
|
async (flags) => {
|
|
@@ -21722,6 +22414,7 @@ function registerRoleCommands(program, deps) {
|
|
|
21722
22414
|
name: flags.name,
|
|
21723
22415
|
actions: flags.actions,
|
|
21724
22416
|
dataScope: flags.dataScope,
|
|
22417
|
+
assumable: flags.assumable,
|
|
21725
22418
|
description: flags.description
|
|
21726
22419
|
}, deps)
|
|
21727
22420
|
);
|
|
@@ -21815,6 +22508,20 @@ function registerIdentityCommands(program, deps) {
|
|
|
21815
22508
|
);
|
|
21816
22509
|
}
|
|
21817
22510
|
);
|
|
22511
|
+
const listAll = identity.command("list-all").description(
|
|
22512
|
+
"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."
|
|
22513
|
+
).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)");
|
|
22514
|
+
addCommonAuthOptions(listAll).action(
|
|
22515
|
+
async (flags) => {
|
|
22516
|
+
const tenant = parseTenantFlag(flags.tenant);
|
|
22517
|
+
process.exit(
|
|
22518
|
+
await runIdentityListAll(
|
|
22519
|
+
{ ...toCommonOptions(flags), externalId: flags.externalId, limit: flags.limit, tenant },
|
|
22520
|
+
deps
|
|
22521
|
+
)
|
|
22522
|
+
);
|
|
22523
|
+
}
|
|
22524
|
+
);
|
|
21818
22525
|
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)");
|
|
21819
22526
|
addCommonAuthOptions(get).action(async (flags) => {
|
|
21820
22527
|
process.exit(await runIdentityGet({ ...toCommonOptions(flags), type: flags.type, id: flags.id }, deps));
|