dsh-agy 0.2.7 → 0.2.8

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.
@@ -4,6 +4,7 @@ import { createCipheriv, createDecipheriv, createHash, randomBytes, randomUUID }
4
4
  import { existsSync, mkdirSync, readFileSync, renameSync, statSync, writeFileSync } from "node:fs";
5
5
  import { dirname, join, resolve } from "node:path";
6
6
  import lockfile from "proper-lockfile";
7
+ import { Document, isMap, isScalar, parseDocument } from "yaml";
7
8
  //#region src/types.ts
8
9
  /**
9
10
  * Host-independent authentication error. The adapter maps `kind` to the DSH
@@ -1092,7 +1093,7 @@ var AgySessionManager = class AgySessionManager {
1092
1093
  try {
1093
1094
  const auth = await this.accessTokenFor(account);
1094
1095
  if (!auth) return null;
1095
- const { fetchAvailableModels } = await import("./models-ExG7kJs1.mjs").then((n) => n.r);
1096
+ const { fetchAvailableModels } = await import("./models-F-1cWO0f.mjs").then((n) => n.r);
1096
1097
  const quotas = ingestFamilyQuotas(await fetchAvailableModels(auth.access, account.projectId, (input, init) => {
1097
1098
  const timeout = AbortSignal.timeout(AgySessionManager.QUOTA_FETCH_TIMEOUT_MS);
1098
1099
  const signal = init?.signal ? AbortSignal.any([init.signal, timeout]) : timeout;
@@ -1243,7 +1244,7 @@ var AgySessionManager = class AgySessionManager {
1243
1244
  }
1244
1245
  const key = this.accountKey(picked.account);
1245
1246
  if (!picked.account.projectId && !this.projectRetryFailed.has(key)) try {
1246
- const { loadCodeAssist } = await import("./exchange-D1sCdzuN.mjs").then((n) => n.n);
1247
+ const { loadCodeAssist } = await import("./exchange-Bdql4kf3.mjs").then((n) => n.n);
1247
1248
  const { projectId } = await loadCodeAssist(auth.access);
1248
1249
  if (projectId) {
1249
1250
  await this.store.mutate((s) => {
@@ -1338,7 +1339,7 @@ var AgySessionManager = class AgySessionManager {
1338
1339
  ok: false,
1339
1340
  error: "No agy account configured — run `dsh-agy login` first."
1340
1341
  };
1341
- const { toAgyRequestBody } = await import("./translate-nK61WoOC.mjs").then((n) => n.n);
1342
+ const { toAgyRequestBody } = await import("./translate-CE9tNXiV.mjs").then((n) => n.n);
1342
1343
  const { fetchAgyFirstOk } = await Promise.resolve().then(() => constants_exports);
1343
1344
  const { parseAgySse } = await import("./parse-Bs4rPkHA.mjs").then((n) => n.n);
1344
1345
  const body = toAgyRequestBody({
@@ -1528,34 +1529,86 @@ function resolveDshHome() {
1528
1529
  function homedir() {
1529
1530
  return process.env.HOME ?? process.env.USERPROFILE ?? ".";
1530
1531
  }
1532
+ /** A credentials reference name must be a POSIX-style identifier. */
1533
+ const REF_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
1534
+ /** The document's non-reference sections. */
1535
+ const SECTION_KEYS = ["version", "records"];
1536
+ /** Every top-level key DSH's credentials provider admits; anything else is rejected. */
1537
+ const DOCUMENT_TOP_LEVEL = /* @__PURE__ */ new Set([...SECTION_KEYS, "refs"]);
1531
1538
  /**
1532
- * Minimal reader for the DSH credentials document: a flat YAML mapping of
1533
- * `KEY: "value"` / `KEY: value` lines. Values are unquoted with JSON-style
1534
- * escaping for double-quoted scalars; anything more complex fails loudly.
1539
+ * Describe one YAML parse failure without quoting the source, which holds
1540
+ * secrets. The parser's own message embeds the offending line.
1541
+ */
1542
+ function describeYamlError(error) {
1543
+ const at = error.linePos?.[0];
1544
+ return `${error.code ?? "parse-error"}${at ? ` at line ${at.line}, column ${at.col}` : ""}`;
1545
+ }
1546
+ /**
1547
+ * Parse the credentials document and locate its `refs` section.
1548
+ *
1549
+ * The document is owned by DSH's credentials provider, which serializes it with
1550
+ * the `yaml` package. Values are folded across physical lines (a long
1551
+ * single-quoted scalar folds at whitespace) and `records` payloads nest
1552
+ * arbitrary keys, so this MUST be a real parse rather than a line scan: a
1553
+ * line-based reader cannot represent folding, and it lets a `records` payload
1554
+ * shadow a real top-level reference.
1555
+ *
1556
+ * Deliberately permissive: it validates only the shape it consumes, so the read
1557
+ * path keeps working on a document written by an older build. What may be
1558
+ * rewritten is enforced separately, in the writer.
1559
+ */
1560
+ function parseCredentialsDocument(text, file, context) {
1561
+ const document = parseDocument(text, {
1562
+ prettyErrors: true,
1563
+ uniqueKeys: true
1564
+ });
1565
+ if (document.errors.length > 0) throw new Error(`${context}: cannot parse ${file}: ` + document.errors.map(describeYamlError).join("; "));
1566
+ const root = document.contents;
1567
+ if (!isMap(root)) throw new Error(`${context}: ${file} must be a mapping`);
1568
+ const refs = root.get("refs");
1569
+ if (refs !== void 0 && refs !== null && !isMap(refs)) throw new Error(`${context}: "refs" in ${file} must be a mapping`);
1570
+ return {
1571
+ document,
1572
+ root,
1573
+ refs: isMap(refs) ? refs : void 0,
1574
+ flat: !root.has("version")
1575
+ };
1576
+ }
1577
+ /**
1578
+ * Read the references from the DSH credentials document.
1579
+ *
1580
+ * Only `refs` holds references, and a `records` payload must never be able to
1581
+ * shadow one. Two compatibility cases matter:
1582
+ *
1583
+ * - A pre-release "flat" document keeps its references at the top level; the
1584
+ * provider still reads that layout, so we do too.
1585
+ * - Version 0.2.7 of this plugin appended the master key as a *top-level* key.
1586
+ * The provider rejects such a document, but the key still decrypts an
1587
+ * existing account store, so it is read as a fallback. Dropping it would
1588
+ * silently mint a new master key and strand those accounts.
1589
+ *
1590
+ * Non-string and empty values are skipped, mirroring the provider.
1535
1591
  */
