@dreb/dashboard 2.55.4 → 2.55.6

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.
@@ -20,6 +20,10 @@ import { createHmac, randomBytes, timingSafeEqual } from "node:crypto";
20
20
  import { promisify } from "node:util";
21
21
  const execFileAsync = promisify(execFile);
22
22
  const PAIRING_CODE_STEP_MS = 30_000;
23
+ const DAY_MS = 24 * 60 * 60 * 1000;
24
+ export const DEFAULT_PAIRING_TTL_DAYS = 180;
25
+ export const MIN_PAIRING_TTL_DAYS = 1;
26
+ export const MAX_PAIRING_TTL_DAYS = 3650;
23
27
  const DEFAULT_PAIRING_MAX_ATTEMPTS = 5;
24
28
  const DEFAULT_PAIRING_LOCKOUT_MS = 60_000;
25
29
  // ---------------------------------------------------------------------------
@@ -69,52 +73,116 @@ export function isAllowedLocalHost(hostHeader) {
69
73
  }
70
74
  return host === "localhost" || host === "::1" || /^127\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(host);
71
75
  }
72
- /** Resolves identities via `tailscale status --json`. Any failure resolves null (deny). */
73
- export class TailscaleStatusResolver {
76
+ export class TailscaleResolverError extends Error {
77
+ kind;
78
+ constructor(kind) {
79
+ super(`Tailscale identity resolver ${kind} failure`);
80
+ this.kind = kind;
81
+ this.name = "TailscaleResolverError";
82
+ }
83
+ }
84
+ async function runTailscaleWhois(address) {
85
+ return execFileAsync("tailscale", ["whois", "--json", address], {
86
+ timeout: 3000,
87
+ maxBuffer: 1024 * 1024,
88
+ });
89
+ }
90
+ function isPeerNotFoundError(error) {
91
+ const stderr = error?.stderr;
92
+ return typeof stderr === "string" && stderr.trim().toLowerCase() === "peer not found";
93
+ }
94
+ function isTimeoutError(error) {
95
+ const candidate = error;
96
+ return candidate?.killed === true || candidate?.signal === "SIGTERM";
97
+ }
98
+ /** Peer-specific Tailscale identity resolution with same-peer in-flight coalescing. */
99
+ export class TailscaleWhoisResolver {
100
+ runWhois;
101
+ inFlight = new Map();
102
+ constructor(runWhois = runTailscaleWhois) {
103
+ this.runWhois = runWhois;
104
+ }
74
105
  async resolve(address) {
75
106
  const target = normalizeAddress(address);
76
107
  if (!target)
77
108
  return null;
78
- let status;
109
+ const existing = this.inFlight.get(target);
110
+ if (existing)
111
+ return existing;
112
+ const lookup = this.lookup(target);
113
+ this.inFlight.set(target, lookup);
79
114
  try {
80
- const { stdout } = await execFileAsync("tailscale", ["status", "--json"], {
81
- timeout: 3000,
82
- maxBuffer: 4 * 1024 * 1024,
83
- });
84
- status = JSON.parse(stdout);
115
+ return await lookup;
85
116
  }
86
- catch {
87
- // Tailscale absent, not running, or unparseable — fail closed.
88
- return null;
117
+ finally {
118
+ if (this.inFlight.get(target) === lookup)
119
+ this.inFlight.delete(target);
89
120
  }
90
- const peers = Object.values(status.Peer ?? {});
91
- if (status.Self)
92
- peers.push(status.Self);
93
- for (const peer of peers) {
94
- if (!peer.TailscaleIPs?.some((ip) => normalizeAddress(ip) === target))
95
- continue;
96
- const userId = peer.UserID;
97
- const loginName = userId !== undefined ? status.User?.[String(userId)]?.LoginName : undefined;
98
- if (!loginName)
99
- return null; // identity unknown — deny rather than guess
100
- return { loginName, device: peer.HostName };
121
+ }
122
+ async lookup(target) {
123
+ let stdout;
124
+ try {
125
+ ({ stdout } = await this.runWhois(target));
126
+ }
127
+ catch (error) {
128
+ if (isTimeoutError(error))
129
+ throw new TailscaleResolverError("timeout");
130
+ if (isPeerNotFoundError(error))
131
+ return null;
132
+ throw new TailscaleResolverError("execution");
133
+ }
134
+ let whois;
135
+ try {
136
+ const parsed = JSON.parse(stdout);
137
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
138
+ throw new TailscaleResolverError("schema");
139
+ }
140
+ whois = parsed;
141
+ }
142
+ catch (error) {
143
+ if (error instanceof TailscaleResolverError)
144
+ throw error;
145
+ throw new TailscaleResolverError("parse");
146
+ }
147
+ if (!whois.Node || typeof whois.Node !== "object" || Array.isArray(whois.Node)) {
148
+ throw new TailscaleResolverError("schema");
149
+ }
150
+ if (whois.Node.Name !== undefined && typeof whois.Node.Name !== "string") {
151
+ throw new TailscaleResolverError("schema");
152
+ }
153
+ if (whois.UserProfile !== undefined && whois.UserProfile !== null) {
154
+ if (typeof whois.UserProfile !== "object" || Array.isArray(whois.UserProfile)) {
155
+ throw new TailscaleResolverError("schema");
156
+ }
157
+ if (whois.UserProfile.LoginName !== undefined && typeof whois.UserProfile.LoginName !== "string") {
158
+ throw new TailscaleResolverError("schema");
159
+ }
101
160
  }
102
- return null;
161
+ const loginName = whois.UserProfile?.LoginName?.trim();
162
+ if (!loginName)
163
+ return null;
164
+ const device = whois.Node.Name?.replace(/\.$/, "") || undefined;
165
+ return { loginName, device };
103
166
  }
104
167
  }
168
+ /** @deprecated Use TailscaleWhoisResolver. Retained for API compatibility. */
169
+ export class TailscaleStatusResolver extends TailscaleWhoisResolver {
170
+ }
105
171
  /** In-memory storage — used in tests and as the base for the file store. */
106
172
  export class MemoryPairingStorage {
107
173
  state = { pairings: [], consumedPairingWindows: [] };
108
174
  async load() {
109
175
  return {
110
- pairings: [...this.state.pairings],
176
+ pairings: this.state.pairings.map((pairing) => ({ ...pairing })),
111
177
  consumedPairingWindows: [...this.state.consumedPairingWindows],
178
+ pairingTtlDays: this.state.pairingTtlDays,
112
179
  };
113
180
  }
114
181
  async save(state) {
115
182
  this.state = {
116
- pairings: [...state.pairings],
183
+ pairings: state.pairings.map((pairing) => ({ ...pairing })),
117
184
  consumedPairingWindows: [...state.consumedPairingWindows],
185
+ pairingTtlDays: state.pairingTtlDays,
118
186
  };
119
187
  }
120
188
  }
@@ -128,7 +196,7 @@ function timingSafeEqualStr(a, b) {
128
196
  export class DashboardAuth {
129
197
  remoteEnabled;
130
198
  allowedIdentities;
131
- pairingTtlMs;
199
+ defaultPairingTtlMs;
132
200
  resolver;
133
201
  storage;
134
202
  secret;
@@ -141,8 +209,8 @@ export class DashboardAuth {
141
209
  constructor(options = {}) {
142
210
  this.remoteEnabled = options.remoteEnabled ?? false;
143
211
  this.allowedIdentities = new Set(options.allowedIdentities ?? []);
144
- this.pairingTtlMs = options.pairingTtlMs ?? 30 * 24 * 60 * 60 * 1000;
145
- this.resolver = options.resolver ?? new TailscaleStatusResolver();
212
+ this.defaultPairingTtlMs = options.pairingTtlMs ?? DEFAULT_PAIRING_TTL_DAYS * DAY_MS;
213
+ this.resolver = options.resolver ?? new TailscaleWhoisResolver();
146
214
  this.storage = options.storage ?? new MemoryPairingStorage();
147
215
  this.secret = options.secret ?? randomBytes(32);
148
216
  this.pairingMaxAttempts = options.pairingMaxAttempts ?? DEFAULT_PAIRING_MAX_ATTEMPTS;
@@ -153,6 +221,30 @@ export class DashboardAuth {
153
221
  get isRemoteEnabled() {
154
222
  return this.remoteEnabled;
155
223
  }
224
+ defaultPairingTtlDays() {
225
+ const days = this.defaultPairingTtlMs / DAY_MS;
226
+ return Number.isSafeInteger(days) && days >= MIN_PAIRING_TTL_DAYS && days <= MAX_PAIRING_TTL_DAYS
227
+ ? days
228
+ : DEFAULT_PAIRING_TTL_DAYS;
229
+ }
230
+ async getPairingSettings() {
231
+ return this.withPairingMutation(async () => {
232
+ const state = await this.loadLiveState();
233
+ return { pairingTtlDays: state.pairingTtlDays ?? this.defaultPairingTtlDays() };
234
+ });
235
+ }
236
+ async setPairingSettings(pairingTtlDays) {
237
+ if (!Number.isSafeInteger(pairingTtlDays) ||
238
+ pairingTtlDays < MIN_PAIRING_TTL_DAYS ||
239
+ pairingTtlDays > MAX_PAIRING_TTL_DAYS) {
240
+ throw Object.assign(new Error(`pairingTtlDays must be a whole number from ${MIN_PAIRING_TTL_DAYS} through ${MAX_PAIRING_TTL_DAYS}`), { status: 400 });
241
+ }
242
+ return this.withPairingMutation(async () => {
243
+ const state = await this.loadLiveState();
244
+ await this.storage.save({ ...state, pairingTtlDays });
245
+ return { pairingTtlDays };
246
+ });
247
+ }
156
248
  hmac(value) {
157
249
  return createHmac("sha256", this.secret).update(value).digest("hex");
158
250
  }
@@ -250,6 +342,9 @@ export class DashboardAuth {
250
342
  return await this.authenticateInner(info);
251
343
  }
252
344
  catch (err) {
345
+ if (err instanceof TailscaleResolverError) {
346
+ this.logger(`identity resolver ${err.kind} failure — denying`);
347
+ }
253
348
  return {
254
349
  allowed: false,
255
350
  status: 500,
@@ -299,7 +394,8 @@ export class DashboardAuth {
299
394
  identity,
300
395
  };
301
396
  }
302
- if (!info.deviceToken || !(await this.isPaired(identity, info.deviceToken))) {
397
+ const pairing = info.deviceToken ? await this.findPairing(identity, info.deviceToken) : undefined;
398
+ if (!pairing) {
303
399
  return {
304
400
  allowed: false,
305
401
  status: 401,
@@ -308,7 +404,7 @@ export class DashboardAuth {
308
404
  identity,
309
405
  };
310
406
  }
311
- return { allowed: true, mode: "remote", identity };
407
+ return { allowed: true, mode: "remote", identity, pairing: this.toPairedDevice(pairing) };
312
408
  }
313
409
  /**
314
410
  * Complete pairing for an allowed remote identity using the current rotating
@@ -336,26 +432,36 @@ export class DashboardAuth {
336
432
  }
337
433
  const token = randomBytes(32).toString("base64url");
338
434
  const nowMs = this.now();
435
+ const pairingTtlMs = state.pairingTtlDays === undefined ? this.defaultPairingTtlMs : state.pairingTtlDays * DAY_MS;
339
436
  const device = {
340
437
  id: randomBytes(8).toString("hex"),
341
438
  identity: identity.loginName,
342
439
  device: identity.device,
343
440
  createdAt: new Date(nowMs).toISOString(),
344
- expiresAt: new Date(nowMs + this.pairingTtlMs).toISOString(),
441
+ expiresAt: new Date(nowMs + pairingTtlMs).toISOString(),
345
442
  };
346
443
  const pairings = [...state.pairings, { ...device, tokenHmac: this.hmac(token) }];
347
444
  const consumedPairingWindows = this.pruneConsumedPairingWindows([
348
445
  ...state.consumedPairingWindows,
349
446
  matchedWindow,
350
447
  ]);
351
- await this.storage.save({ pairings, consumedPairingWindows });
448
+ await this.storage.save({ ...state, pairings, consumedPairingWindows });
352
449
  this.clearPairingFailures(failureKey);
353
450
  return { token, device };
354
451
  });
355
452
  }
453
+ toPairedDevice(pairing) {
454
+ return {
455
+ id: pairing.id,
456
+ identity: pairing.identity,
457
+ device: pairing.device,
458
+ createdAt: pairing.createdAt,
459
+ expiresAt: pairing.expiresAt,
460
+ };
461
+ }
356
462
  /** List paired devices (live only). */
357
463
  async listDevices() {
358
- return this.withPairingMutation(async () => (await this.loadLive()).map(({ tokenHmac: _tokenHmac, ...device }) => device));
464
+ return this.withPairingMutation(async () => (await this.loadLive()).map((pairing) => this.toPairedDevice(pairing)));
359
465
  }
360
466
  /** Remove a paired device by id. Returns true when something was removed. */
361
467
  async unpair(deviceId) {
@@ -368,11 +474,38 @@ export class DashboardAuth {
368
474
  return true;
369
475
  });
370
476
  }
371
- async isPaired(identity, token) {
477
+ async findPairing(identity, token) {
372
478
  const tokenHmac = this.hmac(token);
373
479
  return this.withPairingMutation(async () => {
374
480
  const live = await this.loadLive();
375
- return live.some((p) => p.identity === identity.loginName && timingSafeEqualStr(p.tokenHmac, tokenHmac));
481
+ return live.find((p) => p.identity === identity.loginName && timingSafeEqualStr(p.tokenHmac, tokenHmac));
482
+ });
483
+ }
484
+ /** Atomically claim any due warning and return the next useful browser check time. */
485
+ async claimPairingExpiryStatus(pairingId) {
486
+ return this.withPairingMutation(async () => {
487
+ const state = await this.loadLiveState();
488
+ const pairing = state.pairings.find((candidate) => candidate.id === pairingId);
489
+ if (!pairing)
490
+ return {};
491
+ const nowMs = this.now();
492
+ const createdAt = Date.parse(pairing.createdAt);
493
+ const expiresAt = Date.parse(pairing.expiresAt);
494
+ const originalValidity = expiresAt - createdAt;
495
+ const remaining = expiresAt - nowMs;
496
+ if (!Number.isFinite(originalValidity) || originalValidity <= 0 || remaining <= 0)
497
+ return {};
498
+ const warningStartsAt = expiresAt - originalValidity * 0.1;
499
+ if (nowMs < warningStartsAt)
500
+ return { nextCheckAt: new Date(warningStartsAt).toISOString() };
501
+ const utcDate = new Date(nowMs).toISOString().slice(0, 10);
502
+ const nextUtcMidnight = Date.parse(`${utcDate}T00:00:00.000Z`) + DAY_MS;
503
+ const nextCheckAt = nextUtcMidnight < expiresAt ? new Date(nextUtcMidnight).toISOString() : undefined;
504
+ if (pairing.lastExpiryWarningUtcDate === utcDate)
505
+ return { nextCheckAt };
506
+ const pairings = state.pairings.map((candidate) => candidate.id === pairingId ? { ...candidate, lastExpiryWarningUtcDate: utcDate } : candidate);
507
+ await this.storage.save({ ...state, pairings });
508
+ return { warning: { expiresAt: pairing.expiresAt }, nextCheckAt };
376
509
  });
377
510
  }
378
511
  /** Load pairings, dropping (and persisting the removal of) expired entries. Caller must hold pairingMutation. */
@@ -388,9 +521,9 @@ export class DashboardAuth {
388
521
  if (savePrunedState &&
389
522
  (pairings.length !== state.pairings.length ||
390
523
  !this.samePairingWindows(consumedPairingWindows, state.consumedPairingWindows))) {
391
- await this.storage.save({ pairings, consumedPairingWindows });
524
+ await this.storage.save({ ...state, pairings, consumedPairingWindows });
392
525
  }
393
- return { pairings, consumedPairingWindows };
526
+ return { ...state, pairings, consumedPairingWindows };
394
527
  }
395
528
  }
396
529
  //# sourceMappingURL=auth.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"auth.js","sourceRoot":"","sources":["../../src/server/auth.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Dashboard auth — exactly two modes (this file is the authority):\n *\n * Mode A — local-only (default): the server binds loopback only. Requests are\n * additionally checked for loopback source address AND an allowlisted Host\n * header (DNS-rebinding defense: a malicious website can point its own domain\n * at 127.0.0.1 and drive the API from the victim's browser unless Host is\n * validated). No login, no pairing.\n *\n * Mode B — remote (explicit opt-in): requires Tailscale. Enforcement layers,\n * all fail-closed: (1) Tailscale identity resolution of the peer address,\n * (2) identity allowlist (empty allowlist = deny all), (3) first-login\n * rotating pairing code (visible only from the host/local dashboard),\n * (4) signed per-device cookie thereafter.\n *\n * There is no LAN mode. Any auth-subsystem error denies the request.\n */\n\nimport { execFile } from \"node:child_process\";\nimport { createHmac, randomBytes, timingSafeEqual } from \"node:crypto\";\nimport { promisify } from \"node:util\";\n\nconst execFileAsync = promisify(execFile);\nconst PAIRING_CODE_STEP_MS = 30_000;\nconst DEFAULT_PAIRING_MAX_ATTEMPTS = 5;\nconst DEFAULT_PAIRING_LOCKOUT_MS = 60_000;\n\n// ---------------------------------------------------------------------------\n// Address / Host helpers\n// ---------------------------------------------------------------------------\n\n/** Normalize an address for comparison (strip IPv6-mapped IPv4 prefix and zone). */\nexport function normalizeAddress(address: string | undefined): string {\n\tif (!address) return \"\";\n\tlet a = address.trim();\n\tif (a.startsWith(\"[\") && a.endsWith(\"]\")) a = a.slice(1, -1);\n\tconst zone = a.indexOf(\"%\");\n\tif (zone !== -1) a = a.slice(0, zone);\n\tif (a.startsWith(\"::ffff:\")) a = a.slice(7);\n\treturn a;\n}\n\n/** True when the (normalized) address is a loopback address. */\nexport function isLoopbackAddress(address: string | undefined): boolean {\n\tconst a = normalizeAddress(address);\n\tif (!a) return false;\n\tif (a === \"::1\") return true;\n\treturn /^127\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}$/.test(a);\n}\n\n/**\n * Validate a Host header against the loopback allowlist. Rejecting foreign\n * hosts breaks DNS rebinding: the attacker's page can reach 127.0.0.1, but its\n * requests carry the attacker's hostname in Host.\n */\nexport function isAllowedLocalHost(hostHeader: string | undefined): boolean {\n\tif (!hostHeader) return false;\n\t// Strip port. IPv6 hosts arrive as \"[::1]:port\".\n\tlet host = hostHeader.trim().toLowerCase();\n\tconst v6 = host.match(/^\\[([^\\]]+)\\](?::\\d+)?$/);\n\tif (v6) {\n\t\thost = v6[1];\n\t} else {\n\t\tconst colon = host.lastIndexOf(\":\");\n\t\tif (colon !== -1 && /^\\d+$/.test(host.slice(colon + 1))) host = host.slice(0, colon);\n\t}\n\treturn host === \"localhost\" || host === \"::1\" || /^127\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}$/.test(host);\n}\n\n// ---------------------------------------------------------------------------\n// Tailscale identity\n// ---------------------------------------------------------------------------\n\nexport interface TailscaleIdentity {\n\t/** Login name (e.g. \"alice@example.com\") — the allowlist unit. */\n\tloginName: string;\n\t/** Device host name, when known. */\n\tdevice?: string;\n}\n\nexport interface TailscaleResolver {\n\t/** Resolve a peer IP to a Tailscale identity, or null when unknown. */\n\tresolve(address: string): Promise<TailscaleIdentity | null>;\n}\n\ninterface TailscaleStatusPeer {\n\tTailscaleIPs?: string[];\n\tHostName?: string;\n\tUserID?: number;\n}\n\ninterface TailscaleStatusJson {\n\tSelf?: TailscaleStatusPeer & { UserID?: number };\n\tPeer?: Record<string, TailscaleStatusPeer>;\n\tUser?: Record<string, { LoginName?: string }>;\n}\n\n/** Resolves identities via `tailscale status --json`. Any failure resolves null (deny). */\nexport class TailscaleStatusResolver implements TailscaleResolver {\n\tasync resolve(address: string): Promise<TailscaleIdentity | null> {\n\t\tconst target = normalizeAddress(address);\n\t\tif (!target) return null;\n\t\tlet status: TailscaleStatusJson;\n\t\ttry {\n\t\t\tconst { stdout } = await execFileAsync(\"tailscale\", [\"status\", \"--json\"], {\n\t\t\t\ttimeout: 3000,\n\t\t\t\tmaxBuffer: 4 * 1024 * 1024,\n\t\t\t});\n\t\t\tstatus = JSON.parse(stdout) as TailscaleStatusJson;\n\t\t} catch {\n\t\t\t// Tailscale absent, not running, or unparseable — fail closed.\n\t\t\treturn null;\n\t\t}\n\t\tconst peers = Object.values(status.Peer ?? {});\n\t\tif (status.Self) peers.push(status.Self);\n\t\tfor (const peer of peers) {\n\t\t\tif (!peer.TailscaleIPs?.some((ip) => normalizeAddress(ip) === target)) continue;\n\t\t\tconst userId = peer.UserID;\n\t\t\tconst loginName = userId !== undefined ? status.User?.[String(userId)]?.LoginName : undefined;\n\t\t\tif (!loginName) return null; // identity unknown — deny rather than guess\n\t\t\treturn { loginName, device: peer.HostName };\n\t\t}\n\t\treturn null;\n\t}\n}\n\n// ---------------------------------------------------------------------------\n// Pairing store (rotating pairing codes + device tokens)\n// ---------------------------------------------------------------------------\n\nexport interface PairedDevice {\n\tid: string;\n\tidentity: string;\n\tdevice?: string;\n\tcreatedAt: string;\n\texpiresAt: string;\n}\n\nexport interface StoredPairing extends PairedDevice {\n\t/** HMAC of the device token (raw token never stored). */\n\ttokenHmac: string;\n}\n\nexport interface PairingState {\n\tpairings: StoredPairing[];\n\tconsumedPairingWindows: number[];\n}\n\nexport interface PairingStorage {\n\tload(): Promise<PairingState>;\n\tsave(state: PairingState): Promise<void>;\n}\n\n/** In-memory storage — used in tests and as the base for the file store. */\nexport class MemoryPairingStorage implements PairingStorage {\n\tprivate state: PairingState = { pairings: [], consumedPairingWindows: [] };\n\tasync load(): Promise<PairingState> {\n\t\treturn {\n\t\t\tpairings: [...this.state.pairings],\n\t\t\tconsumedPairingWindows: [...this.state.consumedPairingWindows],\n\t\t};\n\t}\n\tasync save(state: PairingState): Promise<void> {\n\t\tthis.state = {\n\t\t\tpairings: [...state.pairings],\n\t\t\tconsumedPairingWindows: [...state.consumedPairingWindows],\n\t\t};\n\t}\n}\n\nexport interface DashboardAuthOptions {\n\t/** Remote (Tailscale) mode. Default false — loopback only. */\n\tremoteEnabled?: boolean;\n\t/** Allowed Tailscale login names. Empty = deny all remote. */\n\tallowedIdentities?: string[];\n\t/** Device pairing validity. Default 30 days. */\n\tpairingTtlMs?: number;\n\tresolver?: TailscaleResolver;\n\tstorage?: PairingStorage;\n\t/** HMAC/TOTP secret for device tokens and pairing codes. Production passes a per-install persisted secret. */\n\tsecret?: Buffer;\n\t/** Failed PIN attempts before temporary lockout. Default 5. */\n\tpairingMaxAttempts?: number;\n\t/** Temporary lockout duration after too many failed PIN attempts. Default 60s. */\n\tpairingLockoutMs?: number;\n\t/** Security/audit log sink for repeated failed pairing attempts. */\n\tlogger?: (line: string) => void;\n\t/** Clock override for tests. */\n\tnow?: () => number;\n}\n\nexport type AuthDecision =\n\t| { allowed: true; mode: \"local\" }\n\t| { allowed: true; mode: \"remote\"; identity: TailscaleIdentity }\n\t| {\n\t\t\tallowed: false;\n\t\t\tstatus: number;\n\t\t\treason: string;\n\t\t\t/** Set when an allowed identity needs pairing-code entry. */\n\t\t\tneedsPairing?: boolean;\n\t\t\tidentity?: TailscaleIdentity;\n\t };\n\nexport interface AuthRequestInfo {\n\tremoteAddress: string | undefined;\n\thostHeader: string | undefined;\n\t/** Origin header when present. Non-loopback origins are rejected on local requests. */\n\toriginHeader: string | undefined;\n\t/** Value of the dashboard device cookie, when present. */\n\tdeviceToken: string | undefined;\n}\n\nfunction timingSafeEqualStr(a: string, b: string): boolean {\n\tconst ab = Buffer.from(a);\n\tconst bb = Buffer.from(b);\n\tif (ab.length !== bb.length) return false;\n\treturn timingSafeEqual(ab, bb);\n}\n\nexport class DashboardAuth {\n\tprivate readonly remoteEnabled: boolean;\n\tprivate readonly allowedIdentities: Set<string>;\n\tprivate readonly pairingTtlMs: number;\n\tprivate readonly resolver: TailscaleResolver;\n\tprivate readonly storage: PairingStorage;\n\tprivate readonly secret: Buffer;\n\tprivate readonly pairingMaxAttempts: number;\n\tprivate readonly pairingLockoutMs: number;\n\tprivate readonly logger: (line: string) => void;\n\tprivate readonly now: () => number;\n\tprivate pairingMutation: Promise<void> = Promise.resolve();\n\tprivate readonly pairingFailures = new Map<string, { count: number; lockedUntil?: number }>();\n\n\tconstructor(options: DashboardAuthOptions = {}) {\n\t\tthis.remoteEnabled = options.remoteEnabled ?? false;\n\t\tthis.allowedIdentities = new Set(options.allowedIdentities ?? []);\n\t\tthis.pairingTtlMs = options.pairingTtlMs ?? 30 * 24 * 60 * 60 * 1000;\n\t\tthis.resolver = options.resolver ?? new TailscaleStatusResolver();\n\t\tthis.storage = options.storage ?? new MemoryPairingStorage();\n\t\tthis.secret = options.secret ?? randomBytes(32);\n\t\tthis.pairingMaxAttempts = options.pairingMaxAttempts ?? DEFAULT_PAIRING_MAX_ATTEMPTS;\n\t\tthis.pairingLockoutMs = options.pairingLockoutMs ?? DEFAULT_PAIRING_LOCKOUT_MS;\n\t\tthis.logger = options.logger ?? (() => {});\n\t\tthis.now = options.now ?? Date.now;\n\t}\n\n\tget isRemoteEnabled(): boolean {\n\t\treturn this.remoteEnabled;\n\t}\n\n\tprivate hmac(value: string): string {\n\t\treturn createHmac(\"sha256\", this.secret).update(value).digest(\"hex\");\n\t}\n\n\tprivate pairingCodeForWindow(window: number): string {\n\t\tconst counter = Buffer.alloc(8);\n\t\tcounter.writeBigUInt64BE(BigInt(window));\n\t\tconst digest = createHmac(\"sha1\", this.secret).update(counter).digest();\n\t\tconst offset = digest[digest.length - 1]! & 0x0f;\n\t\tconst value =\n\t\t\t((digest[offset]! & 0x7f) << 24) |\n\t\t\t((digest[offset + 1]! & 0xff) << 16) |\n\t\t\t((digest[offset + 2]! & 0xff) << 8) |\n\t\t\t(digest[offset + 3]! & 0xff);\n\t\treturn String(value % 1_000_000).padStart(6, \"0\");\n\t}\n\n\t/** Current RFC-6238-style rotating code for pairing new remote devices. */\n\tcurrentPairingCode(): { code: string; expiresInMs: number } {\n\t\tconst nowMs = this.now();\n\t\tconst window = Math.floor(nowMs / PAIRING_CODE_STEP_MS);\n\t\tconst expiresInMs = (window + 1) * PAIRING_CODE_STEP_MS - nowMs;\n\t\treturn { code: this.pairingCodeForWindow(window), expiresInMs };\n\t}\n\n\tprivate currentPairingWindow(): number {\n\t\treturn Math.floor(this.now() / PAIRING_CODE_STEP_MS);\n\t}\n\n\tprivate matchingPairingCodeWindow(code: string): number | undefined {\n\t\tif (!/^\\d{6}$/.test(code)) return undefined;\n\t\tconst window = this.currentPairingWindow();\n\t\tfor (const candidateWindow of [window - 1, window, window + 1]) {\n\t\t\tif (candidateWindow < 0) continue;\n\t\t\tif (timingSafeEqualStr(code, this.pairingCodeForWindow(candidateWindow))) return candidateWindow;\n\t\t}\n\t\treturn undefined;\n\t}\n\n\tprivate pruneConsumedPairingWindows(windows: Iterable<number>): number[] {\n\t\tconst minimumAcceptedWindow = Math.max(0, this.currentPairingWindow() - 1);\n\t\treturn [\n\t\t\t...new Set([...windows].filter((window) => Number.isSafeInteger(window) && window >= minimumAcceptedWindow)),\n\t\t].sort((a, b) => a - b);\n\t}\n\n\tprivate samePairingWindows(a: number[], b: number[]): boolean {\n\t\treturn a.length === b.length && a.every((window, index) => window === b[index]);\n\t}\n\n\tprivate pairingFailureKey(identity: TailscaleIdentity, remoteAddress: string | undefined): string {\n\t\treturn `${identity.loginName}|${normalizeAddress(remoteAddress)}`;\n\t}\n\n\tprivate assertPairingNotLocked(key: string): void {\n\t\tconst failure = this.pairingFailures.get(key);\n\t\tif (!failure?.lockedUntil) return;\n\t\tif (failure.lockedUntil > this.now()) {\n\t\t\tthrow Object.assign(new Error(\"Too many incorrect pairing attempts; try again later\"), { status: 429 });\n\t\t}\n\t\tthis.pairingFailures.delete(key);\n\t}\n\n\tprivate recordPairingFailure(key: string, identity: TailscaleIdentity): never {\n\t\tconst current = this.pairingFailures.get(key);\n\t\tconst count = (current?.count ?? 0) + 1;\n\t\tif (count >= this.pairingMaxAttempts) {\n\t\t\tconst lockedUntil = this.now() + this.pairingLockoutMs;\n\t\t\tthis.pairingFailures.set(key, { count, lockedUntil });\n\t\t\tthis.logger(\n\t\t\t\t`pairing locked after ${count} failed attempts for ${identity.loginName} until ${new Date(lockedUntil).toISOString()}`,\n\t\t\t);\n\t\t\tthrow Object.assign(new Error(\"Too many incorrect pairing attempts; try again later\"), { status: 429 });\n\t\t}\n\t\tthis.pairingFailures.set(key, { count, lockedUntil: current?.lockedUntil });\n\t\tif (count > 1) this.logger(`pairing failed attempt ${count} for ${identity.loginName}`);\n\t\tthrow Object.assign(new Error(\"Incorrect pairing code\"), { status: 401 });\n\t}\n\n\tprivate clearPairingFailures(key: string): void {\n\t\tthis.pairingFailures.delete(key);\n\t}\n\n\tprivate async withPairingMutation<T>(fn: () => Promise<T>): Promise<T> {\n\t\tconst previous = this.pairingMutation;\n\t\tlet release!: () => void;\n\t\tthis.pairingMutation = new Promise<void>((resolve) => {\n\t\t\trelease = resolve;\n\t\t});\n\t\tawait previous;\n\t\ttry {\n\t\t\treturn await fn();\n\t\t} finally {\n\t\t\trelease();\n\t\t}\n\t}\n\n\t/**\n\t * Authenticate a request. Fail-closed: any resolver/storage error results in\n\t * a deny, never a pass-through.\n\t */\n\tasync authenticate(info: AuthRequestInfo): Promise<AuthDecision> {\n\t\ttry {\n\t\t\treturn await this.authenticateInner(info);\n\t\t} catch (err) {\n\t\t\treturn {\n\t\t\t\tallowed: false,\n\t\t\t\tstatus: 500,\n\t\t\t\treason: `Auth subsystem error — denying: ${err instanceof Error ? err.message : String(err)}`,\n\t\t\t};\n\t\t}\n\t}\n\n\tprivate async authenticateInner(info: AuthRequestInfo): Promise<AuthDecision> {\n\t\tif (isLoopbackAddress(info.remoteAddress)) {\n\t\t\tif (!isAllowedLocalHost(info.hostHeader)) {\n\t\t\t\treturn {\n\t\t\t\t\tallowed: false,\n\t\t\t\t\tstatus: 403,\n\t\t\t\t\treason: `Host header \"${info.hostHeader ?? \"(missing)\"}\" is not a loopback host — rejected (DNS-rebinding defense)`,\n\t\t\t\t};\n\t\t\t}\n\t\t\tif (info.originHeader) {\n\t\t\t\tlet originHost: string | undefined;\n\t\t\t\ttry {\n\t\t\t\t\toriginHost = new URL(info.originHeader).host;\n\t\t\t\t} catch {\n\t\t\t\t\toriginHost = undefined;\n\t\t\t\t}\n\t\t\t\tif (!originHost || !isAllowedLocalHost(originHost)) {\n\t\t\t\t\treturn {\n\t\t\t\t\t\tallowed: false,\n\t\t\t\t\t\tstatus: 403,\n\t\t\t\t\t\treason: `Origin \"${info.originHeader}\" is not a loopback origin — rejected (cross-site defense)`,\n\t\t\t\t\t};\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn { allowed: true, mode: \"local\" };\n\t\t}\n\n\t\tif (!this.remoteEnabled) {\n\t\t\treturn { allowed: false, status: 403, reason: \"Remote dashboard access is disabled\" };\n\t\t}\n\n\t\tconst identity = await this.resolver.resolve(info.remoteAddress ?? \"\");\n\t\tif (!identity) {\n\t\t\treturn { allowed: false, status: 403, reason: \"Client is not a known Tailscale peer\" };\n\t\t}\n\t\tif (this.allowedIdentities.size === 0 || !this.allowedIdentities.has(identity.loginName)) {\n\t\t\treturn {\n\t\t\t\tallowed: false,\n\t\t\t\tstatus: 403,\n\t\t\t\treason: `Tailscale identity \"${identity.loginName}\" is not on the dashboard allowlist`,\n\t\t\t\tidentity,\n\t\t\t};\n\t\t}\n\n\t\tif (!info.deviceToken || !(await this.isPaired(identity, info.deviceToken))) {\n\t\t\treturn {\n\t\t\t\tallowed: false,\n\t\t\t\tstatus: 401,\n\t\t\t\treason: \"Device is not paired — pairing code required\",\n\t\t\t\tneedsPairing: true,\n\t\t\t\tidentity,\n\t\t\t};\n\t\t}\n\n\t\treturn { allowed: true, mode: \"remote\", identity };\n\t}\n\n\t/**\n\t * Complete pairing for an allowed remote identity using the current rotating\n\t * code. Returns the device token to set as a cookie. Throws (with `status`) on\n\t * any failure.\n\t */\n\tasync pair(info: AuthRequestInfo, code: string): Promise<{ token: string; device: PairedDevice }> {\n\t\tif (isLoopbackAddress(info.remoteAddress)) {\n\t\t\tthrow Object.assign(new Error(\"Loopback clients do not pair\"), { status: 400 });\n\t\t}\n\t\tif (!this.remoteEnabled) {\n\t\t\tthrow Object.assign(new Error(\"Remote dashboard access is disabled\"), { status: 403 });\n\t\t}\n\t\tconst identity = await this.resolver.resolve(info.remoteAddress ?? \"\");\n\t\tif (!identity || this.allowedIdentities.size === 0 || !this.allowedIdentities.has(identity.loginName)) {\n\t\t\tthrow Object.assign(new Error(\"Identity is not on the dashboard allowlist\"), { status: 403 });\n\t\t}\n\t\tconst failureKey = this.pairingFailureKey(identity, info.remoteAddress);\n\t\tthis.assertPairingNotLocked(failureKey);\n\n\t\treturn this.withPairingMutation(async () => {\n\t\t\tconst state = await this.loadLiveState();\n\t\t\tconst matchedWindow = this.matchingPairingCodeWindow(code);\n\t\t\tif (matchedWindow === undefined || state.consumedPairingWindows.includes(matchedWindow)) {\n\t\t\t\tthis.recordPairingFailure(failureKey, identity);\n\t\t\t}\n\n\t\t\tconst token = randomBytes(32).toString(\"base64url\");\n\t\t\tconst nowMs = this.now();\n\t\t\tconst device: PairedDevice = {\n\t\t\t\tid: randomBytes(8).toString(\"hex\"),\n\t\t\t\tidentity: identity.loginName,\n\t\t\t\tdevice: identity.device,\n\t\t\t\tcreatedAt: new Date(nowMs).toISOString(),\n\t\t\t\texpiresAt: new Date(nowMs + this.pairingTtlMs).toISOString(),\n\t\t\t};\n\t\t\tconst pairings = [...state.pairings, { ...device, tokenHmac: this.hmac(token) }];\n\t\t\tconst consumedPairingWindows = this.pruneConsumedPairingWindows([\n\t\t\t\t...state.consumedPairingWindows,\n\t\t\t\tmatchedWindow,\n\t\t\t]);\n\t\t\tawait this.storage.save({ pairings, consumedPairingWindows });\n\t\t\tthis.clearPairingFailures(failureKey);\n\t\t\treturn { token, device };\n\t\t});\n\t}\n\n\t/** List paired devices (live only). */\n\tasync listDevices(): Promise<PairedDevice[]> {\n\t\treturn this.withPairingMutation(async () =>\n\t\t\t(await this.loadLive()).map(({ tokenHmac: _tokenHmac, ...device }) => device),\n\t\t);\n\t}\n\n\t/** Remove a paired device by id. Returns true when something was removed. */\n\tasync unpair(deviceId: string): Promise<boolean> {\n\t\treturn this.withPairingMutation(async () => {\n\t\t\tconst state = await this.loadLiveState();\n\t\t\tconst remaining = state.pairings.filter((p) => p.id !== deviceId);\n\t\t\tif (remaining.length === state.pairings.length) return false;\n\t\t\tawait this.storage.save({ ...state, pairings: remaining });\n\t\t\treturn true;\n\t\t});\n\t}\n\n\tprivate async isPaired(identity: TailscaleIdentity, token: string): Promise<boolean> {\n\t\tconst tokenHmac = this.hmac(token);\n\t\treturn this.withPairingMutation(async () => {\n\t\t\tconst live = await this.loadLive();\n\t\t\treturn live.some((p) => p.identity === identity.loginName && timingSafeEqualStr(p.tokenHmac, tokenHmac));\n\t\t});\n\t}\n\n\t/** Load pairings, dropping (and persisting the removal of) expired entries. Caller must hold pairingMutation. */\n\tprivate async loadLive(saveExpiredRemoval = true): Promise<StoredPairing[]> {\n\t\treturn (await this.loadLiveState(saveExpiredRemoval)).pairings;\n\t}\n\n\t/** Load pairing state, dropping expired devices and consumed PIN windows that can no longer match. */\n\tprivate async loadLiveState(savePrunedState = true): Promise<PairingState> {\n\t\tconst state = await this.storage.load();\n\t\tconst nowMs = this.now();\n\t\tconst pairings = state.pairings.filter((p) => new Date(p.expiresAt).getTime() > nowMs);\n\t\tconst consumedPairingWindows = this.pruneConsumedPairingWindows(state.consumedPairingWindows);\n\t\tif (\n\t\t\tsavePrunedState &&\n\t\t\t(pairings.length !== state.pairings.length ||\n\t\t\t\t!this.samePairingWindows(consumedPairingWindows, state.consumedPairingWindows))\n\t\t) {\n\t\t\tawait this.storage.save({ pairings, consumedPairingWindows });\n\t\t}\n\t\treturn { pairings, consumedPairingWindows };\n\t}\n}\n"]}
1
+ {"version":3,"file":"auth.js","sourceRoot":"","sources":["../../src/server/auth.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Dashboard auth — exactly two modes (this file is the authority):\n *\n * Mode A — local-only (default): the server binds loopback only. Requests are\n * additionally checked for loopback source address AND an allowlisted Host\n * header (DNS-rebinding defense: a malicious website can point its own domain\n * at 127.0.0.1 and drive the API from the victim's browser unless Host is\n * validated). No login, no pairing.\n *\n * Mode B — remote (explicit opt-in): requires Tailscale. Enforcement layers,\n * all fail-closed: (1) Tailscale identity resolution of the peer address,\n * (2) identity allowlist (empty allowlist = deny all), (3) first-login\n * rotating pairing code (visible only from the host/local dashboard),\n * (4) signed per-device cookie thereafter.\n *\n * There is no LAN mode. Any auth-subsystem error denies the request.\n */\n\nimport { execFile } from \"node:child_process\";\nimport { createHmac, randomBytes, timingSafeEqual } from \"node:crypto\";\nimport { promisify } from \"node:util\";\n\nconst execFileAsync = promisify(execFile);\nconst PAIRING_CODE_STEP_MS = 30_000;\nconst DAY_MS = 24 * 60 * 60 * 1000;\nexport const DEFAULT_PAIRING_TTL_DAYS = 180;\nexport const MIN_PAIRING_TTL_DAYS = 1;\nexport const MAX_PAIRING_TTL_DAYS = 3650;\nconst DEFAULT_PAIRING_MAX_ATTEMPTS = 5;\nconst DEFAULT_PAIRING_LOCKOUT_MS = 60_000;\n\n// ---------------------------------------------------------------------------\n// Address / Host helpers\n// ---------------------------------------------------------------------------\n\n/** Normalize an address for comparison (strip IPv6-mapped IPv4 prefix and zone). */\nexport function normalizeAddress(address: string | undefined): string {\n\tif (!address) return \"\";\n\tlet a = address.trim();\n\tif (a.startsWith(\"[\") && a.endsWith(\"]\")) a = a.slice(1, -1);\n\tconst zone = a.indexOf(\"%\");\n\tif (zone !== -1) a = a.slice(0, zone);\n\tif (a.startsWith(\"::ffff:\")) a = a.slice(7);\n\treturn a;\n}\n\n/** True when the (normalized) address is a loopback address. */\nexport function isLoopbackAddress(address: string | undefined): boolean {\n\tconst a = normalizeAddress(address);\n\tif (!a) return false;\n\tif (a === \"::1\") return true;\n\treturn /^127\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}$/.test(a);\n}\n\n/**\n * Validate a Host header against the loopback allowlist. Rejecting foreign\n * hosts breaks DNS rebinding: the attacker's page can reach 127.0.0.1, but its\n * requests carry the attacker's hostname in Host.\n */\nexport function isAllowedLocalHost(hostHeader: string | undefined): boolean {\n\tif (!hostHeader) return false;\n\t// Strip port. IPv6 hosts arrive as \"[::1]:port\".\n\tlet host = hostHeader.trim().toLowerCase();\n\tconst v6 = host.match(/^\\[([^\\]]+)\\](?::\\d+)?$/);\n\tif (v6) {\n\t\thost = v6[1];\n\t} else {\n\t\tconst colon = host.lastIndexOf(\":\");\n\t\tif (colon !== -1 && /^\\d+$/.test(host.slice(colon + 1))) host = host.slice(0, colon);\n\t}\n\treturn host === \"localhost\" || host === \"::1\" || /^127\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}$/.test(host);\n}\n\n// ---------------------------------------------------------------------------\n// Tailscale identity\n// ---------------------------------------------------------------------------\n\nexport interface TailscaleIdentity {\n\t/** Login name (e.g. \"alice@example.com\") — the allowlist unit. */\n\tloginName: string;\n\t/** Device host name, when known. */\n\tdevice?: string;\n}\n\nexport interface TailscaleResolver {\n\t/** Resolve a peer IP to a Tailscale identity, or null when unknown. */\n\tresolve(address: string): Promise<TailscaleIdentity | null>;\n}\n\ninterface TailscaleWhoisJson {\n\tNode?: { Name?: string };\n\tUserProfile?: { LoginName?: string } | null;\n}\n\nexport type TailscaleWhoisRunner = (address: string) => Promise<{ stdout: string }>;\n\nexport class TailscaleResolverError extends Error {\n\tconstructor(readonly kind: \"timeout\" | \"execution\" | \"parse\" | \"schema\") {\n\t\tsuper(`Tailscale identity resolver ${kind} failure`);\n\t\tthis.name = \"TailscaleResolverError\";\n\t}\n}\n\nasync function runTailscaleWhois(address: string): Promise<{ stdout: string }> {\n\treturn execFileAsync(\"tailscale\", [\"whois\", \"--json\", address], {\n\t\ttimeout: 3000,\n\t\tmaxBuffer: 1024 * 1024,\n\t});\n}\n\nfunction isPeerNotFoundError(error: unknown): boolean {\n\tconst stderr = (error as { stderr?: unknown })?.stderr;\n\treturn typeof stderr === \"string\" && stderr.trim().toLowerCase() === \"peer not found\";\n}\n\nfunction isTimeoutError(error: unknown): boolean {\n\tconst candidate = error as { killed?: unknown; signal?: unknown };\n\treturn candidate?.killed === true || candidate?.signal === \"SIGTERM\";\n}\n\n/** Peer-specific Tailscale identity resolution with same-peer in-flight coalescing. */\nexport class TailscaleWhoisResolver implements TailscaleResolver {\n\tprivate readonly inFlight = new Map<string, Promise<TailscaleIdentity | null>>();\n\n\tconstructor(private readonly runWhois: TailscaleWhoisRunner = runTailscaleWhois) {}\n\n\tasync resolve(address: string): Promise<TailscaleIdentity | null> {\n\t\tconst target = normalizeAddress(address);\n\t\tif (!target) return null;\n\t\tconst existing = this.inFlight.get(target);\n\t\tif (existing) return existing;\n\n\t\tconst lookup = this.lookup(target);\n\t\tthis.inFlight.set(target, lookup);\n\t\ttry {\n\t\t\treturn await lookup;\n\t\t} finally {\n\t\t\tif (this.inFlight.get(target) === lookup) this.inFlight.delete(target);\n\t\t}\n\t}\n\n\tprivate async lookup(target: string): Promise<TailscaleIdentity | null> {\n\t\tlet stdout: string;\n\t\ttry {\n\t\t\t({ stdout } = await this.runWhois(target));\n\t\t} catch (error) {\n\t\t\tif (isTimeoutError(error)) throw new TailscaleResolverError(\"timeout\");\n\t\t\tif (isPeerNotFoundError(error)) return null;\n\t\t\tthrow new TailscaleResolverError(\"execution\");\n\t\t}\n\n\t\tlet whois: TailscaleWhoisJson;\n\t\ttry {\n\t\t\tconst parsed: unknown = JSON.parse(stdout);\n\t\t\tif (!parsed || typeof parsed !== \"object\" || Array.isArray(parsed)) {\n\t\t\t\tthrow new TailscaleResolverError(\"schema\");\n\t\t\t}\n\t\t\twhois = parsed as TailscaleWhoisJson;\n\t\t} catch (error) {\n\t\t\tif (error instanceof TailscaleResolverError) throw error;\n\t\t\tthrow new TailscaleResolverError(\"parse\");\n\t\t}\n\n\t\tif (!whois.Node || typeof whois.Node !== \"object\" || Array.isArray(whois.Node)) {\n\t\t\tthrow new TailscaleResolverError(\"schema\");\n\t\t}\n\t\tif (whois.Node.Name !== undefined && typeof whois.Node.Name !== \"string\") {\n\t\t\tthrow new TailscaleResolverError(\"schema\");\n\t\t}\n\t\tif (whois.UserProfile !== undefined && whois.UserProfile !== null) {\n\t\t\tif (typeof whois.UserProfile !== \"object\" || Array.isArray(whois.UserProfile)) {\n\t\t\t\tthrow new TailscaleResolverError(\"schema\");\n\t\t\t}\n\t\t\tif (whois.UserProfile.LoginName !== undefined && typeof whois.UserProfile.LoginName !== \"string\") {\n\t\t\t\tthrow new TailscaleResolverError(\"schema\");\n\t\t\t}\n\t\t}\n\n\t\tconst loginName = whois.UserProfile?.LoginName?.trim();\n\t\tif (!loginName) return null;\n\t\tconst device = whois.Node.Name?.replace(/\\.$/, \"\") || undefined;\n\t\treturn { loginName, device };\n\t}\n}\n\n/** @deprecated Use TailscaleWhoisResolver. Retained for API compatibility. */\nexport class TailscaleStatusResolver extends TailscaleWhoisResolver {}\n\n// ---------------------------------------------------------------------------\n// Pairing store (rotating pairing codes + device tokens)\n// ---------------------------------------------------------------------------\n\nexport interface PairedDevice {\n\tid: string;\n\tidentity: string;\n\tdevice?: string;\n\tcreatedAt: string;\n\texpiresAt: string;\n}\n\nexport interface StoredPairing extends PairedDevice {\n\t/** HMAC of the device token (raw token never stored). */\n\ttokenHmac: string;\n\t/** UTC date of the last expiry warning claimed by this pairing. */\n\tlastExpiryWarningUtcDate?: string;\n}\n\nexport interface PairingState {\n\tpairings: StoredPairing[];\n\tconsumedPairingWindows: number[];\n\t/** Whole-day lifetime for newly created pairings. Absent in legacy files. */\n\tpairingTtlDays?: number;\n}\n\nexport interface PairingExpiryStatus {\n\twarning?: { expiresAt: string };\n\tnextCheckAt?: string;\n}\n\nexport interface PairingStorage {\n\tload(): Promise<PairingState>;\n\tsave(state: PairingState): Promise<void>;\n}\n\n/** In-memory storage — used in tests and as the base for the file store. */\nexport class MemoryPairingStorage implements PairingStorage {\n\tprivate state: PairingState = { pairings: [], consumedPairingWindows: [] };\n\tasync load(): Promise<PairingState> {\n\t\treturn {\n\t\t\tpairings: this.state.pairings.map((pairing) => ({ ...pairing })),\n\t\t\tconsumedPairingWindows: [...this.state.consumedPairingWindows],\n\t\t\tpairingTtlDays: this.state.pairingTtlDays,\n\t\t};\n\t}\n\tasync save(state: PairingState): Promise<void> {\n\t\tthis.state = {\n\t\t\tpairings: state.pairings.map((pairing) => ({ ...pairing })),\n\t\t\tconsumedPairingWindows: [...state.consumedPairingWindows],\n\t\t\tpairingTtlDays: state.pairingTtlDays,\n\t\t};\n\t}\n}\n\nexport interface DashboardAuthOptions {\n\t/** Remote (Tailscale) mode. Default false — loopback only. */\n\tremoteEnabled?: boolean;\n\t/** Allowed Tailscale login names. Empty = deny all remote. */\n\tallowedIdentities?: string[];\n\t/** Test/backward-compatible default when no persisted day setting exists. Production defaults to 180 days. */\n\tpairingTtlMs?: number;\n\tresolver?: TailscaleResolver;\n\tstorage?: PairingStorage;\n\t/** HMAC/TOTP secret for device tokens and pairing codes. Production passes a per-install persisted secret. */\n\tsecret?: Buffer;\n\t/** Failed PIN attempts before temporary lockout. Default 5. */\n\tpairingMaxAttempts?: number;\n\t/** Temporary lockout duration after too many failed PIN attempts. Default 60s. */\n\tpairingLockoutMs?: number;\n\t/** Security/audit log sink for repeated failed pairing attempts. */\n\tlogger?: (line: string) => void;\n\t/** Clock override for tests. */\n\tnow?: () => number;\n}\n\nexport type AuthDecision =\n\t| { allowed: true; mode: \"local\" }\n\t| { allowed: true; mode: \"remote\"; identity: TailscaleIdentity; pairing: PairedDevice }\n\t| {\n\t\t\tallowed: false;\n\t\t\tstatus: number;\n\t\t\treason: string;\n\t\t\t/** Set when an allowed identity needs pairing-code entry. */\n\t\t\tneedsPairing?: boolean;\n\t\t\tidentity?: TailscaleIdentity;\n\t };\n\nexport interface AuthRequestInfo {\n\tremoteAddress: string | undefined;\n\thostHeader: string | undefined;\n\t/** Origin header when present. Non-loopback origins are rejected on local requests. */\n\toriginHeader: string | undefined;\n\t/** Value of the dashboard device cookie, when present. */\n\tdeviceToken: string | undefined;\n}\n\nfunction timingSafeEqualStr(a: string, b: string): boolean {\n\tconst ab = Buffer.from(a);\n\tconst bb = Buffer.from(b);\n\tif (ab.length !== bb.length) return false;\n\treturn timingSafeEqual(ab, bb);\n}\n\nexport class DashboardAuth {\n\tprivate readonly remoteEnabled: boolean;\n\tprivate readonly allowedIdentities: Set<string>;\n\tprivate readonly defaultPairingTtlMs: number;\n\tprivate readonly resolver: TailscaleResolver;\n\tprivate readonly storage: PairingStorage;\n\tprivate readonly secret: Buffer;\n\tprivate readonly pairingMaxAttempts: number;\n\tprivate readonly pairingLockoutMs: number;\n\tprivate readonly logger: (line: string) => void;\n\tprivate readonly now: () => number;\n\tprivate pairingMutation: Promise<void> = Promise.resolve();\n\tprivate readonly pairingFailures = new Map<string, { count: number; lockedUntil?: number }>();\n\n\tconstructor(options: DashboardAuthOptions = {}) {\n\t\tthis.remoteEnabled = options.remoteEnabled ?? false;\n\t\tthis.allowedIdentities = new Set(options.allowedIdentities ?? []);\n\t\tthis.defaultPairingTtlMs = options.pairingTtlMs ?? DEFAULT_PAIRING_TTL_DAYS * DAY_MS;\n\t\tthis.resolver = options.resolver ?? new TailscaleWhoisResolver();\n\t\tthis.storage = options.storage ?? new MemoryPairingStorage();\n\t\tthis.secret = options.secret ?? randomBytes(32);\n\t\tthis.pairingMaxAttempts = options.pairingMaxAttempts ?? DEFAULT_PAIRING_MAX_ATTEMPTS;\n\t\tthis.pairingLockoutMs = options.pairingLockoutMs ?? DEFAULT_PAIRING_LOCKOUT_MS;\n\t\tthis.logger = options.logger ?? (() => {});\n\t\tthis.now = options.now ?? Date.now;\n\t}\n\n\tget isRemoteEnabled(): boolean {\n\t\treturn this.remoteEnabled;\n\t}\n\n\tprivate defaultPairingTtlDays(): number {\n\t\tconst days = this.defaultPairingTtlMs / DAY_MS;\n\t\treturn Number.isSafeInteger(days) && days >= MIN_PAIRING_TTL_DAYS && days <= MAX_PAIRING_TTL_DAYS\n\t\t\t? days\n\t\t\t: DEFAULT_PAIRING_TTL_DAYS;\n\t}\n\n\tasync getPairingSettings(): Promise<{ pairingTtlDays: number }> {\n\t\treturn this.withPairingMutation(async () => {\n\t\t\tconst state = await this.loadLiveState();\n\t\t\treturn { pairingTtlDays: state.pairingTtlDays ?? this.defaultPairingTtlDays() };\n\t\t});\n\t}\n\n\tasync setPairingSettings(pairingTtlDays: number): Promise<{ pairingTtlDays: number }> {\n\t\tif (\n\t\t\t!Number.isSafeInteger(pairingTtlDays) ||\n\t\t\tpairingTtlDays < MIN_PAIRING_TTL_DAYS ||\n\t\t\tpairingTtlDays > MAX_PAIRING_TTL_DAYS\n\t\t) {\n\t\t\tthrow Object.assign(\n\t\t\t\tnew Error(\n\t\t\t\t\t`pairingTtlDays must be a whole number from ${MIN_PAIRING_TTL_DAYS} through ${MAX_PAIRING_TTL_DAYS}`,\n\t\t\t\t),\n\t\t\t\t{ status: 400 },\n\t\t\t);\n\t\t}\n\t\treturn this.withPairingMutation(async () => {\n\t\t\tconst state = await this.loadLiveState();\n\t\t\tawait this.storage.save({ ...state, pairingTtlDays });\n\t\t\treturn { pairingTtlDays };\n\t\t});\n\t}\n\n\tprivate hmac(value: string): string {\n\t\treturn createHmac(\"sha256\", this.secret).update(value).digest(\"hex\");\n\t}\n\n\tprivate pairingCodeForWindow(window: number): string {\n\t\tconst counter = Buffer.alloc(8);\n\t\tcounter.writeBigUInt64BE(BigInt(window));\n\t\tconst digest = createHmac(\"sha1\", this.secret).update(counter).digest();\n\t\tconst offset = digest[digest.length - 1]! & 0x0f;\n\t\tconst value =\n\t\t\t((digest[offset]! & 0x7f) << 24) |\n\t\t\t((digest[offset + 1]! & 0xff) << 16) |\n\t\t\t((digest[offset + 2]! & 0xff) << 8) |\n\t\t\t(digest[offset + 3]! & 0xff);\n\t\treturn String(value % 1_000_000).padStart(6, \"0\");\n\t}\n\n\t/** Current RFC-6238-style rotating code for pairing new remote devices. */\n\tcurrentPairingCode(): { code: string; expiresInMs: number } {\n\t\tconst nowMs = this.now();\n\t\tconst window = Math.floor(nowMs / PAIRING_CODE_STEP_MS);\n\t\tconst expiresInMs = (window + 1) * PAIRING_CODE_STEP_MS - nowMs;\n\t\treturn { code: this.pairingCodeForWindow(window), expiresInMs };\n\t}\n\n\tprivate currentPairingWindow(): number {\n\t\treturn Math.floor(this.now() / PAIRING_CODE_STEP_MS);\n\t}\n\n\tprivate matchingPairingCodeWindow(code: string): number | undefined {\n\t\tif (!/^\\d{6}$/.test(code)) return undefined;\n\t\tconst window = this.currentPairingWindow();\n\t\tfor (const candidateWindow of [window - 1, window, window + 1]) {\n\t\t\tif (candidateWindow < 0) continue;\n\t\t\tif (timingSafeEqualStr(code, this.pairingCodeForWindow(candidateWindow))) return candidateWindow;\n\t\t}\n\t\treturn undefined;\n\t}\n\n\tprivate pruneConsumedPairingWindows(windows: Iterable<number>): number[] {\n\t\tconst minimumAcceptedWindow = Math.max(0, this.currentPairingWindow() - 1);\n\t\treturn [\n\t\t\t...new Set([...windows].filter((window) => Number.isSafeInteger(window) && window >= minimumAcceptedWindow)),\n\t\t].sort((a, b) => a - b);\n\t}\n\n\tprivate samePairingWindows(a: number[], b: number[]): boolean {\n\t\treturn a.length === b.length && a.every((window, index) => window === b[index]);\n\t}\n\n\tprivate pairingFailureKey(identity: TailscaleIdentity, remoteAddress: string | undefined): string {\n\t\treturn `${identity.loginName}|${normalizeAddress(remoteAddress)}`;\n\t}\n\n\tprivate assertPairingNotLocked(key: string): void {\n\t\tconst failure = this.pairingFailures.get(key);\n\t\tif (!failure?.lockedUntil) return;\n\t\tif (failure.lockedUntil > this.now()) {\n\t\t\tthrow Object.assign(new Error(\"Too many incorrect pairing attempts; try again later\"), { status: 429 });\n\t\t}\n\t\tthis.pairingFailures.delete(key);\n\t}\n\n\tprivate recordPairingFailure(key: string, identity: TailscaleIdentity): never {\n\t\tconst current = this.pairingFailures.get(key);\n\t\tconst count = (current?.count ?? 0) + 1;\n\t\tif (count >= this.pairingMaxAttempts) {\n\t\t\tconst lockedUntil = this.now() + this.pairingLockoutMs;\n\t\t\tthis.pairingFailures.set(key, { count, lockedUntil });\n\t\t\tthis.logger(\n\t\t\t\t`pairing locked after ${count} failed attempts for ${identity.loginName} until ${new Date(lockedUntil).toISOString()}`,\n\t\t\t);\n\t\t\tthrow Object.assign(new Error(\"Too many incorrect pairing attempts; try again later\"), { status: 429 });\n\t\t}\n\t\tthis.pairingFailures.set(key, { count, lockedUntil: current?.lockedUntil });\n\t\tif (count > 1) this.logger(`pairing failed attempt ${count} for ${identity.loginName}`);\n\t\tthrow Object.assign(new Error(\"Incorrect pairing code\"), { status: 401 });\n\t}\n\n\tprivate clearPairingFailures(key: string): void {\n\t\tthis.pairingFailures.delete(key);\n\t}\n\n\tprivate async withPairingMutation<T>(fn: () => Promise<T>): Promise<T> {\n\t\tconst previous = this.pairingMutation;\n\t\tlet release!: () => void;\n\t\tthis.pairingMutation = new Promise<void>((resolve) => {\n\t\t\trelease = resolve;\n\t\t});\n\t\tawait previous;\n\t\ttry {\n\t\t\treturn await fn();\n\t\t} finally {\n\t\t\trelease();\n\t\t}\n\t}\n\n\t/**\n\t * Authenticate a request. Fail-closed: any resolver/storage error results in\n\t * a deny, never a pass-through.\n\t */\n\tasync authenticate(info: AuthRequestInfo): Promise<AuthDecision> {\n\t\ttry {\n\t\t\treturn await this.authenticateInner(info);\n\t\t} catch (err) {\n\t\t\tif (err instanceof TailscaleResolverError) {\n\t\t\t\tthis.logger(`identity resolver ${err.kind} failure — denying`);\n\t\t\t}\n\t\t\treturn {\n\t\t\t\tallowed: false,\n\t\t\t\tstatus: 500,\n\t\t\t\treason: `Auth subsystem error — denying: ${err instanceof Error ? err.message : String(err)}`,\n\t\t\t};\n\t\t}\n\t}\n\n\tprivate async authenticateInner(info: AuthRequestInfo): Promise<AuthDecision> {\n\t\tif (isLoopbackAddress(info.remoteAddress)) {\n\t\t\tif (!isAllowedLocalHost(info.hostHeader)) {\n\t\t\t\treturn {\n\t\t\t\t\tallowed: false,\n\t\t\t\t\tstatus: 403,\n\t\t\t\t\treason: `Host header \"${info.hostHeader ?? \"(missing)\"}\" is not a loopback host — rejected (DNS-rebinding defense)`,\n\t\t\t\t};\n\t\t\t}\n\t\t\tif (info.originHeader) {\n\t\t\t\tlet originHost: string | undefined;\n\t\t\t\ttry {\n\t\t\t\t\toriginHost = new URL(info.originHeader).host;\n\t\t\t\t} catch {\n\t\t\t\t\toriginHost = undefined;\n\t\t\t\t}\n\t\t\t\tif (!originHost || !isAllowedLocalHost(originHost)) {\n\t\t\t\t\treturn {\n\t\t\t\t\t\tallowed: false,\n\t\t\t\t\t\tstatus: 403,\n\t\t\t\t\t\treason: `Origin \"${info.originHeader}\" is not a loopback origin — rejected (cross-site defense)`,\n\t\t\t\t\t};\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn { allowed: true, mode: \"local\" };\n\t\t}\n\n\t\tif (!this.remoteEnabled) {\n\t\t\treturn { allowed: false, status: 403, reason: \"Remote dashboard access is disabled\" };\n\t\t}\n\n\t\tconst identity = await this.resolver.resolve(info.remoteAddress ?? \"\");\n\t\tif (!identity) {\n\t\t\treturn { allowed: false, status: 403, reason: \"Client is not a known Tailscale peer\" };\n\t\t}\n\t\tif (this.allowedIdentities.size === 0 || !this.allowedIdentities.has(identity.loginName)) {\n\t\t\treturn {\n\t\t\t\tallowed: false,\n\t\t\t\tstatus: 403,\n\t\t\t\treason: `Tailscale identity \"${identity.loginName}\" is not on the dashboard allowlist`,\n\t\t\t\tidentity,\n\t\t\t};\n\t\t}\n\n\t\tconst pairing = info.deviceToken ? await this.findPairing(identity, info.deviceToken) : undefined;\n\t\tif (!pairing) {\n\t\t\treturn {\n\t\t\t\tallowed: false,\n\t\t\t\tstatus: 401,\n\t\t\t\treason: \"Device is not paired — pairing code required\",\n\t\t\t\tneedsPairing: true,\n\t\t\t\tidentity,\n\t\t\t};\n\t\t}\n\n\t\treturn { allowed: true, mode: \"remote\", identity, pairing: this.toPairedDevice(pairing) };\n\t}\n\n\t/**\n\t * Complete pairing for an allowed remote identity using the current rotating\n\t * code. Returns the device token to set as a cookie. Throws (with `status`) on\n\t * any failure.\n\t */\n\tasync pair(info: AuthRequestInfo, code: string): Promise<{ token: string; device: PairedDevice }> {\n\t\tif (isLoopbackAddress(info.remoteAddress)) {\n\t\t\tthrow Object.assign(new Error(\"Loopback clients do not pair\"), { status: 400 });\n\t\t}\n\t\tif (!this.remoteEnabled) {\n\t\t\tthrow Object.assign(new Error(\"Remote dashboard access is disabled\"), { status: 403 });\n\t\t}\n\t\tconst identity = await this.resolver.resolve(info.remoteAddress ?? \"\");\n\t\tif (!identity || this.allowedIdentities.size === 0 || !this.allowedIdentities.has(identity.loginName)) {\n\t\t\tthrow Object.assign(new Error(\"Identity is not on the dashboard allowlist\"), { status: 403 });\n\t\t}\n\t\tconst failureKey = this.pairingFailureKey(identity, info.remoteAddress);\n\t\tthis.assertPairingNotLocked(failureKey);\n\n\t\treturn this.withPairingMutation(async () => {\n\t\t\tconst state = await this.loadLiveState();\n\t\t\tconst matchedWindow = this.matchingPairingCodeWindow(code);\n\t\t\tif (matchedWindow === undefined || state.consumedPairingWindows.includes(matchedWindow)) {\n\t\t\t\tthis.recordPairingFailure(failureKey, identity);\n\t\t\t}\n\n\t\t\tconst token = randomBytes(32).toString(\"base64url\");\n\t\t\tconst nowMs = this.now();\n\t\t\tconst pairingTtlMs =\n\t\t\t\tstate.pairingTtlDays === undefined ? this.defaultPairingTtlMs : state.pairingTtlDays * DAY_MS;\n\t\t\tconst device: PairedDevice = {\n\t\t\t\tid: randomBytes(8).toString(\"hex\"),\n\t\t\t\tidentity: identity.loginName,\n\t\t\t\tdevice: identity.device,\n\t\t\t\tcreatedAt: new Date(nowMs).toISOString(),\n\t\t\t\texpiresAt: new Date(nowMs + pairingTtlMs).toISOString(),\n\t\t\t};\n\t\t\tconst pairings = [...state.pairings, { ...device, tokenHmac: this.hmac(token) }];\n\t\t\tconst consumedPairingWindows = this.pruneConsumedPairingWindows([\n\t\t\t\t...state.consumedPairingWindows,\n\t\t\t\tmatchedWindow,\n\t\t\t]);\n\t\t\tawait this.storage.save({ ...state, pairings, consumedPairingWindows });\n\t\t\tthis.clearPairingFailures(failureKey);\n\t\t\treturn { token, device };\n\t\t});\n\t}\n\n\tprivate toPairedDevice(pairing: StoredPairing): PairedDevice {\n\t\treturn {\n\t\t\tid: pairing.id,\n\t\t\tidentity: pairing.identity,\n\t\t\tdevice: pairing.device,\n\t\t\tcreatedAt: pairing.createdAt,\n\t\t\texpiresAt: pairing.expiresAt,\n\t\t};\n\t}\n\n\t/** List paired devices (live only). */\n\tasync listDevices(): Promise<PairedDevice[]> {\n\t\treturn this.withPairingMutation(async () =>\n\t\t\t(await this.loadLive()).map((pairing) => this.toPairedDevice(pairing)),\n\t\t);\n\t}\n\n\t/** Remove a paired device by id. Returns true when something was removed. */\n\tasync unpair(deviceId: string): Promise<boolean> {\n\t\treturn this.withPairingMutation(async () => {\n\t\t\tconst state = await this.loadLiveState();\n\t\t\tconst remaining = state.pairings.filter((p) => p.id !== deviceId);\n\t\t\tif (remaining.length === state.pairings.length) return false;\n\t\t\tawait this.storage.save({ ...state, pairings: remaining });\n\t\t\treturn true;\n\t\t});\n\t}\n\n\tprivate async findPairing(identity: TailscaleIdentity, token: string): Promise<StoredPairing | undefined> {\n\t\tconst tokenHmac = this.hmac(token);\n\t\treturn this.withPairingMutation(async () => {\n\t\t\tconst live = await this.loadLive();\n\t\t\treturn live.find((p) => p.identity === identity.loginName && timingSafeEqualStr(p.tokenHmac, tokenHmac));\n\t\t});\n\t}\n\n\t/** Atomically claim any due warning and return the next useful browser check time. */\n\tasync claimPairingExpiryStatus(pairingId: string): Promise<PairingExpiryStatus> {\n\t\treturn this.withPairingMutation(async () => {\n\t\t\tconst state = await this.loadLiveState();\n\t\t\tconst pairing = state.pairings.find((candidate) => candidate.id === pairingId);\n\t\t\tif (!pairing) return {};\n\n\t\t\tconst nowMs = this.now();\n\t\t\tconst createdAt = Date.parse(pairing.createdAt);\n\t\t\tconst expiresAt = Date.parse(pairing.expiresAt);\n\t\t\tconst originalValidity = expiresAt - createdAt;\n\t\t\tconst remaining = expiresAt - nowMs;\n\t\t\tif (!Number.isFinite(originalValidity) || originalValidity <= 0 || remaining <= 0) return {};\n\n\t\t\tconst warningStartsAt = expiresAt - originalValidity * 0.1;\n\t\t\tif (nowMs < warningStartsAt) return { nextCheckAt: new Date(warningStartsAt).toISOString() };\n\n\t\t\tconst utcDate = new Date(nowMs).toISOString().slice(0, 10);\n\t\t\tconst nextUtcMidnight = Date.parse(`${utcDate}T00:00:00.000Z`) + DAY_MS;\n\t\t\tconst nextCheckAt = nextUtcMidnight < expiresAt ? new Date(nextUtcMidnight).toISOString() : undefined;\n\t\t\tif (pairing.lastExpiryWarningUtcDate === utcDate) return { nextCheckAt };\n\n\t\t\tconst pairings = state.pairings.map((candidate) =>\n\t\t\t\tcandidate.id === pairingId ? { ...candidate, lastExpiryWarningUtcDate: utcDate } : candidate,\n\t\t\t);\n\t\t\tawait this.storage.save({ ...state, pairings });\n\t\t\treturn { warning: { expiresAt: pairing.expiresAt }, nextCheckAt };\n\t\t});\n\t}\n\n\t/** Load pairings, dropping (and persisting the removal of) expired entries. Caller must hold pairingMutation. */\n\tprivate async loadLive(saveExpiredRemoval = true): Promise<StoredPairing[]> {\n\t\treturn (await this.loadLiveState(saveExpiredRemoval)).pairings;\n\t}\n\n\t/** Load pairing state, dropping expired devices and consumed PIN windows that can no longer match. */\n\tprivate async loadLiveState(savePrunedState = true): Promise<PairingState> {\n\t\tconst state = await this.storage.load();\n\t\tconst nowMs = this.now();\n\t\tconst pairings = state.pairings.filter((p) => new Date(p.expiresAt).getTime() > nowMs);\n\t\tconst consumedPairingWindows = this.pruneConsumedPairingWindows(state.consumedPairingWindows);\n\t\tif (\n\t\t\tsavePrunedState &&\n\t\t\t(pairings.length !== state.pairings.length ||\n\t\t\t\t!this.samePairingWindows(consumedPairingWindows, state.consumedPairingWindows))\n\t\t) {\n\t\t\tawait this.storage.save({ ...state, pairings, consumedPairingWindows });\n\t\t}\n\t\treturn { ...state, pairings, consumedPairingWindows };\n\t}\n}\n"]}
@@ -2,7 +2,7 @@
2
2
  * File-backed pairing storage — persists paired devices under the dreb agent
3
3
  * dir so pairings survive dashboard restarts. Written with mode 0600.
4
4
  */
5
- import type { PairingState } from "./auth.js";
5
+ import { type PairingState } from "./auth.js";
6
6
  /**
7
7
  * Load or create the per-install dashboard auth secret. This secret keys both
8
8
  * device-token HMACs and the rotating pairing code, so it must survive process
@@ -1 +1 @@
1
- {"version":3,"file":"pairing-storage.d.ts","sourceRoot":"","sources":["../../src/server/pairing-storage.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAKH,OAAO,KAAK,EAAE,YAAY,EAAiB,MAAM,WAAW,CAAC;AAQ7D;;;;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,CAwBlC;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 type { PairingState, StoredPairing } from \"./auth.js\";\n\ninterface PairingFile {\n\tversion: 1;\n\tpairings: StoredPairing[];\n\tconsumedPairingWindows?: number[];\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\tparsed.version !== 1 ||\n\t\t\t!Array.isArray(parsed.pairings) ||\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) {\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};\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: PairingFile = { version: 1, ...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"]}
1
+ {"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"]}
@@ -5,6 +5,13 @@
5
5
  import { randomBytes } from "node:crypto";
6
6
  import { mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
7
7
  import { basename, dirname, join } from "node:path";
8
+ import { MAX_PAIRING_TTL_DAYS, MIN_PAIRING_TTL_DAYS } from "./auth.js";
9
+ function isUtcDate(value) {
10
+ if (typeof value !== "string" || !/^\d{4}-\d{2}-\d{2}$/.test(value))
11
+ return false;
12
+ const parsed = Date.parse(`${value}T00:00:00.000Z`);
13
+ return Number.isFinite(parsed) && new Date(parsed).toISOString().slice(0, 10) === value;
14
+ }
8
15
  /**
9
16
  * Load or create the per-install dashboard auth secret. This secret keys both
10
17
  * device-token HMACs and the rotating pairing code, so it must survive process
@@ -53,22 +60,32 @@ export class FilePairingStorage {
53
60
  throw err;
54
61
  }
55
62
  const parsed = JSON.parse(raw);
56
- if (parsed.version !== 1 ||
63
+ if ((parsed.version !== 1 && parsed.version !== 2) ||
57
64
  !Array.isArray(parsed.pairings) ||
65
+ (parsed.version === 2 && !Array.isArray(parsed.consumedPairingWindows)) ||
58
66
  (parsed.consumedPairingWindows !== undefined &&
59
67
  (!Array.isArray(parsed.consumedPairingWindows) ||
60
- !parsed.consumedPairingWindows.every((window) => Number.isSafeInteger(window))))) {
68
+ !parsed.consumedPairingWindows.every((window) => Number.isSafeInteger(window)))) ||
69
+ (parsed.version === 2 &&
70
+ parsed.pairingTtlDays !== undefined &&
71
+ (!Number.isSafeInteger(parsed.pairingTtlDays) ||
72
+ parsed.pairingTtlDays < MIN_PAIRING_TTL_DAYS ||
73
+ parsed.pairingTtlDays > MAX_PAIRING_TTL_DAYS)) ||
74
+ !parsed.pairings.every((pairing) => pairing &&
75
+ typeof pairing === "object" &&
76
+ (pairing.lastExpiryWarningUtcDate === undefined || isUtcDate(pairing.lastExpiryWarningUtcDate)))) {
61
77
  throw new Error(`Unrecognized pairing file format at ${this.path}`);
62
78
  }
63
79
  return {
64
80
  pairings: parsed.pairings,
65
81
  consumedPairingWindows: parsed.consumedPairingWindows ?? [],
82
+ pairingTtlDays: parsed.version === 2 ? parsed.pairingTtlDays : undefined,
66
83
  };
67
84
  }
68
85
  async save(state) {
69
86
  const dir = dirname(this.path);
70
87
  mkdirSync(dir, { recursive: true });
71
- const file = { version: 1, ...state };
88
+ const file = { version: 2, ...state };
72
89
  const tmp = join(dir, `.${basename(this.path)}.${process.pid}.${randomBytes(6).toString("hex")}.tmp`);
73
90
  try {
74
91
  writeFileSync(tmp, `${JSON.stringify(file, null, "\t")}\n`, { mode: 0o600, flag: "wx" });