@datarails-ai/dr-cli 1.6.140 → 1.6.142
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/README.md +23 -0
- package/dist/cli.js +582 -380
- package/dist/scopes-map.json +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -300,6 +300,29 @@ interactively instead. An API-key file is different: see below.
|
|
|
300
300
|
> the operator's own login expires. Use `--as <operator-email>` to pick the
|
|
301
301
|
> operator when several are stored, or `--user-id <n>` to skip the lookup.
|
|
302
302
|
|
|
303
|
+
### Tenant pin (one server, one account)
|
|
304
|
+
|
|
305
|
+
A launcher that opens each customer in its own session can confine `dr` to that
|
|
306
|
+
customer through the environment. With none of these set, `dr` behaves as usual.
|
|
307
|
+
A variable that is set but empty still counts as set, so leave unused ones unset.
|
|
308
|
+
|
|
309
|
+
```
|
|
310
|
+
DR_PIN_SERVER=<key or URL> # required whenever any DR_PIN_* variable is set
|
|
311
|
+
DR_PIN_ACCOUNT=<email> # the login email; required unless org mode
|
|
312
|
+
DR_PIN_ORG_MODE=1 # server only, no account, read-only commands
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
While a pin is set, `dr` refuses: a `--server` / `--account` that differs from
|
|
316
|
+
the pin (compared after canonicalising, so `-sX`, `--server=X` and the URL form
|
|
317
|
+
are covered); a missing `--server`, or a missing `--account` in customer mode;
|
|
318
|
+
`dr logout`, and `dr login-as` to anyone but the pinned account (or with
|
|
319
|
+
`--user-id`); a host override (`--app-space-host`, `DR_APP_SPACE_HOST`,
|
|
320
|
+
`--connectors-url`); in org mode, any command the scopes map does not prove
|
|
321
|
+
read-only, plus the few read-scoped commands with side effects
|
|
322
|
+
(`devices request-logs`, `desk smtp test`, `dashboards prepare`,
|
|
323
|
+
`storyboards prepare`). A pin that is set but malformed refuses every command
|
|
324
|
+
(`--help` and `--version` still print). A refusal exits 3, like a scope denial.
|
|
325
|
+
|
|
303
326
|
### API keys (headless and CI)
|
|
304
327
|
|
|
305
328
|
A user-bound API key is a durable credential `dr` exchanges itself for a
|
package/dist/cli.js
CHANGED
|
@@ -8,7 +8,7 @@ import { fileURLToPath as fileURLToPath3 } from "url";
|
|
|
8
8
|
// package.json
|
|
9
9
|
var package_default = {
|
|
10
10
|
name: "@datarails-ai/dr-cli",
|
|
11
|
-
version: "1.6.
|
|
11
|
+
version: "1.6.142",
|
|
12
12
|
description: "Datarails CLI",
|
|
13
13
|
type: "module",
|
|
14
14
|
engines: {
|
|
@@ -40,7 +40,7 @@ var package_default = {
|
|
|
40
40
|
postinstall: "node scripts/postinstall.mjs"
|
|
41
41
|
},
|
|
42
42
|
dependencies: {
|
|
43
|
-
"@datarails-ai/dr-appspace-cli": "0.12.
|
|
43
|
+
"@datarails-ai/dr-appspace-cli": "0.12.15",
|
|
44
44
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
45
45
|
"@napi-rs/keyring": "^1.3.0",
|
|
46
46
|
commander: "^12.1.0",
|
|
@@ -69,7 +69,7 @@ var package_default = {
|
|
|
69
69
|
};
|
|
70
70
|
|
|
71
71
|
// src/buildInfo.ts
|
|
72
|
-
var BUILD_TIME = "2026-10-
|
|
72
|
+
var BUILD_TIME = "2026-10-19T11:06:59.748Z";
|
|
73
73
|
function formatBuildTime(iso) {
|
|
74
74
|
if (iso === "") return "";
|
|
75
75
|
const d = new Date(iso);
|
|
@@ -4282,6 +4282,575 @@ function isENOENT(err) {
|
|
|
4282
4282
|
return typeof err === "object" && err !== null && "code" in err && err.code === "ENOENT";
|
|
4283
4283
|
}
|
|
4284
4284
|
|
|
4285
|
+
// src/scopes/matcher.ts
|
|
4286
|
+
var SCOPE_RE = /^(\*|[a-z][a-z0-9-]*):(\*|read|write|destructive)$/;
|
|
4287
|
+
var ACTION_RANK = new Map(
|
|
4288
|
+
["read", "write", "destructive"].map((action, rank) => [action, rank])
|
|
4289
|
+
);
|
|
4290
|
+
function actionSatisfies(granted, wanted) {
|
|
4291
|
+
if (granted === "*" || granted === wanted) return true;
|
|
4292
|
+
const g = ACTION_RANK.get(granted);
|
|
4293
|
+
const w = ACTION_RANK.get(wanted);
|
|
4294
|
+
if (g === void 0 || w === void 0) return false;
|
|
4295
|
+
return g > w;
|
|
4296
|
+
}
|
|
4297
|
+
var WHITESPACE = "\\t\\n\\v\\f\\r \\x1c-\\x1f\\x85\\xa0\\u1680\\u2000-\\u200a\\u2028\\u2029\\u202f\\u205f\\u3000\\ufeff";
|
|
4298
|
+
var TRIM_RE = new RegExp(`^[${WHITESPACE}]+|[${WHITESPACE}]+$`, "g");
|
|
4299
|
+
var SPLIT_RE = new RegExp(`[${WHITESPACE}]+`);
|
|
4300
|
+
function trimShared(value) {
|
|
4301
|
+
return value.replace(TRIM_RE, "");
|
|
4302
|
+
}
|
|
4303
|
+
function splitScopeString(value) {
|
|
4304
|
+
return value.split(SPLIT_RE).filter((part) => part !== "");
|
|
4305
|
+
}
|
|
4306
|
+
function parseScope(value) {
|
|
4307
|
+
if (typeof value !== "string") return null;
|
|
4308
|
+
const m = SCOPE_RE.exec(trimShared(value).toLowerCase());
|
|
4309
|
+
return m ? [m[1], m[2]] : null;
|
|
4310
|
+
}
|
|
4311
|
+
function parsed(scopes) {
|
|
4312
|
+
const list28 = scopes ?? [];
|
|
4313
|
+
const entries = typeof list28 === "string" ? splitScopeString(list28) : Array.isArray(list28) ? list28 : [];
|
|
4314
|
+
return entries.map(parseScope).filter((p) => p !== null);
|
|
4315
|
+
}
|
|
4316
|
+
function scopeEntries(scopes) {
|
|
4317
|
+
return parsed(scopes);
|
|
4318
|
+
}
|
|
4319
|
+
function isAgentScoped(scopes) {
|
|
4320
|
+
return parsed(scopes).length > 0;
|
|
4321
|
+
}
|
|
4322
|
+
function isAllowed(scopes, resource, action) {
|
|
4323
|
+
const entries = parsed(scopes);
|
|
4324
|
+
if (entries.length === 0) return true;
|
|
4325
|
+
const res = trimShared(resource).toLowerCase();
|
|
4326
|
+
const act = trimShared(action).toLowerCase();
|
|
4327
|
+
return entries.some(([r, a]) => (r === "*" || r === res) && actionSatisfies(a, act));
|
|
4328
|
+
}
|
|
4329
|
+
function isEntryAllowed(scopes, entry) {
|
|
4330
|
+
return entry.requires.every((r) => isAllowed(scopes, r.resource, r.action));
|
|
4331
|
+
}
|
|
4332
|
+
|
|
4333
|
+
// src/scopes/scopesMap.ts
|
|
4334
|
+
import { readFileSync } from "fs";
|
|
4335
|
+
import * as path3 from "path";
|
|
4336
|
+
import { fileURLToPath } from "url";
|
|
4337
|
+
function isDeniedEntry(entry) {
|
|
4338
|
+
return entry !== void 0 && Object.prototype.hasOwnProperty.call(entry, "denied");
|
|
4339
|
+
}
|
|
4340
|
+
var ACTIONS = /* @__PURE__ */ new Set(["read", "write", "destructive"]);
|
|
4341
|
+
var RESOURCE_RE = /^[a-z][a-z0-9-]*$/;
|
|
4342
|
+
function isValidResourceName(v) {
|
|
4343
|
+
return RESOURCE_RE.test(v);
|
|
4344
|
+
}
|
|
4345
|
+
function isAction(v) {
|
|
4346
|
+
return typeof v === "string" && ACTIONS.has(v);
|
|
4347
|
+
}
|
|
4348
|
+
function isResource(v) {
|
|
4349
|
+
return typeof v === "string" && isValidResourceName(v);
|
|
4350
|
+
}
|
|
4351
|
+
function parseRequirement(v) {
|
|
4352
|
+
if (typeof v !== "object" || v === null) return null;
|
|
4353
|
+
const o = v;
|
|
4354
|
+
if (!isResource(o.resource) || !isAction(o.action)) return null;
|
|
4355
|
+
return { resource: o.resource, action: o.action };
|
|
4356
|
+
}
|
|
4357
|
+
function parseDenial(v) {
|
|
4358
|
+
if (typeof v !== "object" || v === null || Array.isArray(v)) return null;
|
|
4359
|
+
const o = v;
|
|
4360
|
+
if (typeof o.reason !== "string" || o.reason.trim() === "") return null;
|
|
4361
|
+
if (typeof o.until !== "string" || o.until.trim() === "") return null;
|
|
4362
|
+
return { reason: o.reason, until: o.until };
|
|
4363
|
+
}
|
|
4364
|
+
function parseEntry(v) {
|
|
4365
|
+
if (typeof v !== "object" || v === null) return null;
|
|
4366
|
+
const o = v;
|
|
4367
|
+
if (Object.prototype.hasOwnProperty.call(o, "denied")) {
|
|
4368
|
+
if (Object.prototype.hasOwnProperty.call(o, "requires")) return null;
|
|
4369
|
+
const denied = parseDenial(o.denied);
|
|
4370
|
+
if (denied === null) return null;
|
|
4371
|
+
return { denied };
|
|
4372
|
+
}
|
|
4373
|
+
if (!Array.isArray(o.requires)) return null;
|
|
4374
|
+
const requires = [];
|
|
4375
|
+
for (const raw of o.requires) {
|
|
4376
|
+
const req = parseRequirement(raw);
|
|
4377
|
+
if (req === null) return null;
|
|
4378
|
+
requires.push(req);
|
|
4379
|
+
}
|
|
4380
|
+
return { requires };
|
|
4381
|
+
}
|
|
4382
|
+
function emptyTable() {
|
|
4383
|
+
return /* @__PURE__ */ Object.create(null);
|
|
4384
|
+
}
|
|
4385
|
+
function parseStringMap(v) {
|
|
4386
|
+
if (typeof v !== "object" || v === null || Array.isArray(v)) return void 0;
|
|
4387
|
+
const out = emptyTable();
|
|
4388
|
+
for (const [k, val] of Object.entries(v)) {
|
|
4389
|
+
if (typeof val === "string") out[k] = val;
|
|
4390
|
+
}
|
|
4391
|
+
return out;
|
|
4392
|
+
}
|
|
4393
|
+
function parseResources(v) {
|
|
4394
|
+
if (typeof v !== "object" || v === null || Array.isArray(v)) return void 0;
|
|
4395
|
+
const out = emptyTable();
|
|
4396
|
+
for (const [resource, val] of Object.entries(v)) {
|
|
4397
|
+
if (!isResource(resource)) continue;
|
|
4398
|
+
if (typeof val !== "object" || val === null || Array.isArray(val)) continue;
|
|
4399
|
+
const o = val;
|
|
4400
|
+
if (typeof o.product !== "string" || o.product === "") continue;
|
|
4401
|
+
if (typeof o.productName !== "string" || o.productName === "") continue;
|
|
4402
|
+
out[resource] = { product: o.product, productName: o.productName };
|
|
4403
|
+
}
|
|
4404
|
+
return out;
|
|
4405
|
+
}
|
|
4406
|
+
function parsePendingCommands(v, pending) {
|
|
4407
|
+
if (typeof v !== "object" || v === null || Array.isArray(v)) return void 0;
|
|
4408
|
+
const out = emptyTable();
|
|
4409
|
+
for (const [group, val] of Object.entries(v)) {
|
|
4410
|
+
if (pending === void 0 || !Object.prototype.hasOwnProperty.call(pending, group)) continue;
|
|
4411
|
+
if (!Array.isArray(val)) continue;
|
|
4412
|
+
if (!val.every((p) => typeof p === "string" && p.split(" ")[0] === group)) continue;
|
|
4413
|
+
out[group] = val;
|
|
4414
|
+
}
|
|
4415
|
+
return out;
|
|
4416
|
+
}
|
|
4417
|
+
function parseScopesMap(text2) {
|
|
4418
|
+
let raw;
|
|
4419
|
+
try {
|
|
4420
|
+
raw = JSON.parse(text2);
|
|
4421
|
+
} catch {
|
|
4422
|
+
return void 0;
|
|
4423
|
+
}
|
|
4424
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) return void 0;
|
|
4425
|
+
const o = raw;
|
|
4426
|
+
if (o.version !== 1 || o.cliVersion !== void 0 && typeof o.cliVersion !== "string" || typeof o.commands !== "object" || o.commands === null || Array.isArray(o.commands))
|
|
4427
|
+
return void 0;
|
|
4428
|
+
const commands2 = emptyTable();
|
|
4429
|
+
for (const [k, v] of Object.entries(o.commands)) {
|
|
4430
|
+
const entry = parseEntry(v);
|
|
4431
|
+
if (entry === null) return void 0;
|
|
4432
|
+
commands2[k] = entry;
|
|
4433
|
+
}
|
|
4434
|
+
const map = { version: 1, commands: commands2 };
|
|
4435
|
+
if (typeof o.cliVersion === "string") map.cliVersion = o.cliVersion;
|
|
4436
|
+
if (typeof o._contract === "string") map._contract = o._contract;
|
|
4437
|
+
const aliases = parseStringMap(o.aliases);
|
|
4438
|
+
if (aliases !== void 0) map.aliases = aliases;
|
|
4439
|
+
const pending = parseStringMap(o.pending);
|
|
4440
|
+
if (pending !== void 0) map.pending = pending;
|
|
4441
|
+
const pendingCommands = parsePendingCommands(o.pendingCommands, pending);
|
|
4442
|
+
if (pendingCommands !== void 0) map.pendingCommands = pendingCommands;
|
|
4443
|
+
const resources = parseResources(o.resources);
|
|
4444
|
+
if (resources !== void 0) map.resources = resources;
|
|
4445
|
+
const deferred = new Set(Object.values(pendingCommands ?? {}).flat());
|
|
4446
|
+
for (const cmdPath of Object.keys(commands2)) {
|
|
4447
|
+
if (deferred.has(cmdPath)) return void 0;
|
|
4448
|
+
if (pending !== void 0 && Object.prototype.hasOwnProperty.call(pending, cmdPath.split(" ")[0]))
|
|
4449
|
+
return void 0;
|
|
4450
|
+
}
|
|
4451
|
+
return map;
|
|
4452
|
+
}
|
|
4453
|
+
function loadScopesMap(here = path3.dirname(fileURLToPath(import.meta.url))) {
|
|
4454
|
+
let text2;
|
|
4455
|
+
try {
|
|
4456
|
+
text2 = readFileSync(path3.join(here, "scopes-map.json"), "utf8");
|
|
4457
|
+
} catch {
|
|
4458
|
+
return void 0;
|
|
4459
|
+
}
|
|
4460
|
+
return parseScopesMap(text2);
|
|
4461
|
+
}
|
|
4462
|
+
function scopesMapKey(cmd) {
|
|
4463
|
+
const names = [];
|
|
4464
|
+
for (let c = cmd; c !== null && c.parent !== null; c = c.parent)
|
|
4465
|
+
names.unshift(c.name());
|
|
4466
|
+
return names.join(" ");
|
|
4467
|
+
}
|
|
4468
|
+
function commandPathOf(argv, root) {
|
|
4469
|
+
const names = [];
|
|
4470
|
+
let cmd = root;
|
|
4471
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
4472
|
+
const token = argv[i];
|
|
4473
|
+
if (token === "--") break;
|
|
4474
|
+
if (token.startsWith("-") && token !== "-") {
|
|
4475
|
+
if (!token.startsWith("--") && token.length > 2) continue;
|
|
4476
|
+
if (token.includes("=")) continue;
|
|
4477
|
+
if (takesValue(token, cmd)) i += 1;
|
|
4478
|
+
continue;
|
|
4479
|
+
}
|
|
4480
|
+
const child2 = childNamed(cmd, token);
|
|
4481
|
+
if (child2 === void 0) break;
|
|
4482
|
+
names.push(child2.name());
|
|
4483
|
+
cmd = child2;
|
|
4484
|
+
}
|
|
4485
|
+
return names.join(" ");
|
|
4486
|
+
}
|
|
4487
|
+
function childNamed(cmd, token) {
|
|
4488
|
+
return cmd.commands.find((c) => c.name() === token || c.aliases().includes(token));
|
|
4489
|
+
}
|
|
4490
|
+
function takesValue(flag, cmd) {
|
|
4491
|
+
for (let c = cmd; c !== null; c = c.parent) {
|
|
4492
|
+
for (const opt of c.options) {
|
|
4493
|
+
if (opt.short !== flag && opt.long !== flag) continue;
|
|
4494
|
+
return opt.required || opt.optional;
|
|
4495
|
+
}
|
|
4496
|
+
}
|
|
4497
|
+
return declaredWithValueBelow(flag, cmd);
|
|
4498
|
+
}
|
|
4499
|
+
function declaredWithValueBelow(flag, cmd) {
|
|
4500
|
+
for (const child2 of cmd.commands) {
|
|
4501
|
+
for (const opt of child2.options) {
|
|
4502
|
+
if ((opt.short === flag || opt.long === flag) && (opt.required || opt.optional)) return true;
|
|
4503
|
+
}
|
|
4504
|
+
if (declaredWithValueBelow(flag, child2)) return true;
|
|
4505
|
+
}
|
|
4506
|
+
return false;
|
|
4507
|
+
}
|
|
4508
|
+
|
|
4509
|
+
// src/scopes/filter.ts
|
|
4510
|
+
var SCOPE_EXIT_CODE = 3;
|
|
4511
|
+
var ScopeFilterError = class extends Error {
|
|
4512
|
+
constructor(command, message) {
|
|
4513
|
+
super(message);
|
|
4514
|
+
this.command = command;
|
|
4515
|
+
}
|
|
4516
|
+
command;
|
|
4517
|
+
exitCode = SCOPE_EXIT_CODE;
|
|
4518
|
+
};
|
|
4519
|
+
var ScopeHiddenError = class extends ScopeFilterError {
|
|
4520
|
+
constructor(command, required2) {
|
|
4521
|
+
super(
|
|
4522
|
+
command,
|
|
4523
|
+
`'dr ${command}' is not available under this identity's credential scope (needs ${required2.join(", ")}); run 'dr whoami' to see the scopes this identity carries`
|
|
4524
|
+
);
|
|
4525
|
+
this.required = required2;
|
|
4526
|
+
this.name = "ScopeHiddenError";
|
|
4527
|
+
}
|
|
4528
|
+
required;
|
|
4529
|
+
};
|
|
4530
|
+
var ScopeUnavailableError = class extends ScopeFilterError {
|
|
4531
|
+
constructor(command, reason, until) {
|
|
4532
|
+
super(
|
|
4533
|
+
command,
|
|
4534
|
+
`'dr ${command}' is unavailable to any agent-scoped credential (one whose scopes claim carries at least one resource:action entry): ${reason} (this is not a missing scope \u2014 no scope grants it; ${until} lifts the restriction)`
|
|
4535
|
+
);
|
|
4536
|
+
this.reason = reason;
|
|
4537
|
+
this.until = until;
|
|
4538
|
+
this.name = "ScopeUnavailableError";
|
|
4539
|
+
}
|
|
4540
|
+
reason;
|
|
4541
|
+
until;
|
|
4542
|
+
};
|
|
4543
|
+
var ScopeGroupHiddenError = class extends ScopeFilterError {
|
|
4544
|
+
constructor(command, required2, tickets) {
|
|
4545
|
+
super(
|
|
4546
|
+
command,
|
|
4547
|
+
`'dr ${command}' has no subcommand available to this identity.` + (required2.length > 0 ? ` Its subcommands need ${required2.join(", ")} \u2014 run 'dr whoami' to see the scopes this identity carries.` : "") + (tickets.length > 0 ? ` Some are closed to any agent-scoped credential whatever its scopes (${tickets.join(", ")}).` : "")
|
|
4548
|
+
);
|
|
4549
|
+
this.required = required2;
|
|
4550
|
+
this.tickets = tickets;
|
|
4551
|
+
this.name = "ScopeGroupHiddenError";
|
|
4552
|
+
}
|
|
4553
|
+
required;
|
|
4554
|
+
tickets;
|
|
4555
|
+
};
|
|
4556
|
+
function hide(cmd) {
|
|
4557
|
+
cmd._hidden = true;
|
|
4558
|
+
}
|
|
4559
|
+
function refuseHelp(cmd, err) {
|
|
4560
|
+
cmd.outputHelp = () => {
|
|
4561
|
+
throw err;
|
|
4562
|
+
};
|
|
4563
|
+
}
|
|
4564
|
+
function leafAnswer(leaf, scopes, map) {
|
|
4565
|
+
const key = scopesMapKey(leaf);
|
|
4566
|
+
if (!Object.prototype.hasOwnProperty.call(map.commands, key)) return void 0;
|
|
4567
|
+
const entry = map.commands[key];
|
|
4568
|
+
if (entry === void 0) return void 0;
|
|
4569
|
+
if (isDeniedEntry(entry))
|
|
4570
|
+
return {
|
|
4571
|
+
err: new ScopeUnavailableError(key, entry.denied.reason, entry.denied.until),
|
|
4572
|
+
required: [],
|
|
4573
|
+
denial: entry.denied
|
|
4574
|
+
};
|
|
4575
|
+
if (isEntryAllowed(scopes, entry)) return void 0;
|
|
4576
|
+
const required2 = entry.requires.map((r) => `${r.resource}:${r.action}`);
|
|
4577
|
+
return { err: new ScopeHiddenError(key, required2), required: required2, denial: void 0 };
|
|
4578
|
+
}
|
|
4579
|
+
var intercepted = /* @__PURE__ */ new WeakMap();
|
|
4580
|
+
function intercept(cmd, err, armed) {
|
|
4581
|
+
intercepted.set(cmd, err);
|
|
4582
|
+
refuseHelp(cmd, err);
|
|
4583
|
+
cmd.action(() => {
|
|
4584
|
+
throw err;
|
|
4585
|
+
});
|
|
4586
|
+
const parent = cmd.parent;
|
|
4587
|
+
if (parent === null || armed.has(parent)) return;
|
|
4588
|
+
armed.add(parent);
|
|
4589
|
+
parent.hook("preSubcommand", (_parent, sub) => {
|
|
4590
|
+
const denial = intercepted.get(sub);
|
|
4591
|
+
if (denial !== void 0) throw denial;
|
|
4592
|
+
});
|
|
4593
|
+
}
|
|
4594
|
+
function interceptGroup(group, err) {
|
|
4595
|
+
group.allowExcessArguments(true);
|
|
4596
|
+
group.allowUnknownOption(true);
|
|
4597
|
+
group.helpCommand(true);
|
|
4598
|
+
refuseHelp(group, err);
|
|
4599
|
+
group.action(() => {
|
|
4600
|
+
throw err;
|
|
4601
|
+
});
|
|
4602
|
+
}
|
|
4603
|
+
function groupAnswer(group, below) {
|
|
4604
|
+
const key = scopesMapKey(group);
|
|
4605
|
+
const [mark] = below.denials;
|
|
4606
|
+
if (mark !== void 0 && below.denials.length === 1 && below.required.length === 0)
|
|
4607
|
+
return new ScopeUnavailableError(key, mark.reason, mark.until);
|
|
4608
|
+
if (below.required.length === 0 && below.denials.length === 0) return void 0;
|
|
4609
|
+
return new ScopeGroupHiddenError(
|
|
4610
|
+
key,
|
|
4611
|
+
below.required,
|
|
4612
|
+
below.denials.map((d) => d.until).filter((t, i, all) => all.indexOf(t) === i)
|
|
4613
|
+
);
|
|
4614
|
+
}
|
|
4615
|
+
function filterBelow(node, scopes, map, armed) {
|
|
4616
|
+
const here = { count: 0, answer: void 0, required: [], denials: [] };
|
|
4617
|
+
let allAnswered = node.commands.length > 0;
|
|
4618
|
+
for (const child2 of node.commands) {
|
|
4619
|
+
if (child2.commands.length > 0) {
|
|
4620
|
+
const below = filterBelow(child2, scopes, map, armed);
|
|
4621
|
+
here.count += below.count;
|
|
4622
|
+
if (below.answer === void 0) {
|
|
4623
|
+
allAnswered = false;
|
|
4624
|
+
continue;
|
|
4625
|
+
}
|
|
4626
|
+
interceptGroup(child2, below.answer);
|
|
4627
|
+
hide(child2);
|
|
4628
|
+
addRequired(here, below.required);
|
|
4629
|
+
addDenials(here, below.denials);
|
|
4630
|
+
continue;
|
|
4631
|
+
}
|
|
4632
|
+
const answer = leafAnswer(child2, scopes, map);
|
|
4633
|
+
if (answer === void 0) {
|
|
4634
|
+
allAnswered = false;
|
|
4635
|
+
continue;
|
|
4636
|
+
}
|
|
4637
|
+
intercept(child2, answer.err, armed);
|
|
4638
|
+
hide(child2);
|
|
4639
|
+
here.count += 1;
|
|
4640
|
+
addRequired(here, answer.required);
|
|
4641
|
+
if (answer.denial !== void 0) addDenials(here, [answer.denial]);
|
|
4642
|
+
}
|
|
4643
|
+
if (allAnswered) here.answer = groupAnswer(node, here);
|
|
4644
|
+
return here;
|
|
4645
|
+
}
|
|
4646
|
+
function addRequired(into, required2) {
|
|
4647
|
+
for (const r of required2) if (!into.required.includes(r)) into.required.push(r);
|
|
4648
|
+
}
|
|
4649
|
+
function addDenials(into, denials) {
|
|
4650
|
+
for (const d of denials)
|
|
4651
|
+
if (!into.denials.some((x) => x.reason === d.reason && x.until === d.until))
|
|
4652
|
+
into.denials.push(d);
|
|
4653
|
+
}
|
|
4654
|
+
function applyScopeFilter(root, scopes, map) {
|
|
4655
|
+
if (map === void 0 || !isAgentScoped(scopes)) return 0;
|
|
4656
|
+
return filterBelow(root, scopes, map, /* @__PURE__ */ new Set()).count;
|
|
4657
|
+
}
|
|
4658
|
+
|
|
4659
|
+
// src/commands/_tenantPin.ts
|
|
4660
|
+
var SERVER_VAR = "DR_PIN_SERVER";
|
|
4661
|
+
var ACCOUNT_VAR = "DR_PIN_ACCOUNT";
|
|
4662
|
+
var ORG_MODE_VAR = "DR_PIN_ORG_MODE";
|
|
4663
|
+
var TenantPinError = class extends Error {
|
|
4664
|
+
/** "You cannot do this", the same exit code a scope denial uses (spec §8). */
|
|
4665
|
+
exitCode = SCOPE_EXIT_CODE;
|
|
4666
|
+
constructor(message) {
|
|
4667
|
+
super(`tenant pin: ${message}`);
|
|
4668
|
+
this.name = "TenantPinError";
|
|
4669
|
+
}
|
|
4670
|
+
};
|
|
4671
|
+
var ORG_MODE_UNSCOPED_ALLOWED = /* @__PURE__ */ new Set([
|
|
4672
|
+
"login",
|
|
4673
|
+
"whoami",
|
|
4674
|
+
"users me",
|
|
4675
|
+
// Local only: skills, the CLI's own update, the Help Center, MCP registration, and
|
|
4676
|
+
// app-space scaffolding never write to the tenant.
|
|
4677
|
+
"skill list",
|
|
4678
|
+
"skill print",
|
|
4679
|
+
"skill install",
|
|
4680
|
+
"skill init-project",
|
|
4681
|
+
"update",
|
|
4682
|
+
"kb search",
|
|
4683
|
+
"kb get",
|
|
4684
|
+
"kb categories",
|
|
4685
|
+
"kb sections",
|
|
4686
|
+
"kb articles-in-section",
|
|
4687
|
+
"kb recent",
|
|
4688
|
+
"kb attachments",
|
|
4689
|
+
"kb article-images",
|
|
4690
|
+
"mcp register",
|
|
4691
|
+
"mcp unregister",
|
|
4692
|
+
"mcp list",
|
|
4693
|
+
"mcp serve",
|
|
4694
|
+
"app-space scaffold",
|
|
4695
|
+
"app-space contract",
|
|
4696
|
+
"app-space eject"
|
|
4697
|
+
]);
|
|
4698
|
+
var ORG_MODE_READ_SCOPED_REFUSED = /* @__PURE__ */ new Set([
|
|
4699
|
+
"devices request-logs",
|
|
4700
|
+
"desk smtp test",
|
|
4701
|
+
"dashboards prepare",
|
|
4702
|
+
"storyboards prepare"
|
|
4703
|
+
]);
|
|
4704
|
+
var LOCAL_COMMANDS_WITH_TARGET_FLAGS = /* @__PURE__ */ new Set([
|
|
4705
|
+
"app-space scaffold"
|
|
4706
|
+
]);
|
|
4707
|
+
var EMAIL_RE = /^[^\s@]+@[^\s@]+$/;
|
|
4708
|
+
function readTenantPin(env = process.env) {
|
|
4709
|
+
const rawServer = env[SERVER_VAR];
|
|
4710
|
+
const rawAccount = env[ACCOUNT_VAR];
|
|
4711
|
+
const rawOrgMode = env[ORG_MODE_VAR];
|
|
4712
|
+
if (rawServer === void 0 && rawAccount === void 0 && rawOrgMode === void 0) {
|
|
4713
|
+
return void 0;
|
|
4714
|
+
}
|
|
4715
|
+
if (rawServer === void 0 || rawServer.trim() === "") {
|
|
4716
|
+
throw new TenantPinError(`${SERVER_VAR} is required when any DR_PIN_* variable is set`);
|
|
4717
|
+
}
|
|
4718
|
+
let server;
|
|
4719
|
+
try {
|
|
4720
|
+
server = resolve(rawServer).key;
|
|
4721
|
+
} catch {
|
|
4722
|
+
throw new TenantPinError(`${SERVER_VAR} is not a known server key or Datarails URL`);
|
|
4723
|
+
}
|
|
4724
|
+
if (rawOrgMode !== void 0 && rawOrgMode !== "1") {
|
|
4725
|
+
throw new TenantPinError(`${ORG_MODE_VAR} must be 1 when set`);
|
|
4726
|
+
}
|
|
4727
|
+
if (rawOrgMode === "1") {
|
|
4728
|
+
if (rawAccount !== void 0) {
|
|
4729
|
+
throw new TenantPinError(
|
|
4730
|
+
`${ORG_MODE_VAR}=1 pins a server only; ${ACCOUNT_VAR} must not be set with it`
|
|
4731
|
+
);
|
|
4732
|
+
}
|
|
4733
|
+
return { kind: "org", server };
|
|
4734
|
+
}
|
|
4735
|
+
const account = normaliseAccount(rawAccount);
|
|
4736
|
+
if (account === void 0 || !EMAIL_RE.test(account)) {
|
|
4737
|
+
throw new TenantPinError(`${ACCOUNT_VAR} must be the login email (or set ${ORG_MODE_VAR}=1)`);
|
|
4738
|
+
}
|
|
4739
|
+
return { kind: "customer", server, account };
|
|
4740
|
+
}
|
|
4741
|
+
function describePin(pin) {
|
|
4742
|
+
return pin.kind === "customer" ? `${pin.server} as ${pin.account}` : `${pin.server} (org mode)`;
|
|
4743
|
+
}
|
|
4744
|
+
function assertFlagsWithinPin(pin, serverFlag, accountFlag) {
|
|
4745
|
+
assertServerFlagWithinPin(pin, serverFlag);
|
|
4746
|
+
assertAccountFlagWithinPin(pin, accountFlag);
|
|
4747
|
+
}
|
|
4748
|
+
function assertServerFlagWithinPin(pin, serverFlag) {
|
|
4749
|
+
if ((serverFlag ?? "").trim() === "") {
|
|
4750
|
+
throw new TenantPinError(
|
|
4751
|
+
`--server is required while dr is pinned to ${describePin(pin)}; pass --server ${pin.server}`
|
|
4752
|
+
);
|
|
4753
|
+
}
|
|
4754
|
+
const server = resolveServer(serverFlag);
|
|
4755
|
+
if (server !== pin.server) {
|
|
4756
|
+
throw new TenantPinError(
|
|
4757
|
+
`--server ${server} is outside the pin; this session may only reach ${describePin(pin)}`
|
|
4758
|
+
);
|
|
4759
|
+
}
|
|
4760
|
+
}
|
|
4761
|
+
function assertAccountFlagWithinPin(pin, accountFlag) {
|
|
4762
|
+
if (pin.kind !== "customer") return;
|
|
4763
|
+
const account = normaliseAccount(accountFlag);
|
|
4764
|
+
if (account === void 0) {
|
|
4765
|
+
throw new TenantPinError(
|
|
4766
|
+
`--account is required while dr is pinned to ${describePin(pin)}; pass --account ${pin.account}`
|
|
4767
|
+
);
|
|
4768
|
+
}
|
|
4769
|
+
if (account !== pin.account) {
|
|
4770
|
+
throw new TenantPinError(
|
|
4771
|
+
`--account ${account} is outside the pin; this session may only act as ${pin.account}`
|
|
4772
|
+
);
|
|
4773
|
+
}
|
|
4774
|
+
}
|
|
4775
|
+
function assertIdentityWithinPin(pin, serverKey, email) {
|
|
4776
|
+
let canonical = serverKey;
|
|
4777
|
+
try {
|
|
4778
|
+
canonical = resolve(serverKey).key;
|
|
4779
|
+
} catch {
|
|
4780
|
+
}
|
|
4781
|
+
if (canonical !== pin.server) {
|
|
4782
|
+
throw new TenantPinError(
|
|
4783
|
+
`server ${serverKey} is outside the pin; this session may only reach ${describePin(pin)}`
|
|
4784
|
+
);
|
|
4785
|
+
}
|
|
4786
|
+
if (pin.kind === "customer" && email !== pin.account) {
|
|
4787
|
+
throw new TenantPinError(
|
|
4788
|
+
`account ${email} is outside the pin; this session may only act as ${pin.account}`
|
|
4789
|
+
);
|
|
4790
|
+
}
|
|
4791
|
+
}
|
|
4792
|
+
function declaresOption(cmd, flag) {
|
|
4793
|
+
for (let c = cmd; c !== null; c = c.parent) {
|
|
4794
|
+
if (c.options.some((o) => o.long === flag)) return true;
|
|
4795
|
+
}
|
|
4796
|
+
return false;
|
|
4797
|
+
}
|
|
4798
|
+
function assertReadOnly(key, map) {
|
|
4799
|
+
if (map === void 0) {
|
|
4800
|
+
throw new TenantPinError(
|
|
4801
|
+
`org mode is read-only, and without scopes-map.json dr cannot tell whether \`dr ${key}\` writes`
|
|
4802
|
+
);
|
|
4803
|
+
}
|
|
4804
|
+
const entry = Object.prototype.hasOwnProperty.call(map.commands, key) ? map.commands[key] : void 0;
|
|
4805
|
+
const writes = entry === void 0 || isDeniedEntry(entry) || (entry.requires.length === 0 ? !ORG_MODE_UNSCOPED_ALLOWED.has(key) : entry.requires.some((r) => r.action !== "read") || ORG_MODE_READ_SCOPED_REFUSED.has(key));
|
|
4806
|
+
if (writes) {
|
|
4807
|
+
throw new TenantPinError(`org mode is read-only; \`dr ${key}\` is not a read-only command`);
|
|
4808
|
+
}
|
|
4809
|
+
}
|
|
4810
|
+
var defaultTenantPinDeps = {
|
|
4811
|
+
get env() {
|
|
4812
|
+
return process.env;
|
|
4813
|
+
},
|
|
4814
|
+
loadMap: () => loadScopesMap()
|
|
4815
|
+
};
|
|
4816
|
+
function enforceTenantPin(cmd, deps = defaultTenantPinDeps) {
|
|
4817
|
+
const pin = readTenantPin(deps.env);
|
|
4818
|
+
if (pin === void 0) return;
|
|
4819
|
+
const key = scopesMapKey(cmd);
|
|
4820
|
+
if (key === "logout") {
|
|
4821
|
+
throw new TenantPinError(`\`dr logout\` is refused while dr is pinned to ${describePin(pin)}`);
|
|
4822
|
+
}
|
|
4823
|
+
const opts = cmd.optsWithGlobals();
|
|
4824
|
+
if (key === "login-as") {
|
|
4825
|
+
const target = normaliseAccount(cmd.processedArgs[0]);
|
|
4826
|
+
if (pin.kind !== "customer" || target !== pin.account || opts.userId !== void 0) {
|
|
4827
|
+
throw new TenantPinError(
|
|
4828
|
+
`\`dr login-as\` may only target the pinned account, by email, while dr is pinned to ${describePin(pin)}`
|
|
4829
|
+
);
|
|
4830
|
+
}
|
|
4831
|
+
}
|
|
4832
|
+
for (const { flag, value, envVar } of [
|
|
4833
|
+
{ flag: "--app-space-host", value: opts.appSpaceHost, envVar: "DR_APP_SPACE_HOST" },
|
|
4834
|
+
{ flag: "--connectors-url", value: opts.connectorsUrl, envVar: void 0 }
|
|
4835
|
+
]) {
|
|
4836
|
+
if (!declaresOption(cmd, flag)) continue;
|
|
4837
|
+
const fromEnv = envVar === void 0 ? "" : deps.env[envVar] ?? "";
|
|
4838
|
+
if ((value ?? "").trim() !== "" || fromEnv.trim() !== "") {
|
|
4839
|
+
throw new TenantPinError(
|
|
4840
|
+
`${flag}${envVar === void 0 ? "" : ` / ${envVar}`} is refused while dr is pinned to ${describePin(pin)}`
|
|
4841
|
+
);
|
|
4842
|
+
}
|
|
4843
|
+
}
|
|
4844
|
+
const required2 = !LOCAL_COMMANDS_WITH_TARGET_FLAGS.has(key);
|
|
4845
|
+
if (declaresOption(cmd, "--server") && (required2 || opts.server !== void 0)) {
|
|
4846
|
+
assertServerFlagWithinPin(pin, opts.server);
|
|
4847
|
+
}
|
|
4848
|
+
if (declaresOption(cmd, "--account") && (required2 || opts.account !== void 0)) {
|
|
4849
|
+
assertAccountFlagWithinPin(pin, opts.account);
|
|
4850
|
+
}
|
|
4851
|
+
if (pin.kind === "org") assertReadOnly(key, deps.loadMap());
|
|
4852
|
+
}
|
|
4853
|
+
|
|
4285
4854
|
// src/telemetry/context.ts
|
|
4286
4855
|
var NO_IDENTITY = {
|
|
4287
4856
|
operator: "",
|
|
@@ -4551,10 +5120,13 @@ function batchSetupOptions(timeoutFlag) {
|
|
|
4551
5120
|
var BATCH_TIMEOUT_FLAG_HELP = `Minimum whole-command budget in seconds. Default ${DEFAULT_CALL_TIMEOUT_MS / 1e3}s for one item; a batch gets ${DEFAULT_CALL_TIMEOUT_MS / 1e3}s + ${BATCH_PER_TARGET_TIMEOUT_MS / 1e3}s per call (capped at ${BATCH_MAX_TIMEOUT_MS / 1e3}s). This is a floor, so it only ever widens that budget; max ${BATCH_TIMEOUT_MAX_SECONDS}s`;
|
|
4552
5121
|
async function setupCall(serverFlag, accountFlag, options = {}) {
|
|
4553
5122
|
setConfirmCmdAccountFlag(normaliseAccount(accountFlag));
|
|
5123
|
+
const pin = readTenantPin();
|
|
5124
|
+
if (pin !== void 0) assertFlagsWithinPin(pin, serverFlag, accountFlag);
|
|
4554
5125
|
const fileStore = await resolveFileStore(serverFlag);
|
|
4555
5126
|
const key = fileStore?.key ?? resolveServer(serverFlag);
|
|
4556
5127
|
const store = options.store ?? fileStore?.store ?? await KeyringStore.defaultWithCleanup();
|
|
4557
5128
|
const email = await resolveAccount(store, key, accountFlag);
|
|
5129
|
+
if (pin !== void 0) assertIdentityWithinPin(pin, key, email);
|
|
4558
5130
|
const stored = await store.getSession(key, email);
|
|
4559
5131
|
if (stored !== void 0) {
|
|
4560
5132
|
emitApiKeyExpiryWarning(stored, `${key}:${email}`);
|
|
@@ -4610,56 +5182,8 @@ async function setupCall(serverFlag, accountFlag, options = {}) {
|
|
|
4610
5182
|
};
|
|
4611
5183
|
}
|
|
4612
5184
|
|
|
4613
|
-
// src/scopes/matcher.ts
|
|
4614
|
-
var SCOPE_RE = /^(\*|[a-z][a-z0-9-]*):(\*|read|write|destructive)$/;
|
|
4615
|
-
var ACTION_RANK = new Map(
|
|
4616
|
-
["read", "write", "destructive"].map((action, rank) => [action, rank])
|
|
4617
|
-
);
|
|
4618
|
-
function actionSatisfies(granted, wanted) {
|
|
4619
|
-
if (granted === "*" || granted === wanted) return true;
|
|
4620
|
-
const g = ACTION_RANK.get(granted);
|
|
4621
|
-
const w = ACTION_RANK.get(wanted);
|
|
4622
|
-
if (g === void 0 || w === void 0) return false;
|
|
4623
|
-
return g > w;
|
|
4624
|
-
}
|
|
4625
|
-
var WHITESPACE = "\\t\\n\\v\\f\\r \\x1c-\\x1f\\x85\\xa0\\u1680\\u2000-\\u200a\\u2028\\u2029\\u202f\\u205f\\u3000\\ufeff";
|
|
4626
|
-
var TRIM_RE = new RegExp(`^[${WHITESPACE}]+|[${WHITESPACE}]+$`, "g");
|
|
4627
|
-
var SPLIT_RE = new RegExp(`[${WHITESPACE}]+`);
|
|
4628
|
-
function trimShared(value) {
|
|
4629
|
-
return value.replace(TRIM_RE, "");
|
|
4630
|
-
}
|
|
4631
|
-
function splitScopeString(value) {
|
|
4632
|
-
return value.split(SPLIT_RE).filter((part) => part !== "");
|
|
4633
|
-
}
|
|
4634
|
-
function parseScope(value) {
|
|
4635
|
-
if (typeof value !== "string") return null;
|
|
4636
|
-
const m = SCOPE_RE.exec(trimShared(value).toLowerCase());
|
|
4637
|
-
return m ? [m[1], m[2]] : null;
|
|
4638
|
-
}
|
|
4639
|
-
function parsed(scopes) {
|
|
4640
|
-
const list28 = scopes ?? [];
|
|
4641
|
-
const entries = typeof list28 === "string" ? splitScopeString(list28) : Array.isArray(list28) ? list28 : [];
|
|
4642
|
-
return entries.map(parseScope).filter((p) => p !== null);
|
|
4643
|
-
}
|
|
4644
|
-
function scopeEntries(scopes) {
|
|
4645
|
-
return parsed(scopes);
|
|
4646
|
-
}
|
|
4647
|
-
function isAgentScoped(scopes) {
|
|
4648
|
-
return parsed(scopes).length > 0;
|
|
4649
|
-
}
|
|
4650
|
-
function isAllowed(scopes, resource, action) {
|
|
4651
|
-
const entries = parsed(scopes);
|
|
4652
|
-
if (entries.length === 0) return true;
|
|
4653
|
-
const res = trimShared(resource).toLowerCase();
|
|
4654
|
-
const act = trimShared(action).toLowerCase();
|
|
4655
|
-
return entries.some(([r, a]) => (r === "*" || r === res) && actionSatisfies(a, act));
|
|
4656
|
-
}
|
|
4657
|
-
function isEntryAllowed(scopes, entry) {
|
|
4658
|
-
return entry.requires.every((r) => isAllowed(scopes, r.resource, r.action));
|
|
4659
|
-
}
|
|
4660
|
-
|
|
4661
5185
|
// src/scopes/render.ts
|
|
4662
|
-
var
|
|
5186
|
+
var ACTIONS2 = ["read", "write", "destructive"];
|
|
4663
5187
|
var OTHER = "Other";
|
|
4664
5188
|
var ALL_PRODUCTS = "All products";
|
|
4665
5189
|
var ALL_RESOURCES = "* (all resources)";
|
|
@@ -4673,7 +5197,7 @@ function describeScopes(scopes, resources) {
|
|
|
4673
5197
|
const byResource = /* @__PURE__ */ new Map();
|
|
4674
5198
|
for (const [resource, action] of entries) {
|
|
4675
5199
|
const actions = byResource.get(resource) ?? /* @__PURE__ */ new Set();
|
|
4676
|
-
if (action === "*") for (const a of
|
|
5200
|
+
if (action === "*") for (const a of ACTIONS2) actions.add(a);
|
|
4677
5201
|
else actions.add(action);
|
|
4678
5202
|
byResource.set(resource, actions);
|
|
4679
5203
|
}
|
|
@@ -4703,7 +5227,7 @@ function renderTree(byResource, resources) {
|
|
|
4703
5227
|
const width = Math.max(...[...byResource.keys()].map((r) => label(r).length));
|
|
4704
5228
|
const row = (resource, indent) => {
|
|
4705
5229
|
const actions = byResource.get(resource);
|
|
4706
|
-
return `${indent}${label(resource).padEnd(width)} ${
|
|
5230
|
+
return `${indent}${label(resource).padEnd(width)} ${ACTIONS2.filter((a) => actions.has(a)).join(", ")}`;
|
|
4707
5231
|
};
|
|
4708
5232
|
const sorted = [...byResource.keys()].sort();
|
|
4709
5233
|
if (resources === void 0 || Object.keys(resources).length === 0)
|
|
@@ -4724,182 +5248,6 @@ function renderTree(byResource, resources) {
|
|
|
4724
5248
|
return lines;
|
|
4725
5249
|
}
|
|
4726
5250
|
|
|
4727
|
-
// src/scopes/scopesMap.ts
|
|
4728
|
-
import { readFileSync } from "fs";
|
|
4729
|
-
import * as path3 from "path";
|
|
4730
|
-
import { fileURLToPath } from "url";
|
|
4731
|
-
function isDeniedEntry(entry) {
|
|
4732
|
-
return entry !== void 0 && Object.prototype.hasOwnProperty.call(entry, "denied");
|
|
4733
|
-
}
|
|
4734
|
-
var ACTIONS2 = /* @__PURE__ */ new Set(["read", "write", "destructive"]);
|
|
4735
|
-
var RESOURCE_RE = /^[a-z][a-z0-9-]*$/;
|
|
4736
|
-
function isValidResourceName(v) {
|
|
4737
|
-
return RESOURCE_RE.test(v);
|
|
4738
|
-
}
|
|
4739
|
-
function isAction(v) {
|
|
4740
|
-
return typeof v === "string" && ACTIONS2.has(v);
|
|
4741
|
-
}
|
|
4742
|
-
function isResource(v) {
|
|
4743
|
-
return typeof v === "string" && isValidResourceName(v);
|
|
4744
|
-
}
|
|
4745
|
-
function parseRequirement(v) {
|
|
4746
|
-
if (typeof v !== "object" || v === null) return null;
|
|
4747
|
-
const o = v;
|
|
4748
|
-
if (!isResource(o.resource) || !isAction(o.action)) return null;
|
|
4749
|
-
return { resource: o.resource, action: o.action };
|
|
4750
|
-
}
|
|
4751
|
-
function parseDenial(v) {
|
|
4752
|
-
if (typeof v !== "object" || v === null || Array.isArray(v)) return null;
|
|
4753
|
-
const o = v;
|
|
4754
|
-
if (typeof o.reason !== "string" || o.reason.trim() === "") return null;
|
|
4755
|
-
if (typeof o.until !== "string" || o.until.trim() === "") return null;
|
|
4756
|
-
return { reason: o.reason, until: o.until };
|
|
4757
|
-
}
|
|
4758
|
-
function parseEntry(v) {
|
|
4759
|
-
if (typeof v !== "object" || v === null) return null;
|
|
4760
|
-
const o = v;
|
|
4761
|
-
if (Object.prototype.hasOwnProperty.call(o, "denied")) {
|
|
4762
|
-
if (Object.prototype.hasOwnProperty.call(o, "requires")) return null;
|
|
4763
|
-
const denied = parseDenial(o.denied);
|
|
4764
|
-
if (denied === null) return null;
|
|
4765
|
-
return { denied };
|
|
4766
|
-
}
|
|
4767
|
-
if (!Array.isArray(o.requires)) return null;
|
|
4768
|
-
const requires = [];
|
|
4769
|
-
for (const raw of o.requires) {
|
|
4770
|
-
const req = parseRequirement(raw);
|
|
4771
|
-
if (req === null) return null;
|
|
4772
|
-
requires.push(req);
|
|
4773
|
-
}
|
|
4774
|
-
return { requires };
|
|
4775
|
-
}
|
|
4776
|
-
function emptyTable() {
|
|
4777
|
-
return /* @__PURE__ */ Object.create(null);
|
|
4778
|
-
}
|
|
4779
|
-
function parseStringMap(v) {
|
|
4780
|
-
if (typeof v !== "object" || v === null || Array.isArray(v)) return void 0;
|
|
4781
|
-
const out = emptyTable();
|
|
4782
|
-
for (const [k, val] of Object.entries(v)) {
|
|
4783
|
-
if (typeof val === "string") out[k] = val;
|
|
4784
|
-
}
|
|
4785
|
-
return out;
|
|
4786
|
-
}
|
|
4787
|
-
function parseResources(v) {
|
|
4788
|
-
if (typeof v !== "object" || v === null || Array.isArray(v)) return void 0;
|
|
4789
|
-
const out = emptyTable();
|
|
4790
|
-
for (const [resource, val] of Object.entries(v)) {
|
|
4791
|
-
if (!isResource(resource)) continue;
|
|
4792
|
-
if (typeof val !== "object" || val === null || Array.isArray(val)) continue;
|
|
4793
|
-
const o = val;
|
|
4794
|
-
if (typeof o.product !== "string" || o.product === "") continue;
|
|
4795
|
-
if (typeof o.productName !== "string" || o.productName === "") continue;
|
|
4796
|
-
out[resource] = { product: o.product, productName: o.productName };
|
|
4797
|
-
}
|
|
4798
|
-
return out;
|
|
4799
|
-
}
|
|
4800
|
-
function parsePendingCommands(v, pending) {
|
|
4801
|
-
if (typeof v !== "object" || v === null || Array.isArray(v)) return void 0;
|
|
4802
|
-
const out = emptyTable();
|
|
4803
|
-
for (const [group, val] of Object.entries(v)) {
|
|
4804
|
-
if (pending === void 0 || !Object.prototype.hasOwnProperty.call(pending, group)) continue;
|
|
4805
|
-
if (!Array.isArray(val)) continue;
|
|
4806
|
-
if (!val.every((p) => typeof p === "string" && p.split(" ")[0] === group)) continue;
|
|
4807
|
-
out[group] = val;
|
|
4808
|
-
}
|
|
4809
|
-
return out;
|
|
4810
|
-
}
|
|
4811
|
-
function parseScopesMap(text2) {
|
|
4812
|
-
let raw;
|
|
4813
|
-
try {
|
|
4814
|
-
raw = JSON.parse(text2);
|
|
4815
|
-
} catch {
|
|
4816
|
-
return void 0;
|
|
4817
|
-
}
|
|
4818
|
-
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) return void 0;
|
|
4819
|
-
const o = raw;
|
|
4820
|
-
if (o.version !== 1 || o.cliVersion !== void 0 && typeof o.cliVersion !== "string" || typeof o.commands !== "object" || o.commands === null || Array.isArray(o.commands))
|
|
4821
|
-
return void 0;
|
|
4822
|
-
const commands2 = emptyTable();
|
|
4823
|
-
for (const [k, v] of Object.entries(o.commands)) {
|
|
4824
|
-
const entry = parseEntry(v);
|
|
4825
|
-
if (entry === null) return void 0;
|
|
4826
|
-
commands2[k] = entry;
|
|
4827
|
-
}
|
|
4828
|
-
const map = { version: 1, commands: commands2 };
|
|
4829
|
-
if (typeof o.cliVersion === "string") map.cliVersion = o.cliVersion;
|
|
4830
|
-
if (typeof o._contract === "string") map._contract = o._contract;
|
|
4831
|
-
const aliases = parseStringMap(o.aliases);
|
|
4832
|
-
if (aliases !== void 0) map.aliases = aliases;
|
|
4833
|
-
const pending = parseStringMap(o.pending);
|
|
4834
|
-
if (pending !== void 0) map.pending = pending;
|
|
4835
|
-
const pendingCommands = parsePendingCommands(o.pendingCommands, pending);
|
|
4836
|
-
if (pendingCommands !== void 0) map.pendingCommands = pendingCommands;
|
|
4837
|
-
const resources = parseResources(o.resources);
|
|
4838
|
-
if (resources !== void 0) map.resources = resources;
|
|
4839
|
-
const deferred = new Set(Object.values(pendingCommands ?? {}).flat());
|
|
4840
|
-
for (const cmdPath of Object.keys(commands2)) {
|
|
4841
|
-
if (deferred.has(cmdPath)) return void 0;
|
|
4842
|
-
if (pending !== void 0 && Object.prototype.hasOwnProperty.call(pending, cmdPath.split(" ")[0]))
|
|
4843
|
-
return void 0;
|
|
4844
|
-
}
|
|
4845
|
-
return map;
|
|
4846
|
-
}
|
|
4847
|
-
function loadScopesMap(here = path3.dirname(fileURLToPath(import.meta.url))) {
|
|
4848
|
-
let text2;
|
|
4849
|
-
try {
|
|
4850
|
-
text2 = readFileSync(path3.join(here, "scopes-map.json"), "utf8");
|
|
4851
|
-
} catch {
|
|
4852
|
-
return void 0;
|
|
4853
|
-
}
|
|
4854
|
-
return parseScopesMap(text2);
|
|
4855
|
-
}
|
|
4856
|
-
function scopesMapKey(cmd) {
|
|
4857
|
-
const names = [];
|
|
4858
|
-
for (let c = cmd; c !== null && c.parent !== null; c = c.parent)
|
|
4859
|
-
names.unshift(c.name());
|
|
4860
|
-
return names.join(" ");
|
|
4861
|
-
}
|
|
4862
|
-
function commandPathOf(argv, root) {
|
|
4863
|
-
const names = [];
|
|
4864
|
-
let cmd = root;
|
|
4865
|
-
for (let i = 0; i < argv.length; i += 1) {
|
|
4866
|
-
const token = argv[i];
|
|
4867
|
-
if (token === "--") break;
|
|
4868
|
-
if (token.startsWith("-") && token !== "-") {
|
|
4869
|
-
if (!token.startsWith("--") && token.length > 2) continue;
|
|
4870
|
-
if (token.includes("=")) continue;
|
|
4871
|
-
if (takesValue(token, cmd)) i += 1;
|
|
4872
|
-
continue;
|
|
4873
|
-
}
|
|
4874
|
-
const child2 = childNamed(cmd, token);
|
|
4875
|
-
if (child2 === void 0) break;
|
|
4876
|
-
names.push(child2.name());
|
|
4877
|
-
cmd = child2;
|
|
4878
|
-
}
|
|
4879
|
-
return names.join(" ");
|
|
4880
|
-
}
|
|
4881
|
-
function childNamed(cmd, token) {
|
|
4882
|
-
return cmd.commands.find((c) => c.name() === token || c.aliases().includes(token));
|
|
4883
|
-
}
|
|
4884
|
-
function takesValue(flag, cmd) {
|
|
4885
|
-
for (let c = cmd; c !== null; c = c.parent) {
|
|
4886
|
-
for (const opt of c.options) {
|
|
4887
|
-
if (opt.short !== flag && opt.long !== flag) continue;
|
|
4888
|
-
return opt.required || opt.optional;
|
|
4889
|
-
}
|
|
4890
|
-
}
|
|
4891
|
-
return declaredWithValueBelow(flag, cmd);
|
|
4892
|
-
}
|
|
4893
|
-
function declaredWithValueBelow(flag, cmd) {
|
|
4894
|
-
for (const child2 of cmd.commands) {
|
|
4895
|
-
for (const opt of child2.options) {
|
|
4896
|
-
if ((opt.short === flag || opt.long === flag) && (opt.required || opt.optional)) return true;
|
|
4897
|
-
}
|
|
4898
|
-
if (declaredWithValueBelow(flag, child2)) return true;
|
|
4899
|
-
}
|
|
4900
|
-
return false;
|
|
4901
|
-
}
|
|
4902
|
-
|
|
4903
5251
|
// src/commands/whoami.ts
|
|
4904
5252
|
function registerWhoamiCommand(root) {
|
|
4905
5253
|
const cmd = root.command("whoami").description(
|
|
@@ -72064,156 +72412,6 @@ function copySessionCredentials(mountPath = SESSION_AUTH_MOUNT, target = credent
|
|
|
72064
72412
|
}
|
|
72065
72413
|
}
|
|
72066
72414
|
|
|
72067
|
-
// src/scopes/filter.ts
|
|
72068
|
-
var SCOPE_EXIT_CODE = 3;
|
|
72069
|
-
var ScopeFilterError = class extends Error {
|
|
72070
|
-
constructor(command, message) {
|
|
72071
|
-
super(message);
|
|
72072
|
-
this.command = command;
|
|
72073
|
-
}
|
|
72074
|
-
command;
|
|
72075
|
-
exitCode = SCOPE_EXIT_CODE;
|
|
72076
|
-
};
|
|
72077
|
-
var ScopeHiddenError = class extends ScopeFilterError {
|
|
72078
|
-
constructor(command, required2) {
|
|
72079
|
-
super(
|
|
72080
|
-
command,
|
|
72081
|
-
`'dr ${command}' is not available under this identity's credential scope (needs ${required2.join(", ")}); run 'dr whoami' to see the scopes this identity carries`
|
|
72082
|
-
);
|
|
72083
|
-
this.required = required2;
|
|
72084
|
-
this.name = "ScopeHiddenError";
|
|
72085
|
-
}
|
|
72086
|
-
required;
|
|
72087
|
-
};
|
|
72088
|
-
var ScopeUnavailableError = class extends ScopeFilterError {
|
|
72089
|
-
constructor(command, reason, until) {
|
|
72090
|
-
super(
|
|
72091
|
-
command,
|
|
72092
|
-
`'dr ${command}' is unavailable to any agent-scoped credential (one whose scopes claim carries at least one resource:action entry): ${reason} (this is not a missing scope \u2014 no scope grants it; ${until} lifts the restriction)`
|
|
72093
|
-
);
|
|
72094
|
-
this.reason = reason;
|
|
72095
|
-
this.until = until;
|
|
72096
|
-
this.name = "ScopeUnavailableError";
|
|
72097
|
-
}
|
|
72098
|
-
reason;
|
|
72099
|
-
until;
|
|
72100
|
-
};
|
|
72101
|
-
var ScopeGroupHiddenError = class extends ScopeFilterError {
|
|
72102
|
-
constructor(command, required2, tickets) {
|
|
72103
|
-
super(
|
|
72104
|
-
command,
|
|
72105
|
-
`'dr ${command}' has no subcommand available to this identity.` + (required2.length > 0 ? ` Its subcommands need ${required2.join(", ")} \u2014 run 'dr whoami' to see the scopes this identity carries.` : "") + (tickets.length > 0 ? ` Some are closed to any agent-scoped credential whatever its scopes (${tickets.join(", ")}).` : "")
|
|
72106
|
-
);
|
|
72107
|
-
this.required = required2;
|
|
72108
|
-
this.tickets = tickets;
|
|
72109
|
-
this.name = "ScopeGroupHiddenError";
|
|
72110
|
-
}
|
|
72111
|
-
required;
|
|
72112
|
-
tickets;
|
|
72113
|
-
};
|
|
72114
|
-
function hide(cmd) {
|
|
72115
|
-
cmd._hidden = true;
|
|
72116
|
-
}
|
|
72117
|
-
function refuseHelp(cmd, err) {
|
|
72118
|
-
cmd.outputHelp = () => {
|
|
72119
|
-
throw err;
|
|
72120
|
-
};
|
|
72121
|
-
}
|
|
72122
|
-
function leafAnswer(leaf, scopes, map) {
|
|
72123
|
-
const key = scopesMapKey(leaf);
|
|
72124
|
-
if (!Object.prototype.hasOwnProperty.call(map.commands, key)) return void 0;
|
|
72125
|
-
const entry = map.commands[key];
|
|
72126
|
-
if (entry === void 0) return void 0;
|
|
72127
|
-
if (isDeniedEntry(entry))
|
|
72128
|
-
return {
|
|
72129
|
-
err: new ScopeUnavailableError(key, entry.denied.reason, entry.denied.until),
|
|
72130
|
-
required: [],
|
|
72131
|
-
denial: entry.denied
|
|
72132
|
-
};
|
|
72133
|
-
if (isEntryAllowed(scopes, entry)) return void 0;
|
|
72134
|
-
const required2 = entry.requires.map((r) => `${r.resource}:${r.action}`);
|
|
72135
|
-
return { err: new ScopeHiddenError(key, required2), required: required2, denial: void 0 };
|
|
72136
|
-
}
|
|
72137
|
-
var intercepted = /* @__PURE__ */ new WeakMap();
|
|
72138
|
-
function intercept(cmd, err, armed) {
|
|
72139
|
-
intercepted.set(cmd, err);
|
|
72140
|
-
refuseHelp(cmd, err);
|
|
72141
|
-
cmd.action(() => {
|
|
72142
|
-
throw err;
|
|
72143
|
-
});
|
|
72144
|
-
const parent = cmd.parent;
|
|
72145
|
-
if (parent === null || armed.has(parent)) return;
|
|
72146
|
-
armed.add(parent);
|
|
72147
|
-
parent.hook("preSubcommand", (_parent, sub) => {
|
|
72148
|
-
const denial = intercepted.get(sub);
|
|
72149
|
-
if (denial !== void 0) throw denial;
|
|
72150
|
-
});
|
|
72151
|
-
}
|
|
72152
|
-
function interceptGroup(group, err) {
|
|
72153
|
-
group.allowExcessArguments(true);
|
|
72154
|
-
group.allowUnknownOption(true);
|
|
72155
|
-
group.helpCommand(true);
|
|
72156
|
-
refuseHelp(group, err);
|
|
72157
|
-
group.action(() => {
|
|
72158
|
-
throw err;
|
|
72159
|
-
});
|
|
72160
|
-
}
|
|
72161
|
-
function groupAnswer(group, below) {
|
|
72162
|
-
const key = scopesMapKey(group);
|
|
72163
|
-
const [mark] = below.denials;
|
|
72164
|
-
if (mark !== void 0 && below.denials.length === 1 && below.required.length === 0)
|
|
72165
|
-
return new ScopeUnavailableError(key, mark.reason, mark.until);
|
|
72166
|
-
if (below.required.length === 0 && below.denials.length === 0) return void 0;
|
|
72167
|
-
return new ScopeGroupHiddenError(
|
|
72168
|
-
key,
|
|
72169
|
-
below.required,
|
|
72170
|
-
below.denials.map((d) => d.until).filter((t, i, all) => all.indexOf(t) === i)
|
|
72171
|
-
);
|
|
72172
|
-
}
|
|
72173
|
-
function filterBelow(node, scopes, map, armed) {
|
|
72174
|
-
const here = { count: 0, answer: void 0, required: [], denials: [] };
|
|
72175
|
-
let allAnswered = node.commands.length > 0;
|
|
72176
|
-
for (const child2 of node.commands) {
|
|
72177
|
-
if (child2.commands.length > 0) {
|
|
72178
|
-
const below = filterBelow(child2, scopes, map, armed);
|
|
72179
|
-
here.count += below.count;
|
|
72180
|
-
if (below.answer === void 0) {
|
|
72181
|
-
allAnswered = false;
|
|
72182
|
-
continue;
|
|
72183
|
-
}
|
|
72184
|
-
interceptGroup(child2, below.answer);
|
|
72185
|
-
hide(child2);
|
|
72186
|
-
addRequired(here, below.required);
|
|
72187
|
-
addDenials(here, below.denials);
|
|
72188
|
-
continue;
|
|
72189
|
-
}
|
|
72190
|
-
const answer = leafAnswer(child2, scopes, map);
|
|
72191
|
-
if (answer === void 0) {
|
|
72192
|
-
allAnswered = false;
|
|
72193
|
-
continue;
|
|
72194
|
-
}
|
|
72195
|
-
intercept(child2, answer.err, armed);
|
|
72196
|
-
hide(child2);
|
|
72197
|
-
here.count += 1;
|
|
72198
|
-
addRequired(here, answer.required);
|
|
72199
|
-
if (answer.denial !== void 0) addDenials(here, [answer.denial]);
|
|
72200
|
-
}
|
|
72201
|
-
if (allAnswered) here.answer = groupAnswer(node, here);
|
|
72202
|
-
return here;
|
|
72203
|
-
}
|
|
72204
|
-
function addRequired(into, required2) {
|
|
72205
|
-
for (const r of required2) if (!into.required.includes(r)) into.required.push(r);
|
|
72206
|
-
}
|
|
72207
|
-
function addDenials(into, denials) {
|
|
72208
|
-
for (const d of denials)
|
|
72209
|
-
if (!into.denials.some((x) => x.reason === d.reason && x.until === d.until))
|
|
72210
|
-
into.denials.push(d);
|
|
72211
|
-
}
|
|
72212
|
-
function applyScopeFilter(root, scopes, map) {
|
|
72213
|
-
if (map === void 0 || !isAgentScoped(scopes)) return 0;
|
|
72214
|
-
return filterBelow(root, scopes, map, /* @__PURE__ */ new Set()).count;
|
|
72215
|
-
}
|
|
72216
|
-
|
|
72217
72415
|
// src/scopes/identityScopes.ts
|
|
72218
72416
|
function peekIdentityFlags(argv) {
|
|
72219
72417
|
const flags = {};
|
|
@@ -72605,6 +72803,9 @@ function buildRootCommand() {
|
|
|
72605
72803
|
root.hook("preAction", (_thisCommand, actionCommand) => {
|
|
72606
72804
|
recordCommandStart(commandPath(actionCommand), Date.now());
|
|
72607
72805
|
});
|
|
72806
|
+
root.hook("preAction", (_thisCommand, actionCommand) => {
|
|
72807
|
+
enforceTenantPin(actionCommand);
|
|
72808
|
+
});
|
|
72608
72809
|
registerLoginCommand(root);
|
|
72609
72810
|
registerLoginAsCommand(root);
|
|
72610
72811
|
registerWhoamiCommand(root);
|
|
@@ -72758,6 +72959,7 @@ function annotateScopeDenial(err, argv, root, deps = defaultScopeDenialDeps) {
|
|
|
72758
72959
|
function exitCodeFor(err) {
|
|
72759
72960
|
if (err instanceof ScopeFilterError) return err.exitCode;
|
|
72760
72961
|
if (err instanceof ScopeDeniedError) return SCOPE_EXIT_CODE;
|
|
72962
|
+
if (err instanceof TenantPinError) return err.exitCode;
|
|
72761
72963
|
return 1;
|
|
72762
72964
|
}
|
|
72763
72965
|
var UNKNOWN_OPTION_PATTERN = /(unknown option ')(-[\s\S]*)'/g;
|
package/dist/scopes-map.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 1,
|
|
3
|
-
"cliVersion": "1.6.
|
|
3
|
+
"cliVersion": "1.6.142",
|
|
4
4
|
"_contract": "Resolve a command path through `aliases` (canonical prefix + alias segment -> canonical path) before any lookup. A path listed in `pendingCommands`, or whose first segment is a key of `pending`, is UNDECIDED, not unrestricted: fail closed or skip it, and never read a path's absence from `commands` as 'no requirement'. An entry carrying `denied` is UNAVAILABLE to any AGENT-SCOPED credential — one whose `scopes` claim carries at least one `resource:action` entry; a claim that is absent, empty, capability-only or wholly unparseable is UNRESTRICTED, and this mark does not apply to it — whatever else that entry says: it is not a ceiling and it is not always-available, its `denied.reason` says why and `denied.until` names the ticket that lifts it, and the absence of `requires` on it must NEVER be read as 'no requirement'. Only an entry in `commands` with an empty `requires` means always-available.",
|
|
5
5
|
"commands": {
|
|
6
6
|
"login": {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@datarails-ai/dr-cli",
|
|
3
|
-
"version": "1.6.
|
|
3
|
+
"version": "1.6.142",
|
|
4
4
|
"description": "Datarails CLI",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"access": "public"
|
|
18
18
|
},
|
|
19
19
|
"dependencies": {
|
|
20
|
-
"@datarails-ai/dr-appspace-cli": "0.12.
|
|
20
|
+
"@datarails-ai/dr-appspace-cli": "0.12.15",
|
|
21
21
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
22
22
|
"@napi-rs/keyring": "^1.3.0",
|
|
23
23
|
"commander": "^12.1.0",
|