@mean-weasel/lineage 0.1.12 → 0.1.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/server.js CHANGED
@@ -5,8 +5,8 @@ import { existsSync as existsSync14 } from "node:fs";
5
5
  import { join as join14 } from "node:path";
6
6
 
7
7
  // src/server/assetCore.ts
8
- import { existsSync as existsSync6, mkdirSync as mkdirSync5, readdirSync as readdirSync2, readFileSync as readFileSync4, unlinkSync, writeFileSync as writeFileSync2 } from "node:fs";
9
- import { dirname as dirname4, join as join6, resolve as resolve5 } from "node:path";
8
+ import { existsSync as existsSync6, mkdirSync as mkdirSync5, readdirSync as readdirSync2, readFileSync as readFileSync4, unlinkSync, writeFileSync as writeFileSync3 } from "node:fs";
9
+ import { dirname as dirname4, join as join6, resolve as resolve4 } from "node:path";
10
10
  import { spawnSync } from "node:child_process";
11
11
  import { fileURLToPath } from "node:url";
12
12
 
@@ -284,13 +284,31 @@ function createS3StorageAdapter(deps) {
284
284
  // src/server/assetLineageDb.ts
285
285
  import { createRequire as createRequire2 } from "node:module";
286
286
  import { existsSync as existsSync5, mkdirSync as mkdirSync4 } from "node:fs";
287
- import { join as join5, resolve as resolve4 } from "node:path";
287
+ import { join as join5 } from "node:path";
288
+
289
+ // src/server/profileWriterLease.ts
290
+ import { createHash as createHash3, randomUUID as randomUUID2, timingSafeEqual } from "node:crypto";
291
+ import { existsSync as existsSync4, lstatSync, mkdirSync as mkdirSync3, readFileSync as readFileSync3, renameSync, rmSync as rmSync2, writeFileSync as writeFileSync2 } from "node:fs";
292
+ import { dirname as dirname3, join as join4, resolve as resolve3 } from "node:path";
288
293
 
289
294
  // src/server/lineageProfiles.ts
295
+ import { createHash as createHash2, randomUUID } from "node:crypto";
290
296
  import { createRequire } from "node:module";
291
- import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync2, statSync as statSync3 } from "node:fs";
297
+ import {
298
+ chmodSync,
299
+ constants as fsConstants,
300
+ copyFileSync as copyFileSync2,
301
+ existsSync as existsSync3,
302
+ linkSync,
303
+ mkdirSync as mkdirSync2,
304
+ readFileSync as readFileSync2,
305
+ realpathSync,
306
+ rmSync,
307
+ statSync as statSync3,
308
+ writeFileSync
309
+ } from "node:fs";
292
310
  import { homedir, platform } from "node:os";
293
- import { dirname as dirname2, isAbsolute, join as join3, resolve as resolve2 } from "node:path";
311
+ import { dirname as dirname2, isAbsolute, join as join3, relative as relative2, resolve as resolve2 } from "node:path";
294
312
 
295
313
  // src/shared/lineageProfileTypes.ts
296
314
  var lineageProfileSchemaVersion = "lineage.profile.v1";
@@ -346,6 +364,16 @@ function validateChannel(value) {
346
364
  if (value === "stable" || value === "preview" || value === "dev") return value;
347
365
  throw new Error(`Invalid expected runtime channel: ${String(value)}`);
348
366
  }
367
+ function validateCodeOrigin(value) {
368
+ if (value === void 0) return void 0;
369
+ if (value === "checkout" || value === "package") return value;
370
+ throw new Error(`Invalid expected runtime code_origin: ${String(value)}`);
371
+ }
372
+ function validateFingerprint(value, field) {
373
+ if (value === void 0) return void 0;
374
+ if (!/^[a-f0-9]{64}$/i.test(value)) throw new Error(`Profile ${field} must be a 64-character SHA-256 fingerprint`);
375
+ return value.toLowerCase();
376
+ }
349
377
  function resolveManifestPath(manifestPath, value) {
350
378
  return resolve2(dirname2(manifestPath), value);
351
379
  }
@@ -361,9 +389,6 @@ function validateServiceOrigin(value) {
361
389
  if (parsed.username || parsed.password || parsed.pathname !== "/" || parsed.search || parsed.hash) {
362
390
  throw new Error("Profile service_origin must contain only scheme, host, and port");
363
391
  }
364
- const hostname = parsed.hostname.replace(/^\[|\]$/g, "").toLowerCase();
365
- const isLocal = hostname === "localhost" || hostname.endsWith(".localhost") || hostname === "::1" || /^127(?:\.\d{1,3}){3}$/.test(hostname);
366
- if (!isLocal) throw new Error("Profile service_origin must use a loopback or localhost host");
367
392
  return parsed.origin;
368
393
  }
