{"version":3,"file":"pairing-storage.d.ts","sourceRoot":"","sources":["../../src/server/pairing-storage.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAKH,OAAO,EAA8C,KAAK,YAAY,EAAsB,MAAM,WAAW,CAAC;AAuB9G;;;;GAIG;AACH,wBAAgB,2BAA2B,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAoBhE;AAED,qBAAa,kBAAkB;IAClB,OAAO,CAAC,QAAQ,CAAC,IAAI;IAAjC,YAA6B,IAAI,EAAE,MAAM,EAAI;IAEvC,IAAI,IAAI,OAAO,CAAC,YAAY,CAAC,CAqClC;IAEK,IAAI,CAAC,KAAK,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CAY7C;CACD","sourcesContent":["/**\n * File-backed pairing storage — persists paired devices under the dreb agent\n * dir so pairings survive dashboard restarts. Written with mode 0600.\n */\n\nimport { randomBytes } from \"node:crypto\";\nimport { mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from \"node:fs\";\nimport { basename, dirname, join } from \"node:path\";\nimport { MAX_PAIRING_TTL_DAYS, MIN_PAIRING_TTL_DAYS, type PairingState, type StoredPairing } from \"./auth.js\";\n\ninterface PairingFileV1 {\n\tversion: 1;\n\tpairings: StoredPairing[];\n\tconsumedPairingWindows?: number[];\n}\n\ninterface PairingFileV2 {\n\tversion: 2;\n\tpairings: StoredPairing[];\n\tconsumedPairingWindows: number[];\n\tpairingTtlDays?: number;\n}\n\ntype PairingFile = PairingFileV1 | PairingFileV2;\n\nfunction isUtcDate(value: unknown): value is string {\n\tif (typeof value !== \"string\" || !/^\\d{4}-\\d{2}-\\d{2}$/.test(value)) return false;\n\tconst parsed = Date.parse(`${value}T00:00:00.000Z`);\n\treturn Number.isFinite(parsed) && new Date(parsed).toISOString().slice(0, 10) === value;\n}\n\n/**\n * Load or create the per-install dashboard auth secret. This secret keys both\n * device-token HMACs and the rotating pairing code, so it must survive process\n * restarts but must never be shared across installs/servers.\n */\nexport function loadOrCreateDashboardSecret(path: string): Buffer {\n\ttry {\n\t\tconst raw = readFileSync(path, \"utf8\").trim();\n\t\tif (!/^[0-9a-f]{64}$/i.test(raw)) throw new Error(`Invalid dashboard auth secret at ${path}`);\n\t\treturn Buffer.from(raw, \"hex\");\n\t} catch (err) {\n\t\tif ((err as NodeJS.ErrnoException).code !== \"ENOENT\") throw err;\n\t}\n\n\tmkdirSync(dirname(path), { recursive: true });\n\tconst secret = randomBytes(32);\n\ttry {\n\t\twriteFileSync(path, `${secret.toString(\"hex\")}\\n`, { mode: 0o600, flag: \"wx\" });\n\t\treturn secret;\n\t} catch (err) {\n\t\tif ((err as NodeJS.ErrnoException).code !== \"EEXIST\") throw err;\n\t\tconst raw = readFileSync(path, \"utf8\").trim();\n\t\tif (!/^[0-9a-f]{64}$/i.test(raw)) throw new Error(`Invalid dashboard auth secret at ${path}`);\n\t\treturn Buffer.from(raw, \"hex\");\n\t}\n}\n\nexport class FilePairingStorage {\n\tconstructor(private readonly path: string) {}\n\n\tasync load(): Promise<PairingState> {\n\t\tlet raw: string;\n\t\ttry {\n\t\t\traw = readFileSync(this.path, \"utf8\");\n\t\t} catch (err) {\n\t\t\tif ((err as NodeJS.ErrnoException).code === \"ENOENT\") {\n\t\t\t\treturn { pairings: [], consumedPairingWindows: [] };\n\t\t\t}\n\t\t\tthrow err;\n\t\t}\n\t\tconst parsed = JSON.parse(raw) as PairingFile;\n\t\tif (\n\t\t\t(parsed.version !== 1 && parsed.version !== 2) ||\n\t\t\t!Array.isArray(parsed.pairings) ||\n\t\t\t(parsed.version === 2 && !Array.isArray(parsed.consumedPairingWindows)) ||\n\t\t\t(parsed.consumedPairingWindows !== undefined &&\n\t\t\t\t(!Array.isArray(parsed.consumedPairingWindows) ||\n\t\t\t\t\t!parsed.consumedPairingWindows.every((window) => Number.isSafeInteger(window)))) ||\n\t\t\t(parsed.version === 2 &&\n\t\t\t\tparsed.pairingTtlDays !== undefined &&\n\t\t\t\t(!Number.isSafeInteger(parsed.pairingTtlDays) ||\n\t\t\t\t\tparsed.pairingTtlDays < MIN_PAIRING_TTL_DAYS ||\n\t\t\t\t\tparsed.pairingTtlDays > MAX_PAIRING_TTL_DAYS)) ||\n\t\t\t!parsed.pairings.every(\n\t\t\t\t(pairing) =>\n\t\t\t\t\tpairing &&\n\t\t\t\t\ttypeof pairing === \"object\" &&\n\t\t\t\t\t(pairing.lastExpiryWarningUtcDate === undefined || isUtcDate(pairing.lastExpiryWarningUtcDate)),\n\t\t\t)\n\t\t) {\n\t\t\tthrow new Error(`Unrecognized pairing file format at ${this.path}`);\n\t\t}\n\t\treturn {\n\t\t\tpairings: parsed.pairings,\n\t\t\tconsumedPairingWindows: parsed.consumedPairingWindows ?? [],\n\t\t\tpairingTtlDays: parsed.version === 2 ? parsed.pairingTtlDays : undefined,\n\t\t};\n\t}\n\n\tasync save(state: PairingState): Promise<void> {\n\t\tconst dir = dirname(this.path);\n\t\tmkdirSync(dir, { recursive: true });\n\t\tconst file: PairingFileV2 = { version: 2, ...state };\n\t\tconst tmp = join(dir, `.${basename(this.path)}.${process.pid}.${randomBytes(6).toString(\"hex\")}.tmp`);\n\t\ttry {\n\t\t\twriteFileSync(tmp, `${JSON.stringify(file, null, \"\\t\")}\\n`, { mode: 0o600, flag: \"wx\" });\n\t\t\trenameSync(tmp, this.path);\n\t\t} catch (err) {\n\t\t\trmSync(tmp, { force: true });\n\t\t\tthrow err;\n\t\t}\n\t}\n}\n"]}