@datarails-ai/dr-cli 1.6.100 → 1.6.101
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 +17 -13
- package/dist/cli.js +39 -19
- package/dist/scopes-map.json +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -162,19 +162,23 @@ JWT refresh is automatic. Re-run `login` only when the refresh token itself
|
|
|
162
162
|
dies (days of idle, password change, admin revoke). Never paste the
|
|
163
163
|
session/JWT — treat them like a password.
|
|
164
164
|
|
|
165
|
-
**
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
165
|
+
**Device flow and credential scope.** `dr login` uses the monolith's
|
|
166
|
+
`/oauth/v2/` device flow. The consent page lets you pick the credential's scope
|
|
167
|
+
(below) before "Allow", and the login stores a cookie-less JWT pair, with no
|
|
168
|
+
session, refreshed at the external refresh endpoint. The page offers the choice
|
|
169
|
+
only where the org enforces it; elsewhere the token is unrestricted. This works
|
|
170
|
+
with a credential file too (`DR_CREDENTIALS_FILE`), which records the login as
|
|
171
|
+
`"interactiveLogin": true` so that `dr login` and `dr logout` still accept it.
|
|
172
|
+
|
|
173
|
+
`DR_OAUTH_FLOW=legacy` runs the old `/oauth/device/code/` flow instead, for a
|
|
174
|
+
server that does not serve `/oauth/v2/` yet (a 404 from the v2 code endpoint
|
|
175
|
+
says so). A legacy token carries **no** credential scope, whatever the person
|
|
176
|
+
would have picked, and the login prints a line saying so. `v2` or unset selects
|
|
177
|
+
the default; any other value is refused. Log in again to switch an identity
|
|
178
|
+
between flows.
|
|
179
|
+
|
|
180
|
+
```
|
|
181
|
+
DR_OAUTH_FLOW=legacy dr login --server US # PowerShell: $env:DR_OAUTH_FLOW="legacy"; dr login --server US
|
|
178
182
|
```
|
|
179
183
|
|
|
180
184
|
### Credential scope
|
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.101",
|
|
12
12
|
description: "Datarails CLI",
|
|
13
13
|
type: "module",
|
|
14
14
|
engines: {
|
|
@@ -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-05T14:18:41.284Z";
|
|
73
73
|
function formatBuildTime(iso) {
|
|
74
74
|
if (iso === "") return "";
|
|
75
75
|
const d = new Date(iso);
|
|
@@ -336,6 +336,13 @@ var credentialsFileSchema = z.object({
|
|
|
336
336
|
// would rewrite live token material after every successful command.
|
|
337
337
|
sessionId: z.string().min(1).optional(),
|
|
338
338
|
csrfToken: z.string().min(1).optional(),
|
|
339
|
+
// DR-53844: set by a v2 `dr login`, whose pair comes with no cookie, so the
|
|
340
|
+
// file still reads as a person's login — `dr login` may replace it and
|
|
341
|
+
// `dr logout` may remove it — rather than as one provisioned externally.
|
|
342
|
+
// Written by the CLI only. `z.boolean()`, not `z.literal(true)`: a
|
|
343
|
+
// top-level rejection fails the whole file (see `apiKeyExpiresAt`), and a
|
|
344
|
+
// hand-edited `false` must read as absent, not lock the operator out.
|
|
345
|
+
interactiveLogin: z.boolean().optional(),
|
|
339
346
|
// Validated, not just typed: `KeyringStore` turns this straight into a Date
|
|
340
347
|
// and later calls `toISOString()` on it, which throws on an Invalid Date.
|
|
341
348
|
// `datetime()` rejects a bad calendar date (2026-02-30, 2026-13-40) on its
|
|
@@ -2657,7 +2664,8 @@ var FileKeyringBackend = class _FileKeyringBackend {
|
|
|
2657
2664
|
jwtApiKeyFingerprint: creds.accessApiKeyFingerprint
|
|
2658
2665
|
},
|
|
2659
2666
|
// The env var's NAME only — the resolved key never enters any store.
|
|
2660
|
-
...creds.apiKeyEnvVar !== void 0 && { apiKeyEnvVar: creds.apiKeyEnvVar }
|
|
2667
|
+
...creds.apiKeyEnvVar !== void 0 && { apiKeyEnvVar: creds.apiKeyEnvVar },
|
|
2668
|
+
...creds.interactiveLogin === true && { interactiveLogin: true }
|
|
2661
2669
|
});
|
|
2662
2670
|
}
|
|
2663
2671
|
static slotOf(creds, account) {
|
|
@@ -2694,7 +2702,8 @@ var FileKeyringBackend = class _FileKeyringBackend {
|
|
|
2694
2702
|
// Set-if-present, never deleted on a blob that lost it (the
|
|
2695
2703
|
// `apiKeyExpiresAt` rule): it is the identity's only credential pointer,
|
|
2696
2704
|
// and a meta rewrite must not be able to brick the file.
|
|
2697
|
-
...apiKeyEnvVar !== "" && { apiKeyEnvVar }
|
|
2705
|
+
...apiKeyEnvVar !== "" && { apiKeyEnvVar },
|
|
2706
|
+
...parsed2.interactiveLogin === true && { interactiveLogin: true }
|
|
2698
2707
|
};
|
|
2699
2708
|
}
|
|
2700
2709
|
async get(_service, account) {
|
|
@@ -2788,6 +2797,8 @@ var FileKeyringBackend = class _FileKeyringBackend {
|
|
|
2788
2797
|
if (meta.accessApiKeyFingerprint === void 0) delete next.accessApiKeyFingerprint;
|
|
2789
2798
|
else next.accessApiKeyFingerprint = meta.accessApiKeyFingerprint;
|
|
2790
2799
|
if (meta.apiKeyEnvVar !== void 0) next.apiKeyEnvVar = meta.apiKeyEnvVar;
|
|
2800
|
+
if (meta.interactiveLogin === true) next.interactiveLogin = true;
|
|
2801
|
+
else delete next.interactiveLogin;
|
|
2791
2802
|
break;
|
|
2792
2803
|
}
|
|
2793
2804
|
}
|
|
@@ -4290,9 +4301,17 @@ async function refuseIfProvisionedExternally(action, opts) {
|
|
|
4290
4301
|
`cannot ${action}: the credentials in ${path11} are an API key. Run \`dr login --api-key\` to replace the key (and update its \`apiKeyExpiresAt\` in that file, if set \u2014 nothing derives that date, and a stale one warns "expired" on every command), or \`dr logout\` to remove it and log in with a browser instead \u2014 it cannot be read back afterwards, so keep a copy if it is still needed.`
|
|
4291
4302
|
);
|
|
4292
4303
|
}
|
|
4293
|
-
if (creds.
|
|
4304
|
+
if (creds.apiKeyEnvVar !== void 0 && opts?.allowApiKey !== true) {
|
|
4305
|
+
throw new Error(
|
|
4306
|
+
`cannot ${action}: the credentials in ${path11} name the environment variable ${creds.apiKeyEnvVar} as their API key, which is what later commands would authenticate as. Remove that file (\`dr logout\`) to log in with a browser instead.`
|
|
4307
|
+
);
|
|
4308
|
+
}
|
|
4309
|
+
if (isInteractiveFileLogin(creds)) return;
|
|
4294
4310
|
throw provisionedExternallyError(path11, action);
|
|
4295
4311
|
}
|
|
4312
|
+
function isInteractiveFileLogin(creds) {
|
|
4313
|
+
return creds.sessionId !== void 0 && creds.sessionId !== "" || creds.interactiveLogin === true;
|
|
4314
|
+
}
|
|
4296
4315
|
async function refuseLoginOverStoredApiKey(store, serverKey, email) {
|
|
4297
4316
|
let existing;
|
|
4298
4317
|
try {
|
|
@@ -4323,7 +4342,7 @@ async function refuseImpersonationWithCredentialFile() {
|
|
|
4323
4342
|
`cannot ${action}: the credentials in ${path11} are an API key, and an API-key token cannot mint further credentials. Impersonating needs a browser login, so remove that file (\`dr logout\`) and run \`dr login --server <key>\` \u2014 the key cannot be read back afterwards, so keep a copy if it is still needed.`
|
|
4324
4343
|
);
|
|
4325
4344
|
}
|
|
4326
|
-
if (creds
|
|
4345
|
+
if (!isInteractiveFileLogin(creds)) {
|
|
4327
4346
|
throw provisionedExternallyError(path11, action);
|
|
4328
4347
|
}
|
|
4329
4348
|
throw new Error(
|
|
@@ -5141,8 +5160,8 @@ var OAUTH_FLOW_ENV = "DR_OAUTH_FLOW";
|
|
|
5141
5160
|
function oauthFlowFromEnv(env = process.env) {
|
|
5142
5161
|
const raw = (env[OAUTH_FLOW_ENV] ?? "").trim();
|
|
5143
5162
|
const value = raw.toLowerCase();
|
|
5144
|
-
if (value === "" || value === "
|
|
5145
|
-
if (value === "
|
|
5163
|
+
if (value === "" || value === "v2") return "v2";
|
|
5164
|
+
if (value === "legacy") return "legacy";
|
|
5146
5165
|
throw new Error(`${OAUTH_FLOW_ENV} must be "legacy" or "v2", got "${raw}"`);
|
|
5147
5166
|
}
|
|
5148
5167
|
var PATHS = {
|
|
@@ -5152,7 +5171,7 @@ var PATHS = {
|
|
|
5152
5171
|
var GRANT_TYPE = "urn:ietf:params:oauth:grant-type:";
|
|
5153
5172
|
async function requestDeviceCode(host, options) {
|
|
5154
5173
|
const base = host.replace(/\/+$/, "");
|
|
5155
|
-
const flow = options?.flow ?? "
|
|
5174
|
+
const flow = options?.flow ?? "v2";
|
|
5156
5175
|
const paths = PATHS[flow];
|
|
5157
5176
|
const body = new URLSearchParams({ client_id: CLIENT_ID, scope: paths.scope });
|
|
5158
5177
|
const res = await fetch(`${base}${paths.code}`, {
|
|
@@ -5163,7 +5182,12 @@ async function requestDeviceCode(host, options) {
|
|
|
5163
5182
|
});
|
|
5164
5183
|
const text2 = await res.text();
|
|
5165
5184
|
if (!res.ok) {
|
|
5166
|
-
|
|
5185
|
+
let hint = "";
|
|
5186
|
+
if (flow === "v2" && res.status === 429) {
|
|
5187
|
+
hint = " (too many logins from this address: wait a minute and run `dr login` again)";
|
|
5188
|
+
} else if (flow === "v2" && res.status === 404) {
|
|
5189
|
+
hint = ` (this server does not serve the v2 device flow yet: set ${OAUTH_FLOW_ENV}=legacy to log in through the old one, whose token carries no credential scope)`;
|
|
5190
|
+
}
|
|
5167
5191
|
throw new Error(`device/code failed: ${res.status} ${text2.trim()}${hint}`);
|
|
5168
5192
|
}
|
|
5169
5193
|
let raw;
|
|
@@ -5530,11 +5554,6 @@ function registerLoginCommand(root) {
|
|
|
5530
5554
|
await refuseIfProvisionedExternally("log in");
|
|
5531
5555
|
const flow = oauthFlowFromEnv();
|
|
5532
5556
|
const fileStore = await resolveFileStore(opts.server);
|
|
5533
|
-
if (flow === "v2" && fileStore !== void 0) {
|
|
5534
|
-
throw new Error(
|
|
5535
|
-
`DR_OAUTH_FLOW=v2 cannot log into the credential file ${fileStore.path} yet; unset DR_CREDENTIALS_FILE to log in through the keychain, or unset DR_OAUTH_FLOW.`
|
|
5536
|
-
);
|
|
5537
|
-
}
|
|
5538
5557
|
let serverKey = fileStore?.key ?? (opts.server ?? "").trim();
|
|
5539
5558
|
if (fileStore === void 0) {
|
|
5540
5559
|
if (serverKey === "") {
|
|
@@ -5643,13 +5662,14 @@ async function runLogin(cfg) {
|
|
|
5643
5662
|
await refuseIfProvisionedExternally("log in");
|
|
5644
5663
|
const host = cfg.host ?? resolve(cfg.serverKey).host;
|
|
5645
5664
|
const timeoutMs = cfg.timeoutMs !== void 0 && cfg.timeoutMs > 0 ? cfg.timeoutMs : DEFAULT_LOGIN_TIMEOUT_MS;
|
|
5646
|
-
const flow = cfg.flow
|
|
5665
|
+
const flow = cfg.flow;
|
|
5647
5666
|
const controller = new AbortController();
|
|
5648
5667
|
const overallTimer = setTimeout(() => controller.abort(), timeoutMs + ABORT_SLACK_MS);
|
|
5649
5668
|
try {
|
|
5650
|
-
if (flow === "
|
|
5651
|
-
cfg.out.write(
|
|
5652
|
-
|
|
5669
|
+
if (flow === "legacy") {
|
|
5670
|
+
cfg.out.write(
|
|
5671
|
+
"Using the legacy device flow (DR_OAUTH_FLOW=legacy): its token carries no credential scope.\n"
|
|
5672
|
+
);
|
|
5653
5673
|
}
|
|
5654
5674
|
const dc = await requestDeviceCode(host, { signal: controller.signal, flow });
|
|
5655
5675
|
cfg.out.write(`Opening browser to authenticate with Datarails (${cfg.serverKey})...
|
package/dist/scopes-map.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 1,
|
|
3
|
-
"cliVersion": "1.6.
|
|
3
|
+
"cliVersion": "1.6.101",
|
|
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": {
|