@datarails-ai/dr-cli 1.6.172 → 1.6.174
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 +9 -4
- package/dist/cli.js +202 -10
- package/dist/scopes-map.json +1 -1
- package/dist/skills/dr-cli/SKILL.md +9 -4
- package/dist/skills/dr-switch-user/SKILL.md +11 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -302,10 +302,15 @@ interactively instead. An API-key file is different: see below.
|
|
|
302
302
|
> `dr users can-login-as`, mints an impersonation JWT from the monolith
|
|
303
303
|
> (`POST /jwt/api/loginas/`), and stores it as a switchable identity keyed by
|
|
304
304
|
> `(server, email)` — so later commands target it with
|
|
305
|
-
> `--account <email>`. The impersonated token refreshes silently, and
|
|
306
|
-
>
|
|
307
|
-
>
|
|
308
|
-
>
|
|
305
|
+
> `--account <email>`. The impersonated token refreshes silently, and unless
|
|
306
|
+
> the customer approves each login-as, a dead refresh auto-remints from the
|
|
307
|
+
> operator, so you're never re-prompted until the operator's own login
|
|
308
|
+
> expires. Where the customer does approve each login-as, the first
|
|
309
|
+
> `dr login-as` sends their admins a request and exits 4 until it is
|
|
310
|
+
> approved; once the access ends or can no longer be renewed, commands on
|
|
311
|
+
> that identity exit 5, the identity is dropped and nothing re-logs in by
|
|
312
|
+
> itself. Use `--as <operator-email>` to pick the operator when several are
|
|
313
|
+
> stored, or `--user-id <n>` to skip the lookup.
|
|
309
314
|
|
|
310
315
|
### Tenant pin (one server, one account)
|
|
311
316
|
|
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.174",
|
|
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.47",
|
|
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-28T22:47:13.870Z";
|
|
73
73
|
function formatBuildTime(iso) {
|
|
74
74
|
if (iso === "") return "";
|
|
75
75
|
const d = new Date(iso);
|
|
@@ -780,12 +780,16 @@ var LoginAsError = class _LoginAsError extends Error {
|
|
|
780
780
|
httpStatus;
|
|
781
781
|
errorCode;
|
|
782
782
|
detail;
|
|
783
|
+
requestId;
|
|
784
|
+
expiresAt;
|
|
783
785
|
constructor(httpStatus, info = {}) {
|
|
784
786
|
super(_LoginAsError.buildMessage(httpStatus, info));
|
|
785
787
|
this.name = "LoginAsError";
|
|
786
788
|
this.httpStatus = httpStatus;
|
|
787
789
|
this.errorCode = info.code;
|
|
788
790
|
this.detail = info.detail;
|
|
791
|
+
this.requestId = info.requestId;
|
|
792
|
+
this.expiresAt = info.expiresAt;
|
|
789
793
|
}
|
|
790
794
|
/** Alias so callers/tests can read `.code` symmetrically with the sibling errors. */
|
|
791
795
|
get code() {
|
|
@@ -800,6 +804,41 @@ var LoginAsError = class _LoginAsError extends Error {
|
|
|
800
804
|
return `login-as failed: HTTP ${status2}${body}`;
|
|
801
805
|
}
|
|
802
806
|
};
|
|
807
|
+
var LOGIN_AS_AWAITING_APPROVAL_EXIT_CODE = 4;
|
|
808
|
+
var LOGIN_AS_GRANT_ENDED_EXIT_CODE = 5;
|
|
809
|
+
var LoginAsAwaitingApprovalError = class _LoginAsAwaitingApprovalError extends Error {
|
|
810
|
+
exitCode = LOGIN_AS_AWAITING_APPROVAL_EXIT_CODE;
|
|
811
|
+
code;
|
|
812
|
+
requestId;
|
|
813
|
+
expiresAt;
|
|
814
|
+
constructor(err, targetEmail) {
|
|
815
|
+
super(_LoginAsAwaitingApprovalError.buildMessage(err, targetEmail), { cause: err });
|
|
816
|
+
this.name = "LoginAsAwaitingApprovalError";
|
|
817
|
+
this.code = err.code ?? "";
|
|
818
|
+
this.requestId = err.requestId;
|
|
819
|
+
this.expiresAt = err.expiresAt;
|
|
820
|
+
}
|
|
821
|
+
static isAwaitingCode(code) {
|
|
822
|
+
return code === "login_as_requested" || code === "login_as_pending";
|
|
823
|
+
}
|
|
824
|
+
static buildMessage(err, targetEmail) {
|
|
825
|
+
const facts = [
|
|
826
|
+
...err.requestId !== void 0 ? [`request ${err.requestId}`] : [],
|
|
827
|
+
...err.expiresAt !== void 0 ? [`expires ${err.expiresAt}`] : []
|
|
828
|
+
];
|
|
829
|
+
const tail = facts.length > 0 ? ` (${facts.join(", ")})` : "";
|
|
830
|
+
const head = err.code === "login_as_requested" ? `Login as ${targetEmail} needs the customer's approval: a request was sent to their admins${tail}.` : `Login as ${targetEmail} is still waiting for the customer's admins to approve${tail}.`;
|
|
831
|
+
return `${head} Run dr login-as again once it is approved; do not retry before that. (${err.code ?? ""})`;
|
|
832
|
+
}
|
|
833
|
+
};
|
|
834
|
+
var LoginAsGrantEndedError = class extends Error {
|
|
835
|
+
code = "login_as_grant_ended";
|
|
836
|
+
exitCode = LOGIN_AS_GRANT_ENDED_EXIT_CODE;
|
|
837
|
+
constructor(message, options) {
|
|
838
|
+
super(message ?? "login-as access ended", options);
|
|
839
|
+
this.name = "LoginAsGrantEndedError";
|
|
840
|
+
}
|
|
841
|
+
};
|
|
803
842
|
var AmbiguousAccountError = class extends Error {
|
|
804
843
|
code = "ERR_AMBIGUOUS_ACCOUNT";
|
|
805
844
|
/** The emails stored for the server, sorted most-recently-used first. */
|
|
@@ -1061,6 +1100,21 @@ async function refreshJwt(host, refreshToken, options) {
|
|
|
1061
1100
|
}
|
|
1062
1101
|
return { access: out.access, refresh: out.refresh, expiresAt };
|
|
1063
1102
|
}
|
|
1103
|
+
function isGrantEndedResponse(status2, text2) {
|
|
1104
|
+
if (status2 !== 401) return false;
|
|
1105
|
+
try {
|
|
1106
|
+
const parsed2 = JSON.parse(text2);
|
|
1107
|
+
return parsed2 !== null && typeof parsed2 === "object" && parsed2["code"] === "login_as_grant_ended";
|
|
1108
|
+
} catch {
|
|
1109
|
+
return false;
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
var RefreshRejectedError = class extends Error {
|
|
1113
|
+
constructor(message) {
|
|
1114
|
+
super(message);
|
|
1115
|
+
this.name = "RefreshRejectedError";
|
|
1116
|
+
}
|
|
1117
|
+
};
|
|
1064
1118
|
async function refreshExternalJwt(host, refreshToken, options) {
|
|
1065
1119
|
const base = trimTrailingSlash(host);
|
|
1066
1120
|
const res = await fetch(`${base}/jwt/api/external/token/refresh/`, {
|
|
@@ -1070,8 +1124,12 @@ async function refreshExternalJwt(host, refreshToken, options) {
|
|
|
1070
1124
|
signal: options?.signal
|
|
1071
1125
|
});
|
|
1072
1126
|
const text2 = await res.text();
|
|
1127
|
+
if (isGrantEndedResponse(res.status, text2)) {
|
|
1128
|
+
throw new LoginAsGrantEndedError("jwt external refresh: HTTP 401 (login_as_grant_ended)");
|
|
1129
|
+
}
|
|
1073
1130
|
if (!res.ok) {
|
|
1074
|
-
|
|
1131
|
+
const message = `jwt external refresh: HTTP ${res.status}: ${truncate(text2, TRUNCATE)}`;
|
|
1132
|
+
throw res.status === 401 ? new RefreshRejectedError(message) : new Error(message);
|
|
1075
1133
|
}
|
|
1076
1134
|
const out = parseTokenBody(text2, "refresh");
|
|
1077
1135
|
if (out.access === "") {
|
|
@@ -1172,13 +1230,23 @@ async function loginAs(host, operatorAccessToken, userId, options) {
|
|
|
1172
1230
|
if (!res.ok) {
|
|
1173
1231
|
let code;
|
|
1174
1232
|
let detail;
|
|
1233
|
+
let requestId;
|
|
1234
|
+
let expiresAt2;
|
|
1175
1235
|
try {
|
|
1176
1236
|
const parsed2 = JSON.parse(text2);
|
|
1177
1237
|
if (typeof parsed2["code"] === "string") code = parsed2["code"];
|
|
1178
1238
|
if (typeof parsed2["detail"] === "string") detail = parsed2["detail"];
|
|
1239
|
+
if (Number.isSafeInteger(parsed2["request_id"])) requestId = parsed2["request_id"];
|
|
1240
|
+
if (typeof parsed2["expires_at"] === "string") expiresAt2 = parsed2["expires_at"];
|
|
1179
1241
|
} catch {
|
|
1180
1242
|
}
|
|
1181
|
-
throw new LoginAsError(res.status, {
|
|
1243
|
+
throw new LoginAsError(res.status, {
|
|
1244
|
+
code,
|
|
1245
|
+
detail,
|
|
1246
|
+
requestId,
|
|
1247
|
+
expiresAt: expiresAt2,
|
|
1248
|
+
body: truncate(text2, TRUNCATE)
|
|
1249
|
+
});
|
|
1182
1250
|
}
|
|
1183
1251
|
let raw2;
|
|
1184
1252
|
try {
|
|
@@ -1531,7 +1599,42 @@ async function ensureJwt(store, serverKey, email, opts) {
|
|
|
1531
1599
|
const detail = reasons.length > 0 ? reasons.join("; ") : "no recovery path available";
|
|
1532
1600
|
throw new SessionExpiredError(`${serverKey}:${email} (${detail})`);
|
|
1533
1601
|
}
|
|
1602
|
+
var APPROVAL_REQUIRED_GRANT_MODE = "approval_required";
|
|
1603
|
+
function isApprovalRequiredGrant(session) {
|
|
1604
|
+
for (const token of [session.jwtAccess, session.jwtRefresh]) {
|
|
1605
|
+
if (token === void 0 || token === "") continue;
|
|
1606
|
+
try {
|
|
1607
|
+
return decodeJwtClaims(token)["grant_mode"] === APPROVAL_REQUIRED_GRANT_MODE;
|
|
1608
|
+
} catch {
|
|
1609
|
+
continue;
|
|
1610
|
+
}
|
|
1611
|
+
}
|
|
1612
|
+
return false;
|
|
1613
|
+
}
|
|
1614
|
+
async function dropLoginAs(store, serverKey, email, session) {
|
|
1615
|
+
if (!hasApiKey(session)) {
|
|
1616
|
+
await store.removeSession(serverKey, email).catch(() => false);
|
|
1617
|
+
return;
|
|
1618
|
+
}
|
|
1619
|
+
await store.saveSession(serverKey, {
|
|
1620
|
+
email,
|
|
1621
|
+
sessionId: "",
|
|
1622
|
+
csrfToken: "",
|
|
1623
|
+
savedAt: /* @__PURE__ */ new Date(),
|
|
1624
|
+
apiKey: session.apiKey,
|
|
1625
|
+
...session.apiKeyExpiresAt !== void 0 && { apiKeyExpiresAt: session.apiKeyExpiresAt }
|
|
1626
|
+
}).catch(() => void 0);
|
|
1627
|
+
}
|
|
1628
|
+
async function endImpersonation(store, serverKey, email, session, message, cause) {
|
|
1629
|
+
await dropLoginAs(store, serverKey, email, session);
|
|
1630
|
+
return new LoginAsGrantEndedError(message, { cause });
|
|
1631
|
+
}
|
|
1632
|
+
function accessEndedMessage(serverKey, email) {
|
|
1633
|
+
return `login-as access to ${serverKey}:${email} has ended (the customer's admin ended it, or it expired). Run dr login-as ${email} --server ${serverKey} to log in again; where the customer approves logins, that sends them a new request.`;
|
|
1634
|
+
}
|
|
1534
1635
|
async function recoverImpersonatedJwt(store, serverKey, email, session, host, opts) {
|
|
1636
|
+
let refreshRejected = session.jwtRefresh === void 0 || session.jwtRefresh === "";
|
|
1637
|
+
let refreshErr = null;
|
|
1535
1638
|
if (session.jwtRefresh !== void 0 && session.jwtRefresh !== "" && host !== "") {
|
|
1536
1639
|
try {
|
|
1537
1640
|
const tok = await refreshExternalJwt(host, session.jwtRefresh, { signal: opts.signal });
|
|
@@ -1543,9 +1646,37 @@ async function recoverImpersonatedJwt(store, serverKey, email, session, host, op
|
|
|
1543
1646
|
};
|
|
1544
1647
|
await store.saveSession(serverKey, next2);
|
|
1545
1648
|
return next2;
|
|
1546
|
-
} catch {
|
|
1649
|
+
} catch (err) {
|
|
1650
|
+
if (err instanceof LoginAsGrantEndedError) {
|
|
1651
|
+
throw await endImpersonation(
|
|
1652
|
+
store,
|
|
1653
|
+
serverKey,
|
|
1654
|
+
email,
|
|
1655
|
+
session,
|
|
1656
|
+
accessEndedMessage(serverKey, email),
|
|
1657
|
+
err
|
|
1658
|
+
);
|
|
1659
|
+
}
|
|
1660
|
+
refreshRejected = err instanceof RefreshRejectedError;
|
|
1661
|
+
refreshErr = err;
|
|
1547
1662
|
}
|
|
1548
1663
|
}
|
|
1664
|
+
if (isApprovalRequiredGrant(session)) {
|
|
1665
|
+
if (!refreshRejected) {
|
|
1666
|
+
throw new Error(
|
|
1667
|
+
`login-as session ${serverKey}:${email} could not be renewed (${msgOf4(refreshErr)}); try again shortly.`,
|
|
1668
|
+
{ cause: refreshErr }
|
|
1669
|
+
);
|
|
1670
|
+
}
|
|
1671
|
+
throw await endImpersonation(
|
|
1672
|
+
store,
|
|
1673
|
+
serverKey,
|
|
1674
|
+
email,
|
|
1675
|
+
session,
|
|
1676
|
+
`login-as session ${serverKey}:${email} could not be renewed, and this customer approves each login, so dr does not log in again on its own. Run dr login-as ${email} --server ${serverKey}; if the access has ended, that sends the customer's admins a new request.`,
|
|
1677
|
+
refreshErr
|
|
1678
|
+
);
|
|
1679
|
+
}
|
|
1549
1680
|
const operatorEmail2 = session.impersonatorEmail;
|
|
1550
1681
|
const userId = session.impersonatedUserId;
|
|
1551
1682
|
if (userId === void 0) {
|
|
@@ -1570,10 +1701,29 @@ async function recoverImpersonatedJwt(store, serverKey, email, session, host, op
|
|
|
1570
1701
|
`${serverKey}:${email} (operator ${operatorEmail2} has no JWT \u2014 re-run dr login --server ${serverKey})`
|
|
1571
1702
|
);
|
|
1572
1703
|
}
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1704
|
+
let result;
|
|
1705
|
+
try {
|
|
1706
|
+
result = await loginAs(host, operatorAccess, userId, {
|
|
1707
|
+
prevRefresh: session.jwtRefresh,
|
|
1708
|
+
signal: opts.signal
|
|
1709
|
+
});
|
|
1710
|
+
} catch (err) {
|
|
1711
|
+
if (err instanceof LoginAsError && LoginAsAwaitingApprovalError.isAwaitingCode(err.code)) {
|
|
1712
|
+
await dropLoginAs(store, serverKey, email, session);
|
|
1713
|
+
throw new LoginAsAwaitingApprovalError(err, email);
|
|
1714
|
+
}
|
|
1715
|
+
if (err instanceof LoginAsError && err.code === "login_as_approval_required") {
|
|
1716
|
+
throw await endImpersonation(
|
|
1717
|
+
store,
|
|
1718
|
+
serverKey,
|
|
1719
|
+
email,
|
|
1720
|
+
session,
|
|
1721
|
+
accessEndedMessage(serverKey, email),
|
|
1722
|
+
err
|
|
1723
|
+
);
|
|
1724
|
+
}
|
|
1725
|
+
throw err;
|
|
1726
|
+
}
|
|
1577
1727
|
const next = {
|
|
1578
1728
|
...session,
|
|
1579
1729
|
jwtAccess: result.access,
|
|
@@ -2454,9 +2604,11 @@ var AuthorizedClient = class _AuthorizedClient {
|
|
|
2454
2604
|
this._session = fresh;
|
|
2455
2605
|
res = await this.doWithAuth(method, path13, body, options, true);
|
|
2456
2606
|
} catch (err) {
|
|
2607
|
+
_AuthorizedClient.throwIfLoginAsOutcome(err);
|
|
2457
2608
|
_AuthorizedClient.logRefreshFailure("post-401 force-refresh", err);
|
|
2458
2609
|
}
|
|
2459
2610
|
await _AuthorizedClient.throwIfScopeDenied(method, path13, res);
|
|
2611
|
+
await this.throwIfGrantEnded(method, path13, res);
|
|
2460
2612
|
}
|
|
2461
2613
|
if (!this.isAuthRejected(res) && res.ok) {
|
|
2462
2614
|
try {
|
|
@@ -2466,6 +2618,12 @@ var AuthorizedClient = class _AuthorizedClient {
|
|
|
2466
2618
|
}
|
|
2467
2619
|
return res;
|
|
2468
2620
|
}
|
|
2621
|
+
/** A login-as outcome (DR-56077) is the answer, not a refresh failure to route around. */
|
|
2622
|
+
static throwIfLoginAsOutcome(err) {
|
|
2623
|
+
if (err instanceof LoginAsGrantEndedError || err instanceof LoginAsAwaitingApprovalError) {
|
|
2624
|
+
throw err;
|
|
2625
|
+
}
|
|
2626
|
+
}
|
|
2469
2627
|
/**
|
|
2470
2628
|
* Surface an auto-refresh failure on stderr. Always logs (no env-var gate)
|
|
2471
2629
|
* — silent failures here historically masked server-side issues (refresh
|
|
@@ -2808,12 +2966,14 @@ var AuthorizedClient = class _AuthorizedClient {
|
|
|
2808
2966
|
refreshed = true;
|
|
2809
2967
|
res = await this.doWithAuth(method, path13, body, options, true);
|
|
2810
2968
|
} catch (err) {
|
|
2969
|
+
_AuthorizedClient.throwIfLoginAsOutcome(err);
|
|
2811
2970
|
refreshErr = err;
|
|
2812
2971
|
}
|
|
2813
2972
|
}
|
|
2814
2973
|
if (this.isAuthRejected(res)) {
|
|
2815
2974
|
const text3 = await res.text();
|
|
2816
2975
|
_AuthorizedClient.throwIfScopeDeniedBody(method, path13, res.status, text3);
|
|
2976
|
+
await this.throwIfGrantEndedBody(method, path13, res.status, text3);
|
|
2817
2977
|
if (refreshed || triedSessionOnly) {
|
|
2818
2978
|
const attempted = refreshed ? "session-only retry and JWT refresh" : "session-only retry";
|
|
2819
2979
|
throw new Error(
|
|
@@ -2876,6 +3036,7 @@ var AuthorizedClient = class _AuthorizedClient {
|
|
|
2876
3036
|
});
|
|
2877
3037
|
this._session = fresh;
|
|
2878
3038
|
} catch (err) {
|
|
3039
|
+
_AuthorizedClient.throwIfLoginAsOutcome(err);
|
|
2879
3040
|
_AuthorizedClient.logRefreshFailure("pre-flight", err);
|
|
2880
3041
|
}
|
|
2881
3042
|
}
|
|
@@ -2916,6 +3077,26 @@ var AuthorizedClient = class _AuthorizedClient {
|
|
|
2916
3077
|
}
|
|
2917
3078
|
_AuthorizedClient.throwIfScopeDeniedBody(method, path13, res.status, text2);
|
|
2918
3079
|
}
|
|
3080
|
+
/** The ladder ended on 401 `login_as_grant_ended` even though the refresh passed: drop the
|
|
3081
|
+
* identity and exit 5, as when the refresh itself reports the end. */
|
|
3082
|
+
async throwIfGrantEnded(method, path13, res) {
|
|
3083
|
+
if (res.status !== 401) return;
|
|
3084
|
+
let text2;
|
|
3085
|
+
try {
|
|
3086
|
+
text2 = await res.clone().text();
|
|
3087
|
+
} catch {
|
|
3088
|
+
return;
|
|
3089
|
+
}
|
|
3090
|
+
await this.throwIfGrantEndedBody(method, path13, res.status, text2);
|
|
3091
|
+
}
|
|
3092
|
+
/** {@link AuthorizedClient.throwIfGrantEnded} for a body already in hand. */
|
|
3093
|
+
async throwIfGrantEndedBody(method, path13, status2, text2) {
|
|
3094
|
+
if (!isGrantEndedResponse(status2, text2)) return;
|
|
3095
|
+
await dropLoginAs(this._store, this._serverKey, this._email, this._session);
|
|
3096
|
+
throw new LoginAsGrantEndedError(
|
|
3097
|
+
`${method} ${path13}: ${accessEndedMessage(this._serverKey, this._email)}`
|
|
3098
|
+
);
|
|
3099
|
+
}
|
|
2919
3100
|
/** {@link AuthorizedClient.throwIfScopeDenied} for a body already in hand — the multipart
|
|
2920
3101
|
* methods and `requestJson`'s final branch have read theirs and must not read it twice. */
|
|
2921
3102
|
static throwIfScopeDeniedBody(method, path13, status2, text2) {
|
|
@@ -6941,6 +7122,9 @@ function friendlyLoginAsError(err, targetEmail, explicitOperator) {
|
|
|
6941
7122
|
explicitOperator
|
|
6942
7123
|
);
|
|
6943
7124
|
}
|
|
7125
|
+
if (LoginAsAwaitingApprovalError.isAwaitingCode(err.code)) {
|
|
7126
|
+
return new LoginAsAwaitingApprovalError(err, targetEmail);
|
|
7127
|
+
}
|
|
6944
7128
|
switch (err.code) {
|
|
6945
7129
|
case "not_permitted":
|
|
6946
7130
|
return new Error(`You are not permitted to impersonate ${targetEmail}.`);
|
|
@@ -6948,6 +7132,11 @@ function friendlyLoginAsError(err, targetEmail, explicitOperator) {
|
|
|
6948
7132
|
return new Error(`${targetEmail} is inactive and cannot be impersonated.`);
|
|
6949
7133
|
case "impersonation_unavailable":
|
|
6950
7134
|
return new Error("Impersonation service is temporarily unavailable; try again shortly.");
|
|
7135
|
+
case "login_as_approval_required":
|
|
7136
|
+
return new Error(
|
|
7137
|
+
`The approval for ${targetEmail} ended while logging in. Run dr login-as again; where the customer approves logins, that sends their admins a new request.`,
|
|
7138
|
+
{ cause: err }
|
|
7139
|
+
);
|
|
6951
7140
|
default:
|
|
6952
7141
|
return err;
|
|
6953
7142
|
}
|
|
@@ -79038,6 +79227,9 @@ function exitCodeFor(err) {
|
|
|
79038
79227
|
if (err instanceof ScopeFilterError) return err.exitCode;
|
|
79039
79228
|
if (err instanceof ScopeDeniedError) return SCOPE_EXIT_CODE;
|
|
79040
79229
|
if (err instanceof TenantPinError) return err.exitCode;
|
|
79230
|
+
if (err instanceof LoginAsAwaitingApprovalError || err instanceof LoginAsGrantEndedError) {
|
|
79231
|
+
return err.exitCode;
|
|
79232
|
+
}
|
|
79041
79233
|
return 1;
|
|
79042
79234
|
}
|
|
79043
79235
|
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.174",
|
|
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": {
|
|
@@ -75,7 +75,10 @@ per server can be logged in at once.
|
|
|
75
75
|
browser**. Needs an operator `login` first; stores a switchable identity you
|
|
76
76
|
then target with `--account <email>`. `--as <operator-email>` picks the
|
|
77
77
|
operator if several are stored; `--user-id <n>` skips the email lookup.
|
|
78
|
-
Refreshes/auto-remints silently — no re-prompt until the operator expires
|
|
78
|
+
Refreshes/auto-remints silently — no re-prompt until the operator expires,
|
|
79
|
+
except where the customer approves each login-as: exit **4** means it awaits
|
|
80
|
+
that approval, exit **5** that the access ended. Do not retry either; see
|
|
81
|
+
`dr-switch-user`.
|
|
79
82
|
- `dr whoami --server US` — confirm who/where you are. Offline (decodes the local
|
|
80
83
|
JWT, no network) — **except** an API-key credential holding no token its current
|
|
81
84
|
key minted (a key written straight into a session file — e.g. a managed-agent
|
|
@@ -192,9 +195,11 @@ permission / tenant / feature-flag refusal, still exits **1**, and is worth a
|
|
|
192
195
|
second look at `--server`, the org context, and the request body.
|
|
193
196
|
|
|
194
197
|
**JWT refresh is automatic** — the CLI refreshes before each request and on
|
|
195
|
-
401/403, except on the two scope codes above, which no refresh can lift
|
|
196
|
-
|
|
197
|
-
|
|
198
|
+
401/403, except on the two scope codes above, which no refresh can lift, and
|
|
199
|
+
on an impersonated identity that exits **4** or **5** (see `dr login-as`
|
|
200
|
+
above). Only re-`login` when you see `session expired` as the *final* error
|
|
201
|
+
after an auto-refresh attempt. Do not re-login proactively on an expiry
|
|
202
|
+
countdown.
|
|
198
203
|
|
|
199
204
|
**Connectors over VPN (TLS):** `dr connectors …` reaches the control-plane over
|
|
200
205
|
the corporate VPN. A TLS/certificate error there (`self signed certificate in
|
|
@@ -153,6 +153,17 @@ switchable `(server, email)` identity — no browser, no TTY prompt.
|
|
|
153
153
|
- **Denials** surface as clear messages — *not permitted to impersonate*,
|
|
154
154
|
*inactive*, *impersonation service unavailable*. Surface verbatim; do not
|
|
155
155
|
retry; do not record a default.
|
|
156
|
+
- **Exit 4 — waiting for the customer's approval.** Some customers approve
|
|
157
|
+
each login-as. The first `dr login-as` sends their admins a request; until
|
|
158
|
+
an admin approves it, every run exits 4 with *a request was sent* or
|
|
159
|
+
*still waiting*. Surface it, stop, and tell the user to run `dr login-as`
|
|
160
|
+
again after the approval. Never retry in a loop.
|
|
161
|
+
- **Exit 5 — access ended.** Any command on an impersonated identity exits 5
|
|
162
|
+
when the customer's admin ends the access or it expires, or when an
|
|
163
|
+
approved login-as can no longer be renewed. The CLI drops the identity and
|
|
164
|
+
does not log in again by itself. Surface the message and stop; run
|
|
165
|
+
`dr login-as` again only if the user asks (it may send the customer's
|
|
166
|
+
admins a new request).
|
|
156
167
|
- **`login-as` needs dr ≥ 1.3.24.** Older installs answer `unknown command
|
|
157
168
|
'login-as'`. If the browser Login-As works but the CLI "can't", check
|
|
158
169
|
`dr --version` / `dr update` first — that's the common cause, not permissions.
|
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.174",
|
|
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.47",
|
|
21
21
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
22
22
|
"@napi-rs/keyring": "^1.3.0",
|
|
23
23
|
"commander": "^12.1.0",
|