1536
1592
  function readCredentialsDocument(file) {
1537
- if (!existsSync(file)) return /* @__PURE__ */ new Map();
1538
1593
  const entries = /* @__PURE__ */ new Map();
1594
+ if (!existsSync(file)) return entries;
1539
1595
  const text = readFileSync(file, "utf8");
1540
- for (const line of text.split(/\r?\n/)) {
1541
- const trimmed = line.trim();
1542
- if (!trimmed || trimmed.startsWith("#") || trimmed.startsWith("---")) continue;
1543
- const match = /^([A-Za-z_][A-Za-z0-9_]*):\s*(.*)$/.exec(trimmed);
1544
- if (!match) continue;
1545
- const [, key = "", rawValue = ""] = match;
1546
- let value;
1547
- if (rawValue.startsWith("\"")) try {
1548
- value = JSON.parse(rawValue);
1549
- } catch {
1550
- throw new Error(`readCredentialsDocument: invalid quoted value for "${key}" in ${file}`);
1551
- }
1552
- else if (rawValue.startsWith("'")) {
1553
- if (!rawValue.endsWith("'") || rawValue.length < 2) throw new Error(`readCredentialsDocument: unterminated single-quoted value for "${key}" in ${file}`);
1554
- value = rawValue.slice(1, -1).replace(/''/g, "'");
1555
- } else value = rawValue;
1556
- if (value.length === 0) continue;
1596
+ if (text.trim().length === 0) return entries;
1597
+ const { root, refs, flat } = parseCredentialsDocument(text, file, "readCredentialsDocument");
1598
+ const section = flat ? root : refs;
1599
+ if (section) for (const item of section.items) {
1600
+ if (!isScalar(item.key) || !isScalar(item.value)) continue;
1601
+ const key = item.key.value;
1602
+ const value = item.value.value;
1603
+ if (typeof key !== "string" || !REF_NAME.test(key)) continue;
1604
+ if (flat && SECTION_KEYS.includes(key)) continue;
1605
+ if (typeof value !== "string" || value.length === 0) continue;
1557
1606
  entries.set(key, value);
1558
1607
  }
1608
+ if (!flat && !entries.has("AGY_MASTER_KEY")) {
1609
+ const legacy = root.get(MASTER_KEY_REF);
1610
+ if (typeof legacy === "string" && legacy.length > 0) entries.set(MASTER_KEY_REF, legacy);
1611
+ }
1559
1612
  return entries;
1560
1613
  }
1561
1614
  /**
@@ -1579,21 +1632,83 @@ function loadMasterKey(dshHome) {
1579
1632
  assertOwnerOnly(file);
1580
1633
  return readCredentialsDocument(file).get(MASTER_KEY_REF);
1581
1634
  }
1635
+ /** Top-level keys DSH's credentials provider admits; anything else is rejected. */
1636
+ /**
1637
+ * Render the version-1 layout for a pre-release "flat" document — a non-empty
1638
+ * top-level mapping of reference names to non-empty string scalars, with no
1639
+ * `version` key and no document directives. The original lines are nested
1640
+ * verbatim under `refs:`, so comments, blank lines, and each value's spelling
1641
+ * survive byte for byte. Returns undefined for anything the recognizer
1642
+ * declines, which the caller must then refuse to rewrite.
1643
+ *
1644
+ * Nesting whole lines is safe for block scalars: an explicit indent indicator
1645
+ * (`|2`) is absolute and the implied one is detected relative to the key, so
1646
+ * shifting every line by the same two spaces preserves the parsed value.
1647
+ */
1648
+ function renderFlatLayoutMigration(text) {
1649
+ for (const line of text.split("\n")) if (/^(%|---|\.\.\.)/.test(line)) return void 0;
1650
+ const document = parseDocument(text, {
1651
+ prettyErrors: true,
1652
+ uniqueKeys: true
1653
+ });
1654
+ if (document.errors.length > 0) return void 0;
1655
+ const flat = document.contents;
1656
+ if (!isMap(flat) || flat.items.length === 0) return void 0;
1657
+ for (const pair of flat.items) {
1658
+ if (!isScalar(pair.key) || typeof pair.key.value !== "string") return void 0;
1659
+ if (DOCUMENT_TOP_LEVEL.has(pair.key.value) || !REF_NAME.test(pair.key.value)) return void 0;
1660
+ if (!isScalar(pair.value) || typeof pair.value.value !== "string") return void 0;
1661
+ if (pair.value.value.length === 0) return void 0;
1662
+ }
1663
+ return `version: 1\nrefs:\n${text.split("\n").map((line) => line.length === 0 ? line : ` ${line}`).join("\n")}${text.endsWith("\n") ? "" : "\n"}`;
1664
+ }
1665
+ /**
1666
+ * Render the credentials document with {@link MASTER_KEY_REF} added under `refs`.
1667
+ *
1668
+ * The key MUST be nested under `refs`: DSH's credentials provider rejects any
1669
+ * other top-level key, and a document it rejects makes *every* credential in
1670
+ * the file unreadable, not just ours. Editing the parsed document keeps the
1671
+ * comments and formatting of every untouched entry; a document this build
1672
+ * cannot prove it understands is never rewritten.
1673
+ */
1674
+ function renderWithMasterKey(existingText, masterKey, file) {
1675
+ const fresh = () => new Document({
1676
+ version: 1,
1677
+ refs: { [MASTER_KEY_REF]: masterKey }
1678
+ }).toString();
1679
+ if (existingText.trim().length === 0) return fresh();
1680
+ const { document, root, refs, flat } = parseCredentialsDocument(existingText, file, "persistMasterKey");
1681
+ if (flat) {
1682
+ const migrated = renderFlatLayoutMigration(existingText);
1683
+ if (migrated === void 0) throw new Error(`persistMasterKey: ${file} is not a credentials document this build can prove it understands; refusing to rewrite it`);
1684
+ const next = parseDocument(migrated);
1685
+ next.setIn(["refs", MASTER_KEY_REF], masterKey);
1686
+ return next.toString();
1687
+ }
1688
+ for (const key of root.items) {
1689
+ if (!isScalar(key.key) || typeof key.key.value !== "string") continue;
1690
+ if (!DOCUMENT_TOP_LEVEL.has(key.key.value)) throw new Error(`persistMasterKey: ${file} has unknown top-level key "${key.key.value}"; refusing to rewrite it`);
1691
+ }
1692
+ const version = root.get("version");
1693
+ if (version !== 1) throw new Error(`persistMasterKey: ${file} declares version ${JSON.stringify(version)}; refusing to rewrite it`);
1694
+ if (refs) document.setIn(["refs", MASTER_KEY_REF], masterKey);
1695
+ else document.set("refs", { [MASTER_KEY_REF]: masterKey });
1696
+ return document.toString();
1697
+ }
1582
1698
  /**
1583
1699
  * Generate and persist a fresh master key (0600) in the DSH credentials document.
1584
1700
  *
1585
- * Append-only + atomic rename: the DSH credentials service owns this file and
1586
- * preserves comments and the formatting of untouched entries (and may hold
1587
- * YAML constructs this minimal reader cannot parse). Rewriting the whole file
1588
- * from our parsed view would silently drop those; appending keeps them.
1701
+ * The key is added under `refs` by editing the parsed document, then written
1702
+ * with an atomic tmp+rename, so every other credential — and the comments and
1703
+ * formatting DSH services rely on — survives. Never rewrite the whole file from
1704
+ * a rebuilt view: that would silently drop entries this build does not model.
1589
1705
  */
1590
1706
  function persistMasterKey(dshHome, masterKey) {
1591
1707
  const file = join(dshHome, ".credentials.yaml");
1592
1708
  mkdirSync(dirname(file), { recursive: true });
1593
1709
  const existingText = existsSync(file) ? readFileSync(file, "utf8") : "";
1594
1710
  if (existingText.length > 0 && readCredentialsDocument(file).has("AGY_MASTER_KEY")) throw new Error(`persistMasterKey: ${MASTER_KEY_REF} already exists in ${file}`);
1595
- const entry = `${MASTER_KEY_REF}: ${JSON.stringify(masterKey)}`;
1596
- const next = existingText.length > 0 && !existingText.endsWith("\n") ? `${existingText}\n${entry}\n` : `${existingText}${entry}\n`;
1711
+ const next = renderWithMasterKey(existingText, masterKey, file);
1597
1712
  const tmp = `${file}.tmp-masterkey`;
1598
1713
  writeFileSync(tmp, next, { mode: 384 });
1599
1714
  renameSync(tmp, file);
@@ -1832,4 +1947,4 @@ var JsonAccountStore = class {
1832
1947
  //#endregion
1833
1948
  export { AgyPoolBlockedError as A, OAUTH_TOKEN_URL as C, getAgyBootstrapUserAgent as D, getAgyBootstrapClientMetadata as E, resolveAgyClientCredentials as O, OAUTH_AUTHORIZE_URL as S, fetchAgyFirstOk as T, deriveAntigravitySessionId as _, deriveKey as a, AGY_ENDPOINT_FALLBACKS as b, resolveDshHome as c, isAgyDisabled as d, generateFingerprint as f, resolveAntigravityVersionBounded as g, resolveAntigravityVersion as h, createAesGcmCodec as i, AgyAuthError as k, resolveMasterKeyCodec as l, calculateTokenExpiry as m, maskProxyUrl as n, loadMasterKey as o, recordFingerprintVersion as p, MASTER_KEY_REF as r, persistMasterKey as s, JsonAccountStore as t, AgySessionManager as u, generateAntigravityRequestId as v, OAUTH_USERINFO_URL as w, AGY_SCOPES as x, AGY_DEFAULT_REDIRECT_URI as y };
1834
1949
 
1835
- //# sourceMappingURL=accounts-D5LlcqCO.mjs.map
1950
+ //# sourceMappingURL=accounts-Um8N9I56.mjs.map