@datarails-ai/dr-appspace-cli 0.12.1 → 0.12.2
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 +287 -30
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -13,7 +13,7 @@ import { fileURLToPath as fileURLToPath6 } from "url";
|
|
|
13
13
|
// package.json
|
|
14
14
|
var package_default = {
|
|
15
15
|
name: "@datarails-ai/dr-appspace-cli",
|
|
16
|
-
version: "0.12.
|
|
16
|
+
version: "0.12.2",
|
|
17
17
|
description: "Datarails App Space CLI \u2014 build, try, push and publish App Space apps",
|
|
18
18
|
type: "module",
|
|
19
19
|
engines: {
|
|
@@ -57,7 +57,7 @@ var package_default = {
|
|
|
57
57
|
};
|
|
58
58
|
|
|
59
59
|
// src/buildInfo.ts
|
|
60
|
-
var BUILD_TIME = "2026-10-
|
|
60
|
+
var BUILD_TIME = "2026-10-08T08:05:29.934Z";
|
|
61
61
|
function formatBuildTime(iso) {
|
|
62
62
|
if (iso === "") return "";
|
|
63
63
|
const d = new Date(iso);
|
|
@@ -208,7 +208,17 @@ function decodeShape(value) {
|
|
|
208
208
|
|
|
209
209
|
// src/auth/keyring/types.ts
|
|
210
210
|
function isCookielessSession(s) {
|
|
211
|
-
return s.impersonatorEmail !== void 0 && s.impersonatorEmail !== "" || s.cookieless === true;
|
|
211
|
+
return s.impersonatorEmail !== void 0 && s.impersonatorEmail !== "" || s.cookieless === true || hasApiKey(s) || hasApiKeyEnvVar(s);
|
|
212
|
+
}
|
|
213
|
+
function hasApiKey(s) {
|
|
214
|
+
return s.apiKey !== void 0 && s.apiKey !== "";
|
|
215
|
+
}
|
|
216
|
+
function hasApiKeyEnvVar(s) {
|
|
217
|
+
return s.apiKeyEnvVar !== void 0 && s.apiKeyEnvVar !== "";
|
|
218
|
+
}
|
|
219
|
+
function usesApiKey(s) {
|
|
220
|
+
const impersonated = s.impersonatorEmail !== void 0 && s.impersonatorEmail !== "";
|
|
221
|
+
return (hasApiKey(s) || hasApiKeyEnvVar(s)) && !impersonated;
|
|
212
222
|
}
|
|
213
223
|
var KeychainError = class extends Error {
|
|
214
224
|
constructor(message, options) {
|
|
@@ -428,7 +438,9 @@ var KeyringStore = class _KeyringStore {
|
|
|
428
438
|
try {
|
|
429
439
|
parsed = JSON.parse(raw);
|
|
430
440
|
} catch (err) {
|
|
431
|
-
throw new KeychainError(`decode session for ${serverKey}:${email}`, {
|
|
441
|
+
throw new KeychainError(`decode session for ${serverKey}:${email}`, {
|
|
442
|
+
cause: err
|
|
443
|
+
});
|
|
432
444
|
}
|
|
433
445
|
const sessionId = typeof parsed.sessionId === "string" ? parsed.sessionId : "";
|
|
434
446
|
const csrfToken = typeof parsed.csrfToken === "string" ? parsed.csrfToken : "";
|
|
@@ -436,7 +448,10 @@ var KeyringStore = class _KeyringStore {
|
|
|
436
448
|
const impersonatorEmail = typeof parsed.impersonatorEmail === "string" ? parsed.impersonatorEmail : "";
|
|
437
449
|
const impersonated = impersonatorEmail !== "";
|
|
438
450
|
const cookielessFlag = parsed.cookieless === true;
|
|
439
|
-
const
|
|
451
|
+
const apiKey = typeof parsed.apiKey === "string" && parsed.apiKey !== "" ? parsed.apiKey : "";
|
|
452
|
+
const apiKeyEnvVar = typeof parsed.apiKeyEnvVar === "string" && parsed.apiKeyEnvVar !== "" ? parsed.apiKeyEnvVar : "";
|
|
453
|
+
const keyed = apiKey !== "" || apiKeyEnvVar !== "";
|
|
454
|
+
const cookieless = impersonated || cookielessFlag || keyed;
|
|
440
455
|
if (savedAtStr === "" || !cookieless && (sessionId === "" || csrfToken === "")) {
|
|
441
456
|
throw new KeychainError(`session for ${serverKey}:${email} is malformed`);
|
|
442
457
|
}
|
|
@@ -458,6 +473,18 @@ var KeyringStore = class _KeyringStore {
|
|
|
458
473
|
if (cookielessFlag) {
|
|
459
474
|
session.cookieless = true;
|
|
460
475
|
}
|
|
476
|
+
if (apiKey !== "") {
|
|
477
|
+
session.apiKey = apiKey;
|
|
478
|
+
}
|
|
479
|
+
if (apiKeyEnvVar !== "") {
|
|
480
|
+
session.apiKeyEnvVar = apiKeyEnvVar;
|
|
481
|
+
}
|
|
482
|
+
if (typeof parsed.apiKeyExpiresAt === "string" && parsed.apiKeyExpiresAt !== "") {
|
|
483
|
+
session.apiKeyExpiresAt = new Date(parsed.apiKeyExpiresAt);
|
|
484
|
+
}
|
|
485
|
+
if (typeof parsed.jwtApiKeyFingerprint === "string" && parsed.jwtApiKeyFingerprint !== "") {
|
|
486
|
+
session.jwtApiKeyFingerprint = parsed.jwtApiKeyFingerprint;
|
|
487
|
+
}
|
|
461
488
|
if (typeof parsed.jwtExpiresAt === "string" && parsed.jwtExpiresAt !== "") {
|
|
462
489
|
session.jwtExpiresAt = new Date(parsed.jwtExpiresAt);
|
|
463
490
|
}
|
|
@@ -499,7 +526,7 @@ var KeyringStore = class _KeyringStore {
|
|
|
499
526
|
const impersonated = session.impersonatorEmail !== void 0 && session.impersonatorEmail !== "";
|
|
500
527
|
const cookieless = session.cookieless === true;
|
|
501
528
|
if (isCookielessSession(session)) {
|
|
502
|
-
if (session.jwtAccess === void 0 || session.jwtAccess === "") {
|
|
529
|
+
if (!usesApiKey(session) && (session.jwtAccess === void 0 || session.jwtAccess === "")) {
|
|
503
530
|
throw new Error("saveSession: cookie-less session must have a jwtAccess token");
|
|
504
531
|
}
|
|
505
532
|
} else {
|
|
@@ -533,6 +560,18 @@ var KeyringStore = class _KeyringStore {
|
|
|
533
560
|
if (cookieless) {
|
|
534
561
|
metaBlob.cookieless = true;
|
|
535
562
|
}
|
|
563
|
+
if (session.apiKey !== void 0 && session.apiKey !== "") {
|
|
564
|
+
metaBlob.apiKey = session.apiKey;
|
|
565
|
+
}
|
|
566
|
+
if (session.apiKeyEnvVar !== void 0 && session.apiKeyEnvVar !== "") {
|
|
567
|
+
metaBlob.apiKeyEnvVar = session.apiKeyEnvVar;
|
|
568
|
+
}
|
|
569
|
+
if (session.apiKeyExpiresAt !== void 0) {
|
|
570
|
+
metaBlob.apiKeyExpiresAt = session.apiKeyExpiresAt.toISOString();
|
|
571
|
+
}
|
|
572
|
+
if (session.jwtApiKeyFingerprint !== void 0 && session.jwtApiKeyFingerprint !== "") {
|
|
573
|
+
metaBlob.jwtApiKeyFingerprint = session.jwtApiKeyFingerprint;
|
|
574
|
+
}
|
|
536
575
|
await this.backend.set(SERVICE, sessionAccount, JSON.stringify(metaBlob));
|
|
537
576
|
if (session.jwtAccess !== void 0 && session.jwtAccess !== "") {
|
|
538
577
|
await this.backend.set(SERVICE, `${sessionAccount}:access`, session.jwtAccess);
|
|
@@ -564,7 +603,11 @@ var KeyringStore = class _KeyringStore {
|
|
|
564
603
|
blob.lastUsedAt = now.toISOString();
|
|
565
604
|
await this.backend.set(SERVICE, sessionAccount, JSON.stringify(blob));
|
|
566
605
|
const savedAt = parseDate(blob.savedAt) ?? now;
|
|
567
|
-
await this.tryUpsertLegacyIndex(serverKey, {
|
|
606
|
+
await this.tryUpsertLegacyIndex(serverKey, {
|
|
607
|
+
email,
|
|
608
|
+
savedAt,
|
|
609
|
+
lastUsedAt: now
|
|
610
|
+
});
|
|
568
611
|
}
|
|
569
612
|
/**
|
|
570
613
|
* Upserts one account into the legacy `__index__:<server>` entry via a
|
|
@@ -826,6 +869,9 @@ async function pollForToken(host, deviceCode, options = {}) {
|
|
|
826
869
|
}
|
|
827
870
|
}
|
|
828
871
|
|
|
872
|
+
// src/auth/jwt.ts
|
|
873
|
+
import { createHash } from "crypto";
|
|
874
|
+
|
|
829
875
|
// src/auth/types.ts
|
|
830
876
|
function jwtExpiringSoon(e, bufferMs) {
|
|
831
877
|
if (!e.jwtExpiresAt) return true;
|
|
@@ -1040,7 +1086,11 @@ async function loginAs(host, operatorAccessToken, userId, options) {
|
|
|
1040
1086
|
if (typeof parsed["detail"] === "string") detail = parsed["detail"];
|
|
1041
1087
|
} catch {
|
|
1042
1088
|
}
|
|
1043
|
-
throw new LoginAsError(res.status, {
|
|
1089
|
+
throw new LoginAsError(res.status, {
|
|
1090
|
+
code,
|
|
1091
|
+
detail,
|
|
1092
|
+
body: truncate(text3, TRUNCATE)
|
|
1093
|
+
});
|
|
1044
1094
|
}
|
|
1045
1095
|
let raw;
|
|
1046
1096
|
try {
|
|
@@ -1065,6 +1115,93 @@ async function loginAs(host, operatorAccessToken, userId, options) {
|
|
|
1065
1115
|
}
|
|
1066
1116
|
return { access, refresh, expiresAt, user: obj4["user"] };
|
|
1067
1117
|
}
|
|
1118
|
+
async function exchangeApiKey(host, apiKey, options) {
|
|
1119
|
+
const key = apiKey.trim();
|
|
1120
|
+
if (key === "") {
|
|
1121
|
+
throw new Error("api-key exchange: the stored API key is empty");
|
|
1122
|
+
}
|
|
1123
|
+
if (options?.opaque === true) {
|
|
1124
|
+
if (/[\r\n\0]/.test(key) || !/^[\x00-\xff]+$/.test(key)) {
|
|
1125
|
+
throw new Error(
|
|
1126
|
+
"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)"
|
|
1127
|
+
);
|
|
1128
|
+
}
|
|
1129
|
+
} else if (!/^[\x21-\x7e]+$/.test(key)) {
|
|
1130
|
+
throw new Error(
|
|
1131
|
+
"api-key exchange: only a printable-ASCII API key with no spaces is accepted; the stored one contains whitespace, control characters, or non-ASCII characters"
|
|
1132
|
+
);
|
|
1133
|
+
}
|
|
1134
|
+
const base = trimTrailingSlash(host);
|
|
1135
|
+
const res = await fetch(`${base}/jwt/api/apikey/token/`, {
|
|
1136
|
+
method: "POST",
|
|
1137
|
+
headers: { Authorization: `Bearer ${key}`, Accept: "application/json" },
|
|
1138
|
+
signal: options?.signal
|
|
1139
|
+
});
|
|
1140
|
+
const text3 = await res.text();
|
|
1141
|
+
if (!res.ok) {
|
|
1142
|
+
let code;
|
|
1143
|
+
try {
|
|
1144
|
+
const value = JSON.parse(text3)["error"];
|
|
1145
|
+
if (value === "invalid_key" || value === "exchange_unavailable") code = value;
|
|
1146
|
+
} catch {
|
|
1147
|
+
}
|
|
1148
|
+
throw new Error(
|
|
1149
|
+
`api-key exchange: HTTP ${res.status}${code !== void 0 ? ` (${code})` : ""}`
|
|
1150
|
+
);
|
|
1151
|
+
}
|
|
1152
|
+
let raw;
|
|
1153
|
+
try {
|
|
1154
|
+
raw = JSON.parse(text3);
|
|
1155
|
+
} catch {
|
|
1156
|
+
throw new Error("api-key exchange parse: the response is not JSON");
|
|
1157
|
+
}
|
|
1158
|
+
if (raw === null || typeof raw !== "object") {
|
|
1159
|
+
throw new Error("api-key exchange parse: expected object");
|
|
1160
|
+
}
|
|
1161
|
+
const access = raw["access_token"];
|
|
1162
|
+
if (typeof access !== "string" || access === "") {
|
|
1163
|
+
throw new Error("api-key exchange: empty access token");
|
|
1164
|
+
}
|
|
1165
|
+
let expiresAt;
|
|
1166
|
+
try {
|
|
1167
|
+
expiresAt = decodeJwtExpiration(access);
|
|
1168
|
+
} catch (err) {
|
|
1169
|
+
throw new Error(`api-key exchange: ${msgOf3(err)}`);
|
|
1170
|
+
}
|
|
1171
|
+
return { access, expiresAt };
|
|
1172
|
+
}
|
|
1173
|
+
function apiKeyFingerprint(apiKey) {
|
|
1174
|
+
return createHash("sha256").update(apiKey.trim()).digest("hex").slice(0, 32);
|
|
1175
|
+
}
|
|
1176
|
+
function resolveApiKeyMaterial(session) {
|
|
1177
|
+
if (session.apiKey !== void 0 && session.apiKey !== "") {
|
|
1178
|
+
return { key: session.apiKey, fromEnv: false };
|
|
1179
|
+
}
|
|
1180
|
+
const name = session.apiKeyEnvVar;
|
|
1181
|
+
if (name === void 0 || name === "") {
|
|
1182
|
+
throw new Error("api-key exchange: the session carries no API key");
|
|
1183
|
+
}
|
|
1184
|
+
const value = process.env[name];
|
|
1185
|
+
if (value === void 0 || value === "") {
|
|
1186
|
+
throw new Error(
|
|
1187
|
+
`api-key exchange: the credential file names environment variable ${name} as the API-key carrier, but it is unset or empty in this environment`
|
|
1188
|
+
);
|
|
1189
|
+
}
|
|
1190
|
+
return { key: value, fromEnv: true };
|
|
1191
|
+
}
|
|
1192
|
+
function mintedByStoredApiKey(session) {
|
|
1193
|
+
if (session.jwtAccess === void 0 || session.jwtAccess === "") return false;
|
|
1194
|
+
if (session.jwtApiKeyFingerprint === void 0 || session.jwtApiKeyFingerprint === "") {
|
|
1195
|
+
return false;
|
|
1196
|
+
}
|
|
1197
|
+
let material;
|
|
1198
|
+
try {
|
|
1199
|
+
material = resolveApiKeyMaterial(session);
|
|
1200
|
+
} catch {
|
|
1201
|
+
return false;
|
|
1202
|
+
}
|
|
1203
|
+
return session.jwtApiKeyFingerprint === apiKeyFingerprint(material.key);
|
|
1204
|
+
}
|
|
1068
1205
|
async function ensureValidJwt(store, serverKey, email, options) {
|
|
1069
1206
|
return ensureJwt(store, serverKey, email, {
|
|
1070
1207
|
buffer: options?.buffer ?? 0,
|
|
@@ -1086,6 +1223,10 @@ async function ensureJwt(store, serverKey, email, opts) {
|
|
|
1086
1223
|
if (session === void 0) {
|
|
1087
1224
|
throw new NotLoggedInError(`not logged in: ${serverKey}:${email}`);
|
|
1088
1225
|
}
|
|
1226
|
+
if (usesApiKey(session)) {
|
|
1227
|
+
const host2 = opts.host ?? resolve(serverKey).host;
|
|
1228
|
+
return exchangeKeyedSession(store, serverKey, email, session, host2, opts);
|
|
1229
|
+
}
|
|
1089
1230
|
if (!opts.force && session.jwtAccess !== void 0 && session.jwtAccess !== "" && !jwtExpiringSoon(session, opts.buffer)) {
|
|
1090
1231
|
return session;
|
|
1091
1232
|
}
|
|
@@ -1099,7 +1240,9 @@ async function ensureJwt(store, serverKey, email, opts) {
|
|
|
1099
1240
|
if (session.jwtRefresh !== void 0 && session.jwtRefresh !== "" && host !== "") {
|
|
1100
1241
|
try {
|
|
1101
1242
|
const refresh = session.cookieless === true ? refreshExternalJwt : refreshJwt;
|
|
1102
|
-
const tok = await refresh(host, session.jwtRefresh, {
|
|
1243
|
+
const tok = await refresh(host, session.jwtRefresh, {
|
|
1244
|
+
signal: opts.signal
|
|
1245
|
+
});
|
|
1103
1246
|
refreshNext = {
|
|
1104
1247
|
...session,
|
|
1105
1248
|
jwtAccess: tok.access,
|
|
@@ -1157,10 +1300,47 @@ async function ensureJwt(store, serverKey, email, opts) {
|
|
|
1157
1300
|
const detail = reasons.length > 0 ? reasons.join("; ") : "no recovery path available";
|
|
1158
1301
|
throw new SessionExpiredError(`${serverKey}:${email} (${detail})`);
|
|
1159
1302
|
}
|
|
1303
|
+
async function exchangeKeyedSession(store, serverKey, email, session, host, opts) {
|
|
1304
|
+
if (!opts.force && mintedByStoredApiKey(session) && !jwtExpiringSoon(session, opts.buffer)) {
|
|
1305
|
+
return session;
|
|
1306
|
+
}
|
|
1307
|
+
if (host === "") {
|
|
1308
|
+
throw new SessionExpiredError(
|
|
1309
|
+
`${serverKey}:${email} (no host to exchange the API key against)`
|
|
1310
|
+
);
|
|
1311
|
+
}
|
|
1312
|
+
const material = resolveApiKeyMaterial(session);
|
|
1313
|
+
let tok;
|
|
1314
|
+
try {
|
|
1315
|
+
tok = await exchangeApiKey(host, material.key, {
|
|
1316
|
+
signal: opts.signal,
|
|
1317
|
+
opaque: material.fromEnv
|
|
1318
|
+
});
|
|
1319
|
+
} catch (err) {
|
|
1320
|
+
throw new SessionExpiredError(`${serverKey}:${email} (${msgOf3(err)})`);
|
|
1321
|
+
}
|
|
1322
|
+
const next = {
|
|
1323
|
+
...session,
|
|
1324
|
+
jwtAccess: tok.access,
|
|
1325
|
+
jwtExpiresAt: tok.expiresAt,
|
|
1326
|
+
jwtApiKeyFingerprint: apiKeyFingerprint(material.key),
|
|
1327
|
+
// An API-key identity has no refresh token; clear any stale one so the
|
|
1328
|
+
// refresh ladder is never tempted by it on a later non-keyed read.
|
|
1329
|
+
jwtRefresh: void 0
|
|
1330
|
+
};
|
|
1331
|
+
try {
|
|
1332
|
+
await store.saveSession(serverKey, next);
|
|
1333
|
+
} catch (err) {
|
|
1334
|
+
throw new Error(`save credentials: ${msgOf3(err)}`, { cause: err });
|
|
1335
|
+
}
|
|
1336
|
+
return next;
|
|
1337
|
+
}
|
|
1160
1338
|
async function recoverImpersonatedJwt(store, serverKey, email, session, host, opts) {
|
|
1161
1339
|
if (session.jwtRefresh !== void 0 && session.jwtRefresh !== "" && host !== "") {
|
|
1162
1340
|
try {
|
|
1163
|
-
const tok = await refreshExternalJwt(host, session.jwtRefresh, {
|
|
1341
|
+
const tok = await refreshExternalJwt(host, session.jwtRefresh, {
|
|
1342
|
+
signal: opts.signal
|
|
1343
|
+
});
|
|
1164
1344
|
const next2 = {
|
|
1165
1345
|
...session,
|
|
1166
1346
|
jwtAccess: tok.access,
|
|
@@ -1341,7 +1521,7 @@ var AuthorizedClient = class _AuthorizedClient {
|
|
|
1341
1521
|
}
|
|
1342
1522
|
const cookieless = session.cookieless === true;
|
|
1343
1523
|
if (isCookielessSession(session)) {
|
|
1344
|
-
if (session.jwtAccess === void 0 || session.jwtAccess === "") {
|
|
1524
|
+
if (!usesApiKey(session) && (session.jwtAccess === void 0 || session.jwtAccess === "")) {
|
|
1345
1525
|
throw new NotLoggedInError(
|
|
1346
1526
|
cookieless ? `not logged in: ${serverKey}:${email} (cookie-less session has no JWT)` : `not logged in: ${serverKey}:${email} (impersonated session has no JWT; re-run dr-appspace login-as)`
|
|
1347
1527
|
);
|
|
@@ -1845,7 +2025,7 @@ var AuthorizedClient = class _AuthorizedClient {
|
|
|
1845
2025
|
async ensureFreshAuth(bufferMs, signal) {
|
|
1846
2026
|
const access = this._session.jwtAccess ?? "";
|
|
1847
2027
|
const refresh = this._session.jwtRefresh ?? "";
|
|
1848
|
-
if (access === "" && refresh === "") {
|
|
2028
|
+
if (access === "" && refresh === "" && !usesApiKey(this._session)) {
|
|
1849
2029
|
return;
|
|
1850
2030
|
}
|
|
1851
2031
|
try {
|
|
@@ -1894,8 +2074,27 @@ import { z } from "zod";
|
|
|
1894
2074
|
var CREDENTIALS_FILE_ENV = "DR_CREDENTIALS_FILE";
|
|
1895
2075
|
var DEFAULT_CREDENTIALS_FILE = join3(homedir3(), ".dr", "session.json");
|
|
1896
2076
|
var credentialsFileSchema = z.object({
|
|
1897
|
-
access: z.string().min(1),
|
|
1898
|
-
refresh: z.string().min(1),
|
|
2077
|
+
access: z.string().min(1).optional(),
|
|
2078
|
+
refresh: z.string().min(1).optional(),
|
|
2079
|
+
// A user-bound API key, exchanged for `access` on demand. A file may hold
|
|
2080
|
+
// only this; `access` then appears after the first exchange.
|
|
2081
|
+
apiKey: z.string().min(1).optional(),
|
|
2082
|
+
// DR-52657: the NAME of an environment variable holding the API key, for
|
|
2083
|
+
// provisioners that must not put the key on disk — an agent sandbox, where
|
|
2084
|
+
// the variable's in-sandbox value is an opaque vault placeholder, or a CI
|
|
2085
|
+
// runner keeping the key in its secret env. Resolved at exchange time and
|
|
2086
|
+
// passed verbatim. Mutually exclusive with `apiKey` (refinement below).
|
|
2087
|
+
apiKeyEnvVar: z.string().min(1).optional(),
|
|
2088
|
+
// Carried verbatim, judged where it is read. `z.string()` (not validated or
|
|
2089
|
+
// `.catch`): a rotation rewrites the whole file from the parsed object, so a
|
|
2090
|
+
// value this schema dropped would be deleted silently, and a top-level
|
|
2091
|
+
// rejection would fail the WHOLE file and lock the operator out of logout.
|
|
2092
|
+
apiKeyExpiresAt: z.string().optional(),
|
|
2093
|
+
// Which key minted `access`, as a non-secret fingerprint. Written by the
|
|
2094
|
+
// CLI, not a provisioner: a rotation replaces `apiKey` and leaves this
|
|
2095
|
+
// naming the old one, which is how the next command knows `access` is not
|
|
2096
|
+
// the new key's. `z.string()` for the same lock-out reason as above.
|
|
2097
|
+
accessApiKeyFingerprint: z.string().optional(),
|
|
1899
2098
|
server: z.string().min(1),
|
|
1900
2099
|
email: z.string().min(1),
|
|
1901
2100
|
// min(1): an empty string is how a hand-rolled producer spells "no cookie",
|
|
@@ -1911,7 +2110,19 @@ var credentialsFileSchema = z.object({
|
|
|
1911
2110
|
}).loose().refine((c) => c.sessionId === void 0 === (c.csrfToken === void 0), {
|
|
1912
2111
|
message: "sessionId and csrfToken must be provided together",
|
|
1913
2112
|
path: ["sessionId"]
|
|
1914
|
-
})
|
|
2113
|
+
}).refine((c) => !(c.apiKey !== void 0 && c.apiKeyEnvVar !== void 0), {
|
|
2114
|
+
message: "apiKey and apiKeyEnvVar are mutually exclusive",
|
|
2115
|
+
path: ["apiKeyEnvVar"]
|
|
2116
|
+
}).refine((c) => c.access !== void 0 || c.apiKey !== void 0 || c.apiKeyEnvVar !== void 0, {
|
|
2117
|
+
message: "one of access, apiKey or apiKeyEnvVar is required",
|
|
2118
|
+
path: ["access"]
|
|
2119
|
+
}).refine(
|
|
2120
|
+
(c) => c.apiKey !== void 0 || c.apiKeyEnvVar !== void 0 || c.refresh !== void 0 || c.sessionId !== void 0,
|
|
2121
|
+
{
|
|
2122
|
+
message: "refresh is required unless an apiKey, apiKeyEnvVar or a session cookie is present",
|
|
2123
|
+
path: ["refresh"]
|
|
2124
|
+
}
|
|
2125
|
+
);
|
|
1915
2126
|
function credentialsFilePath() {
|
|
1916
2127
|
const override = process.env[CREDENTIALS_FILE_ENV];
|
|
1917
2128
|
return override !== void 0 && override !== "" ? override : DEFAULT_CREDENTIALS_FILE;
|
|
@@ -1924,8 +2135,14 @@ async function readCredentialsFile(path3) {
|
|
|
1924
2135
|
if (err.code === "ENOENT") return void 0;
|
|
1925
2136
|
throw err;
|
|
1926
2137
|
}
|
|
2138
|
+
let parsed;
|
|
2139
|
+
try {
|
|
2140
|
+
parsed = JSON.parse(raw);
|
|
2141
|
+
} catch {
|
|
2142
|
+
throw new Error(`credential file ${path3} is not valid JSON`);
|
|
2143
|
+
}
|
|
1927
2144
|
try {
|
|
1928
|
-
return credentialsFileSchema.parse(
|
|
2145
|
+
return credentialsFileSchema.parse(parsed);
|
|
1929
2146
|
} catch (err) {
|
|
1930
2147
|
const reason = err instanceof Error ? err.message : String(err);
|
|
1931
2148
|
throw new Error(`credential file ${path3} is malformed: ${reason}`, { cause: err });
|
|
@@ -1933,11 +2150,19 @@ async function readCredentialsFile(path3) {
|
|
|
1933
2150
|
}
|
|
1934
2151
|
async function writeCredentialsFile(path3, creds) {
|
|
1935
2152
|
await mkdir(dirname2(path3), { recursive: true, mode: 448 });
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
2153
|
+
let next;
|
|
2154
|
+
try {
|
|
2155
|
+
next = credentialsFileSchema.parse({
|
|
2156
|
+
...creds,
|
|
2157
|
+
savedAt: creds.savedAt ?? (/* @__PURE__ */ new Date()).toISOString()
|
|
2158
|
+
});
|
|
2159
|
+
} catch (err) {
|
|
2160
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
2161
|
+
throw new Error(`refusing to write an unreadable credential file ${path3}: ${reason}`, {
|
|
2162
|
+
cause: err
|
|
2163
|
+
});
|
|
2164
|
+
}
|
|
2165
|
+
const body = JSON.stringify(next, null, 2);
|
|
1941
2166
|
const tmp = `${path3}.${process.pid}.tmp`;
|
|
1942
2167
|
await writeFile(tmp, body, { mode: 384 });
|
|
1943
2168
|
await chmod(tmp, 384);
|
|
@@ -1966,9 +2191,11 @@ var FileKeyringBackend = class _FileKeyringBackend {
|
|
|
1966
2191
|
const savedAt = creds.savedAt ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
1967
2192
|
const sessionId = creds.sessionId ?? "";
|
|
1968
2193
|
let jwtExpiresAt;
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
2194
|
+
if (creds.access !== void 0 && creds.access !== "") {
|
|
2195
|
+
try {
|
|
2196
|
+
jwtExpiresAt = decodeJwtExpiration(creds.access).toISOString();
|
|
2197
|
+
} catch {
|
|
2198
|
+
}
|
|
1972
2199
|
}
|
|
1973
2200
|
return JSON.stringify({
|
|
1974
2201
|
sessionId,
|
|
@@ -1976,7 +2203,19 @@ var FileKeyringBackend = class _FileKeyringBackend {
|
|
|
1976
2203
|
savedAt,
|
|
1977
2204
|
lastUsedAt: savedAt,
|
|
1978
2205
|
...sessionId === "" && { cookieless: true },
|
|
1979
|
-
...jwtExpiresAt !== void 0 && { jwtExpiresAt }
|
|
2206
|
+
...jwtExpiresAt !== void 0 && { jwtExpiresAt },
|
|
2207
|
+
// Carry the API-key carrier through so `KeyringStore.getSession` sees a
|
|
2208
|
+
// keyed identity and routes it to the exchange, plus the fingerprint of the
|
|
2209
|
+
// key that minted the stored `access` so a reload can tell the cached JWT
|
|
2210
|
+
// is still the current key's. Without it a file store would re-exchange on
|
|
2211
|
+
// EVERY request — `ensureFreshAuth` reloads the session each time, and
|
|
2212
|
+
// `mintedByStoredApiKey` can never match a fingerprint that was never
|
|
2213
|
+
// written. `set('meta')` persists it back below.
|
|
2214
|
+
...creds.apiKey !== void 0 && creds.apiKey !== "" && { apiKey: creds.apiKey },
|
|
2215
|
+
...creds.apiKeyEnvVar !== void 0 && creds.apiKeyEnvVar !== "" && { apiKeyEnvVar: creds.apiKeyEnvVar },
|
|
2216
|
+
...creds.accessApiKeyFingerprint !== void 0 && creds.accessApiKeyFingerprint !== "" && {
|
|
2217
|
+
jwtApiKeyFingerprint: creds.accessApiKeyFingerprint
|
|
2218
|
+
}
|
|
1980
2219
|
});
|
|
1981
2220
|
}
|
|
1982
2221
|
static slotOf(creds, account) {
|
|
@@ -2001,6 +2240,20 @@ var FileKeyringBackend = class _FileKeyringBackend {
|
|
|
2001
2240
|
...csrfToken !== "" && { csrfToken }
|
|
2002
2241
|
};
|
|
2003
2242
|
}
|
|
2243
|
+
/**
|
|
2244
|
+
* The key fingerprint out of a metadata blob (`KeyringStore` emits it as
|
|
2245
|
+
* `jwtApiKeyFingerprint`), empty when absent. Unlike the cookie it is NOT
|
|
2246
|
+
* derived, so it has to be persisted back to the file or every request
|
|
2247
|
+
* re-exchanges.
|
|
2248
|
+
*/
|
|
2249
|
+
static fingerprintOf(blob) {
|
|
2250
|
+
try {
|
|
2251
|
+
const parsed = JSON.parse(blob);
|
|
2252
|
+
return typeof parsed.jwtApiKeyFingerprint === "string" ? parsed.jwtApiKeyFingerprint : "";
|
|
2253
|
+
} catch {
|
|
2254
|
+
return "";
|
|
2255
|
+
}
|
|
2256
|
+
}
|
|
2004
2257
|
async get(_service, account) {
|
|
2005
2258
|
const creds = await this.load();
|
|
2006
2259
|
if (creds === void 0) return void 0;
|
|
@@ -2026,10 +2279,14 @@ var FileKeyringBackend = class _FileKeyringBackend {
|
|
|
2026
2279
|
switch (_FileKeyringBackend.slotOf(creds, account)) {
|
|
2027
2280
|
case "meta": {
|
|
2028
2281
|
const cookie = _FileKeyringBackend.cookieOf(value);
|
|
2029
|
-
|
|
2282
|
+
const fingerprint = _FileKeyringBackend.fingerprintOf(value);
|
|
2283
|
+
if (cookie.sessionId === creds.sessionId && cookie.csrfToken === creds.csrfToken && fingerprint === (creds.accessApiKeyFingerprint ?? "")) {
|
|
2030
2284
|
return;
|
|
2031
2285
|
}
|
|
2032
|
-
|
|
2286
|
+
const next = { ...creds, ...cookie };
|
|
2287
|
+
if (fingerprint !== "") next.accessApiKeyFingerprint = fingerprint;
|
|
2288
|
+
else delete next.accessApiKeyFingerprint;
|
|
2289
|
+
await writeCredentialsFile(this.path, next);
|
|
2033
2290
|
return;
|
|
2034
2291
|
}
|
|
2035
2292
|
case "access":
|
|
@@ -8810,7 +9067,7 @@ import { tmpdir as tmpdir2 } from "os";
|
|
|
8810
9067
|
import { dirname as dirname3, join as join10 } from "path";
|
|
8811
9068
|
|
|
8812
9069
|
// src/commands/appspace/generate/report.ts
|
|
8813
|
-
import { createHash } from "crypto";
|
|
9070
|
+
import { createHash as createHash2 } from "crypto";
|
|
8814
9071
|
import { mkdir as mkdir3, readFile as readFile7, writeFile as writeFile4 } from "fs/promises";
|
|
8815
9072
|
import { join as join9 } from "path";
|
|
8816
9073
|
function reportPath(dir) {
|
|
@@ -8858,7 +9115,7 @@ async function ownedFiles(dir) {
|
|
|
8858
9115
|
}
|
|
8859
9116
|
}
|
|
8860
9117
|
function digestOf(text3) {
|
|
8861
|
-
return
|
|
9118
|
+
return createHash2("sha256").update(text3, "utf8").digest("hex");
|
|
8862
9119
|
}
|
|
8863
9120
|
async function recordedDigests(dir) {
|
|
8864
9121
|
try {
|
|
@@ -9124,7 +9381,7 @@ function registerGetCommand(parent) {
|
|
|
9124
9381
|
|
|
9125
9382
|
// src/commands/appspace/generate/catalog.ts
|
|
9126
9383
|
import { readFile as readFile9 } from "fs/promises";
|
|
9127
|
-
import { createHash as
|
|
9384
|
+
import { createHash as createHash3 } from "crypto";
|
|
9128
9385
|
var NO_CATALOG = {
|
|
9129
9386
|
source: "none",
|
|
9130
9387
|
metrics: [],
|