{"version":3,"file":"refs-kd62Ljkh.mjs","names":[],"sources":["../src/refs.ts"],"sourcesContent":["import { mkdir, readdir, readFile, rename, rmdir, unlink, writeFile } from 'node:fs/promises';\nimport { type } from 'arktype';\nimport { dirname, join, relative } from 'pathe';\nimport {\n  errorInvalidRefFile,\n  errorInvalidRefName,\n  errorInvalidRefValue,\n  MigrationToolsError,\n} from './errors';\n\nexport interface RefEntry {\n  readonly hash: string;\n  readonly invariants: readonly string[];\n}\n\nexport type Refs = Readonly<Record<string, RefEntry>>;\n\n/**\n * The system head ref lives at `refs/head.json`. It is read (and its\n * corruption judged) through `readContractSpaceHeadRef`, not as a\n * user-authored ref, so {@link readRefsTolerant} excludes it.\n */\nexport const HEAD_REF_NAME = 'head';\n\n/**\n * A single ref file that exists on disk but cannot be turned into a\n * {@link RefEntry} (unparseable JSON or schema-invalid content). The ref\n * is omitted from the result; the problem is surfaced for the integrity\n * layer to report as `refUnreadable` rather than aborting the load.\n */\nexport interface RefLoadProblem {\n  readonly refName: string;\n  readonly detail: string;\n}\n\nexport interface TolerantRefsResult {\n  readonly refs: Refs;\n  readonly problems: readonly RefLoadProblem[];\n}\n\nconst REF_NAME_PATTERN = /^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\/[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$/;\nconst REF_VALUE_PATTERN = /^sha256:(empty|[0-9a-f]{64})$/;\n\nexport function validateRefName(name: string): boolean {\n  if (name.length === 0) return false;\n  if (name.includes('..')) return false;\n  if (name.includes('//')) return false;\n  if (name.startsWith('.')) return false;\n  return REF_NAME_PATTERN.test(name);\n}\n\nexport function validateRefValue(value: string): boolean {\n  return REF_VALUE_PATTERN.test(value);\n}\n\nconst RefEntrySchema = type({\n  hash: 'string',\n  invariants: 'string[]',\n}).narrow((entry, ctx) => {\n  if (!validateRefValue(entry.hash))\n    return ctx.mustBe(`a valid contract hash (got \"${entry.hash}\")`);\n  return true;\n});\n\nfunction refFilePath(refsDir: string, name: string): string {\n  return join(refsDir, `${name}.json`);\n}\n\nfunction refNameFromPath(refsDir: string, filePath: string): string {\n  const rel = relative(refsDir, filePath);\n  return rel.replace(/\\.json$/, '');\n}\n\nexport async function readRef(refsDir: string, name: string): Promise<RefEntry> {\n  if (!validateRefName(name)) {\n    throw errorInvalidRefName(name);\n  }\n\n  const filePath = refFilePath(refsDir, name);\n  let raw: string;\n  try {\n    raw = await readFile(filePath, 'utf-8');\n  } catch (error) {\n    if (error instanceof Error && (error as { code?: string }).code === 'ENOENT') {\n      throw new MigrationToolsError('MIGRATION.UNKNOWN_REF', `Unknown ref \"${name}\"`, {\n        why: `No ref file found at \"${filePath}\".`,\n        fix: `Create the ref with: prisma-next ref set ${name} <hash>`,\n        details: { refName: name, filePath },\n      });\n    }\n    throw error;\n  }\n\n  let parsed: unknown;\n  try {\n    parsed = JSON.parse(raw);\n  } catch {\n    throw errorInvalidRefFile(filePath, 'Failed to parse as JSON');\n  }\n\n  const result = RefEntrySchema(parsed);\n  if (result instanceof type.errors) {\n    throw errorInvalidRefFile(filePath, result.summary);\n  }\n\n  return result;\n}\n\nexport async function readRefs(refsDir: string): Promise<Refs> {\n  let entries: string[];\n  try {\n    entries = await readdir(refsDir, { recursive: true, encoding: 'utf-8' });\n  } catch (error) {\n    if (error instanceof Error && (error as { code?: string }).code === 'ENOENT') {\n      return {};\n    }\n    throw error;\n  }\n\n  const jsonFiles = entries.filter(\n    (entry) => entry.endsWith('.json') && !entry.endsWith('.contract.json'),\n  );\n  const refs: Record<string, RefEntry> = {};\n\n  for (const jsonFile of jsonFiles) {\n    const filePath = join(refsDir, jsonFile);\n    const name = refNameFromPath(refsDir, filePath);\n\n    let raw: string;\n    try {\n      raw = await readFile(filePath, 'utf-8');\n    } catch (error) {\n      // Tolerate the TOCTOU race between `readdir` and `readFile` (ENOENT) and\n      // benign EISDIR if a directory happens to end in `.json`. Anything else\n      // (EACCES, EIO, EMFILE, …) is a real failure and propagates so the CLI\n      // surfaces it rather than silently dropping the ref.\n      const code = error instanceof Error ? (error as { code?: string }).code : undefined;\n      if (code === 'ENOENT' || code === 'EISDIR') {\n        continue;\n      }\n      throw error;\n    }\n\n    let parsed: unknown;\n    try {\n      parsed = JSON.parse(raw);\n    } catch {\n      throw errorInvalidRefFile(filePath, 'Failed to parse as JSON');\n    }\n\n    const result = RefEntrySchema(parsed);\n    if (result instanceof type.errors) {\n      throw errorInvalidRefFile(filePath, result.summary);\n    }\n\n    refs[name] = result;\n  }\n\n  return refs;\n}\n\n/**\n * Read a space's user-authored refs without ever throwing on disk\n * content. A ref whose JSON is unparseable or whose shape fails\n * {@link RefEntrySchema} is omitted from `refs` and reported as a\n * {@link RefLoadProblem}; the remaining well-formed refs are still\n * returned. A missing `refs/` directory yields no refs and no problems.\n *\n * `refs/head.json` is deliberately skipped here: the system head ref is\n * read through `readContractSpaceHeadRef` (which validates head-ref\n * shape, distinct from the strict user-ref hash grammar), so it is judged\n * there and never doubles as a user ref. Genuine I/O faults (EACCES, EIO,\n * …) still propagate — only parse / schema problems are made tolerant.\n */\nexport async function readRefsTolerant(refsDir: string): Promise<TolerantRefsResult> {\n  let entries: string[];\n  try {\n    entries = await readdir(refsDir, { recursive: true, encoding: 'utf-8' });\n  } catch (error) {\n    if (error instanceof Error && (error as { code?: string }).code === 'ENOENT') {\n      return { refs: {}, problems: [] };\n    }\n    throw error;\n  }\n\n  const jsonFiles = entries.filter(\n    (entry) =>\n      entry.endsWith('.json') &&\n      !entry.endsWith('.contract.json') &&\n      entry !== `${HEAD_REF_NAME}.json`,\n  );\n  const refs: Record<string, RefEntry> = {};\n  const problems: RefLoadProblem[] = [];\n\n  for (const jsonFile of jsonFiles) {\n    const filePath = join(refsDir, jsonFile);\n    const name = refNameFromPath(refsDir, filePath);\n\n    let raw: string;\n    try {\n      raw = await readFile(filePath, 'utf-8');\n    } catch (error) {\n      // Tolerate the TOCTOU race between `readdir` and `readFile` (ENOENT)\n      // and benign EISDIR if a directory happens to end in `.json`.\n      // Anything else (EACCES, EIO, …) is a real failure and propagates.\n      const code = error instanceof Error ? (error as { code?: string }).code : undefined;\n      if (code === 'ENOENT' || code === 'EISDIR') {\n        continue;\n      }\n      throw error;\n    }\n\n    let parsed: unknown;\n    try {\n      parsed = JSON.parse(raw);\n    } catch (e) {\n      problems.push({ refName: name, detail: e instanceof Error ? e.message : String(e) });\n      continue;\n    }\n\n    const result = RefEntrySchema(parsed);\n    if (result instanceof type.errors) {\n      problems.push({ refName: name, detail: result.summary });\n      continue;\n    }\n\n    refs[name] = result;\n  }\n\n  return { refs, problems };\n}\n\nexport async function writeRef(refsDir: string, name: string, entry: RefEntry): Promise<void> {\n  if (!validateRefName(name)) {\n    throw errorInvalidRefName(name);\n  }\n  if (!validateRefValue(entry.hash)) {\n    throw errorInvalidRefValue(entry.hash);\n  }\n\n  const filePath = refFilePath(refsDir, name);\n  const dir = dirname(filePath);\n  await mkdir(dir, { recursive: true });\n\n  const tmpPath = join(dir, `.${name.split('/').pop()}.json.${Date.now()}.tmp`);\n  await writeFile(\n    tmpPath,\n    `${JSON.stringify({ hash: entry.hash, invariants: [...entry.invariants] }, null, 2)}\\n`,\n  );\n  await rename(tmpPath, filePath);\n}\n\nexport async function deleteRef(refsDir: string, name: string): Promise<void> {\n  if (!validateRefName(name)) {\n    throw errorInvalidRefName(name);\n  }\n\n  const filePath = refFilePath(refsDir, name);\n  try {\n    await unlink(filePath);\n  } catch (error) {\n    if (error instanceof Error && (error as { code?: string }).code === 'ENOENT') {\n      throw new MigrationToolsError('MIGRATION.UNKNOWN_REF', `Unknown ref \"${name}\"`, {\n        why: `No ref file found at \"${filePath}\".`,\n        fix: 'Run `prisma-next ref list` to see available refs.',\n        details: { refName: name, filePath },\n      });\n    }\n    throw error;\n  }\n\n  // Clean empty parent directories up to refsDir. Stop walking on the expected\n  // \"directory has siblings\" signal (ENOTEMPTY on Linux, EEXIST on some BSDs)\n  // and on ENOENT (concurrent removal). Anything else (EACCES, EIO, …) is a\n  // real failure and propagates.\n  let dir = dirname(filePath);\n  while (dir !== refsDir && dir.startsWith(refsDir)) {\n    try {\n      await rmdir(dir);\n      dir = dirname(dir);\n    } catch (error) {\n      const code = error instanceof Error ? (error as { code?: string }).code : undefined;\n      if (code === 'ENOTEMPTY' || code === 'EEXIST' || code === 'ENOENT') {\n        break;\n      }\n      throw error;\n    }\n  }\n}\n\n/**\n * Index user-authored refs by the contract hash each ref points at.\n * Each bucket is sorted lex-asc for deterministic output.\n */\nexport function refsByContractHash(refs: Refs): ReadonlyMap<string, readonly string[]> {\n  const byHash = new Map<string, string[]>();\n  for (const [name, entry] of Object.entries(refs)) {\n    const bucket = byHash.get(entry.hash);\n    if (bucket) bucket.push(name);\n    else byHash.set(entry.hash, [name]);\n  }\n  for (const bucket of byHash.values()) {\n    bucket.sort();\n  }\n  return byHash;\n}\n\n/**\n * Read `migrations/<space>/refs/*.json` and index by destination hash.\n * Returns an empty map when the refs directory does not exist.\n */\nexport async function resolveRefsByContractHash(\n  refsDir: string,\n): Promise<ReadonlyMap<string, readonly string[]>> {\n  return refsByContractHash(await readRefs(refsDir));\n}\n\nexport function resolveRef(refs: Refs, name: string): RefEntry {\n  if (!validateRefName(name)) {\n    throw errorInvalidRefName(name);\n  }\n\n  // Object.hasOwn gate: plain-object `refs` would otherwise let\n  // `refs['constructor']` return Object.prototype.constructor and bypass the\n  // UNKNOWN_REF throw. validateRefName accepts `\"constructor\"` as a name shape.\n  if (!Object.hasOwn(refs, name)) {\n    throw new MigrationToolsError('MIGRATION.UNKNOWN_REF', `Unknown ref \"${name}\"`, {\n      why: `No ref named \"${name}\" exists.`,\n      fix: `Available refs: ${Object.keys(refs).join(', ') || '(none)'}. Create a ref with: prisma-next ref set ${name} <hash>`,\n      details: { refName: name, availableRefs: Object.keys(refs) },\n    });\n  }\n\n  // biome-ignore lint/style/noNonNullAssertion: Object.hasOwn gate above guarantees this is defined\n  return refs[name]!;\n}\n"],"mappings":";;;;;;;;;;AAsBA,MAAa,gBAAgB;AAkB7B,MAAM,mBAAmB;AACzB,MAAM,oBAAoB;AAE1B,SAAgB,gBAAgB,MAAuB;CACrD,IAAI,KAAK,WAAW,GAAG,OAAO;CAC9B,IAAI,KAAK,SAAS,IAAI,GAAG,OAAO;CAChC,IAAI,KAAK,SAAS,IAAI,GAAG,OAAO;CAChC,IAAI,KAAK,WAAW,GAAG,GAAG,OAAO;CACjC,OAAO,iBAAiB,KAAK,IAAI;AACnC;AAEA,SAAgB,iBAAiB,OAAwB;CACvD,OAAO,kBAAkB,KAAK,KAAK;AACrC;AAEA,MAAM,iBAAiB,KAAK;CAC1B,MAAM;CACN,YAAY;AACd,CAAC,CAAC,CAAC,QAAQ,OAAO,QAAQ;CACxB,IAAI,CAAC,iBAAiB,MAAM,IAAI,GAC9B,OAAO,IAAI,OAAO,+BAA+B,MAAM,KAAK,GAAG;CACjE,OAAO;AACT,CAAC;AAED,SAAS,YAAY,SAAiB,MAAsB;CAC1D,OAAO,KAAK,SAAS,GAAG,KAAK,MAAM;AACrC;AAEA,SAAS,gBAAgB,SAAiB,UAA0B;CAElE,OADY,SAAS,SAAS,QACrB,CAAC,CAAC,QAAQ,WAAW,EAAE;AAClC;AAEA,eAAsB,QAAQ,SAAiB,MAAiC;CAC9E,IAAI,CAAC,gBAAgB,IAAI,GACvB,MAAM,oBAAoB,IAAI;CAGhC,MAAM,WAAW,YAAY,SAAS,IAAI;CAC1C,IAAI;CACJ,IAAI;EACF,MAAM,MAAM,SAAS,UAAU,OAAO;CACxC,SAAS,OAAO;EACd,IAAI,iBAAiB,SAAU,MAA4B,SAAS,UAClE,MAAM,IAAI,oBAAoB,yBAAyB,gBAAgB,KAAK,IAAI;GAC9E,KAAK,yBAAyB,SAAS;GACvC,KAAK,4CAA4C,KAAK;GACtD,SAAS;IAAE,SAAS;IAAM;GAAS;EACrC,CAAC;EAEH,MAAM;CACR;CAEA,IAAI;CACJ,IAAI;EACF,SAAS,KAAK,MAAM,GAAG;CACzB,QAAQ;EACN,MAAM,oBAAoB,UAAU,yBAAyB;CAC/D;CAEA,MAAM,SAAS,eAAe,MAAM;CACpC,IAAI,kBAAkB,KAAK,QACzB,MAAM,oBAAoB,UAAU,OAAO,OAAO;CAGpD,OAAO;AACT;AAEA,eAAsB,SAAS,SAAgC;CAC7D,IAAI;CACJ,IAAI;EACF,UAAU,MAAM,QAAQ,SAAS;GAAE,WAAW;GAAM,UAAU;EAAQ,CAAC;CACzE,SAAS,OAAO;EACd,IAAI,iBAAiB,SAAU,MAA4B,SAAS,UAClE,OAAO,CAAC;EAEV,MAAM;CACR;CAEA,MAAM,YAAY,QAAQ,QACvB,UAAU,MAAM,SAAS,OAAO,KAAK,CAAC,MAAM,SAAS,gBAAgB,CACxE;CACA,MAAM,OAAiC,CAAC;CAExC,KAAK,MAAM,YAAY,WAAW;EAChC,MAAM,WAAW,KAAK,SAAS,QAAQ;EACvC,MAAM,OAAO,gBAAgB,SAAS,QAAQ;EAE9C,IAAI;EACJ,IAAI;GACF,MAAM,MAAM,SAAS,UAAU,OAAO;EACxC,SAAS,OAAO;GAKd,MAAM,OAAO,iBAAiB,QAAS,MAA4B,OAAO,KAAA;GAC1E,IAAI,SAAS,YAAY,SAAS,UAChC;GAEF,MAAM;EACR;EAEA,IAAI;EACJ,IAAI;GACF,SAAS,KAAK,MAAM,GAAG;EACzB,QAAQ;GACN,MAAM,oBAAoB,UAAU,yBAAyB;EAC/D;EAEA,MAAM,SAAS,eAAe,MAAM;EACpC,IAAI,kBAAkB,KAAK,QACzB,MAAM,oBAAoB,UAAU,OAAO,OAAO;EAGpD,KAAK,QAAQ;CACf;CAEA,OAAO;AACT;;;;;;;;;;;;;;AAeA,eAAsB,iBAAiB,SAA8C;CACnF,IAAI;CACJ,IAAI;EACF,UAAU,MAAM,QAAQ,SAAS;GAAE,WAAW;GAAM,UAAU;EAAQ,CAAC;CACzE,SAAS,OAAO;EACd,IAAI,iBAAiB,SAAU,MAA4B,SAAS,UAClE,OAAO;GAAE,MAAM,CAAC;GAAG,UAAU,CAAC;EAAE;EAElC,MAAM;CACR;CAEA,MAAM,YAAY,QAAQ,QACvB,UACC,MAAM,SAAS,OAAO,KACtB,CAAC,MAAM,SAAS,gBAAgB,KAChC,UAAU,WACd;CACA,MAAM,OAAiC,CAAC;CACxC,MAAM,WAA6B,CAAC;CAEpC,KAAK,MAAM,YAAY,WAAW;EAChC,MAAM,WAAW,KAAK,SAAS,QAAQ;EACvC,MAAM,OAAO,gBAAgB,SAAS,QAAQ;EAE9C,IAAI;EACJ,IAAI;GACF,MAAM,MAAM,SAAS,UAAU,OAAO;EACxC,SAAS,OAAO;GAId,MAAM,OAAO,iBAAiB,QAAS,MAA4B,OAAO,KAAA;GAC1E,IAAI,SAAS,YAAY,SAAS,UAChC;GAEF,MAAM;EACR;EAEA,IAAI;EACJ,IAAI;GACF,SAAS,KAAK,MAAM,GAAG;EACzB,SAAS,GAAG;GACV,SAAS,KAAK;IAAE,SAAS;IAAM,QAAQ,aAAa,QAAQ,EAAE,UAAU,OAAO,CAAC;GAAE,CAAC;GACnF;EACF;EAEA,MAAM,SAAS,eAAe,MAAM;EACpC,IAAI,kBAAkB,KAAK,QAAQ;GACjC,SAAS,KAAK;IAAE,SAAS;IAAM,QAAQ,OAAO;GAAQ,CAAC;GACvD;EACF;EAEA,KAAK,QAAQ;CACf;CAEA,OAAO;EAAE;EAAM;CAAS;AAC1B;AAEA,eAAsB,SAAS,SAAiB,MAAc,OAAgC;CAC5F,IAAI,CAAC,gBAAgB,IAAI,GACvB,MAAM,oBAAoB,IAAI;CAEhC,IAAI,CAAC,iBAAiB,MAAM,IAAI,GAC9B,MAAM,qBAAqB,MAAM,IAAI;CAGvC,MAAM,WAAW,YAAY,SAAS,IAAI;CAC1C,MAAM,MAAM,QAAQ,QAAQ;CAC5B,MAAM,MAAM,KAAK,EAAE,WAAW,KAAK,CAAC;CAEpC,MAAM,UAAU,KAAK,KAAK,IAAI,KAAK,MAAM,GAAG,CAAC,CAAC,IAAI,EAAE,QAAQ,KAAK,IAAI,EAAE,KAAK;CAC5E,MAAM,UACJ,SACA,GAAG,KAAK,UAAU;EAAE,MAAM,MAAM;EAAM,YAAY,CAAC,GAAG,MAAM,UAAU;CAAE,GAAG,MAAM,CAAC,EAAE,GACtF;CACA,MAAM,OAAO,SAAS,QAAQ;AAChC;AAEA,eAAsB,UAAU,SAAiB,MAA6B;CAC5E,IAAI,CAAC,gBAAgB,IAAI,GACvB,MAAM,oBAAoB,IAAI;CAGhC,MAAM,WAAW,YAAY,SAAS,IAAI;CAC1C,IAAI;EACF,MAAM,OAAO,QAAQ;CACvB,SAAS,OAAO;EACd,IAAI,iBAAiB,SAAU,MAA4B,SAAS,UAClE,MAAM,IAAI,oBAAoB,yBAAyB,gBAAgB,KAAK,IAAI;GAC9E,KAAK,yBAAyB,SAAS;GACvC,KAAK;GACL,SAAS;IAAE,SAAS;IAAM;GAAS;EACrC,CAAC;EAEH,MAAM;CACR;CAMA,IAAI,MAAM,QAAQ,QAAQ;CAC1B,OAAO,QAAQ,WAAW,IAAI,WAAW,OAAO,GAC9C,IAAI;EACF,MAAM,MAAM,GAAG;EACf,MAAM,QAAQ,GAAG;CACnB,SAAS,OAAO;EACd,MAAM,OAAO,iBAAiB,QAAS,MAA4B,OAAO,KAAA;EAC1E,IAAI,SAAS,eAAe,SAAS,YAAY,SAAS,UACxD;EAEF,MAAM;CACR;AAEJ;;;;;AAMA,SAAgB,mBAAmB,MAAoD;CACrF,MAAM,yBAAS,IAAI,IAAsB;CACzC,KAAK,MAAM,CAAC,MAAM,UAAU,OAAO,QAAQ,IAAI,GAAG;EAChD,MAAM,SAAS,OAAO,IAAI,MAAM,IAAI;EACpC,IAAI,QAAQ,OAAO,KAAK,IAAI;OACvB,OAAO,IAAI,MAAM,MAAM,CAAC,IAAI,CAAC;CACpC;CACA,KAAK,MAAM,UAAU,OAAO,OAAO,GACjC,OAAO,KAAK;CAEd,OAAO;AACT;;;;;AAMA,eAAsB,0BACpB,SACiD;CACjD,OAAO,mBAAmB,MAAM,SAAS,OAAO,CAAC;AACnD;AAEA,SAAgB,WAAW,MAAY,MAAwB;CAC7D,IAAI,CAAC,gBAAgB,IAAI,GACvB,MAAM,oBAAoB,IAAI;CAMhC,IAAI,CAAC,OAAO,OAAO,MAAM,IAAI,GAC3B,MAAM,IAAI,oBAAoB,yBAAyB,gBAAgB,KAAK,IAAI;EAC9E,KAAK,iBAAiB,KAAK;EAC3B,KAAK,mBAAmB,OAAO,KAAK,IAAI,CAAC,CAAC,KAAK,IAAI,KAAK,SAAS,2CAA2C,KAAK;EACjH,SAAS;GAAE,SAAS;GAAM,eAAe,OAAO,KAAK,IAAI;EAAE;CAC7D,CAAC;CAIH,OAAO,KAAK;AACd"}