@datarails-ai/dr-cli 1.6.104 → 1.6.105

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/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.104",
11
+ version: "1.6.105",
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.11.1",
43
+ "@datarails-ai/dr-appspace-cli": "0.11.2",
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-07T12:03:26.799Z";
72
+ var BUILD_TIME = "2026-10-07T13:28:02.898Z";
73
73
  function formatBuildTime(iso) {
74
74
  if (iso === "") return "";
75
75
  const d = new Date(iso);
@@ -1165,13 +1165,8 @@ function exchangeForSession(store, serverKey, email, session, host, opts) {
1165
1165
  inFlightExchanges.set(key, entry);
1166
1166
  return joinExchange(entry, opts.signal);
1167
1167
  }
1168
- async function ensureJwt(store, serverKey, email, opts) {
1169
- const stored = await store.getSession(serverKey, email);
1170
- if (stored === void 0) {
1171
- throw new NotLoggedInError(`not logged in: ${serverKey}:${email}`);
1172
- }
1173
- const keyed = usesApiKey(stored);
1174
- const unsaved = keyed ? unsavedExchanges.get(`${serverKey}:${email}`) : void 0;
1168
+ async function exchangeKeyedSession(store, serverKey, email, stored, opts) {
1169
+ const unsaved = unsavedExchanges.get(`${serverKey}:${email}`);
1175
1170
  const session = unsaved !== void 0 && unsaved.apiKey === stored.apiKey ? {
1176
1171
  ...stored,
1177
1172
  jwtAccess: unsaved.jwtAccess,
@@ -1184,16 +1179,40 @@ async function ensureJwt(store, serverKey, email, opts) {
1184
1179
  // The store still holds whatever the failed save meant to remove.
1185
1180
  jwtRefresh: void 0
1186
1181
  } : stored;
1187
- if (!opts.force && (!keyed || mintedByStoredApiKey(session)) && session.jwtAccess !== void 0 && session.jwtAccess !== "" && !jwtExpiringSoon(
1188
- session,
1189
- keyed ? apiKeyStaleBuffer(session, `${serverKey}:${email}`, opts.buffer) : opts.buffer
1190
- )) {
1182
+ if (!opts.force && mintedByStoredApiKey(session) && session.jwtAccess !== void 0 && session.jwtAccess !== "" && !jwtExpiringSoon(session, apiKeyStaleBuffer(session, `${serverKey}:${email}`, opts.buffer))) {
1191
1183
  return session;
1192
1184
  }
1193
1185
  const host = opts.host ?? resolve(serverKey).host;
1194
- if (keyed) {
1195
- return exchangeForSession(store, serverKey, email, session, host, opts);
1186
+ return exchangeForSession(store, serverKey, email, session, host, opts);
1187
+ }
1188
+ async function ensureApiKeyJwt(store, serverKey, email, options) {
1189
+ const stored = options?.session ?? await store.getSession(serverKey, email);
1190
+ if (stored === void 0) {
1191
+ throw new NotLoggedInError(`not logged in: ${serverKey}:${email}`);
1192
+ }
1193
+ if (!usesApiKey(stored)) {
1194
+ throw new Error(`${serverKey}:${email} is not an api-key identity`);
1196
1195
  }
1196
+ return exchangeKeyedSession(store, serverKey, email, stored, {
1197
+ buffer: options?.buffer ?? 0,
1198
+ force: false,
1199
+ signal: options?.signal,
1200
+ host: options?.host
1201
+ });
1202
+ }
1203
+ async function ensureJwt(store, serverKey, email, opts) {
1204
+ const stored = await store.getSession(serverKey, email);
1205
+ if (stored === void 0) {
1206
+ throw new NotLoggedInError(`not logged in: ${serverKey}:${email}`);
1207
+ }
1208
+ if (usesApiKey(stored)) {
1209
+ return exchangeKeyedSession(store, serverKey, email, stored, opts);
1210
+ }
1211
+ const session = stored;
1212
+ if (!opts.force && session.jwtAccess !== void 0 && session.jwtAccess !== "" && !jwtExpiringSoon(session, opts.buffer)) {
1213
+ return session;
1214
+ }
1215
+ const host = opts.host ?? resolve(serverKey).host;
1197
1216
  if (session.impersonatorEmail !== void 0 && session.impersonatorEmail !== "") {
1198
1217
  return recoverImpersonatedJwt(store, serverKey, email, session, host, opts);
1199
1218
  }
@@ -2101,7 +2120,8 @@ var AuthorizedClient = class _AuthorizedClient {
2101
2120
  * How this identity gets a usable JWT again. `dr login` is refused over an
2102
2121
  * api-key credential, and it re-authenticates the operator rather than the
2103
2122
  * impersonated user, so naming it for either sends the reader at a command
2104
- * that cannot help — the same loop `runWhoami`'s three-way split breaks.
2123
+ * that cannot help — the same loop `runWhoami` avoids by never naming
2124
+ * `dr login` for either.
2105
2125
  *
2106
2126
  * `cause` splits the keyed answer, which is the only one that differs: a
2107
2127
  * token that never arrived is not a token whose `organization_id` is wrong,
@@ -4807,7 +4827,7 @@ function declaredWithValueBelow(flag, cmd) {
4807
4827
  // src/commands/whoami.ts
4808
4828
  function registerWhoamiCommand(root) {
4809
4829
  const cmd = root.command("whoami").description(
4810
- "whoami decodes the locally-stored JWT for a server and prints the identity claims (user_id, username, email, expiration). Purely offline \u2014 no network."
4830
+ "whoami decodes the locally-stored JWT for a server and prints the identity claims (user_id, username, email, expiration). Offline, except that an api-key identity holding no token its current key minted is exchanged once to obtain one."
4811
4831
  ).summary("Show the identity encoded in the stored JWT for a server").option("--json", "Output the full claims map as JSON", false).option("--verbose", "Print every claim in a table, sorted alphabetically", false).action(
4812
4832
  async (opts) => {
4813
4833
  const fileStore = await resolveFileStore(opts.server);
@@ -4845,19 +4865,34 @@ async function runWhoami(cfg) {
4845
4865
  }
4846
4866
  emitApiKeyExpiryWarning(session, `${cfg.serverKey}:${email}`);
4847
4867
  const foreignJwt = usesApiKey(session) && session.jwtAccess !== void 0 && session.jwtAccess !== "" && !mintedByStoredApiKey(session);
4868
+ let active = session;
4848
4869
  if (session.jwtAccess === void 0 || session.jwtAccess === "" || foreignJwt) {
4849
- const missing = foreignJwt ? `the JWT stored for ${cfg.serverKey}:${email} was not minted by its API key` : `no JWT stored for ${cfg.serverKey}:${email}`;
4850
- throw new Error(
4851
- usesApiKey(session) ? `${missing}; run any authenticated command to exchange the API key` : isImpersonatedSession(session) ? `${missing}; re-run dr login-as` : `${missing}; run dr login --server ${cfg.serverKey} to refresh`
4852
- );
4870
+ if (usesApiKey(session)) {
4871
+ try {
4872
+ active = await ensureApiKeyJwt(cfg.store, cfg.serverKey, email, { session });
4873
+ } catch (err) {
4874
+ const reason = err instanceof Error ? err.message : String(err);
4875
+ throw new Error(`cannot show identity for ${cfg.serverKey}:${email}: ${reason}`, {
4876
+ cause: err
4877
+ });
4878
+ }
4879
+ } else {
4880
+ const missing = `no JWT stored for ${cfg.serverKey}:${email}`;
4881
+ throw new Error(
4882
+ isImpersonatedSession(session) ? `${missing}; re-run dr login-as` : `${missing}; run dr login --server ${cfg.serverKey} to refresh`
4883
+ );
4884
+ }
4885
+ }
4886
+ if (active.jwtAccess === void 0 || active.jwtAccess === "") {
4887
+ throw new Error(`no JWT stored for ${cfg.serverKey}:${email}`);
4853
4888
  }
4854
4889
  try {
4855
- setTelemetryTarget(cfg.serverKey, deriveIdentity(session));
4890
+ setTelemetryTarget(cfg.serverKey, deriveIdentity(active));
4856
4891
  } catch {
4857
4892
  }
4858
4893
  let claims;
4859
4894
  try {
4860
- claims = decodeJwtClaims(session.jwtAccess);
4895
+ claims = decodeJwtClaims(active.jwtAccess);
4861
4896
  } catch (err) {
4862
4897
  const msg = err instanceof Error ? err.message : String(err);
4863
4898
  throw new Error(`decode JWT for ${cfg.serverKey}: ${msg}`);
@@ -4867,7 +4902,7 @@ async function runWhoami(cfg) {
4867
4902
  host = resolve(cfg.serverKey).host;
4868
4903
  } catch {
4869
4904
  }
4870
- const impersonatorEmail = session.impersonatorEmail !== void 0 && session.impersonatorEmail !== "" ? session.impersonatorEmail : void 0;
4905
+ const impersonatorEmail = active.impersonatorEmail !== void 0 && active.impersonatorEmail !== "" ? active.impersonatorEmail : void 0;
4871
4906
  if (cfg.asJson) {
4872
4907
  writeWhoamiJSON(cfg.out, cfg.serverKey, host, claims, impersonatorEmail);
4873
4908
  return;
@@ -5657,6 +5692,10 @@ async function runApiKeyLogin(cfg) {
5657
5692
  apiKey: cfg.apiKey,
5658
5693
  jwtAccess: tok.access,
5659
5694
  jwtExpiresAt: tok.expiresAt,
5695
+ // Records which key minted this token, as every exchange does. Without it
5696
+ // `mintedByStoredApiKey` reads the token as unknown provenance, so `whoami`
5697
+ // and the next command would exchange again instead of using it.
5698
+ jwtApiKeyFingerprint: apiKeyFingerprint(cfg.apiKey),
5660
5699
  savedAt: /* @__PURE__ */ new Date(),
5661
5700
  cookieless: true
5662
5701
  };
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "version": 1,
3
- "cliVersion": "1.6.104",
3
+ "cliVersion": "1.6.105",
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": {
@@ -66,16 +66,21 @@ per server can be logged in at once.
66
66
  required unless a credential file already names the server; don't run the
67
67
  piped form under shell tracing or command logging). Never pass the key as an
68
68
  argument or paste it into chat. `dr` exchanges it for a JWT itself, so no
69
- re-login is ever needed while the key is live. Keys come from an operator
70
- today (no self-service command yet). `login-as` cannot run from an API-key
71
- identity.
69
+ re-login is ever needed while the key is live — and `login --api-key` does that
70
+ exchange **at login** and stores the JWT, so `whoami` and the next command work
71
+ right away. Keys come from an operator today (no self-service command yet).
72
+ `login-as` cannot run from an API-key identity.
72
73
  - `dr login-as <email> --server US` — impersonate a user, **JWT only, no
73
74
  browser**. Needs an operator `login` first; stores a switchable identity you
74
75
  then target with `--account <email>`. `--as <operator-email>` picks the
75
76
  operator if several are stored; `--user-id <n>` skips the email lookup.
76
77
  Refreshes/auto-remints silently — no re-prompt until the operator expires.
77
78
  - `dr whoami --server US` — confirm who/where you are. Offline (decodes the local
78
- JWT, no network). **Always sanity-check after login or a server switch.**
79
+ JWT, no network) — **except** an API-key credential holding no token its current
80
+ key minted (a key written straight into a session file — e.g. a managed-agent
81
+ session — or a key rotated since its token was stored), which `whoami` exchanges
82
+ once to obtain one and saves, so it works even as the very first command.
83
+ **Always sanity-check after login or a server switch.**
79
84
  Shows `Impersonated by:` for a `login-as` identity, and a `Scope:` row for
80
85
  every identity (see below).
81
86
  - `dr logout --server US` / `dr logout --all`.
@@ -92,7 +92,8 @@ Run a single Bash call:
92
92
  dr whoami --server <X> --account <email> 2>&1 || true
93
93
  ```
94
94
 
95
- `whoami` is purely offline (decodes the locally-stored JWT, no network).
95
+ `whoami` is offline (decodes the locally-stored JWT, no network), except that an
96
+ API-key identity holding no token its current key minted is exchanged once.
96
97
  Classify the output:
97
98
 
98
99
  | Result | Meaning |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@datarails-ai/dr-cli",
3
- "version": "1.6.104",
3
+ "version": "1.6.105",
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.11.1",
20
+ "@datarails-ai/dr-appspace-cli": "0.11.2",
21
21
  "@modelcontextprotocol/sdk": "^1.29.0",
22
22
  "@napi-rs/keyring": "^1.3.0",
23
23
  "commander": "^12.1.0",