369
394
  function resolveLineageProfile(selector) {
@@ -391,6 +416,8 @@ function resolveLineageProfile(selector) {
391
416
  const expected = expectedRaw;
392
417
  const expectedGitSha = expected ? optionalString(expected, "git_sha") : void 0;
393
418
  const expectedVersion = expected ? optionalString(expected, "version") : void 0;
419
+ const expectedCodeFingerprint = validateFingerprint(expected ? optionalString(expected, "code_fingerprint") : void 0, "expected_runtime.code_fingerprint");
420
+ const expectedCodeOrigin = validateCodeOrigin(expected?.code_origin);
394
421
  const migrationsRaw = record.required_schema_migrations;
395
422
  if (migrationsRaw !== void 0 && (!Array.isArray(migrationsRaw) || migrationsRaw.some((value) => typeof value !== "string" || !value.trim()))) {
396
423
  throw new Error("Profile required_schema_migrations must be an array of non-empty strings");
@@ -407,6 +434,8 @@ function resolveLineageProfile(selector) {
407
434
  ...expected ? {
408
435
  expected_runtime: {
409
436
  ...expectedChannel ? { channel: expectedChannel } : {},
437
+ ...expectedCodeFingerprint ? { code_fingerprint: expectedCodeFingerprint } : {},
438
+ ...expectedCodeOrigin ? { code_origin: expectedCodeOrigin } : {},
410
439
  ...expectedGitSha ? { git_sha: expectedGitSha } : {},
411
440
  ...expectedVersion ? { version: expectedVersion } : {}
412
441
  }
@@ -416,7 +445,31 @@ function resolveLineageProfile(selector) {
416
445
  schema_version: lineageProfileSchemaVersion,
417
446
  service_origin: validateServiceOrigin(requiredString(record, "service_origin"))
418
447
  };
419
- return { ...manifest, manifest_path: manifestPath };
448
+ return { ...manifest, manifest_path: manifestPath, profile_fingerprint: lineageProfileFingerprint(manifest) };
449
+ }
450
+ function lineageProfileFingerprint(profile) {
451
+ return createHash2("sha256").update(JSON.stringify({
452
+ asset_root: resolve2(profile.asset_root),
453
+ database_path: resolve2(profile.database_path),
454
+ environment: profile.environment,
455
+ profile_id: profile.profile_id,
456
+ schema_version: profile.schema_version,
457
+ service_origin: profile.service_origin
458
+ })).digest("hex");
459
+ }
460
+ function assertProfileRuntimePin(profile, runtime) {
461
+ if (!profile.expected_runtime?.code_fingerprint || !profile.expected_runtime.code_origin) {
462
+ throw new Error(`Profile ${profile.profile_id} must pin expected_runtime.code_fingerprint and expected_runtime.code_origin before binding or writing`);
463
+ }
464
+ if (!runtime?.code?.verified) {
465
+ throw new Error(`Profile ${profile.profile_id} requires a verified runtime code identity before binding or writing`);
466
+ }
467
+ if (runtime.code.fingerprint !== profile.expected_runtime.code_fingerprint) {
468
+ throw new Error(`Profile ${profile.profile_id} expects code fingerprint ${profile.expected_runtime.code_fingerprint}, found ${runtime.code.fingerprint}`);
469
+ }
470
+ if (runtime.code.origin !== profile.expected_runtime.code_origin) {
471
+ throw new Error(`Profile ${profile.profile_id} expects code origin ${profile.expected_runtime.code_origin}, found ${runtime.code.origin}`);
472
+ }
420
473
  }
421
474
  function assertProfileChannel(profile, channel) {
422
475
  const requiredChannel = environmentChannel(profile.environment);
@@ -429,12 +482,9 @@ function assertProfileChannel(profile, channel) {
429
482
  function tableExists(database, table) {
430
483
  return Boolean(database.prepare("select name from sqlite_master where type = 'table' and name = ?").get(table));
431
484
  }
432
- function gitShasMatch(expected, actual) {
433
- if (!actual) return false;
434
- const normalizedExpected = expected.toLowerCase();
435
- const normalizedActual = actual.toLowerCase();
436
- const shorterLength = Math.min(normalizedExpected.length, normalizedActual.length);
437
- return shorterLength >= 12 && normalizedExpected.slice(0, shorterLength) === normalizedActual.slice(0, shorterLength);
485
+ function tableColumns(database, table) {
486
+ if (!tableExists(database, table)) return /* @__PURE__ */ new Set();
487
+ return new Set(database.prepare(`pragma table_info(${table})`).all().map((row) => row.name));
438
488
  }
439
489
  function inspectDatabase(profile) {
440
490
  const result = {
@@ -448,11 +498,14 @@ function inspectDatabase(profile) {
448
498
  const database = new DatabaseSync(profile.database_path, { readOnly: true });
449
499
  try {
450
500
  if (tableExists(database, "lineage_profile_identity")) {
451
- const rows = database.prepare("select profile_id, environment, bound_at from lineage_profile_identity").all();
501
+ const columns = tableColumns(database, "lineage_profile_identity");
502
+ const fingerprintExpression = columns.has("profile_fingerprint") ? "profile_fingerprint" : "'' as profile_fingerprint";
503
+ const rows = database.prepare(`select profile_id, environment, bound_at, ${fingerprintExpression} from lineage_profile_identity`).all();
452
504
  if (rows.length === 1) {
453
505
  const identity = {
454
506
  bound_at: typeof rows[0].bound_at === "string" ? rows[0].bound_at : void 0,
455
507
  environment: String(rows[0].environment),
508
+ profile_fingerprint: typeof rows[0].profile_fingerprint === "string" ? rows[0].profile_fingerprint : "",
456
509
  profile_id: String(rows[0].profile_id)
457
510
  };
458
511
  result.identity = identity;
@@ -476,7 +529,14 @@ function doctorLineageProfile(selector, runtime) {
476
529
  const result = {
477
530
  checks,
478
531
  ok: false,
479
- runtime: { channel: runtime.channel, git_sha: runtime.gitSha, version: runtime.version },
532
+ runtime: {
533
+ channel: runtime.channel,
534
+ code_fingerprint: runtime.code?.fingerprint,
535
+ code_origin: runtime.code?.origin,
536
+ code_verified: runtime.code?.verified,
537
+ git_sha: runtime.gitSha,
538
+ version: runtime.version
539
+ },
480
540
  schema_version: lineageProfileDoctorSchemaVersion
481
541
  };
482
542
  let profile;
@@ -495,12 +555,18 @@ function doctorLineageProfile(selector, runtime) {
495
555
  } catch (error) {
496
556
  checks.push({ id: "runtime_channel", message: error instanceof Error ? error.message : String(error), status: "fail" });
497
557
  }
558
+ try {
559
+ assertProfileRuntimePin(profile, runtime);
560
+ checks.push({ id: "runtime_code", message: `Verified ${runtime.code.origin} code ${runtime.code.fingerprint}`, status: "pass" });
561
+ } catch (error) {
562
+ checks.push({ id: "runtime_code", message: error instanceof Error ? error.message : String(error), status: "fail" });
563
+ }
498
564
  if (profile.expected_runtime?.version && profile.expected_runtime.version !== runtime.version) {
499
565
  checks.push({ id: "runtime_version", message: `Expected version ${profile.expected_runtime.version}, found ${runtime.version}`, status: "fail" });
500
566
  } else {
501
567
  checks.push({ id: "runtime_version", message: `Runtime version ${runtime.version}`, status: "pass" });
502
568
  }
503
- if (profile.expected_runtime?.git_sha && !gitShasMatch(profile.expected_runtime.git_sha, runtime.gitSha)) {
569
+ if (profile.expected_runtime?.git_sha && profile.expected_runtime.git_sha !== runtime.gitSha) {
504
570
  checks.push({ id: "runtime_git_sha", message: `Expected Git SHA ${profile.expected_runtime.git_sha}, found ${runtime.gitSha || "unavailable"}`, status: "fail" });
505
571
  } else if (profile.expected_runtime?.git_sha) {
506
572
  checks.push({ id: "runtime_git_sha", message: `Git SHA ${runtime.gitSha}`, status: "pass" });
@@ -523,10 +589,10 @@ function doctorLineageProfile(selector, runtime) {
523
589
  if (database.error) checks.push({ id: "database_read", message: database.error, status: "fail" });
524
590
  if (!database.identity) {
525
591
  checks.push({ id: "database_identity", message: "Database is not bound to a Lineage profile", status: "fail" });
526
- } else if (database.identity.profile_id !== profile.profile_id || database.identity.environment !== profile.environment) {
592
+ } else if (database.identity.profile_id !== profile.profile_id || database.identity.environment !== profile.environment || database.identity.profile_fingerprint !== profile.profile_fingerprint) {
527
593
  checks.push({
528
594
  id: "database_identity",
529
- message: `Database identity ${database.identity.profile_id}/${database.identity.environment} does not match ${profile.profile_id}/${profile.environment}`,
595
+ message: `Database identity ${database.identity.profile_id}/${database.identity.environment}/${database.identity.profile_fingerprint || "no-fingerprint"} does not match ${profile.profile_id}/${profile.environment}/${profile.profile_fingerprint}`,
530
596
  status: "fail"
531
597
  });
532
598
  } else {
@@ -549,6 +615,7 @@ function runtimeProfileIdentity(channel) {
549
615
  return {
550
616
  bound: true,
551
617
  environment,
618
+ fingerprint: process.env.LINEAGE_PROFILE_FINGERPRINT,
552
619
  id,
553
620
  manifest_path: process.env.LINEAGE_PROFILE_MANIFEST,
554
621
  service_origin: process.env.LINEAGE_PROFILE_SERVICE_ORIGIN
@@ -558,12 +625,12 @@ function runtimeProfileIdentity(channel) {
558
625
  bound: false,
559
626
  environment: channelEnvironment(channel),
560
627
  id: "legacy-unbound",
561
- warning: id || environment || process.env.LINEAGE_PROFILE_MANIFEST ? "Invalid unbound runtime: derived profile identity was supplied without a resolved LINEAGE_PROFILE selector." : "Legacy unbound runtime: database and asset paths are not protected by a named profile."
628
+ warning: id || environment || process.env.LINEAGE_PROFILE_FINGERPRINT || process.env.LINEAGE_PROFILE_MANIFEST ? "Invalid unbound runtime: derived profile identity was supplied without a resolved LINEAGE_PROFILE selector." : "Legacy unbound runtime: database and asset paths are not protected by a named profile."
562
629
  };
563
630
  }
564
631
  function assertRuntimeProfileSafety(channel) {
565
632
  const hasDerivedIdentity = Boolean(
566
- process.env.LINEAGE_PROFILE_ID || process.env.LINEAGE_PROFILE_ENVIRONMENT || process.env.LINEAGE_PROFILE_MANIFEST || process.env.LINEAGE_PROFILE_SERVICE_ORIGIN
633
+ process.env.LINEAGE_PROFILE_ID || process.env.LINEAGE_PROFILE_ENVIRONMENT || process.env.LINEAGE_PROFILE_FINGERPRINT || process.env.LINEAGE_PROFILE_MANIFEST || process.env.LINEAGE_PROFILE_SERVICE_ORIGIN
567
634
  );
568
635
  if (hasDerivedIdentity && !process.env.LINEAGE_PROFILE) {
569
636
  throw new Error("Derived Lineage profile identity requires LINEAGE_PROFILE; start through the Lineage CLI with --profile");
@@ -574,16 +641,6 @@ function assertRuntimeProfileSafety(channel) {
574
641
  }
575
642
  }
576
643
  function assertUnselectedDatabaseIsUnbound(runtime) {
577
- if (runtime.database.exists && runtime.database.error) {
578
- throw new Error(
579
- `Database ${runtime.database.path} identity could not be verified: ${runtime.database.error}; refusing unselected access`
580
- );
581
- }
582
- if (runtime.schema.profile_identity_rows !== void 0 && runtime.schema.profile_identity_rows !== 1) {
583
- throw new Error(
584
- `Database ${runtime.database.path} has invalid Lineage profile identity row count ${runtime.schema.profile_identity_rows}; refusing unselected access`
585
- );
586
- }
587
644
  if (!runtime.schema.profile_id) return;
588
645
  const environment = runtime.schema.profile_environment || "unknown";
589
646
  throw new Error(
@@ -592,15 +649,15 @@ function assertUnselectedDatabaseIsUnbound(runtime) {
592
649
  }
593
650
  function assertResolvedRuntimeProfileEnvironment(profile) {
594
651
  const serviceUrl = new URL(profile.service_origin);
595
- const serviceHost = serviceUrl.hostname.startsWith("[") && serviceUrl.hostname.endsWith("]") ? serviceUrl.hostname.slice(1, -1) : serviceUrl.hostname;
596
652
  const expected = /* @__PURE__ */ new Map([
597
653
  ["LINEAGE_PROFILE_ID", profile.profile_id],
598
654
  ["LINEAGE_PROFILE_ENVIRONMENT", profile.environment],
655
+ ["LINEAGE_PROFILE_FINGERPRINT", profile.profile_fingerprint],
599
656
  ["LINEAGE_PROFILE_MANIFEST", profile.manifest_path],
600
657
  ["LINEAGE_PROFILE_SERVICE_ORIGIN", profile.service_origin],
601
658
  ["LINEAGE_DB", profile.database_path],
602
659
  ["LINEAGE_ASSET_ROOT", profile.asset_root],
603
- ["HOST", serviceHost],
660
+ ["HOST", serviceUrl.hostname],
604
661
  ["PORT", serviceUrl.port]
605
662
  ]);
606
663
  const conflicts = [];
@@ -616,9 +673,6 @@ function assertResolvedRuntimeProfileEnvironment(profile) {
616
673
  }
617
674
 
618
675
  // src/server/profileWriterLease.ts
619
- import { createHash as createHash2, randomUUID, timingSafeEqual } from "node:crypto";
620
- import { existsSync as existsSync4, lstatSync, mkdirSync as mkdirSync3, readFileSync as readFileSync3, realpathSync, renameSync, rmSync, writeFileSync } from "node:fs";
621
- import { basename as basename3, dirname as dirname3, join as join4, resolve as resolve3 } from "node:path";
622
676
  var writerLeaseSchemaVersion = "lineage.profile_writer_lease.v1";
623
677
  var ownerFileName = "owner.json";
624
678
  var ProfileWriterLeaseConflictError = class extends Error {
@@ -629,24 +683,8 @@ var ProfileWriterLeaseConflictError = class extends Error {
629
683
  }
630
684
  owner;
631
685
  };
632
- function canonicalDatabasePath(databasePath) {
633
- const resolved = resolve3(databasePath);
634
- const missingSegments = [];
635
- let candidate = resolved;
636
- while (!existsSync4(candidate)) {
637
- const parent = dirname3(candidate);
638
- if (parent === candidate) return resolved;
639
- missingSegments.unshift(basename3(candidate));
640
- candidate = parent;
641
- }
642
- try {
643
- return join4(realpathSync(candidate), ...missingSegments);
644
- } catch {
645
- return resolved;
646
- }
647
- }
648
686
  function profileWriterLockPath(profile) {
649
- return `${canonicalDatabasePath(profile.database_path)}.writer.lock`;
687
+ return join4(dirname3(profile.manifest_path), "writer.lock");
650
688
  }
651
689
  function ownerPath(lockPath) {
652
690
  return join4(lockPath, ownerFileName);
@@ -669,7 +707,7 @@ function readOwner(lockPath) {
669
707
  throw new Error(`Writer lease metadata is invalid at ${lockPath}; refusing automatic recovery`);
670
708
  }
671
709
  const owner = raw;
672
- if (owner.schema_version !== writerLeaseSchemaVersion || typeof owner.profile_id !== "string" || owner.environment !== "production" && owner.environment !== "preview" && owner.environment !== "development" || owner.role !== "service" && owner.role !== "cli" || !Number.isSafeInteger(owner.pid) || Number(owner.pid) <= 0 || typeof owner.acquired_at !== "string" || Number.isNaN(Date.parse(owner.acquired_at)) || owner.role === "service" && (typeof owner.service_origin !== "string" || !owner.service_origin) || owner.role === "cli" && owner.service_origin !== void 0 || typeof owner.token !== "string" || owner.token.length < 16) {
710
+ if (owner.schema_version !== writerLeaseSchemaVersion || typeof owner.profile_id !== "string" || typeof owner.profile_fingerprint !== "string" || !/^[a-f0-9]{64}$/i.test(owner.profile_fingerprint) || owner.environment !== "production" && owner.environment !== "preview" && owner.environment !== "development" || owner.role !== "service" && owner.role !== "cli" || !Number.isSafeInteger(owner.pid) || Number(owner.pid) <= 0 || typeof owner.acquired_at !== "string" || Number.isNaN(Date.parse(owner.acquired_at)) || owner.role === "service" && (typeof owner.service_origin !== "string" || !owner.service_origin) || owner.role === "cli" && owner.service_origin !== void 0 || typeof owner.token !== "string" || owner.token.length < 16) {
673
711
  throw new Error(`Writer lease metadata is invalid at ${lockPath}; refusing automatic recovery`);
674
712
  }
675
713
  return owner;
@@ -686,10 +724,10 @@ function processIsAlive(pid) {
686
724
  function createLeaseDirectory(lockPath, owner) {
687
725
  mkdirSync3(lockPath, { mode: 448 });
688
726
  try {
689
- writeFileSync(ownerPath(lockPath), `${JSON.stringify(owner, null, 2)}
727
+ writeFileSync2(ownerPath(lockPath), `${JSON.stringify(owner, null, 2)}
690
728
  `, { encoding: "utf8", flag: "wx", mode: 384 });
691
729
  } catch (error) {
692
- rmSync(lockPath, { force: true, recursive: true });
730
+ rmSync2(lockPath, { force: true, recursive: true });
693
731
  throw error;
694
732
  }
695
733
  }
@@ -701,14 +739,15 @@ function reclaimDeadOwner(lockPath, owner) {
701
739
  publicOwner
702
740
  );
703
741
  }
704
- const tokenFence = createHash2("sha256").update(owner.token).digest("hex").slice(0, 24);
742
+ const tokenFence = createHash3("sha256").update(owner.token).digest("hex").slice(0, 24);
705
743
  const tombstone = `${lockPath}.stale-${owner.pid}-${tokenFence}`;
706
744
  try {
707
745
  renameSync(lockPath, tombstone);
708
746
  } catch (error) {
709
- if (errorCode(error) === "ENOENT" || errorCode(error) === "EEXIST" || errorCode(error) === "ENOTEMPTY") return;
747
+ if (errorCode(error) === "ENOENT") return;
710
748
  throw error;
711
749
  }
750
+ rmSync2(tombstone, { force: true, recursive: true });
712
751
  }
713
752
  function acquireProfileWriterLease(profile, channel, role = "service") {
714
753
  assertProfileChannel(profile, channel);
@@ -717,11 +756,12 @@ function acquireProfileWriterLease(profile, channel, role = "service") {
717
756
  acquired_at: (/* @__PURE__ */ new Date()).toISOString(),
718
757
  environment: profile.environment,
719
758
  pid: process.pid,
759
+ profile_fingerprint: profile.profile_fingerprint,
720
760
  profile_id: profile.profile_id,
721
761
  role,
722
762
  schema_version: writerLeaseSchemaVersion,
723
763
  ...role === "service" ? { service_origin: profile.service_origin } : {},
724
- token: randomUUID()
764
+ token: randomUUID2()
725
765
  };
726
766
  for (let attempt = 0; attempt < 4; attempt += 1) {
727
767
  try {
@@ -744,7 +784,7 @@ function acquireProfileWriterLease(profile, channel, role = "service") {
744
784
  released = true;
745
785
  try {
746
786
  const current = readOwner(lockPath);
747
- if (current.token === owner.token && current.pid === owner.pid) rmSync(lockPath, { force: true, recursive: true });
787
+ if (current.token === owner.token && current.pid === owner.pid) rmSync2(lockPath, { force: true, recursive: true });
748
788
  } catch {
749
789
  }
750
790
  if (process.env.LINEAGE_WRITER_LEASE_TOKEN === owner.token) delete process.env.LINEAGE_WRITER_LEASE_TOKEN;
@@ -760,7 +800,7 @@ function acquireProfileWriterLease(profile, channel, role = "service") {
760
800
  if (errorCode(readError) === "ENOENT") continue;
761
801
  throw readError;
762
802
  }
763
- if (existing.profile_id !== profile.profile_id || existing.environment !== profile.environment) {
803
+ if (existing.profile_id !== profile.profile_id || existing.environment !== profile.environment || existing.profile_fingerprint !== profile.profile_fingerprint) {
764
804
  throw new Error(`Writer lease identity at ${lockPath} does not match ${profile.profile_id}/${profile.environment}; manual inspection is required`, { cause: error });
765
805
  }
766
806
  reclaimDeadOwner(lockPath, existing);
@@ -769,23 +809,47 @@ function acquireProfileWriterLease(profile, channel, role = "service") {
769
809
  throw new Error(`Could not acquire writer lease for Lineage profile ${profile.profile_id}; another writer is racing for ownership`);
770
810
  }
771
811
  function assertProfileWriterLeaseHeld() {
772
- if (!process.env.LINEAGE_PROFILE) return;
812
+ if (!process.env.LINEAGE_PROFILE) {
813
+ throw new Error("Persistent writes require a selected named Lineage profile and its writer lease; legacy-unbound access is read-only");
814
+ }
773
815
  const profileId = process.env.LINEAGE_PROFILE_ID;
816
+ const profileFingerprint = process.env.LINEAGE_PROFILE_FINGERPRINT;
774
817
  const environment = process.env.LINEAGE_PROFILE_ENVIRONMENT;
775
818
  const manifestPath = process.env.LINEAGE_PROFILE_MANIFEST;
776
- const databasePath = process.env.LINEAGE_DB;
777
819
  const lockPath = process.env.LINEAGE_WRITER_LOCK_PATH;
778
820
  const token = process.env.LINEAGE_WRITER_LEASE_TOKEN;
779
- if (!profileId || !environment || !manifestPath || !databasePath || !lockPath || !token) {
821
+ if (!profileId || !profileFingerprint || !environment || !manifestPath || !lockPath || !token) {
780
822
  throw new Error("Named Lineage profiles may write only through a process holding the profile writer lease");
781
823
  }
782
- const expectedLockPath = `${canonicalDatabasePath(databasePath)}.writer.lock`;
783
- if (resolve3(lockPath) !== expectedLockPath) throw new Error("Profile writer lease path does not match the selected profile database");
824
+ const expectedLockPath = join4(dirname3(resolve3(manifestPath)), "writer.lock");
825
+ if (resolve3(lockPath) !== expectedLockPath) throw new Error("Profile writer lease path does not match the selected profile manifest");
784
826
  const owner = readOwner(lockPath);
785
- if (owner.pid !== process.pid || owner.token !== token || owner.profile_id !== profileId || owner.environment !== environment) {
827
+ if (owner.pid !== process.pid || owner.token !== token || owner.profile_id !== profileId || owner.profile_fingerprint !== profileFingerprint || owner.environment !== environment) {
786
828
  throw new Error("Current process does not own the selected Lineage profile writer lease");
787
829
  }
788
830
  }
831
+ function assertSelectedProfileDatabaseIdentity(database) {
832
+ const profileId = process.env.LINEAGE_PROFILE_ID;
833
+ const environment = process.env.LINEAGE_PROFILE_ENVIRONMENT;
834
+ const profileFingerprint = process.env.LINEAGE_PROFILE_FINGERPRINT;
835
+ if (!process.env.LINEAGE_PROFILE || !profileId || !environment || !profileFingerprint) {
836
+ throw new Error("Writable SQLite identity validation requires a fully resolved named Lineage profile");
837
+ }
838
+ const table = database.prepare("select name from sqlite_master where type = 'table' and name = 'lineage_profile_identity'").get();
839
+ if (!table) throw new Error(`Refusing writable open: database is not bound to Lineage profile ${profileId}`);
840
+ const columns = new Set(database.prepare("pragma table_info(lineage_profile_identity)").all().map((row) => row.name));
841
+ if (!columns.has("profile_id") || !columns.has("environment") || !columns.has("profile_fingerprint")) {
842
+ throw new Error(`Refusing writable open: database identity for ${profileId} is missing required fingerprint fields`);
843
+ }
844
+ const rows = database.prepare("select profile_id, environment, profile_fingerprint from lineage_profile_identity").all();
845
+ if (rows.length !== 1) throw new Error(`Refusing writable open: expected exactly one database profile identity, found ${rows.length}`);
846
+ const identity = rows[0];
847
+ if (identity.profile_id !== profileId || identity.environment !== environment || identity.profile_fingerprint !== profileFingerprint) {
848
+ throw new Error(
849
+ `Refusing writable open: database identity ${String(identity.profile_id)}/${String(identity.environment)}/${String(identity.profile_fingerprint)} does not match selected profile ${profileId}/${environment}/${profileFingerprint}`
850
+ );
851
+ }
852
+ }
789
853
 
790
854
  // src/server/assetLineageDb.ts
791
855
  var require3 = createRequire2(import.meta.url);
@@ -795,44 +859,19 @@ function nowIso() {
795
859
  function lineageDbPath() {
796
860
  return process.env.LINEAGE_DB || join5(packageRoot, ".lineage", "asset-lineage.sqlite");
797
861
  }
798
- function assertOpenedProfileIdentity(database, databasePath) {
799
- const selector = process.env.LINEAGE_PROFILE;
800
- if (!selector) return;
801
- const profile = resolveLineageProfile(selector);
802
- if (resolve4(databasePath) !== profile.database_path) {
803
- throw new Error(`Profile ${profile.profile_id} requires database ${profile.database_path}, not ${databasePath}`);
804
- }
805
- const table = database.prepare("select 1 from sqlite_master where type = 'table' and name = 'lineage_profile_identity'").get();
806
- if (!table) throw new Error(`Profile database ${databasePath} is not bound to a Lineage profile identity`);
807
- const rows = database.prepare("select profile_id, environment from lineage_profile_identity").all();
808
- if (rows.length !== 1) {
809
- throw new Error(`Profile database ${databasePath} has invalid Lineage profile identity row count ${rows.length}`);
810
- }
811
- if (rows[0].profile_id !== profile.profile_id || rows[0].environment !== profile.environment) {
812
- throw new Error(
813
- `Profile database ${databasePath} is bound to ${rows[0].profile_id}/${rows[0].environment}, not ${profile.profile_id}/${profile.environment}`
814
- );
815
- }
816
- }
817
862
  function lineageDb() {
818
863
  const readOnly = process.env.LINEAGE_DB_ACCESS === "read-only";
819
- const databasePath = lineageDbPath();
820
864
  if (!readOnly) assertProfileWriterLeaseHeld();
821
- if (process.env.LINEAGE_PROFILE && !existsSync5(databasePath)) {
822
- throw new Error(`Profile database does not exist: ${databasePath}; bind the profile before opening it`);
865
+ if (!readOnly && !existsSync5(lineageDbPath())) {
866
+ throw new Error(`Refusing writable open of missing profile database ${lineageDbPath()}; bind an existing database or create a non-production clone first`);
823
867
  }
824
- if (!readOnly) mkdirSync4(join5(databasePath, ".."), { recursive: true });
868
+ if (!readOnly) mkdirSync4(join5(lineageDbPath(), ".."), { recursive: true });
825
869
  const { DatabaseSync } = require3("node:sqlite");
826
- const database = readOnly ? new DatabaseSync(databasePath, { readOnly: true }) : new DatabaseSync(databasePath);
827
- try {
828
- assertOpenedProfileIdentity(database, databasePath);
829
- database.exec("PRAGMA foreign_keys = ON");
830
- database.exec("PRAGMA busy_timeout = 5000");
831
- if (readOnly) return database;
832
- } catch (error) {
833
- database.close();
834
- throw error;
835
- }
870
+ const database = readOnly ? new DatabaseSync(lineageDbPath(), { readOnly: true }) : new DatabaseSync(lineageDbPath());
871
+ if (process.env.LINEAGE_PROFILE) assertSelectedProfileDatabaseIdentity(database);
872
+ database.exec("PRAGMA foreign_keys = ON");
873
+ database.exec("PRAGMA busy_timeout = 5000");
874
+ if (readOnly) return database;
836
875
  database.exec("PRAGMA journal_mode = WAL");
837
876
  database.exec(`
838
877
  create table if not exists projects (
@@ -1680,8 +1719,8 @@ function resolvePackageRoot() {
1680
1719
  const moduleDir = dirname4(fileURLToPath(import.meta.url));
1681
1720
  const candidates = [
1682
1721
  process.env.LINEAGE_REPO_ROOT,
1683
- resolve5(moduleDir, ".."),
1684
- resolve5(moduleDir, "../.."),
1722
+ resolve4(moduleDir, ".."),
1723
+ resolve4(moduleDir, "../.."),
1685
1724
  process.cwd()
1686
1725
  ].filter((candidate) => Boolean(candidate));
1687
1726
  const root = candidates.find(isPackageRoot);
@@ -1689,9 +1728,9 @@ function resolvePackageRoot() {
1689
1728
  return root;
1690
1729
  }
1691
1730
  var packageRoot = resolvePackageRoot();
1692
- var repoRoot = resolve5(process.env.LINEAGE_ASSET_ROOT || packageRoot);
1731
+ var repoRoot = resolve4(process.env.LINEAGE_ASSET_ROOT || packageRoot);
1693
1732
  function setLineageAssetRoot(path) {
1694
- repoRoot = resolve5(path || packageRoot);
1733
+ repoRoot = resolve4(path || packageRoot);
1695
1734
  return repoRoot;
1696
1735
  }
1697
1736
  var defaultProject = "demo-project";
@@ -1881,7 +1920,7 @@ function loadCatalog(project = defaultProject) {
1881
1920
  function saveCatalog(project, catalog) {
1882
1921
  const normalized = normalizeCatalog(catalog, project);
1883
1922
  mkdirSync5(dirname4(catalogPath(project)), { recursive: true });
1884
- writeFileSync2(catalogPath(project), `${JSON.stringify(normalized, null, 2)}
1923
+ writeFileSync3(catalogPath(project), `${JSON.stringify(normalized, null, 2)}
1885
1924
  `);
1886
1925
  return normalized;
1887
1926
  }
@@ -2072,7 +2111,7 @@ function ensureUploadDir() {
2072
2111
  function cleanupUploadedTemp(file) {
2073
2112
  if (!file) return;
2074
2113
  const uploadRoot = ensureUploadDir();
2075
- const resolved = resolve5(file);
2114
+ const resolved = resolve4(file);
2076
2115
  if (resolved.startsWith(`${uploadRoot}/`) && existsSync6(resolved)) {
2077
2116
  unlinkSync(resolved);
2078
2117
  }
@@ -2130,7 +2169,7 @@ function normalizeSelectionInput(fields) {
2130
2169
  import { randomBytes as randomBytes2 } from "node:crypto";
2131
2170
 
2132
2171
  // src/server/agentClaims.ts
2133
- import { createHash as createHash3, randomBytes, timingSafeEqual as timingSafeEqual2 } from "node:crypto";
2172
+ import { createHash as createHash4, randomBytes, timingSafeEqual as timingSafeEqual2 } from "node:crypto";
2134
2173
  var defaultTtlSeconds = 20 * 60;
2135
2174
  var idleAfterSeconds = 5 * 60;
2136
2175
  var staleAfterSeconds = 15 * 60;
@@ -2166,7 +2205,7 @@ function randomId(prefix) {
2166
2205
  return `${prefix}_${Date.now().toString(36)}_${randomBytes(6).toString("base64url").toLowerCase()}`;
2167
2206
  }
2168
2207
  function tokenHash(token) {
2169
- return createHash3("sha256").update(token).digest("hex");
2208
+ return createHash4("sha256").update(token).digest("hex");
2170
2209
  }
2171
2210
  function safeEqual(a, b) {
2172
2211
  const left = Buffer.from(a);
@@ -3474,8 +3513,8 @@ function rootFor(database, project, assetId) {
3474
3513
  }
3475
3514
  function assertCanonicalRoot(database, project, root) {
3476
3515
  requireAsset3(database, project, root);
3477
- const canonicalRoot = rootFor(database, project, root);
3478
- if (canonicalRoot !== root) throw new LineageError(`Asset ${root} is not a lineage root`, 400);
3516
+ const canonicalRoot2 = rootFor(database, project, root);
3517
+ if (canonicalRoot2 !== root) throw new LineageError(`Asset ${root} is not a lineage root`, 400);
3479
3518
  }
3480
3519
  function explicitWorkspaceRoot(database, project, assetId) {
3481
3520
  const row = database.prepare(`
@@ -5947,7 +5986,7 @@ function getAdapterStatus(project, env = process.env) {
5947
5986
  }
5948
5987
 
5949
5988
  // src/server/adapters/posting/bufferPostingService.ts
5950
- import { mkdirSync as mkdirSync6, writeFileSync as writeFileSync3 } from "node:fs";
5989
+ import { mkdirSync as mkdirSync6, writeFileSync as writeFileSync4 } from "node:fs";
5951
5990
  import { join as join9 } from "node:path";
5952
5991
 
5953
5992
  // src/server/contentPostHandoff.ts
@@ -6449,7 +6488,7 @@ function payloadPath(project, postId) {
6449
6488
  function writePayload(project, postId, payload) {
6450
6489
  const file = payloadPath(project, postId);
6451
6490
  mkdirSync6(join9(file, ".."), { recursive: true });
6452
- writeFileSync3(file, `${JSON.stringify(payload, null, 2)}
6491
+ writeFileSync4(file, `${JSON.stringify(payload, null, 2)}
6453
6492
  `);
6454
6493
  return file;
6455
6494
  }
@@ -6559,7 +6598,7 @@ import { Router } from "express";
6559
6598
 
6560
6599
  // src/server/contentBatchImport.ts
6561
6600
  import { existsSync as existsSync8, readdirSync as readdirSync3, readFileSync as readFileSync5 } from "node:fs";
6562
- import { basename as basename4, join as join10, relative as relative2 } from "node:path";
6601
+ import { basename as basename3, join as join10, relative as relative3 } from "node:path";
6563
6602
  function walk(dir) {
6564
6603
  return readdirSync3(dir, { withFileTypes: true }).flatMap((entry) => {
6565
6604
  const path = join10(dir, entry.name);
@@ -6587,7 +6626,7 @@ function phaseFromStatus(value) {
6587
6626
  return "draft";
6588
6627
  }
6589
6628
  function postIdFor(kind, file) {
6590
- const slug2 = basename4(file, ".md").replace(/^\d{4}-\d{2}-/, "");
6629
+ const slug2 = basename3(file, ".md").replace(/^\d{4}-\d{2}-/, "");
6591
6630
  return `${kind}-${slug2}`;
6592
6631
  }
6593
6632
  function itemFromFile(file) {
@@ -6607,8 +6646,8 @@ function itemFromFile(file) {
6607
6646
  phase: phaseFromStatus(meta.status),
6608
6647
  postId: postIdFor(kind, file),
6609
6648
  relatedAsset,
6610
- sourcePath: relative2(repoRoot, file),
6611
- title: text.match(/^#\s+(.+)$/m)?.[1] || basename4(file, ".md")
6649
+ sourcePath: relative3(repoRoot, file),
6650
+ title: text.match(/^#\s+(.+)$/m)?.[1] || basename3(file, ".md")
6612
6651
  };
6613
6652
  }
6614
6653
  function demoMarkdownItems(kind) {
@@ -6953,8 +6992,8 @@ function contentBatchRouter(projectFrom2) {
6953
6992
 
6954
6993
  // src/server/generationReceipts.ts
6955
6994
  import { existsSync as existsSync9, realpathSync as realpathSync2, statSync as statSync4 } from "node:fs";
6956
- import { relative as relative3, resolve as resolve6 } from "node:path";
6957
- import { randomUUID as randomUUID2 } from "node:crypto";
6995
+ import { relative as relative4, resolve as resolve5 } from "node:path";
6996
+ import { randomUUID as randomUUID3 } from "node:crypto";
6958
6997
  var adapterVersion = "generation-receipts-v1";
6959
6998
  var provider = "codex-handoff";
6960
6999
  var GenerationReceiptError = class extends Error {
@@ -6968,7 +7007,7 @@ function isGenerationReceiptError(error) {
6968
7007
  return error instanceof GenerationReceiptError;
6969
7008
  }
6970
7009
  function jobId() {
6971
- return `gen-${Date.now().toString(36)}-${randomUUID2().slice(0, 8)}`;
7010
+ return `gen-${Date.now().toString(36)}-${randomUUID3().slice(0, 8)}`;
6972
7011
  }
6973
7012
  function quote(value) {
6974
7013
  return JSON.stringify(value);
@@ -7164,12 +7203,12 @@ function planImageReroll(project = defaultProject, fields) {
7164
7203
  }
7165
7204
  }
7166
7205
  function isPathInside2(child, parent) {
7167
- const rel = relative3(parent, child);
7206
+ const rel = relative4(parent, child);
7168
7207
  return Boolean(rel) && !rel.startsWith("..") && !rel.startsWith("/");
7169
7208
  }
7170
7209
  function resolveScratchFile(file) {
7171
- const scratchRoot = resolve6(repoRoot, ".asset-scratch");
7172
- const candidate = file.startsWith(".asset-scratch/") || resolve6(file).startsWith(scratchRoot) ? resolve6(repoRoot, file) : resolve6(scratchRoot, file);
7210
+ const scratchRoot = resolve5(repoRoot, ".asset-scratch");
7211
+ const candidate = file.startsWith(".asset-scratch/") || resolve5(file).startsWith(scratchRoot) ? resolve5(repoRoot, file) : resolve5(scratchRoot, file);
7173
7212
  if (!isPathInside2(candidate, scratchRoot)) throw new GenerationReceiptError(`Import file must be under .asset-scratch: ${file}`);
7174
7213
  if (!existsSync9(candidate)) throw new GenerationReceiptError(`Missing import file: ${file}`, 404);
7175
7214
  const realScratchRoot = realpathSync2(scratchRoot);
@@ -7179,7 +7218,7 @@ function resolveScratchFile(file) {
7179
7218
  if (!stats.isFile()) throw new GenerationReceiptError(`Import path is not a file: ${file}`);
7180
7219
  const checksum = fileSha256(candidate);
7181
7220
  return {
7182
- relativePath: relative3(scratchRoot, candidate),
7221
+ relativePath: relative4(scratchRoot, candidate),
7183
7222
  checksum,
7184
7223
  size: stats.size,
7185
7224
  contentType: contentTypeFor(candidate),
@@ -7343,8 +7382,8 @@ function registerLineageTaskRoutes(app2, projectFrom2, asyncRoute2) {
7343
7382
  }
7344
7383
 
7345
7384
  // src/server/assetLineageDemo.ts
7346
- import { createHash as createHash4 } from "node:crypto";
7347
- import { copyFileSync as copyFileSync2, existsSync as existsSync10, mkdirSync as mkdirSync7, mkdtempSync, readFileSync as readFileSync6, rmSync as rmSync2, writeFileSync as writeFileSync4 } from "node:fs";
7385
+ import { createHash as createHash5 } from "node:crypto";
7386
+ import { copyFileSync as copyFileSync3, existsSync as existsSync10, mkdirSync as mkdirSync7, mkdtempSync, readFileSync as readFileSync6, rmSync as rmSync3, writeFileSync as writeFileSync5 } from "node:fs";
7348
7387
  import { tmpdir } from "node:os";
7349
7388
  import { dirname as dirname5, join as join11, posix } from "node:path";
7350
7389
  import { gunzipSync } from "node:zlib";
@@ -7435,7 +7474,7 @@ function swissifierMediaState(manifest) {
7435
7474
  return { invalid, missing };
7436
7475
  }
7437
7476
  function sha256Hex(input) {
7438
- return createHash4("sha256").update(input).digest("hex");
7477
+ return createHash5("sha256").update(input).digest("hex");
7439
7478
  }
7440
7479
  function safeTarEntryName(rawName) {
7441
7480
  const stripped = rawName.replace(/\0.*$/, "").replace(/^\.\//, "");
@@ -7500,7 +7539,7 @@ function extractSwissifierMediaArchive(archive, manifest, destination) {
7500
7539
  if (actualSha !== asset.checksum_sha256) throw new Error(`Checksum mismatch for Swissifier media archive entry: ${name}`);
7501
7540
  const target = join11(destination, directName);
7502
7541
  mkdirSync7(dirname5(target), { recursive: true });
7503
- writeFileSync4(target, file);
7542
+ writeFileSync5(target, file);
7504
7543
  extracted.add(directName);
7505
7544
  offset = nextOffset;
7506
7545
  }
@@ -7607,7 +7646,7 @@ async function downloadSwissifierRichDemoMedia(project = defaultProject, fields
7607
7646
  media_status: swissifierRichDemoMediaStatus(project)
7608
7647
  };
7609
7648
  } finally {
7610
- rmSync2(sourceDir, { force: true, recursive: true });
7649
+ rmSync3(sourceDir, { force: true, recursive: true });
7611
7650
  }
7612
7651
  }
7613
7652
  function restoreDemoSeedMedia(project = defaultProject, fields = { confirmWrite: false }) {
@@ -7666,7 +7705,7 @@ function restoreSwissifierRichDemoMedia(project = defaultProject, fields = { con
7666
7705
  for (const item of copyable) {
7667
7706
  const target = swissifierFilePath(manifest, item.asset);
7668
7707
  mkdirSync7(dirname5(target), { recursive: true });
7669
- copyFileSync2(item.source, target);
7708
+ copyFileSync3(item.source, target);
7670
7709
  }
7671
7710
  }
7672
7711
  return {
@@ -7696,7 +7735,7 @@ function svg(label, fill, stroke) {
7696
7735
  `;
7697
7736
  }
7698
7737
  function assetIdFor(asset) {
7699
- return `local-${createHash4("sha256").update(svg(asset.label, asset.fill, asset.stroke)).digest("hex").slice(0, 12)}`;
7738
+ return `local-${createHash5("sha256").update(svg(asset.label, asset.fill, asset.stroke)).digest("hex").slice(0, 12)}`;
7700
7739
  }
7701
7740
  function demoAssetIds() {
7702
7741
  return Object.fromEntries(demoAssets.map((asset) => [asset.key, assetIdFor(asset)]));
@@ -7704,7 +7743,7 @@ function demoAssetIds() {
7704
7743
  function writeDemoFile(project, asset) {
7705
7744
  const path = demoFilePath(project, asset);
7706
7745
  mkdirSync7(dirname5(path), { recursive: true });
7707
- writeFileSync4(path, svg(asset.label, asset.fill, asset.stroke));
7746
+ writeFileSync5(path, svg(asset.label, asset.fill, asset.stroke));
7708
7747
  }
7709
7748
  function writeDemoFiles(project) {
7710
7749
  const ids = demoAssetIds();
@@ -7729,7 +7768,7 @@ function writeSwissifierRerollFiles(manifest) {
7729
7768
  swissifierRerollAsset(attempt);
7730
7769
  const target = swissifierRerollFilePath(manifest, attempt);
7731
7770
  mkdirSync7(dirname5(target), { recursive: true });
7732
- copyFileSync2(swissifierRerollFixturePath(attempt), target);
7771
+ copyFileSync3(swissifierRerollFixturePath(attempt), target);
7733
7772
  }
7734
7773
  }
7735
7774
  function upsertDemoAssets(project, ids) {
@@ -7763,7 +7802,7 @@ function upsertDemoAssets(project, ids) {
7763
7802
  ids[asset.key],
7764
7803
  project,
7765
7804
  demoRelativePath(project, asset),
7766
- createHash4("sha256").update(body).digest("hex"),
7805
+ createHash5("sha256").update(body).digest("hex"),
7767
7806
  asset.label,
7768
7807
  asset.channel,
7769
7808
  Buffer.byteLength(body),
@@ -8022,7 +8061,7 @@ function archiveDemoLineageWorkspace(project, confirmWrite) {
8022
8061
  if (!isLineageWorkspaceError(error) || error.status !== 404) throw error;
8023
8062
  archived = { ok: true, message: "No demo lineage workspace exists yet", workspace: null };
8024
8063
  }
8025
- if (confirmWrite) rmSync2(demoProjectDir(project), { force: true, recursive: true });
8064
+ if (confirmWrite) rmSync3(demoProjectDir(project), { force: true, recursive: true });
8026
8065
  return {
8027
8066
  ok: true,
8028
8067
  message: confirmWrite ? `Archived ${demoWorkspaceTitle}` : `Would archive ${demoWorkspaceTitle}`,
@@ -8116,14 +8155,49 @@ function registerLineageWorkspaceRoutes(app2, projectFrom2, asyncRoute2) {
8116
8155
  }
8117
8156
 
8118
8157
  // src/server/runtimeInfo.ts
8158
+ import { createHash as createHash6 } from "node:crypto";
8119
8159
  import { spawnSync as spawnSync2 } from "node:child_process";
8120
8160
  import { createRequire as createRequire4 } from "node:module";
8121
- import { existsSync as existsSync11, readFileSync as readFileSync7, statSync as statSync5 } from "node:fs";
8122
- import { join as join12 } from "node:path";
8161
+ import { existsSync as existsSync11, lstatSync as lstatSync2, readFileSync as readFileSync7, readdirSync as readdirSync4, readlinkSync, realpathSync as realpathSync3, statSync as statSync5 } from "node:fs";
8162
+ import { dirname as dirname6, isAbsolute as isAbsolute2, join as join12, resolve as resolve6 } from "node:path";
8163
+ import { fileURLToPath as fileURLToPath2 } from "node:url";
8164
+
8165
+ // src/shared/runtimeInfoTypes.ts
8166
+ var lineageRuntimeBuildSchemaVersion = "lineage.runtime_build.v1";
8167
+ var lineageRuntimeInstallSchemaVersion = "lineage.runtime_install.v1";
8168
+
8169
+ // src/server/runtimeInfo.ts
8123
8170
  var require5 = createRequire4(import.meta.url);
8124
- function packageInfo() {
8171
+ var processStartedAt = (/* @__PURE__ */ new Date()).toISOString();
8172
+ function isLineagePackageRoot(root) {
8125
8173
  try {
8126
- const info = JSON.parse(readFileSync7(join12(packageRoot, "package.json"), "utf8"));
8174
+ const info = JSON.parse(readFileSync7(join12(root, "package.json"), "utf8"));
8175
+ return info.name === "@mean-weasel/lineage";
8176
+ } catch {
8177
+ return false;
8178
+ }
8179
+ }
8180
+ function executingCodeRoot() {
8181
+ const moduleDirectory = dirname6(fileURLToPath2(import.meta.url));
8182
+ const candidates = [resolve6(moduleDirectory, "../.."), resolve6(moduleDirectory, "..")];
8183
+ const root = candidates.find(isLineagePackageRoot);
8184
+ if (!root) throw new Error(`Unable to derive Lineage code root from executing module ${fileURLToPath2(import.meta.url)}`);
8185
+ return canonicalRoot(root);
8186
+ }
8187
+ var codeRoot = executingCodeRoot();
8188
+ function sha256(value) {
8189
+ return createHash6("sha256").update(value).digest("hex");
8190
+ }
8191
+ function canonicalRoot(root) {
8192
+ try {
8193
+ return realpathSync3(root);
8194
+ } catch {
8195
+ return resolve6(root);
8196
+ }
8197
+ }
8198
+ function packageInfo(root = codeRoot) {
8199
+ try {
8200
+ const info = JSON.parse(readFileSync7(join12(root, "package.json"), "utf8"));
8127
8201
  return { name: info.name || "@mean-weasel/lineage", version: info.version || "0.0.0" };
8128
8202
  } catch {
8129
8203
  return { name: "@mean-weasel/lineage", version: "0.0.0" };
@@ -8136,12 +8210,199 @@ function normalizeRuntimeChannel(value) {
8136
8210
  if (value === "development") return "dev";
8137
8211
  return process.env.NODE_ENV === "production" ? "stable" : "dev";
8138
8212
  }
8139
- function gitSha() {
8140
- const envSha = process.env.LINEAGE_GIT_SHA || process.env.GITHUB_SHA;
8141
- if (envSha) return envSha.slice(0, 40);
8142
- if (!existsSync11(join12(packageRoot, ".git"))) return void 0;
8143
- const result = spawnSync2("git", ["rev-parse", "--short=12", "HEAD"], { cwd: packageRoot, encoding: "utf8" });
8144
- return result.status === 0 ? result.stdout.trim() || void 0 : void 0;
8213
+ function git(root, args) {
8214
+ const result = spawnSync2("git", args, { cwd: root, encoding: "utf8", maxBuffer: 32 * 1024 * 1024 });
8215
+ return result.status === 0 ? result.stdout : void 0;
8216
+ }
8217
+ function untrackedFingerprint(root, status) {
8218
+ const hash = createHash6("sha256");
8219
+ hash.update(status);
8220
+ const listed = git(root, ["ls-files", "--others", "--exclude-standard", "-z"]);
8221
+ if (listed === void 0) return hash.digest("hex");
8222
+ for (const relativePath of listed.split("\0").filter(Boolean).sort()) {
8223
+ hash.update("\0");
8224
+ hash.update(relativePath);
8225
+ const path = join12(root, relativePath);
8226
+ try {
8227
+ const stat = lstatSync2(path);
8228
+ if (stat.isSymbolicLink()) hash.update(readlinkSync(path));
8229
+ else if (stat.isFile()) hash.update(readFileSync7(path));
8230
+ else hash.update(`[${stat.mode}:${stat.size}]`);
8231
+ } catch (error) {
8232
+ hash.update(`[unreadable:${error instanceof Error ? error.message : String(error)}]`);
8233
+ }
8234
+ }
8235
+ return hash.digest("hex");
8236
+ }
8237
+ function checkoutCodeIdentity(root, channel, version) {
8238
+ const errors = [];
8239
+ const gitRootRaw = git(root, ["rev-parse", "--show-toplevel"])?.trim();
8240
+ const gitSha = git(root, ["rev-parse", "HEAD"])?.trim();
8241
+ const status = git(root, ["status", "--porcelain=v1", "-z", "--untracked-files=all"]);
8242
+ const diff = git(root, ["diff", "--binary", "HEAD", "--"]);
8243
+ const canonicalGitRoot = gitRootRaw ? canonicalRoot(gitRootRaw) : void 0;
8244
+ if (!canonicalGitRoot || canonicalGitRoot !== root) errors.push("Code root is not the canonical root of a Git checkout/worktree");
8245
+ if (!gitSha || !/^[a-f0-9]{40}$/i.test(gitSha)) errors.push("Checkout Git revision is unavailable");
8246
+ if (status === void 0 || diff === void 0) errors.push("Checkout dirty state could not be inspected");
8247
+ if (channel !== "dev") errors.push(`Checkout code may run only as dev, not ${channel}`);
8248
+ const dirty = Boolean(status);
8249
+ const sourceFingerprint = sha256(`${gitSha || "unknown"}\0${sha256(diff || "")}\0${untrackedFingerprint(root, status || "")}`);
8250
+ return {
8251
+ channel,
8252
+ dirty,
8253
+ errors,
8254
+ fingerprint: sha256(JSON.stringify({ channel, dirty, git_sha: gitSha, origin: "checkout", root, source_fingerprint: sourceFingerprint, version })),
8255
+ git_sha: gitSha,
8256
+ origin: "checkout",
8257
+ package_version: version,
8258
+ root,
8259
+ source_fingerprint: sourceFingerprint,
8260
+ verified: errors.length === 0
8261
+ };
8262
+ }
8263
+ function buildFingerprint(build) {
8264
+ return sha256(JSON.stringify({
8265
+ package_name: build.package_name,
8266
+ package_version: build.package_version,
8267
+ schema_version: build.schema_version,
8268
+ source_dirty: build.source_dirty,
8269
+ source_fingerprint: build.source_fingerprint,
8270
+ source_git_sha: build.source_git_sha
8271
+ }));
8272
+ }
8273
+ function readBuildIdentity(root, errors) {
8274
+ const path = join12(root, "dist", "runtime-build.json");
8275
+ let build;
8276
+ try {
8277
+ build = JSON.parse(readFileSync7(path, "utf8"));
8278
+ } catch (error) {
8279
+ errors.push(`Embedded build identity is missing or unreadable at ${path}: ${error instanceof Error ? error.message : String(error)}`);
8280
+ return void 0;
8281
+ }
8282
+ if (build.schema_version !== lineageRuntimeBuildSchemaVersion) errors.push(`Unsupported build identity schema: ${String(build.schema_version)}`);
8283
+ if (!/^[a-f0-9]{40}$/i.test(build.source_git_sha || "")) errors.push("Embedded build Git revision is invalid");
8284
+ if (!/^[a-f0-9]{64}$/i.test(build.source_fingerprint || "")) errors.push("Embedded source fingerprint is invalid");
8285
+ const expected = buildFingerprint(build);
8286
+ if (build.build_fingerprint !== expected) errors.push("Embedded build fingerprint does not match its contents");
8287
+ return build;
8288
+ }
8289
+ function readInstallReceipt(path, errors) {
8290
+ if (!path) {
8291
+ errors.push("Packaged runtime was not launched through a channel install receipt");
8292
+ return void 0;
8293
+ }
8294
+ let receipt;
8295
+ const receiptPath = resolve6(path);
8296
+ try {
8297
+ receipt = JSON.parse(readFileSync7(receiptPath, "utf8"));
8298
+ } catch (error) {
8299
+ errors.push(`Runtime install receipt is missing or unreadable at ${receiptPath}: ${error instanceof Error ? error.message : String(error)}`);
8300
+ return void 0;
8301
+ }
8302
+ if (receipt.schema_version !== lineageRuntimeInstallSchemaVersion) errors.push(`Unsupported runtime install receipt schema: ${String(receipt.schema_version)}`);
8303
+ if (!isAbsolute2(receipt.package_root || "")) errors.push("Runtime install receipt package_root must be absolute");
8304
+ if (!receipt.package_integrity?.startsWith("sha512-")) errors.push("Runtime install receipt package integrity is invalid");
8305
+ if (receipt.package_source !== "registry" && receipt.package_source !== "local") errors.push("Runtime install receipt package source is invalid");
8306
+ if (typeof receipt.package_spec !== "string" || !receipt.package_spec) errors.push("Runtime install receipt package spec is invalid");
8307
+ if (!/^[a-f0-9]{64}$/i.test(receipt.package_tree_sha256 || "")) errors.push("Runtime install receipt package tree hash is invalid");
8308
+ if (Number.isNaN(Date.parse(receipt.installed_at))) errors.push("Runtime install receipt timestamp is invalid");
8309
+ return { ...receipt, receipt_path: receiptPath };
8310
+ }
8311
+ function lineagePackageTreeSha256(root) {
8312
+ const hash = createHash6("sha256");
8313
+ const visit = (directory, relativeDirectory = "") => {
8314
+ for (const entry of readdirSync4(directory, { withFileTypes: true }).sort((left, right) => left.name.localeCompare(right.name))) {
8315
+ const relativePath = relativeDirectory ? join12(relativeDirectory, entry.name) : entry.name;
8316
+ const path = join12(directory, entry.name);
8317
+ hash.update(relativePath.replaceAll("\\", "/"));
8318
+ hash.update("\0");
8319
+ if (entry.isDirectory()) {
8320
+ hash.update("directory\0");
8321
+ visit(path, relativePath);
8322
+ } else if (entry.isSymbolicLink()) {
8323
+ hash.update("symlink\0");
8324
+ hash.update(readlinkSync(path));
8325
+ } else if (entry.isFile()) {
8326
+ hash.update("file\0");
8327
+ hash.update(readFileSync7(path));
8328
+ } else {
8329
+ hash.update("other\0");
8330
+ }
8331
+ hash.update("\0");
8332
+ }
8333
+ };
8334
+ visit(root);
8335
+ return hash.digest("hex");
8336
+ }
8337
+ function packageCodeIdentity(root, channel, info, receiptPath) {
8338
+ const errors = [];
8339
+ const build = readBuildIdentity(root, errors);
8340
+ const install = readInstallReceipt(receiptPath, errors);
8341
+ if (channel === "dev") errors.push("Published package code cannot run as dev; use a Git checkout/worktree");
8342
+ if (build?.package_name !== info.name || build?.package_version !== info.version) {
8343
+ errors.push("Embedded build package identity does not match package.json");
8344
+ }
8345
+ if (build?.source_dirty) errors.push("Stable and preview package installs require a clean-source build");
8346
+ if (install) {
8347
+ if (install.channel !== channel) errors.push(`Install receipt channel ${install.channel} does not match requested ${channel}`);
8348
+ if (canonicalRoot(install.package_root) !== root) errors.push("Install receipt package root does not match the executing package root");
8349
+ if (install.package_name !== info.name || install.package_version !== info.version) errors.push("Install receipt package identity does not match package.json");
8350
+ if (build && install.build_fingerprint !== build.build_fingerprint) errors.push("Install receipt build fingerprint does not match the embedded build");
8351
+ try {
8352
+ if (lineagePackageTreeSha256(root) !== install.package_tree_sha256) errors.push("Installed package tree does not match the channel install receipt");
8353
+ } catch (error) {
8354
+ errors.push(`Installed package tree could not be verified: ${error instanceof Error ? error.message : String(error)}`);
8355
+ }
8356
+ }
8357
+ const gitSha = build?.source_git_sha;
8358
+ const sourceFingerprint = build?.source_fingerprint;
8359
+ return {
8360
+ build,
8361
+ channel,
8362
+ dirty: build?.source_dirty,
8363
+ errors,
8364
+ fingerprint: sha256(JSON.stringify({
8365
+ build_fingerprint: build?.build_fingerprint,
8366
+ channel,
8367
+ install_integrity: install?.package_integrity,
8368
+ origin: "package",
8369
+ root,
8370
+ version: info.version
8371
+ })),
8372
+ git_sha: gitSha,
8373
+ install,
8374
+ origin: "package",
8375
+ package_version: info.version,
8376
+ root,
8377
+ source_fingerprint: sourceFingerprint,
8378
+ verified: errors.length === 0
8379
+ };
8380
+ }
8381
+ function getLineageCodeIdentity(channel, options = {}) {
8382
+ const root = canonicalRoot(options.root || codeRoot);
8383
+ const info = packageInfo(root);
8384
+ if (existsSync11(join12(root, ".git"))) return checkoutCodeIdentity(root, channel, info.version);
8385
+ if (existsSync11(join12(root, "package.json"))) {
8386
+ return packageCodeIdentity(root, channel, info, options.receiptPath ?? process.env.LINEAGE_RUNTIME_RECEIPT);
8387
+ }
8388
+ const errors = [`Code root has neither checkout metadata nor package.json: ${root}`];
8389
+ return {
8390
+ channel,
8391
+ errors,
8392
+ fingerprint: sha256(JSON.stringify({ channel, origin: "unknown", root, version: info.version })),
8393
+ origin: "unknown",
8394
+ package_version: info.version,
8395
+ root,
8396
+ verified: false
8397
+ };
8398
+ }
8399
+ function assertLineageCodeOrigin(channel) {
8400
+ const identity = getLineageCodeIdentity(channel);
8401
+ if (!identity.verified) {
8402
+ const migration = channel === "dev" ? "Run dev from a Git checkout with npm run lineage:dev -- <command>." : `Install and launch an isolated ${channel} runtime with lineage-channel install ${channel}.`;
8403
+ throw new Error(`Unverified ${channel} code origin: ${identity.errors.join("; ")}. ${migration}`);
8404
+ }
8405
+ return identity;
8145
8406
  }
8146
8407
  function tableExists2(database, table) {
8147
8408
  return Boolean(database.prepare("select name from sqlite_master where type = 'table' and name = ?").get(table));
@@ -8159,6 +8420,7 @@ function getLineageRuntimeInfo(options = {}) {
8159
8420
  const info = packageInfo();
8160
8421
  const dbPath = options.dbPath || lineageDbPath();
8161
8422
  const channel = normalizeRuntimeChannel(options.channel || process.env.LINEAGE_CHANNEL || process.env.LINEAGE_RELEASE_CHANNEL);
8423
+ const code = options.code || getLineageCodeIdentity(channel);
8162
8424
  const databaseInfo = { exists: existsSync11(dbPath), path: dbPath };
8163
8425
  const schema = { migration_keys: [] };
8164
8426
  if (databaseInfo.exists) {
@@ -8173,11 +8435,13 @@ function getLineageRuntimeInfo(options = {}) {
8173
8435
  databaseInfo.workspaces = tableCount(database, "lineage_workspaces");
8174
8436
  schema.migration_keys = migrationKeys(database);
8175
8437
  if (tableExists2(database, "lineage_profile_identity")) {
8176
- const rows = database.prepare("select profile_id, environment from lineage_profile_identity").all();
8177
- schema.profile_identity_rows = rows.length;
8438
+ const columns = new Set(database.prepare("pragma table_info(lineage_profile_identity)").all().map((row) => row.name));
8439
+ const fingerprintExpression = columns.has("profile_fingerprint") ? "profile_fingerprint" : "null as profile_fingerprint";
8440
+ const rows = database.prepare(`select profile_id, environment, ${fingerprintExpression} from lineage_profile_identity`).all();
8178
8441
  if (rows.length === 1) {
8179
8442
  schema.profile_id = rows[0].profile_id;
8180
8443
  schema.profile_environment = rows[0].environment;
8444
+ if (rows[0].profile_fingerprint) schema.profile_fingerprint = rows[0].profile_fingerprint;
8181
8445
  }
8182
8446
  }
8183
8447
  } finally {
@@ -8190,13 +8454,20 @@ function getLineageRuntimeInfo(options = {}) {
8190
8454
  return {
8191
8455
  asset_root: repoRoot,
8192
8456
  channel,
8457
+ code,
8193
8458
  database: databaseInfo,
8194
8459
  fetchedAt: nowIso(),
8195
- git_sha: gitSha(),
8460
+ git_sha: code.git_sha,
8196
8461
  node_env: process.env.NODE_ENV,
8197
8462
  package_name: info.name,
8198
8463
  profile: runtimeProfileIdentity(channel),
8199
8464
  schema,
8465
+ service: {
8466
+ instance_id: process.env.LINEAGE_SERVICE_INSTANCE_ID,
8467
+ launcher_pid: process.env.LINEAGE_LAUNCHER_PID ? Number(process.env.LINEAGE_LAUNCHER_PID) : void 0,
8468
+ pid: process.pid,
8469
+ started_at: processStartedAt
8470
+ },
8200
8471
  version: info.version
8201
8472
  };
8202
8473
  }
@@ -8275,14 +8546,14 @@ function registerManagedWriterRoute(app2, fields) {
8275
8546
  }
8276
8547
 
8277
8548
  // src/cli/lineageCli.ts
8278
- import { existsSync as existsSync13, mkdirSync as mkdirSync8, readFileSync as readFileSync8, writeFileSync as writeFileSync5 } from "node:fs";
8279
- import { dirname as dirname6, join as join13, resolve as resolve8 } from "node:path";
8280
- import { fileURLToPath as fileURLToPath2 } from "node:url";
8549
+ import { existsSync as existsSync13, mkdirSync as mkdirSync8, readFileSync as readFileSync8, writeFileSync as writeFileSync6 } from "node:fs";
8550
+ import { dirname as dirname7, join as join13, resolve as resolve8 } from "node:path";
8551
+ import { fileURLToPath as fileURLToPath3 } from "node:url";
8281
8552
 
8282
8553
  // src/server/lineageSelectionPacket.ts
8283
- import { createHash as createHash5 } from "node:crypto";
8554
+ import { createHash as createHash7 } from "node:crypto";
8284
8555
  import { existsSync as existsSync12, statSync as statSync6 } from "node:fs";
8285
- import { isAbsolute as isAbsolute2, resolve as resolve7 } from "node:path";
8556
+ import { isAbsolute as isAbsolute3, resolve as resolve7 } from "node:path";
8286
8557
  var LineageSelectionPacketError = class extends Error {
8287
8558
  constructor(message, warnings = [], errors = [], diagnostics = []) {
8288
8559
  super(message);
@@ -8328,7 +8599,7 @@ function resolveWorkspace(project, options) {
8328
8599
  }
8329
8600
  function resolveLocalReference(reference) {
8330
8601
  if (!reference) return void 0;
8331
- if (isAbsolute2(reference)) return reference;
8602
+ if (isAbsolute3(reference)) return reference;
8332
8603
  const repoRelative = resolve7(repoRoot, reference);
8333
8604
  if (existsSync12(repoRelative)) return repoRelative;
8334
8605
  return resolve7(repoRoot, ".asset-scratch", reference);
@@ -8407,7 +8678,7 @@ function assetForNode(node, catalogAsset, warnings) {
8407
8678
  };
8408
8679
  }
8409
8680
  function packetId(packetIdentity) {
8410
- const digest = createHash5("sha256").update(JSON.stringify(packetIdentity)).digest("hex").slice(0, 24);
8681
+ const digest = createHash7("sha256").update(JSON.stringify(packetIdentity)).digest("hex").slice(0, 24);
8411
8682
  return `lineage_packet_${digest}`;
8412
8683
  }
8413
8684
  var SHA256_PATTERN = /^[a-f0-9]{64}$/;
@@ -8478,7 +8749,7 @@ function lineageSelectionPacketV2IdentityProjection(packet) {
8478
8749
  }
8479
8750
  function lineageSelectionPacketV2IdentitySha256(packet) {
8480
8751
  const projection = lineageSelectionPacketV2IdentityProjection(packet);
8481
- return createHash5("sha256").update(canonicalLineageSelectionPacketIdentityJson(projection)).digest("hex");
8752
+ return createHash7("sha256").update(canonicalLineageSelectionPacketIdentityJson(projection)).digest("hex");
8482
8753
  }
8483
8754
  function addDiagnostic(diagnostics, diagnostic) {
8484
8755
  if (diagnostics.some((item) => item.code === diagnostic.code && item.severity === diagnostic.severity && item.asset_id === diagnostic.asset_id)) return;
@@ -8784,7 +9055,7 @@ function getLineageSelectionPacket(project, options = {}) {
8784
9055
 
8785
9056
  // src/cli/lineageCli.ts
8786
9057
  function packageRoot2() {
8787
- return resolve8(dirname6(fileURLToPath2(import.meta.url)), "..", "..");
9058
+ return resolve8(dirname7(fileURLToPath3(import.meta.url)), "..", "..");
8788
9059
  }
8789
9060
  function packageVersion() {
8790
9061
  try {
@@ -8881,8 +9152,8 @@ function runLineageDataCommand(command, args) {
8881
9152
  const out = readOption(args, "--out");
8882
9153
  if (out) {
8883
9154
  const outPath = resolve8(out);
8884
- mkdirSync8(dirname6(outPath), { recursive: true });
8885
- writeFileSync5(outPath, `${JSON.stringify(packet, null, 2)}
9155
+ mkdirSync8(dirname7(outPath), { recursive: true });
9156
+ writeFileSync6(outPath, `${JSON.stringify(packet, null, 2)}
8886
9157
  `);
8887
9158
  }
8888
9159
  return packet;
@@ -9092,15 +9363,21 @@ function runLineageAgentCommand(command, args) {
9092
9363
 
9093
9364
  // src/server.ts
9094
9365
  var runtimeChannel = normalizeRuntimeChannel(process.env.LINEAGE_CHANNEL || process.env.LINEAGE_RELEASE_CHANNEL);
9366
+ var startupCode = assertLineageCodeOrigin(runtimeChannel);
9095
9367
  assertRuntimeProfileSafety(runtimeChannel);
9096
- var startupRuntime = getLineageRuntimeInfo({ channel: runtimeChannel });
9368
+ var startupRuntime = getLineageRuntimeInfo({ channel: runtimeChannel, code: startupCode });
9097
9369
  if (!process.env.LINEAGE_PROFILE) assertUnselectedDatabaseIsUnbound(startupRuntime);
9098
9370
  var startupProfile;
9099
9371
  if (process.env.LINEAGE_PROFILE) {
9100
9372
  if (!process.env.LINEAGE_PROFILE_ID || !process.env.LINEAGE_PROFILE_ENVIRONMENT || !process.env.LINEAGE_PROFILE_MANIFEST) {
9101
9373
  throw new Error("LINEAGE_PROFILE must be resolved by the Lineage CLI before server startup; use lineage start --profile or lineage-dev start --profile");
9102
9374
  }
9103
- const doctor = doctorLineageProfile(process.env.LINEAGE_PROFILE, { channel: runtimeChannel, gitSha: startupRuntime.git_sha, version: startupRuntime.version });
9375
+ const doctor = doctorLineageProfile(process.env.LINEAGE_PROFILE, {
9376
+ channel: runtimeChannel,
9377
+ code: startupRuntime.code,
9378
+ gitSha: startupRuntime.git_sha,
9379
+ version: startupRuntime.version
9380
+ });
9104
9381
  if (!doctor.ok) {
9105
9382
  const failures = doctor.checks.filter((check) => check.status === "fail").map((check) => `${check.id}: ${check.message}`).join("; ");
9106
9383
  throw new Error(`Configured Lineage profile failed doctor: ${failures}`);
@@ -9109,7 +9386,8 @@ if (process.env.LINEAGE_PROFILE) {
9109
9386
  startupProfile = doctor.profile;
9110
9387
  }
9111
9388
  var writerLease = startupProfile ? acquireProfileWriterLease(startupProfile, runtimeChannel, "service") : void 0;
9112
- delete process.env.LINEAGE_DB_ACCESS;
9389
+ if (startupProfile) delete process.env.LINEAGE_DB_ACCESS;
9390
+ else process.env.LINEAGE_DB_ACCESS = "read-only";
9113
9391
  if (writerLease) {
9114
9392
  try {
9115
9393
  const startupDatabase = lineageDb();
@@ -9124,7 +9402,10 @@ var port = Number(process.env.PORT || 5173);
9124
9402
  var host = process.env.HOST || "lineage.localhost";
9125
9403
  var isProduction = process.env.NODE_ENV === "production";
9126
9404
  var maxUploadBytes = Number(process.env.LINEAGE_MAX_UPLOAD_MB || 200) * 1024 * 1024;
9127
- var upload = multer({ dest: ensureUploadDir(), limits: { fileSize: maxUploadBytes } });
9405
+ var upload = multer({
9406
+ limits: { fileSize: maxUploadBytes },
9407
+ storage: startupProfile ? multer.diskStorage({ destination: ensureUploadDir() }) : multer.memoryStorage()
9408
+ });
9128
9409
  app.use(express2.json({ limit: "1mb" }));
9129
9410
  registerManagedWriterRoute(app, {
9130
9411
  accepts: lineageCliCanDelegateMutation,
@@ -9133,6 +9414,13 @@ registerManagedWriterRoute(app, {
9133
9414
  profile: startupProfile,
9134
9415
  writerLease
9135
9416
  });
9417
+ app.use((req, res, next) => {
9418
+ if (startupProfile || req.method === "GET" || req.method === "HEAD" || req.method === "OPTIONS") return next();
9419
+ res.status(409).json({
9420
+ error: "profile_required",
9421
+ message: "Persistent writes require a selected named Lineage profile; legacy-unbound service access is read-only."
9422
+ });
9423
+ });
9136
9424
  function projectFrom(input) {
9137
9425
  const candidate = input.body?.project || input.body?.product || input.query?.project || input.query?.product;
9138
9426
  return typeof candidate === "string" ? candidate : defaultProduct;
@@ -9146,7 +9434,7 @@ app.get("/api/projects", asyncRoute((_req, res) => {
9146
9434
  res.json({ projects: listProjects() });
9147
9435
  }));
9148
9436
  app.get("/api/runtime", asyncRoute((_req, res) => {
9149
- res.json({ ok: true, runtime: getLineageRuntimeInfo() });
9437
+ res.json({ ok: true, runtime: getLineageRuntimeInfo({ channel: runtimeChannel, code: startupCode }) });
9150
9438
  }));
9151
9439
  app.get(
9152
9440
  "/api/assets",