@datarails-ai/dr-cli 1.6.97 → 1.6.98
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 +14 -0
- package/dist/cli.js +92 -30
- package/dist/scopes-map.json +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -230,6 +230,20 @@ under one identity and run as another.
|
|
|
230
230
|
{ "apiKey": "<key>", "server": "DEV", "email": "svc@datarails.com" }
|
|
231
231
|
```
|
|
232
232
|
|
|
233
|
+
The key may instead live in an environment variable, with the file carrying
|
|
234
|
+
only the variable's **name** — the shape for CI (the key stays in the runner's
|
|
235
|
+
secret env, never on disk) and for agent sandboxes (where the variable's
|
|
236
|
+
in-sandbox value is an opaque vault placeholder substituted at egress, passed
|
|
237
|
+
verbatim and never validated):
|
|
238
|
+
|
|
239
|
+
```json
|
|
240
|
+
{ "apiKeyEnvVar": "DR_API_KEY", "server": "DEV", "email": "svc@datarails.com" }
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
`apiKey` and `apiKeyEnvVar` are mutually exclusive — a file carrying both is
|
|
244
|
+
refused rather than guessed about. An unset variable fails the command with an
|
|
245
|
+
error naming it; the stored file is never modified by that failure.
|
|
246
|
+
|
|
233
247
|
`email` names the stored identity — what `--account` selects and what the
|
|
234
248
|
keychain entry is keyed by. It need not match the principal the key is bound to;
|
|
235
249
|
the token the exchange mints is what every request authenticates as, and
|
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.98",
|
|
12
12
|
description: "Datarails CLI",
|
|
13
13
|
type: "module",
|
|
14
14
|
engines: {
|
|
@@ -38,7 +38,7 @@ var package_default = {
|
|
|
38
38
|
postinstall: "node scripts/postinstall.mjs"
|
|
39
39
|
},
|
|
40
40
|
dependencies: {
|
|
41
|
-
"@datarails-ai/dr-appspace-cli": "0.10.
|
|
41
|
+
"@datarails-ai/dr-appspace-cli": "0.10.21",
|
|
42
42
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
43
43
|
"@napi-rs/keyring": "^1.3.0",
|
|
44
44
|
commander: "^12.1.0",
|
|
@@ -67,7 +67,7 @@ var package_default = {
|
|
|
67
67
|
};
|
|
68
68
|
|
|
69
69
|
// src/buildInfo.ts
|
|
70
|
-
var BUILD_TIME = "2026-10-
|
|
70
|
+
var BUILD_TIME = "2026-10-05T10:25:04.884Z";
|
|
71
71
|
function formatBuildTime(iso) {
|
|
72
72
|
if (iso === "") return "";
|
|
73
73
|
const d = new Date(iso);
|
|
@@ -151,14 +151,17 @@ function parseApiKeyExpiry(value) {
|
|
|
151
151
|
function hasApiKey(s) {
|
|
152
152
|
return s.apiKey !== void 0 && s.apiKey !== "";
|
|
153
153
|
}
|
|
154
|
+
function hasApiKeyEnvVar(s) {
|
|
155
|
+
return s.apiKeyEnvVar !== void 0 && s.apiKeyEnvVar !== "";
|
|
156
|
+
}
|
|
154
157
|
function isImpersonatedSession(s) {
|
|
155
158
|
return s.impersonatorEmail !== void 0 && s.impersonatorEmail !== "";
|
|
156
159
|
}
|
|
157
160
|
function isCookielessSession(s) {
|
|
158
|
-
return isImpersonatedSession(s) || s.cookieless === true || hasApiKey(s);
|
|
161
|
+
return isImpersonatedSession(s) || s.cookieless === true || hasApiKey(s) || hasApiKeyEnvVar(s);
|
|
159
162
|
}
|
|
160
163
|
function usesApiKey(s) {
|
|
161
|
-
return hasApiKey(s) && !isImpersonatedSession(s);
|
|
164
|
+
return (hasApiKey(s) || hasApiKeyEnvVar(s)) && !isImpersonatedSession(s);
|
|
162
165
|
}
|
|
163
166
|
var KeychainError = class extends Error {
|
|
164
167
|
constructor(message, options) {
|
|
@@ -294,6 +297,16 @@ var credentialsFileSchema = z.object({
|
|
|
294
297
|
// A user-bound API key, exchanged for `access` on demand. A file may hold
|
|
295
298
|
// only this; `access` then appears after the first exchange.
|
|
296
299
|
apiKey: z.string().min(1).optional(),
|
|
300
|
+
// DR-52657: the NAME of an environment variable holding the API key, for
|
|
301
|
+
// provisioners that must not put the key on disk — an agent sandbox, where
|
|
302
|
+
// the variable's in-sandbox value is an opaque vault placeholder
|
|
303
|
+
// substituted at egress, or a CI runner keeping the key in its secret env.
|
|
304
|
+
// The value is resolved at exchange time and passed verbatim; dr-cli never
|
|
305
|
+
// inspects, validates, or logs it. Mutually exclusive with `apiKey` (a
|
|
306
|
+
// refinement below): a file carrying both is ambiguous about which
|
|
307
|
+
// credential authenticates it, and silently preferring either one turns a
|
|
308
|
+
// provisioning mistake into commands running as the wrong key.
|
|
309
|
+
apiKeyEnvVar: z.string().min(1).optional(),
|
|
297
310
|
// Carried verbatim, judged by `parseApiKeyExpiry` where it is read. Not
|
|
298
311
|
// validated here, and not `.catch(undefined)` either: a rotation rewrites
|
|
299
312
|
// the whole file from the parsed object, so a value this schema dropped
|
|
@@ -332,13 +345,19 @@ var credentialsFileSchema = z.object({
|
|
|
332
345
|
}).passthrough().refine((c) => c.sessionId === void 0 === (c.csrfToken === void 0), {
|
|
333
346
|
message: "sessionId and csrfToken must be provided together",
|
|
334
347
|
path: ["sessionId"]
|
|
335
|
-
}).refine((c) => c.
|
|
336
|
-
message: "
|
|
348
|
+
}).refine((c) => !(c.apiKey !== void 0 && c.apiKeyEnvVar !== void 0), {
|
|
349
|
+
message: "apiKey and apiKeyEnvVar are mutually exclusive",
|
|
350
|
+
path: ["apiKeyEnvVar"]
|
|
351
|
+
}).refine((c) => c.access !== void 0 || c.apiKey !== void 0 || c.apiKeyEnvVar !== void 0, {
|
|
352
|
+
message: "one of access, apiKey or apiKeyEnvVar is required",
|
|
337
353
|
path: ["access"]
|
|
338
|
-
}).refine(
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
354
|
+
}).refine(
|
|
355
|
+
(c) => c.apiKey !== void 0 || c.apiKeyEnvVar !== void 0 || c.refresh !== void 0 || c.sessionId !== void 0,
|
|
356
|
+
{
|
|
357
|
+
message: "refresh is required unless an apiKey, apiKeyEnvVar or a session cookie is present",
|
|
358
|
+
path: ["refresh"]
|
|
359
|
+
}
|
|
360
|
+
);
|
|
342
361
|
function credentialsFilePath() {
|
|
343
362
|
const override = process.env[CREDENTIALS_FILE_ENV];
|
|
344
363
|
return override !== void 0 && override !== "" ? override : DEFAULT_CREDENTIALS_FILE;
|
|
@@ -801,7 +820,13 @@ async function exchangeApiKey(host, apiKey, options) {
|
|
|
801
820
|
if (key === "") {
|
|
802
821
|
throw new Error("api-key exchange: the stored API key is empty");
|
|
803
822
|
}
|
|
804
|
-
if (
|
|
823
|
+
if (options?.opaque === true) {
|
|
824
|
+
if (/[\r\n\0]/.test(key) || !/^[\x00-\xff]+$/.test(key)) {
|
|
825
|
+
throw new Error(
|
|
826
|
+
"api-key exchange: the environment variable named by apiKeyEnvVar holds a value that cannot travel in an HTTP header (a line break, NUL, or a character above U+00FF)"
|
|
827
|
+
);
|
|
828
|
+
}
|
|
829
|
+
} else if (!/^[\x21-\x7e]+$/.test(key)) {
|
|
805
830
|
throw new Error(
|
|
806
831
|
"api-key exchange: dr only accepts an API key of printable ASCII with no spaces; the stored one contains whitespace, control characters, or non-ASCII characters"
|
|
807
832
|
);
|
|
@@ -965,10 +990,31 @@ function mintedByApiKey(token) {
|
|
|
965
990
|
function apiKeyFingerprint(apiKey) {
|
|
966
991
|
return createHash("sha256").update(apiKey.trim()).digest("hex").slice(0, 32);
|
|
967
992
|
}
|
|
993
|
+
function resolveApiKeyMaterial(session) {
|
|
994
|
+
if (session.apiKey !== void 0 && session.apiKey !== "") {
|
|
995
|
+
return { key: session.apiKey, fromEnv: false };
|
|
996
|
+
}
|
|
997
|
+
const name = session.apiKeyEnvVar;
|
|
998
|
+
if (name === void 0 || name === "") {
|
|
999
|
+
throw new Error("api-key exchange: the session carries no API key");
|
|
1000
|
+
}
|
|
1001
|
+
const value = process.env[name];
|
|
1002
|
+
if (value === void 0 || value === "") {
|
|
1003
|
+
throw new Error(
|
|
1004
|
+
`api-key exchange: the credential file names environment variable ${name} as the API-key carrier, but it is unset or empty in this environment`
|
|
1005
|
+
);
|
|
1006
|
+
}
|
|
1007
|
+
return { key: value, fromEnv: true };
|
|
1008
|
+
}
|
|
968
1009
|
function mintedByStoredApiKey(session) {
|
|
969
1010
|
if (!mintedByApiKey(session.jwtAccess)) return false;
|
|
970
|
-
|
|
971
|
-
|
|
1011
|
+
let material;
|
|
1012
|
+
try {
|
|
1013
|
+
material = resolveApiKeyMaterial(session);
|
|
1014
|
+
} catch {
|
|
1015
|
+
return false;
|
|
1016
|
+
}
|
|
1017
|
+
return session.jwtApiKeyFingerprint === apiKeyFingerprint(material.key);
|
|
972
1018
|
}
|
|
973
1019
|
function apiKeyStaleBuffer(session, identity2, fallbackMs) {
|
|
974
1020
|
if (session.jwtAccess === void 0 || session.jwtAccess === "") return fallbackMs;
|
|
@@ -995,12 +1041,13 @@ function ttlOf(token) {
|
|
|
995
1041
|
function exchangeForSession(store, serverKey, email, session, host, opts) {
|
|
996
1042
|
const identity2 = `${serverKey}:${email}`;
|
|
997
1043
|
if (opts.signal?.aborted === true) return Promise.reject(opts.signal.reason);
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1044
|
+
let material;
|
|
1045
|
+
try {
|
|
1046
|
+
material = resolveApiKeyMaterial(session);
|
|
1047
|
+
} catch (err) {
|
|
1048
|
+
throw new SessionExpiredError(`${identity2} (${msgOf3(err)})`);
|
|
1049
|
+
}
|
|
1050
|
+
const key = JSON.stringify([identity2, host, opts.force, apiKeyFingerprint(material.key)]);
|
|
1004
1051
|
const pending = inFlightExchanges.get(key);
|
|
1005
1052
|
if (pending !== void 0) return joinExchange(pending, opts.signal);
|
|
1006
1053
|
const controller = new AbortController();
|
|
@@ -1015,7 +1062,7 @@ function exchangeForSession(store, serverKey, email, session, host, opts) {
|
|
|
1015
1062
|
for (; ; ) {
|
|
1016
1063
|
let tok;
|
|
1017
1064
|
try {
|
|
1018
|
-
tok = await exchangeApiKey(host,
|
|
1065
|
+
tok = await exchangeApiKey(host, material.key, { signal, opaque: material.fromEnv });
|
|
1019
1066
|
} catch (err) {
|
|
1020
1067
|
const cachedIsUsable = !opts.force && mintedByStoredApiKey(session) && !jwtExpiringSoon(session, 0);
|
|
1021
1068
|
if (!(err instanceof ApiKeyExchangeError)) {
|
|
@@ -1054,7 +1101,7 @@ function exchangeForSession(store, serverKey, email, session, host, opts) {
|
|
|
1054
1101
|
// the token did not come from. Read back by `mintedByStoredApiKey` on
|
|
1055
1102
|
// every later command to tell this token apart from one the previous
|
|
1056
1103
|
// key minted.
|
|
1057
|
-
jwtApiKeyFingerprint: apiKeyFingerprint(
|
|
1104
|
+
jwtApiKeyFingerprint: apiKeyFingerprint(material.key),
|
|
1058
1105
|
// Dropped, not carried: the exchange mints no refresh token, and the
|
|
1059
1106
|
// refresh rung is closed to a keyed identity by contract. Clearing it
|
|
1060
1107
|
// is what lets "keyed, and still holding a refresh token" mean "these
|
|
@@ -1065,7 +1112,7 @@ function exchangeForSession(store, serverKey, email, session, host, opts) {
|
|
|
1065
1112
|
};
|
|
1066
1113
|
try {
|
|
1067
1114
|
const current = await store.getSession(serverKey, email);
|
|
1068
|
-
if (current === void 0 || current.apiKey !== session.apiKey) {
|
|
1115
|
+
if (current === void 0 || current.apiKey !== session.apiKey || current.apiKeyEnvVar !== session.apiKeyEnvVar) {
|
|
1069
1116
|
return next;
|
|
1070
1117
|
}
|
|
1071
1118
|
await store.saveSession(serverKey, next);
|
|
@@ -1383,7 +1430,8 @@ var KeyringStore = class _KeyringStore {
|
|
|
1383
1430
|
const impersonated = impersonatorEmail !== "";
|
|
1384
1431
|
const cookielessFlag = parsed2.cookieless === true;
|
|
1385
1432
|
const apiKey = await this.backend.get(SERVICE, `${sessionAccount}:apikey`);
|
|
1386
|
-
const
|
|
1433
|
+
const apiKeyEnvVar = typeof parsed2.apiKeyEnvVar === "string" ? parsed2.apiKeyEnvVar : "";
|
|
1434
|
+
const keyPresent = apiKey !== void 0 && apiKey !== "" || apiKeyEnvVar !== "";
|
|
1387
1435
|
const cookieless = impersonated || cookielessFlag || keyPresent;
|
|
1388
1436
|
const halfCookie = sessionId === "" !== (csrfToken === "");
|
|
1389
1437
|
if (savedAtStr === "" || halfCookie || !cookieless && sessionId === "") {
|
|
@@ -1429,9 +1477,12 @@ var KeyringStore = class _KeyringStore {
|
|
|
1429
1477
|
if (jwtRefresh !== void 0 && jwtRefresh !== "") {
|
|
1430
1478
|
session.jwtRefresh = jwtRefresh;
|
|
1431
1479
|
}
|
|
1432
|
-
if (
|
|
1480
|
+
if (apiKey !== void 0 && apiKey !== "") {
|
|
1433
1481
|
session.apiKey = apiKey;
|
|
1434
1482
|
}
|
|
1483
|
+
if (apiKeyEnvVar !== "") {
|
|
1484
|
+
session.apiKeyEnvVar = apiKeyEnvVar;
|
|
1485
|
+
}
|
|
1435
1486
|
return session;
|
|
1436
1487
|
}
|
|
1437
1488
|
/**
|
|
@@ -1463,9 +1514,9 @@ var KeyringStore = class _KeyringStore {
|
|
|
1463
1514
|
const impersonated = isImpersonatedSession(session);
|
|
1464
1515
|
if (isCookielessSession(session)) {
|
|
1465
1516
|
const hasJwt = session.jwtAccess !== void 0 && session.jwtAccess !== "";
|
|
1466
|
-
if (!hasJwt && !hasApiKey(session)) {
|
|
1517
|
+
if (!hasJwt && !hasApiKey(session) && !hasApiKeyEnvVar(session)) {
|
|
1467
1518
|
throw new Error(
|
|
1468
|
-
"saveSession: cookie-less session must have a jwtAccess token or an
|
|
1519
|
+
"saveSession: cookie-less session must have a jwtAccess token, an apiKey, or an apiKeyEnvVar"
|
|
1469
1520
|
);
|
|
1470
1521
|
}
|
|
1471
1522
|
if (session.sessionId === "" !== (session.csrfToken === "")) {
|
|
@@ -1496,6 +1547,9 @@ var KeyringStore = class _KeyringStore {
|
|
|
1496
1547
|
if (session.jwtApiKeyFingerprint !== void 0 && session.jwtApiKeyFingerprint !== "") {
|
|
1497
1548
|
metaBlob.jwtApiKeyFingerprint = session.jwtApiKeyFingerprint;
|
|
1498
1549
|
}
|
|
1550
|
+
if (hasApiKeyEnvVar(session)) {
|
|
1551
|
+
metaBlob.apiKeyEnvVar = session.apiKeyEnvVar;
|
|
1552
|
+
}
|
|
1499
1553
|
if (impersonated) {
|
|
1500
1554
|
metaBlob.impersonatorEmail = session.impersonatorEmail;
|
|
1501
1555
|
if (session.impersonatedUserId !== void 0) {
|
|
@@ -2599,7 +2653,9 @@ var FileKeyringBackend = class _FileKeyringBackend {
|
|
|
2599
2653
|
// Named as the Session field, since the blob is what `getSession` reads.
|
|
2600
2654
|
...creds.accessApiKeyFingerprint !== void 0 && {
|
|
2601
2655
|
jwtApiKeyFingerprint: creds.accessApiKeyFingerprint
|
|
2602
|
-
}
|
|
2656
|
+
},
|
|
2657
|
+
// The env var's NAME only — the resolved key never enters any store.
|
|
2658
|
+
...creds.apiKeyEnvVar !== void 0 && { apiKeyEnvVar: creds.apiKeyEnvVar }
|
|
2603
2659
|
});
|
|
2604
2660
|
}
|
|
2605
2661
|
static slotOf(creds, account) {
|
|
@@ -2627,11 +2683,16 @@ var FileKeyringBackend = class _FileKeyringBackend {
|
|
|
2627
2683
|
const csrfToken = typeof parsed2.csrfToken === "string" ? parsed2.csrfToken : "";
|
|
2628
2684
|
const apiKeyExpiresAt = typeof parsed2.apiKeyExpiresAt === "string" ? parsed2.apiKeyExpiresAt : "";
|
|
2629
2685
|
const accessApiKeyFingerprint = typeof parsed2.jwtApiKeyFingerprint === "string" ? parsed2.jwtApiKeyFingerprint : "";
|
|
2686
|
+
const apiKeyEnvVar = typeof parsed2.apiKeyEnvVar === "string" ? parsed2.apiKeyEnvVar : "";
|
|
2630
2687
|
return {
|
|
2631
2688
|
...sessionId !== "" && { sessionId },
|
|
2632
2689
|
...csrfToken !== "" && { csrfToken },
|
|
2633
2690
|
...apiKeyExpiresAt !== "" && { apiKeyExpiresAt },
|
|
2634
|
-
...accessApiKeyFingerprint !== "" && { accessApiKeyFingerprint }
|
|
2691
|
+
...accessApiKeyFingerprint !== "" && { accessApiKeyFingerprint },
|
|
2692
|
+
// Set-if-present, never deleted on a blob that lost it (the
|
|
2693
|
+
// `apiKeyExpiresAt` rule): it is the identity's only credential pointer,
|
|
2694
|
+
// and a meta rewrite must not be able to brick the file.
|
|
2695
|
+
...apiKeyEnvVar !== "" && { apiKeyEnvVar }
|
|
2635
2696
|
};
|
|
2636
2697
|
}
|
|
2637
2698
|
async get(_service, account) {
|
|
@@ -2724,6 +2785,7 @@ var FileKeyringBackend = class _FileKeyringBackend {
|
|
|
2724
2785
|
if (meta.apiKeyExpiresAt !== void 0) next.apiKeyExpiresAt = meta.apiKeyExpiresAt;
|
|
2725
2786
|
if (meta.accessApiKeyFingerprint === void 0) delete next.accessApiKeyFingerprint;
|
|
2726
2787
|
else next.accessApiKeyFingerprint = meta.accessApiKeyFingerprint;
|
|
2788
|
+
if (meta.apiKeyEnvVar !== void 0) next.apiKeyEnvVar = meta.apiKeyEnvVar;
|
|
2727
2789
|
break;
|
|
2728
2790
|
}
|
|
2729
2791
|
}
|
|
@@ -44947,7 +45009,7 @@ import * as nodePath2 from "path";
|
|
|
44947
45009
|
var API_PREFIX = "/api/v1";
|
|
44948
45010
|
var OPERATOR_EMAIL_HEADER = "X-Operator-Email";
|
|
44949
45011
|
function withOperatorAttribution(client, options) {
|
|
44950
|
-
const operatorEmail = isImpersonatedSession(client.session) ? client.session.impersonatorEmail : client.session.cookieless === true && !hasApiKey(client.session) && client.session.interactiveLogin !== true ? void 0 : client.email;
|
|
45012
|
+
const operatorEmail = isImpersonatedSession(client.session) ? client.session.impersonatorEmail : client.session.cookieless === true && !hasApiKey(client.session) && !hasApiKeyEnvVar(client.session) && client.session.interactiveLogin !== true ? void 0 : client.email;
|
|
44951
45013
|
if (operatorEmail === void 0) return options ?? {};
|
|
44952
45014
|
return {
|
|
44953
45015
|
...options,
|
package/dist/scopes-map.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 1,
|
|
3
|
-
"cliVersion": "1.6.
|
|
3
|
+
"cliVersion": "1.6.98",
|
|
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.98",
|
|
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.10.
|
|
20
|
+
"@datarails-ai/dr-appspace-cli": "0.10.21",
|
|
21
21
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
22
22
|
"@napi-rs/keyring": "^1.3.0",
|
|
23
23
|
"commander": "^12.1.0",
|