@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/CHANGELOG.md +12 -0
- package/README.md +152 -56
- package/dist/cli/lineage-channel.js +303 -0
- package/dist/cli/lineage-channel.js.map +7 -0
- package/dist/cli/lineage-dev.js +900 -220
- package/dist/cli/lineage-dev.js.map +4 -4
- package/dist/cli/lineage-preview.js +6182 -0
- package/dist/cli/lineage-preview.js.map +7 -0
- package/dist/cli/lineage.js +891 -216
- package/dist/cli/lineage.js.map +4 -4
- package/dist/cli/managed-service.js +430 -0
- package/dist/cli/managed-service.js.map +7 -0
- package/dist/runtime-build.json +9 -0
- package/dist/server.js +452 -164
- package/dist/server.js.map +4 -4
- package/dist/web/assets/{index-OkymIyPO.js → index-DJ5UR_oa.js} +1 -1
- package/dist/web/index.html +1 -1
- package/package.json +22 -9
package/dist/cli/lineage.js
CHANGED
|
@@ -1,19 +1,20 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/cli/lineageCli.ts
|
|
4
|
-
import { existsSync as existsSync11, mkdirSync as mkdirSync6, readFileSync as readFileSync6, writeFileSync as
|
|
4
|
+
import { existsSync as existsSync11, mkdirSync as mkdirSync6, readFileSync as readFileSync6, writeFileSync as writeFileSync4 } from "node:fs";
|
|
5
|
+
import { randomUUID as randomUUID4 } from "node:crypto";
|
|
5
6
|
import { homedir as homedir2, platform as platform2 } from "node:os";
|
|
6
|
-
import { dirname as
|
|
7
|
+
import { dirname as dirname6, join as join10, resolve as resolve8 } from "node:path";
|
|
7
8
|
import { spawn } from "node:child_process";
|
|
8
|
-
import { fileURLToPath as
|
|
9
|
+
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
9
10
|
|
|
10
11
|
// src/server/agentClaims.ts
|
|
11
|
-
import { createHash as
|
|
12
|
+
import { createHash as createHash4, randomBytes, timingSafeEqual as timingSafeEqual2 } from "node:crypto";
|
|
12
13
|
|
|
13
14
|
// src/server/assetLineageDb.ts
|
|
14
15
|
import { createRequire as createRequire2 } from "node:module";
|
|
15
16
|
import { existsSync as existsSync6, mkdirSync as mkdirSync5 } from "node:fs";
|
|
16
|
-
import { join as join6
|
|
17
|
+
import { join as join6 } from "node:path";
|
|
17
18
|
|
|
18
19
|
// src/server/assetCore.ts
|
|
19
20
|
import { existsSync as existsSync3, mkdirSync as mkdirSync2, readdirSync as readdirSync2, readFileSync as readFileSync2, unlinkSync, writeFileSync } from "node:fs";
|
|
@@ -587,15 +588,35 @@ function validateProject(project = defaultProject) {
|
|
|
587
588
|
return { project: catalog.project, product: catalog.product, catalogPath: resolvedCatalogPath(project), default_bucket: catalog.default_bucket, default_region: catalog.default_region, asset_count: catalog.assets.length };
|
|
588
589
|
}
|
|
589
590
|
|
|
591
|
+
// src/server/profileWriterLease.ts
|
|
592
|
+
import { createHash as createHash3, randomUUID as randomUUID2, timingSafeEqual } from "node:crypto";
|
|
593
|
+
import { existsSync as existsSync5, lstatSync, mkdirSync as mkdirSync4, readFileSync as readFileSync4, renameSync, rmSync as rmSync2, writeFileSync as writeFileSync3 } from "node:fs";
|
|
594
|
+
import { dirname as dirname4, join as join5, resolve as resolve4 } from "node:path";
|
|
595
|
+
|
|
590
596
|
// src/server/lineageProfiles.ts
|
|
597
|
+
import { createHash as createHash2, randomUUID } from "node:crypto";
|
|
591
598
|
import { createRequire } from "node:module";
|
|
592
|
-
import {
|
|
599
|
+
import {
|
|
600
|
+
chmodSync,
|
|
601
|
+
constants as fsConstants,
|
|
602
|
+
copyFileSync as copyFileSync2,
|
|
603
|
+
existsSync as existsSync4,
|
|
604
|
+
linkSync,
|
|
605
|
+
mkdirSync as mkdirSync3,
|
|
606
|
+
readFileSync as readFileSync3,
|
|
607
|
+
realpathSync,
|
|
608
|
+
rmSync,
|
|
609
|
+
statSync as statSync3,
|
|
610
|
+
writeFileSync as writeFileSync2
|
|
611
|
+
} from "node:fs";
|
|
593
612
|
import { homedir, platform } from "node:os";
|
|
594
|
-
import { dirname as dirname3, isAbsolute, join as join4, resolve as resolve3 } from "node:path";
|
|
613
|
+
import { dirname as dirname3, isAbsolute, join as join4, relative as relative2, resolve as resolve3 } from "node:path";
|
|
595
614
|
|
|
596
615
|
// src/shared/lineageProfileTypes.ts
|
|
597
616
|
var lineageProfileSchemaVersion = "lineage.profile.v1";
|
|
598
617
|
var lineageProfileDoctorSchemaVersion = "lineage.profile_doctor.v1";
|
|
618
|
+
var lineageProfileCloneReceiptSchemaVersion = "lineage.profile_clone_receipt.v1";
|
|
619
|
+
var lineageProfileAssetsCloneReceiptSchemaVersion = "lineage.profile_assets_clone_receipt.v1";
|
|
599
620
|
|
|
600
621
|
// src/server/lineageProfiles.ts
|
|
601
622
|
var require2 = createRequire(import.meta.url);
|
|
@@ -647,6 +668,16 @@ function validateChannel(value) {
|
|
|
647
668
|
if (value === "stable" || value === "preview" || value === "dev") return value;
|
|
648
669
|
throw new Error(`Invalid expected runtime channel: ${String(value)}`);
|
|
649
670
|
}
|
|
671
|
+
function validateCodeOrigin(value) {
|
|
672
|
+
if (value === void 0) return void 0;
|
|
673
|
+
if (value === "checkout" || value === "package") return value;
|
|
674
|
+
throw new Error(`Invalid expected runtime code_origin: ${String(value)}`);
|
|
675
|
+
}
|
|
676
|
+
function validateFingerprint(value, field) {
|
|
677
|
+
if (value === void 0) return void 0;
|
|
678
|
+
if (!/^[a-f0-9]{64}$/i.test(value)) throw new Error(`Profile ${field} must be a 64-character SHA-256 fingerprint`);
|
|
679
|
+
return value.toLowerCase();
|
|
680
|
+
}
|
|
650
681
|
function resolveManifestPath(manifestPath, value) {
|
|
651
682
|
return resolve3(dirname3(manifestPath), value);
|
|
652
683
|
}
|
|
@@ -662,9 +693,6 @@ function validateServiceOrigin(value) {
|
|
|
662
693
|
if (parsed.username || parsed.password || parsed.pathname !== "/" || parsed.search || parsed.hash) {
|
|
663
694
|
throw new Error("Profile service_origin must contain only scheme, host, and port");
|
|
664
695
|
}
|
|
665
|
-
const hostname = parsed.hostname.replace(/^\[|\]$/g, "").toLowerCase();
|
|
666
|
-
const isLocal = hostname === "localhost" || hostname.endsWith(".localhost") || hostname === "::1" || /^127(?:\.\d{1,3}){3}$/.test(hostname);
|
|
667
|
-
if (!isLocal) throw new Error("Profile service_origin must use a loopback or localhost host");
|
|
668
696
|
return parsed.origin;
|
|
669
697
|
}
|
|
670
698
|
function resolveLineageProfile(selector) {
|
|
@@ -692,6 +720,8 @@ function resolveLineageProfile(selector) {
|
|
|
692
720
|
const expected = expectedRaw;
|
|
693
721
|
const expectedGitSha = expected ? optionalString(expected, "git_sha") : void 0;
|
|
694
722
|
const expectedVersion = expected ? optionalString(expected, "version") : void 0;
|
|
723
|
+
const expectedCodeFingerprint = validateFingerprint(expected ? optionalString(expected, "code_fingerprint") : void 0, "expected_runtime.code_fingerprint");
|
|
724
|
+
const expectedCodeOrigin = validateCodeOrigin(expected?.code_origin);
|
|
695
725
|
const migrationsRaw = record.required_schema_migrations;
|
|
696
726
|
if (migrationsRaw !== void 0 && (!Array.isArray(migrationsRaw) || migrationsRaw.some((value) => typeof value !== "string" || !value.trim()))) {
|
|
697
727
|
throw new Error("Profile required_schema_migrations must be an array of non-empty strings");
|
|
@@ -708,6 +738,8 @@ function resolveLineageProfile(selector) {
|
|
|
708
738
|
...expected ? {
|
|
709
739
|
expected_runtime: {
|
|
710
740
|
...expectedChannel ? { channel: expectedChannel } : {},
|
|
741
|
+
...expectedCodeFingerprint ? { code_fingerprint: expectedCodeFingerprint } : {},
|
|
742
|
+
...expectedCodeOrigin ? { code_origin: expectedCodeOrigin } : {},
|
|
711
743
|
...expectedGitSha ? { git_sha: expectedGitSha } : {},
|
|
712
744
|
...expectedVersion ? { version: expectedVersion } : {}
|
|
713
745
|
}
|
|
@@ -717,48 +749,30 @@ function resolveLineageProfile(selector) {
|
|
|
717
749
|
schema_version: lineageProfileSchemaVersion,
|
|
718
750
|
service_origin: validateServiceOrigin(requiredString(record, "service_origin"))
|
|
719
751
|
};
|
|
720
|
-
return { ...manifest, manifest_path: manifestPath };
|
|
752
|
+
return { ...manifest, manifest_path: manifestPath, profile_fingerprint: lineageProfileFingerprint(manifest) };
|
|
721
753
|
}
|
|
722
|
-
function
|
|
723
|
-
|
|
724
|
-
|
|
754
|
+
function lineageProfileFingerprint(profile) {
|
|
755
|
+
return createHash2("sha256").update(JSON.stringify({
|
|
756
|
+
asset_root: resolve3(profile.asset_root),
|
|
757
|
+
database_path: resolve3(profile.database_path),
|
|
725
758
|
environment: profile.environment,
|
|
726
|
-
ok: true,
|
|
727
759
|
profile_id: profile.profile_id,
|
|
728
|
-
schema_version:
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
const rows = database.prepare("select profile_id, environment, bound_at from lineage_profile_identity").all();
|
|
745
|
-
if (rows.length > 0) {
|
|
746
|
-
if (rows.length !== 1 || rows[0].profile_id !== profile.profile_id || rows[0].environment !== profile.environment) {
|
|
747
|
-
throw new Error(`Database ${profile.database_path} is already bound to a different Lineage profile identity`);
|
|
748
|
-
}
|
|
749
|
-
database.exec("COMMIT");
|
|
750
|
-
return { ...result, already_bound: true, bound_at: rows[0].bound_at };
|
|
751
|
-
}
|
|
752
|
-
const boundAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
753
|
-
database.prepare("insert into lineage_profile_identity (profile_id, environment, bound_at) values (?, ?, ?)").run(profile.profile_id, profile.environment, boundAt);
|
|
754
|
-
database.exec("COMMIT");
|
|
755
|
-
return { ...result, bound_at: boundAt };
|
|
756
|
-
} catch (error) {
|
|
757
|
-
database.exec("ROLLBACK");
|
|
758
|
-
throw error;
|
|
759
|
-
}
|
|
760
|
-
} finally {
|
|
761
|
-
database.close();
|
|
760
|
+
schema_version: profile.schema_version,
|
|
761
|
+
service_origin: profile.service_origin
|
|
762
|
+
})).digest("hex");
|
|
763
|
+
}
|
|
764
|
+
function assertProfileRuntimePin(profile, runtime) {
|
|
765
|
+
if (!profile.expected_runtime?.code_fingerprint || !profile.expected_runtime.code_origin) {
|
|
766
|
+
throw new Error(`Profile ${profile.profile_id} must pin expected_runtime.code_fingerprint and expected_runtime.code_origin before binding or writing`);
|
|
767
|
+
}
|
|
768
|
+
if (!runtime?.code?.verified) {
|
|
769
|
+
throw new Error(`Profile ${profile.profile_id} requires a verified runtime code identity before binding or writing`);
|
|
770
|
+
}
|
|
771
|
+
if (runtime.code.fingerprint !== profile.expected_runtime.code_fingerprint) {
|
|
772
|
+
throw new Error(`Profile ${profile.profile_id} expects code fingerprint ${profile.expected_runtime.code_fingerprint}, found ${runtime.code.fingerprint}`);
|
|
773
|
+
}
|
|
774
|
+
if (runtime.code.origin !== profile.expected_runtime.code_origin) {
|
|
775
|
+
throw new Error(`Profile ${profile.profile_id} expects code origin ${profile.expected_runtime.code_origin}, found ${runtime.code.origin}`);
|
|
762
776
|
}
|
|
763
777
|
}
|
|
764
778
|
function assertProfileChannel(profile, channel) {
|
|
@@ -772,12 +786,9 @@ function assertProfileChannel(profile, channel) {
|
|
|
772
786
|
function tableExists(database, table) {
|
|
773
787
|
return Boolean(database.prepare("select name from sqlite_master where type = 'table' and name = ?").get(table));
|
|
774
788
|
}
|
|
775
|
-
function
|
|
776
|
-
if (!
|
|
777
|
-
|
|
778
|
-
const normalizedActual = actual.toLowerCase();
|
|
779
|
-
const shorterLength = Math.min(normalizedExpected.length, normalizedActual.length);
|
|
780
|
-
return shorterLength >= 12 && normalizedExpected.slice(0, shorterLength) === normalizedActual.slice(0, shorterLength);
|
|
789
|
+
function tableColumns(database, table) {
|
|
790
|
+
if (!tableExists(database, table)) return /* @__PURE__ */ new Set();
|
|
791
|
+
return new Set(database.prepare(`pragma table_info(${table})`).all().map((row) => row.name));
|
|
781
792
|
}
|
|
782
793
|
function inspectDatabase(profile) {
|
|
783
794
|
const result = {
|
|
@@ -791,11 +802,14 @@ function inspectDatabase(profile) {
|
|
|
791
802
|
const database = new DatabaseSync(profile.database_path, { readOnly: true });
|
|
792
803
|
try {
|
|
793
804
|
if (tableExists(database, "lineage_profile_identity")) {
|
|
794
|
-
const
|
|
805
|
+
const columns = tableColumns(database, "lineage_profile_identity");
|
|
806
|
+
const fingerprintExpression = columns.has("profile_fingerprint") ? "profile_fingerprint" : "'' as profile_fingerprint";
|
|
807
|
+
const rows = database.prepare(`select profile_id, environment, bound_at, ${fingerprintExpression} from lineage_profile_identity`).all();
|
|
795
808
|
if (rows.length === 1) {
|
|
796
809
|
const identity = {
|
|
797
810
|
bound_at: typeof rows[0].bound_at === "string" ? rows[0].bound_at : void 0,
|
|
798
811
|
environment: String(rows[0].environment),
|
|
812
|
+
profile_fingerprint: typeof rows[0].profile_fingerprint === "string" ? rows[0].profile_fingerprint : "",
|
|
799
813
|
profile_id: String(rows[0].profile_id)
|
|
800
814
|
};
|
|
801
815
|
result.identity = identity;
|
|
@@ -819,7 +833,14 @@ function doctorLineageProfile(selector, runtime) {
|
|
|
819
833
|
const result = {
|
|
820
834
|
checks,
|
|
821
835
|
ok: false,
|
|
822
|
-
runtime: {
|
|
836
|
+
runtime: {
|
|
837
|
+
channel: runtime.channel,
|
|
838
|
+
code_fingerprint: runtime.code?.fingerprint,
|
|
839
|
+
code_origin: runtime.code?.origin,
|
|
840
|
+
code_verified: runtime.code?.verified,
|
|
841
|
+
git_sha: runtime.gitSha,
|
|
842
|
+
version: runtime.version
|
|
843
|
+
},
|
|
823
844
|
schema_version: lineageProfileDoctorSchemaVersion
|
|
824
845
|
};
|
|
825
846
|
let profile;
|
|
@@ -838,12 +859,18 @@ function doctorLineageProfile(selector, runtime) {
|
|
|
838
859
|
} catch (error) {
|
|
839
860
|
checks.push({ id: "runtime_channel", message: error instanceof Error ? error.message : String(error), status: "fail" });
|
|
840
861
|
}
|
|
862
|
+
try {
|
|
863
|
+
assertProfileRuntimePin(profile, runtime);
|
|
864
|
+
checks.push({ id: "runtime_code", message: `Verified ${runtime.code.origin} code ${runtime.code.fingerprint}`, status: "pass" });
|
|
865
|
+
} catch (error) {
|
|
866
|
+
checks.push({ id: "runtime_code", message: error instanceof Error ? error.message : String(error), status: "fail" });
|
|
867
|
+
}
|
|
841
868
|
if (profile.expected_runtime?.version && profile.expected_runtime.version !== runtime.version) {
|
|
842
869
|
checks.push({ id: "runtime_version", message: `Expected version ${profile.expected_runtime.version}, found ${runtime.version}`, status: "fail" });
|
|
843
870
|
} else {
|
|
844
871
|
checks.push({ id: "runtime_version", message: `Runtime version ${runtime.version}`, status: "pass" });
|
|
845
872
|
}
|
|
846
|
-
if (profile.expected_runtime?.git_sha &&
|
|
873
|
+
if (profile.expected_runtime?.git_sha && profile.expected_runtime.git_sha !== runtime.gitSha) {
|
|
847
874
|
checks.push({ id: "runtime_git_sha", message: `Expected Git SHA ${profile.expected_runtime.git_sha}, found ${runtime.gitSha || "unavailable"}`, status: "fail" });
|
|
848
875
|
} else if (profile.expected_runtime?.git_sha) {
|
|
849
876
|
checks.push({ id: "runtime_git_sha", message: `Git SHA ${runtime.gitSha}`, status: "pass" });
|
|
@@ -866,10 +893,10 @@ function doctorLineageProfile(selector, runtime) {
|
|
|
866
893
|
if (database.error) checks.push({ id: "database_read", message: database.error, status: "fail" });
|
|
867
894
|
if (!database.identity) {
|
|
868
895
|
checks.push({ id: "database_identity", message: "Database is not bound to a Lineage profile", status: "fail" });
|
|
869
|
-
} else if (database.identity.profile_id !== profile.profile_id || database.identity.environment !== profile.environment) {
|
|
896
|
+
} else if (database.identity.profile_id !== profile.profile_id || database.identity.environment !== profile.environment || database.identity.profile_fingerprint !== profile.profile_fingerprint) {
|
|
870
897
|
checks.push({
|
|
871
898
|
id: "database_identity",
|
|
872
|
-
message: `Database identity ${database.identity.profile_id}/${database.identity.environment} does not match ${profile.profile_id}/${profile.environment}`,
|
|
899
|
+
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}`,
|
|
873
900
|
status: "fail"
|
|
874
901
|
});
|
|
875
902
|
} else {
|
|
@@ -892,6 +919,7 @@ function runtimeProfileIdentity(channel) {
|
|
|
892
919
|
return {
|
|
893
920
|
bound: true,
|
|
894
921
|
environment,
|
|
922
|
+
fingerprint: process.env.LINEAGE_PROFILE_FINGERPRINT,
|
|
895
923
|
id,
|
|
896
924
|
manifest_path: process.env.LINEAGE_PROFILE_MANIFEST,
|
|
897
925
|
service_origin: process.env.LINEAGE_PROFILE_SERVICE_ORIGIN
|
|
@@ -901,31 +929,308 @@ function runtimeProfileIdentity(channel) {
|
|
|
901
929
|
bound: false,
|
|
902
930
|
environment: channelEnvironment(channel),
|
|
903
931
|
id: "legacy-unbound",
|
|
904
|
-
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."
|
|
932
|
+
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."
|
|
905
933
|
};
|
|
906
934
|
}
|
|
907
935
|
function assertUnselectedDatabaseIsUnbound(runtime) {
|
|
908
|
-
if (runtime.database.exists && runtime.database.error) {
|
|
909
|
-
throw new Error(
|
|
910
|
-
`Database ${runtime.database.path} identity could not be verified: ${runtime.database.error}; refusing unselected access`
|
|
911
|
-
);
|
|
912
|
-
}
|
|
913
|
-
if (runtime.schema.profile_identity_rows !== void 0 && runtime.schema.profile_identity_rows !== 1) {
|
|
914
|
-
throw new Error(
|
|
915
|
-
`Database ${runtime.database.path} has invalid Lineage profile identity row count ${runtime.schema.profile_identity_rows}; refusing unselected access`
|
|
916
|
-
);
|
|
917
|
-
}
|
|
918
936
|
if (!runtime.schema.profile_id) return;
|
|
919
937
|
const environment = runtime.schema.profile_environment || "unknown";
|
|
920
938
|
throw new Error(
|
|
921
939
|
`Database ${runtime.database.path} is bound to Lineage profile ${runtime.schema.profile_id}/${environment}; select that profile with --profile`
|
|
922
940
|
);
|
|
923
941
|
}
|
|
942
|
+
function ensureProfileIdentityTable(database) {
|
|
943
|
+
if (!tableExists(database, "lineage_profile_identity")) {
|
|
944
|
+
database.exec(`
|
|
945
|
+
create table lineage_profile_identity (
|
|
946
|
+
profile_id text primary key,
|
|
947
|
+
environment text not null,
|
|
948
|
+
profile_fingerprint text not null,
|
|
949
|
+
bound_at text not null
|
|
950
|
+
)
|
|
951
|
+
`);
|
|
952
|
+
return;
|
|
953
|
+
}
|
|
954
|
+
const columns = tableColumns(database, "lineage_profile_identity");
|
|
955
|
+
if (!columns.has("profile_id") || !columns.has("environment") || !columns.has("bound_at")) {
|
|
956
|
+
throw new Error("Existing lineage_profile_identity table has an unsupported shape");
|
|
957
|
+
}
|
|
958
|
+
if (!columns.has("profile_fingerprint")) {
|
|
959
|
+
database.exec("alter table lineage_profile_identity add column profile_fingerprint text not null default ''");
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
function assertRequiredMigrations(database, profile) {
|
|
963
|
+
const required = profile.required_schema_migrations || [];
|
|
964
|
+
if (required.length === 0) return;
|
|
965
|
+
if (!tableExists(database, "lineage_schema_migrations")) {
|
|
966
|
+
throw new Error(`Database is missing required schema migrations: ${required.join(", ")}`);
|
|
967
|
+
}
|
|
968
|
+
const available = new Set(database.prepare("select key from lineage_schema_migrations").all().map((row) => row.key));
|
|
969
|
+
const missing = required.filter((key) => !available.has(key));
|
|
970
|
+
if (missing.length > 0) throw new Error(`Database is missing required schema migrations: ${missing.join(", ")}`);
|
|
971
|
+
}
|
|
972
|
+
function profileIdentity(profile) {
|
|
973
|
+
return {
|
|
974
|
+
bound_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
975
|
+
environment: profile.environment,
|
|
976
|
+
profile_fingerprint: profile.profile_fingerprint,
|
|
977
|
+
profile_id: profile.profile_id
|
|
978
|
+
};
|
|
979
|
+
}
|
|
980
|
+
function bindOpenDatabase(database, profile, replace) {
|
|
981
|
+
database.exec("begin immediate");
|
|
982
|
+
try {
|
|
983
|
+
ensureProfileIdentityTable(database);
|
|
984
|
+
assertRequiredMigrations(database, profile);
|
|
985
|
+
const rows = database.prepare("select profile_id, environment, profile_fingerprint, bound_at from lineage_profile_identity").all();
|
|
986
|
+
if (!replace && rows.length > 0) {
|
|
987
|
+
if (rows.length !== 1) throw new Error(`Expected at most one lineage_profile_identity row, found ${rows.length}`);
|
|
988
|
+
const existing = rows[0];
|
|
989
|
+
const sameBaseIdentity = existing.profile_id === profile.profile_id && existing.environment === profile.environment;
|
|
990
|
+
const migratableLegacyIdentity = sameBaseIdentity && !existing.profile_fingerprint;
|
|
991
|
+
if (!migratableLegacyIdentity && existing.profile_fingerprint !== profile.profile_fingerprint) {
|
|
992
|
+
throw new Error(`Database is already bound to ${existing.profile_id}/${existing.environment}/${existing.profile_fingerprint || "no-fingerprint"}`);
|
|
993
|
+
}
|
|
994
|
+
if (!migratableLegacyIdentity) {
|
|
995
|
+
database.exec("commit");
|
|
996
|
+
return existing;
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
const identity = profileIdentity(profile);
|
|
1000
|
+
database.exec("delete from lineage_profile_identity");
|
|
1001
|
+
database.prepare("insert into lineage_profile_identity (profile_id, environment, profile_fingerprint, bound_at) values (?, ?, ?, ?)").run(identity.profile_id, identity.environment, identity.profile_fingerprint, identity.bound_at);
|
|
1002
|
+
const integrity = database.prepare("pragma integrity_check").get();
|
|
1003
|
+
if (integrity?.integrity_check !== "ok") throw new Error(`SQLite integrity check failed: ${integrity?.integrity_check || "unknown result"}`);
|
|
1004
|
+
database.exec("commit");
|
|
1005
|
+
return identity;
|
|
1006
|
+
} catch (error) {
|
|
1007
|
+
try {
|
|
1008
|
+
database.exec("rollback");
|
|
1009
|
+
} catch {
|
|
1010
|
+
}
|
|
1011
|
+
throw error;
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
function bindLineageProfileDatabase(selector, runtime, confirmWrite) {
|
|
1015
|
+
if (!confirmWrite) throw new Error("Profile bind requires --confirm-write");
|
|
1016
|
+
const profile = resolveLineageProfile(selector);
|
|
1017
|
+
assertProfileChannel(profile, runtime.channel);
|
|
1018
|
+
assertProfileRuntimePin(profile, runtime);
|
|
1019
|
+
if (!existsSync4(profile.database_path)) throw new Error(`Profile database does not exist: ${profile.database_path}`);
|
|
1020
|
+
const { DatabaseSync } = require2("node:sqlite");
|
|
1021
|
+
const database = new DatabaseSync(profile.database_path);
|
|
1022
|
+
try {
|
|
1023
|
+
const before = inspectDatabase(profile).identity;
|
|
1024
|
+
const identity = bindOpenDatabase(database, profile, false);
|
|
1025
|
+
return {
|
|
1026
|
+
already_bound: Boolean(before?.profile_fingerprint === profile.profile_fingerprint),
|
|
1027
|
+
database_path: profile.database_path,
|
|
1028
|
+
identity,
|
|
1029
|
+
schema_version: "lineage.profile_bind.v1"
|
|
1030
|
+
};
|
|
1031
|
+
} finally {
|
|
1032
|
+
database.close();
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
async function cloneLineageProfileDatabase(sourceDatabasePath, targetSelector, runtime, confirmWrite) {
|
|
1036
|
+
if (!confirmWrite) throw new Error("Profile clone requires --confirm-write");
|
|
1037
|
+
const profile = resolveLineageProfile(targetSelector);
|
|
1038
|
+
assertProfileChannel(profile, runtime.channel);
|
|
1039
|
+
assertProfileRuntimePin(profile, runtime);
|
|
1040
|
+
if (profile.environment === "production") throw new Error("Profile clone target must be preview or development, never production");
|
|
1041
|
+
const sourcePath = resolve3(sourceDatabasePath);
|
|
1042
|
+
const targetPath = resolve3(profile.database_path);
|
|
1043
|
+
if (sourcePath === targetPath) throw new Error("Profile clone source and target database paths must be different");
|
|
1044
|
+
if (!existsSync4(sourcePath)) throw new Error(`Profile clone source database does not exist: ${sourcePath}`);
|
|
1045
|
+
if (existsSync4(targetPath)) throw new Error(`Profile clone target database already exists: ${targetPath}`);
|
|
1046
|
+
mkdirSync3(dirname3(targetPath), { recursive: true });
|
|
1047
|
+
const temporaryPath = `${targetPath}.clone-${randomUUID()}.tmp`;
|
|
1048
|
+
const receiptDirectory = join4(dirname3(profile.manifest_path), "clone-receipts");
|
|
1049
|
+
let targetCreated = false;
|
|
1050
|
+
const { DatabaseSync, backup } = require2("node:sqlite");
|
|
1051
|
+
const source = new DatabaseSync(sourcePath, { readOnly: true });
|
|
1052
|
+
try {
|
|
1053
|
+
const pagesCopied = await backup(source, temporaryPath);
|
|
1054
|
+
const cloned = new DatabaseSync(temporaryPath);
|
|
1055
|
+
let identity;
|
|
1056
|
+
try {
|
|
1057
|
+
identity = bindOpenDatabase(cloned, profile, true);
|
|
1058
|
+
} finally {
|
|
1059
|
+
cloned.close();
|
|
1060
|
+
}
|
|
1061
|
+
linkSync(temporaryPath, targetPath);
|
|
1062
|
+
targetCreated = true;
|
|
1063
|
+
rmSync(temporaryPath, { force: true });
|
|
1064
|
+
mkdirSync3(receiptDirectory, { recursive: true, mode: 448 });
|
|
1065
|
+
const receiptPath = join4(receiptDirectory, `${Date.now()}-${randomUUID()}.json`);
|
|
1066
|
+
const result = {
|
|
1067
|
+
database_path: targetPath,
|
|
1068
|
+
pages_copied: pagesCopied,
|
|
1069
|
+
receipt_path: receiptPath,
|
|
1070
|
+
schema_version: lineageProfileCloneReceiptSchemaVersion,
|
|
1071
|
+
source_database_path: sourcePath,
|
|
1072
|
+
target_identity: identity
|
|
1073
|
+
};
|
|
1074
|
+
writeFileSync2(receiptPath, `${JSON.stringify({ ...result, created_at: (/* @__PURE__ */ new Date()).toISOString() }, null, 2)}
|
|
1075
|
+
`, { encoding: "utf8", flag: "wx", mode: 384 });
|
|
1076
|
+
return result;
|
|
1077
|
+
} catch (error) {
|
|
1078
|
+
rmSync(temporaryPath, { force: true });
|
|
1079
|
+
if (targetCreated) rmSync(targetPath, { force: true });
|
|
1080
|
+
throw error;
|
|
1081
|
+
} finally {
|
|
1082
|
+
source.close();
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
function pathIsInside(path, parent) {
|
|
1086
|
+
const child = relative2(parent, path);
|
|
1087
|
+
return child !== "" && !child.startsWith("..") && !isAbsolute(child);
|
|
1088
|
+
}
|
|
1089
|
+
function fileSha2562(path) {
|
|
1090
|
+
return createHash2("sha256").update(readFileSync3(path)).digest("hex");
|
|
1091
|
+
}
|
|
1092
|
+
function profileAssetReferences(database) {
|
|
1093
|
+
const references = [];
|
|
1094
|
+
const addColumn = (table, column, kind) => {
|
|
1095
|
+
if (!tableColumns(database, table).has(column)) return;
|
|
1096
|
+
const rows = database.prepare(`select ${column} value from ${table} where ${column} is not null and ${column} <> ''`).all();
|
|
1097
|
+
for (const row of rows) references.push({ kind, value: row.value });
|
|
1098
|
+
};
|
|
1099
|
+
addColumn("assets", "local_path", "scratch");
|
|
1100
|
+
addColumn("asset_attempts", "file_path", "scratch");
|
|
1101
|
+
addColumn("generation_job_outputs", "file_path", "scratch");
|
|
1102
|
+
addColumn("asset_ledger_sources", "local_path", "scratch");
|
|
1103
|
+
addColumn("content_posts", "source_path", "root");
|
|
1104
|
+
if (tableColumns(database, "projects").has("id")) {
|
|
1105
|
+
const projects = database.prepare("select id from projects where id is not null and id <> ''").all();
|
|
1106
|
+
for (const project of projects) {
|
|
1107
|
+
if (!/^[a-z0-9][a-z0-9-]*$/.test(project.id)) throw new Error("A project ID cannot be mapped safely into the source asset root");
|
|
1108
|
+
references.push({ kind: "root", value: join4(project.id, "assets", "catalog.json") });
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
return references;
|
|
1112
|
+
}
|
|
1113
|
+
function resolveProfileAssetReference(sourceRoot, reference) {
|
|
1114
|
+
if (reference.value.includes("\0")) throw new Error("A referenced asset path contains a null byte");
|
|
1115
|
+
const sourceScratch = join4(sourceRoot, ".asset-scratch");
|
|
1116
|
+
const base = reference.kind === "scratch" ? sourceScratch : sourceRoot;
|
|
1117
|
+
const value = reference.kind === "scratch" && reference.value.startsWith(".asset-scratch/") ? reference.value.slice(".asset-scratch/".length) : reference.value;
|
|
1118
|
+
const candidate = isAbsolute(value) ? resolve3(value) : resolve3(base, value);
|
|
1119
|
+
if (!pathIsInside(candidate, base)) throw new Error("A referenced asset path escapes the declared source asset root");
|
|
1120
|
+
return { path: candidate, relative_path: relative2(sourceRoot, candidate) };
|
|
1121
|
+
}
|
|
1122
|
+
function cloneLineageProfileAssets(sourceAssetRoot, targetSelector, runtime, confirmWrite) {
|
|
1123
|
+
if (!confirmWrite) throw new Error("Profile asset clone requires --confirm-write");
|
|
1124
|
+
const profile = resolveLineageProfile(targetSelector);
|
|
1125
|
+
assertProfileChannel(profile, runtime.channel);
|
|
1126
|
+
assertProfileRuntimePin(profile, runtime);
|
|
1127
|
+
if (!existsSync4(profile.database_path)) throw new Error(`Profile database does not exist: ${profile.database_path}`);
|
|
1128
|
+
const sourceRoot = resolve3(sourceAssetRoot);
|
|
1129
|
+
const targetRoot = resolve3(profile.asset_root);
|
|
1130
|
+
if (!existsSync4(sourceRoot) || !statSync3(sourceRoot).isDirectory()) throw new Error(`Source asset root does not exist: ${sourceRoot}`);
|
|
1131
|
+
if (sourceRoot === targetRoot || pathIsInside(targetRoot, sourceRoot) || pathIsInside(sourceRoot, targetRoot)) {
|
|
1132
|
+
throw new Error("Source and target asset roots must be disjoint");
|
|
1133
|
+
}
|
|
1134
|
+
if (pathIsInside(profile.database_path, targetRoot)) throw new Error("Profile database must not be stored inside its asset root");
|
|
1135
|
+
if (existsSync4(targetRoot)) throw new Error(`Profile asset clone target already exists: ${targetRoot}`);
|
|
1136
|
+
const databaseIdentity = inspectDatabase(profile).identity;
|
|
1137
|
+
if (databaseIdentity && (databaseIdentity.profile_id !== profile.profile_id || databaseIdentity.environment !== profile.environment || databaseIdentity.profile_fingerprint && databaseIdentity.profile_fingerprint !== profile.profile_fingerprint)) {
|
|
1138
|
+
throw new Error(`Profile database is already bound to ${databaseIdentity.profile_id}/${databaseIdentity.environment}`);
|
|
1139
|
+
}
|
|
1140
|
+
const { DatabaseSync } = require2("node:sqlite");
|
|
1141
|
+
const database = new DatabaseSync(profile.database_path, { readOnly: true });
|
|
1142
|
+
let references;
|
|
1143
|
+
try {
|
|
1144
|
+
database.exec("begin");
|
|
1145
|
+
references = profileAssetReferences(database);
|
|
1146
|
+
database.exec("commit");
|
|
1147
|
+
} catch (error) {
|
|
1148
|
+
try {
|
|
1149
|
+
database.exec("rollback");
|
|
1150
|
+
} catch {
|
|
1151
|
+
}
|
|
1152
|
+
throw error;
|
|
1153
|
+
} finally {
|
|
1154
|
+
database.close();
|
|
1155
|
+
}
|
|
1156
|
+
const sourceRealRoot = realpathSync(sourceRoot);
|
|
1157
|
+
const files = /* @__PURE__ */ new Map();
|
|
1158
|
+
let missingReferences = 0;
|
|
1159
|
+
let duplicateReferences = 0;
|
|
1160
|
+
for (const reference of references) {
|
|
1161
|
+
const resolved = resolveProfileAssetReference(sourceRoot, reference);
|
|
1162
|
+
if (!existsSync4(resolved.path)) {
|
|
1163
|
+
missingReferences += 1;
|
|
1164
|
+
continue;
|
|
1165
|
+
}
|
|
1166
|
+
const stats = statSync3(resolved.path);
|
|
1167
|
+
if (!stats.isFile()) {
|
|
1168
|
+
missingReferences += 1;
|
|
1169
|
+
continue;
|
|
1170
|
+
}
|
|
1171
|
+
const realSource = realpathSync(resolved.path);
|
|
1172
|
+
if (!pathIsInside(realSource, sourceRealRoot)) throw new Error("A referenced asset symlink escapes the declared source asset root");
|
|
1173
|
+
const existing = files.get(resolved.relative_path);
|
|
1174
|
+
if (existing) {
|
|
1175
|
+
if (realpathSync(existing) !== realSource) throw new Error("Two source assets map to the same target path");
|
|
1176
|
+
duplicateReferences += 1;
|
|
1177
|
+
continue;
|
|
1178
|
+
}
|
|
1179
|
+
files.set(resolved.relative_path, resolved.path);
|
|
1180
|
+
}
|
|
1181
|
+
mkdirSync3(dirname3(targetRoot), { recursive: true, mode: 448 });
|
|
1182
|
+
mkdirSync3(targetRoot, { mode: 448 });
|
|
1183
|
+
try {
|
|
1184
|
+
let bytesCopied = 0;
|
|
1185
|
+
const treeHash = createHash2("sha256");
|
|
1186
|
+
for (const relativePath of [...files.keys()].sort()) {
|
|
1187
|
+
const sourcePath = files.get(relativePath);
|
|
1188
|
+
const destinationPath = join4(targetRoot, relativePath);
|
|
1189
|
+
mkdirSync3(dirname3(destinationPath), { recursive: true, mode: 448 });
|
|
1190
|
+
copyFileSync2(sourcePath, destinationPath, fsConstants.COPYFILE_EXCL);
|
|
1191
|
+
chmodSync(destinationPath, 384);
|
|
1192
|
+
const sourceHash = fileSha2562(sourcePath);
|
|
1193
|
+
const destinationHash = fileSha2562(destinationPath);
|
|
1194
|
+
if (sourceHash !== destinationHash) throw new Error("A source asset changed or failed checksum verification during clone");
|
|
1195
|
+
bytesCopied += statSync3(destinationPath).size;
|
|
1196
|
+
treeHash.update(relativePath.replaceAll("\\", "/"));
|
|
1197
|
+
treeHash.update("\0");
|
|
1198
|
+
treeHash.update(destinationHash);
|
|
1199
|
+
treeHash.update("\0");
|
|
1200
|
+
}
|
|
1201
|
+
chmodSync(targetRoot, 448);
|
|
1202
|
+
const receiptPath = join4(targetRoot, ".lineage-profile-assets.json");
|
|
1203
|
+
const result = {
|
|
1204
|
+
asset_root: targetRoot,
|
|
1205
|
+
bytes_copied: bytesCopied,
|
|
1206
|
+
code_fingerprint: runtime.code.fingerprint,
|
|
1207
|
+
database_path: profile.database_path,
|
|
1208
|
+
duplicate_references: duplicateReferences,
|
|
1209
|
+
files_copied: files.size,
|
|
1210
|
+
missing_references: missingReferences,
|
|
1211
|
+
profile_fingerprint: profile.profile_fingerprint,
|
|
1212
|
+
profile_id: profile.profile_id,
|
|
1213
|
+
receipt_path: receiptPath,
|
|
1214
|
+
references_discovered: references.length,
|
|
1215
|
+
runtime_channel: runtime.channel,
|
|
1216
|
+
schema_version: lineageProfileAssetsCloneReceiptSchemaVersion,
|
|
1217
|
+
source_asset_root: sourceRoot,
|
|
1218
|
+
tree_sha256: treeHash.digest("hex")
|
|
1219
|
+
};
|
|
1220
|
+
writeFileSync2(receiptPath, `${JSON.stringify({ ...result, created_at: (/* @__PURE__ */ new Date()).toISOString() }, null, 2)}
|
|
1221
|
+
`, {
|
|
1222
|
+
encoding: "utf8",
|
|
1223
|
+
flag: "wx",
|
|
1224
|
+
mode: 384
|
|
1225
|
+
});
|
|
1226
|
+
return result;
|
|
1227
|
+
} catch (error) {
|
|
1228
|
+
rmSync(targetRoot, { force: true, recursive: true });
|
|
1229
|
+
throw error;
|
|
1230
|
+
}
|
|
1231
|
+
}
|
|
924
1232
|
|
|
925
1233
|
// src/server/profileWriterLease.ts
|
|
926
|
-
import { createHash as createHash2, randomUUID, timingSafeEqual } from "node:crypto";
|
|
927
|
-
import { existsSync as existsSync5, lstatSync, mkdirSync as mkdirSync4, readFileSync as readFileSync4, realpathSync, renameSync, rmSync, writeFileSync as writeFileSync2 } from "node:fs";
|
|
928
|
-
import { basename as basename3, dirname as dirname4, join as join5, resolve as resolve4 } from "node:path";
|
|
929
1234
|
var writerLeaseSchemaVersion = "lineage.profile_writer_lease.v1";
|
|
930
1235
|
var ownerFileName = "owner.json";
|
|
931
1236
|
var ProfileWriterLeaseConflictError = class extends Error {
|
|
@@ -936,24 +1241,8 @@ var ProfileWriterLeaseConflictError = class extends Error {
|
|
|
936
1241
|
}
|
|
937
1242
|
owner;
|
|
938
1243
|
};
|
|
939
|
-
function canonicalDatabasePath(databasePath) {
|
|
940
|
-
const resolved = resolve4(databasePath);
|
|
941
|
-
const missingSegments = [];
|
|
942
|
-
let candidate = resolved;
|
|
943
|
-
while (!existsSync5(candidate)) {
|
|
944
|
-
const parent = dirname4(candidate);
|
|
945
|
-
if (parent === candidate) return resolved;
|
|
946
|
-
missingSegments.unshift(basename3(candidate));
|
|
947
|
-
candidate = parent;
|
|
948
|
-
}
|
|
949
|
-
try {
|
|
950
|
-
return join5(realpathSync(candidate), ...missingSegments);
|
|
951
|
-
} catch {
|
|
952
|
-
return resolved;
|
|
953
|
-
}
|
|
954
|
-
}
|
|
955
1244
|
function profileWriterLockPath(profile) {
|
|
956
|
-
return
|
|
1245
|
+
return join5(dirname4(profile.manifest_path), "writer.lock");
|
|
957
1246
|
}
|
|
958
1247
|
function ownerPath(lockPath) {
|
|
959
1248
|
return join5(lockPath, ownerFileName);
|
|
@@ -976,7 +1265,7 @@ function readOwner(lockPath) {
|
|
|
976
1265
|
throw new Error(`Writer lease metadata is invalid at ${lockPath}; refusing automatic recovery`);
|
|
977
1266
|
}
|
|
978
1267
|
const owner = raw;
|
|
979
|
-
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) {
|
|
1268
|
+
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) {
|
|
980
1269
|
throw new Error(`Writer lease metadata is invalid at ${lockPath}; refusing automatic recovery`);
|
|
981
1270
|
}
|
|
982
1271
|
return owner;
|
|
@@ -993,10 +1282,10 @@ function processIsAlive(pid) {
|
|
|
993
1282
|
function createLeaseDirectory(lockPath, owner) {
|
|
994
1283
|
mkdirSync4(lockPath, { mode: 448 });
|
|
995
1284
|
try {
|
|
996
|
-
|
|
1285
|
+
writeFileSync3(ownerPath(lockPath), `${JSON.stringify(owner, null, 2)}
|
|
997
1286
|
`, { encoding: "utf8", flag: "wx", mode: 384 });
|
|
998
1287
|
} catch (error) {
|
|
999
|
-
|
|
1288
|
+
rmSync2(lockPath, { force: true, recursive: true });
|
|
1000
1289
|
throw error;
|
|
1001
1290
|
}
|
|
1002
1291
|
}
|
|
@@ -1008,14 +1297,15 @@ function reclaimDeadOwner(lockPath, owner) {
|
|
|
1008
1297
|
publicOwner
|
|
1009
1298
|
);
|
|
1010
1299
|
}
|
|
1011
|
-
const tokenFence =
|
|
1300
|
+
const tokenFence = createHash3("sha256").update(owner.token).digest("hex").slice(0, 24);
|
|
1012
1301
|
const tombstone = `${lockPath}.stale-${owner.pid}-${tokenFence}`;
|
|
1013
1302
|
try {
|
|
1014
1303
|
renameSync(lockPath, tombstone);
|
|
1015
1304
|
} catch (error) {
|
|
1016
|
-
if (errorCode(error) === "ENOENT"
|
|
1305
|
+
if (errorCode(error) === "ENOENT") return;
|
|
1017
1306
|
throw error;
|
|
1018
1307
|
}
|
|
1308
|
+
rmSync2(tombstone, { force: true, recursive: true });
|
|
1019
1309
|
}
|
|
1020
1310
|
function acquireProfileWriterLease(profile, channel, role = "service") {
|
|
1021
1311
|
assertProfileChannel(profile, channel);
|
|
@@ -1024,11 +1314,12 @@ function acquireProfileWriterLease(profile, channel, role = "service") {
|
|
|
1024
1314
|
acquired_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1025
1315
|
environment: profile.environment,
|
|
1026
1316
|
pid: process.pid,
|
|
1317
|
+
profile_fingerprint: profile.profile_fingerprint,
|
|
1027
1318
|
profile_id: profile.profile_id,
|
|
1028
1319
|
role,
|
|
1029
1320
|
schema_version: writerLeaseSchemaVersion,
|
|
1030
1321
|
...role === "service" ? { service_origin: profile.service_origin } : {},
|
|
1031
|
-
token:
|
|
1322
|
+
token: randomUUID2()
|
|
1032
1323
|
};
|
|
1033
1324
|
for (let attempt = 0; attempt < 4; attempt += 1) {
|
|
1034
1325
|
try {
|
|
@@ -1051,7 +1342,7 @@ function acquireProfileWriterLease(profile, channel, role = "service") {
|
|
|
1051
1342
|
released = true;
|
|
1052
1343
|
try {
|
|
1053
1344
|
const current = readOwner(lockPath);
|
|
1054
|
-
if (current.token === owner.token && current.pid === owner.pid)
|
|
1345
|
+
if (current.token === owner.token && current.pid === owner.pid) rmSync2(lockPath, { force: true, recursive: true });
|
|
1055
1346
|
} catch {
|
|
1056
1347
|
}
|
|
1057
1348
|
if (process.env.LINEAGE_WRITER_LEASE_TOKEN === owner.token) delete process.env.LINEAGE_WRITER_LEASE_TOKEN;
|
|
@@ -1067,7 +1358,7 @@ function acquireProfileWriterLease(profile, channel, role = "service") {
|
|
|
1067
1358
|
if (errorCode(readError) === "ENOENT") continue;
|
|
1068
1359
|
throw readError;
|
|
1069
1360
|
}
|
|
1070
|
-
if (existing.profile_id !== profile.profile_id || existing.environment !== profile.environment) {
|
|
1361
|
+
if (existing.profile_id !== profile.profile_id || existing.environment !== profile.environment || existing.profile_fingerprint !== profile.profile_fingerprint) {
|
|
1071
1362
|
throw new Error(`Writer lease identity at ${lockPath} does not match ${profile.profile_id}/${profile.environment}; manual inspection is required`, { cause: error });
|
|
1072
1363
|
}
|
|
1073
1364
|
reclaimDeadOwner(lockPath, existing);
|
|
@@ -1076,23 +1367,47 @@ function acquireProfileWriterLease(profile, channel, role = "service") {
|
|
|
1076
1367
|
throw new Error(`Could not acquire writer lease for Lineage profile ${profile.profile_id}; another writer is racing for ownership`);
|
|
1077
1368
|
}
|
|
1078
1369
|
function assertProfileWriterLeaseHeld() {
|
|
1079
|
-
if (!process.env.LINEAGE_PROFILE)
|
|
1370
|
+
if (!process.env.LINEAGE_PROFILE) {
|
|
1371
|
+
throw new Error("Persistent writes require a selected named Lineage profile and its writer lease; legacy-unbound access is read-only");
|
|
1372
|
+
}
|
|
1080
1373
|
const profileId = process.env.LINEAGE_PROFILE_ID;
|
|
1374
|
+
const profileFingerprint = process.env.LINEAGE_PROFILE_FINGERPRINT;
|
|
1081
1375
|
const environment = process.env.LINEAGE_PROFILE_ENVIRONMENT;
|
|
1082
1376
|
const manifestPath = process.env.LINEAGE_PROFILE_MANIFEST;
|
|
1083
|
-
const databasePath = process.env.LINEAGE_DB;
|
|
1084
1377
|
const lockPath = process.env.LINEAGE_WRITER_LOCK_PATH;
|
|
1085
1378
|
const token = process.env.LINEAGE_WRITER_LEASE_TOKEN;
|
|
1086
|
-
if (!profileId || !
|
|
1379
|
+
if (!profileId || !profileFingerprint || !environment || !manifestPath || !lockPath || !token) {
|
|
1087
1380
|
throw new Error("Named Lineage profiles may write only through a process holding the profile writer lease");
|
|
1088
1381
|
}
|
|
1089
|
-
const expectedLockPath =
|
|
1090
|
-
if (resolve4(lockPath) !== expectedLockPath) throw new Error("Profile writer lease path does not match the selected profile
|
|
1382
|
+
const expectedLockPath = join5(dirname4(resolve4(manifestPath)), "writer.lock");
|
|
1383
|
+
if (resolve4(lockPath) !== expectedLockPath) throw new Error("Profile writer lease path does not match the selected profile manifest");
|
|
1091
1384
|
const owner = readOwner(lockPath);
|
|
1092
|
-
if (owner.pid !== process.pid || owner.token !== token || owner.profile_id !== profileId || owner.environment !== environment) {
|
|
1385
|
+
if (owner.pid !== process.pid || owner.token !== token || owner.profile_id !== profileId || owner.profile_fingerprint !== profileFingerprint || owner.environment !== environment) {
|
|
1093
1386
|
throw new Error("Current process does not own the selected Lineage profile writer lease");
|
|
1094
1387
|
}
|
|
1095
1388
|
}
|
|
1389
|
+
function assertSelectedProfileDatabaseIdentity(database) {
|
|
1390
|
+
const profileId = process.env.LINEAGE_PROFILE_ID;
|
|
1391
|
+
const environment = process.env.LINEAGE_PROFILE_ENVIRONMENT;
|
|
1392
|
+
const profileFingerprint = process.env.LINEAGE_PROFILE_FINGERPRINT;
|
|
1393
|
+
if (!process.env.LINEAGE_PROFILE || !profileId || !environment || !profileFingerprint) {
|
|
1394
|
+
throw new Error("Writable SQLite identity validation requires a fully resolved named Lineage profile");
|
|
1395
|
+
}
|
|
1396
|
+
const table = database.prepare("select name from sqlite_master where type = 'table' and name = 'lineage_profile_identity'").get();
|
|
1397
|
+
if (!table) throw new Error(`Refusing writable open: database is not bound to Lineage profile ${profileId}`);
|
|
1398
|
+
const columns = new Set(database.prepare("pragma table_info(lineage_profile_identity)").all().map((row) => row.name));
|
|
1399
|
+
if (!columns.has("profile_id") || !columns.has("environment") || !columns.has("profile_fingerprint")) {
|
|
1400
|
+
throw new Error(`Refusing writable open: database identity for ${profileId} is missing required fingerprint fields`);
|
|
1401
|
+
}
|
|
1402
|
+
const rows = database.prepare("select profile_id, environment, profile_fingerprint from lineage_profile_identity").all();
|
|
1403
|
+
if (rows.length !== 1) throw new Error(`Refusing writable open: expected exactly one database profile identity, found ${rows.length}`);
|
|
1404
|
+
const identity = rows[0];
|
|
1405
|
+
if (identity.profile_id !== profileId || identity.environment !== environment || identity.profile_fingerprint !== profileFingerprint) {
|
|
1406
|
+
throw new Error(
|
|
1407
|
+
`Refusing writable open: database identity ${String(identity.profile_id)}/${String(identity.environment)}/${String(identity.profile_fingerprint)} does not match selected profile ${profileId}/${environment}/${profileFingerprint}`
|
|
1408
|
+
);
|
|
1409
|
+
}
|
|
1410
|
+
}
|
|
1096
1411
|
function getProfileWriterDelegation(profile) {
|
|
1097
1412
|
const lockPath = profileWriterLockPath(profile);
|
|
1098
1413
|
if (!existsSync5(lockPath)) {
|
|
@@ -1101,7 +1416,7 @@ function getProfileWriterDelegation(profile) {
|
|
|
1101
1416
|
);
|
|
1102
1417
|
}
|
|
1103
1418
|
const owner = readOwner(lockPath);
|
|
1104
|
-
if (owner.profile_id !== profile.profile_id || owner.environment !== profile.environment) {
|
|
1419
|
+
if (owner.profile_id !== profile.profile_id || owner.environment !== profile.environment || owner.profile_fingerprint !== profile.profile_fingerprint) {
|
|
1105
1420
|
throw new Error(`Writer lease identity at ${lockPath} does not match ${profile.profile_id}/${profile.environment}; refusing delegation`);
|
|
1106
1421
|
}
|
|
1107
1422
|
if (owner.role !== "service" || owner.service_origin !== profile.service_origin) {
|
|
@@ -1122,44 +1437,19 @@ function nowIso() {
|
|
|
1122
1437
|
function lineageDbPath() {
|
|
1123
1438
|
return process.env.LINEAGE_DB || join6(packageRoot, ".lineage", "asset-lineage.sqlite");
|
|
1124
1439
|
}
|
|
1125
|
-
function assertOpenedProfileIdentity(database, databasePath) {
|
|
1126
|
-
const selector = process.env.LINEAGE_PROFILE;
|
|
1127
|
-
if (!selector) return;
|
|
1128
|
-
const profile = resolveLineageProfile(selector);
|
|
1129
|
-
if (resolve5(databasePath) !== profile.database_path) {
|
|
1130
|
-
throw new Error(`Profile ${profile.profile_id} requires database ${profile.database_path}, not ${databasePath}`);
|
|
1131
|
-
}
|
|
1132
|
-
const table = database.prepare("select 1 from sqlite_master where type = 'table' and name = 'lineage_profile_identity'").get();
|
|
1133
|
-
if (!table) throw new Error(`Profile database ${databasePath} is not bound to a Lineage profile identity`);
|
|
1134
|
-
const rows = database.prepare("select profile_id, environment from lineage_profile_identity").all();
|
|
1135
|
-
if (rows.length !== 1) {
|
|
1136
|
-
throw new Error(`Profile database ${databasePath} has invalid Lineage profile identity row count ${rows.length}`);
|
|
1137
|
-
}
|
|
1138
|
-
if (rows[0].profile_id !== profile.profile_id || rows[0].environment !== profile.environment) {
|
|
1139
|
-
throw new Error(
|
|
1140
|
-
`Profile database ${databasePath} is bound to ${rows[0].profile_id}/${rows[0].environment}, not ${profile.profile_id}/${profile.environment}`
|
|
1141
|
-
);
|
|
1142
|
-
}
|
|
1143
|
-
}
|
|
1144
1440
|
function lineageDb() {
|
|
1145
1441
|
const readOnly = process.env.LINEAGE_DB_ACCESS === "read-only";
|
|
1146
|
-
const databasePath = lineageDbPath();
|
|
1147
1442
|
if (!readOnly) assertProfileWriterLeaseHeld();
|
|
1148
|
-
if (
|
|
1149
|
-
throw new Error(`
|
|
1443
|
+
if (!readOnly && !existsSync6(lineageDbPath())) {
|
|
1444
|
+
throw new Error(`Refusing writable open of missing profile database ${lineageDbPath()}; bind an existing database or create a non-production clone first`);
|
|
1150
1445
|
}
|
|
1151
|
-
if (!readOnly) mkdirSync5(join6(
|
|
1446
|
+
if (!readOnly) mkdirSync5(join6(lineageDbPath(), ".."), { recursive: true });
|
|
1152
1447
|
const { DatabaseSync } = require3("node:sqlite");
|
|
1153
|
-
const database = readOnly ? new DatabaseSync(
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
if (readOnly) return database;
|
|
1159
|
-
} catch (error) {
|
|
1160
|
-
database.close();
|
|
1161
|
-
throw error;
|
|
1162
|
-
}
|
|
1448
|
+
const database = readOnly ? new DatabaseSync(lineageDbPath(), { readOnly: true }) : new DatabaseSync(lineageDbPath());
|
|
1449
|
+
if (process.env.LINEAGE_PROFILE) assertSelectedProfileDatabaseIdentity(database);
|
|
1450
|
+
database.exec("PRAGMA foreign_keys = ON");
|
|
1451
|
+
database.exec("PRAGMA busy_timeout = 5000");
|
|
1452
|
+
if (readOnly) return database;
|
|
1163
1453
|
database.exec("PRAGMA journal_mode = WAL");
|
|
1164
1454
|
database.exec(`
|
|
1165
1455
|
create table if not exists projects (
|
|
@@ -1876,7 +2166,7 @@ function randomId(prefix) {
|
|
|
1876
2166
|
return `${prefix}_${Date.now().toString(36)}_${randomBytes(6).toString("base64url").toLowerCase()}`;
|
|
1877
2167
|
}
|
|
1878
2168
|
function tokenHash(token) {
|
|
1879
|
-
return
|
|
2169
|
+
return createHash4("sha256").update(token).digest("hex");
|
|
1880
2170
|
}
|
|
1881
2171
|
function safeEqual(a, b) {
|
|
1882
2172
|
const left = Buffer.from(a);
|
|
@@ -2998,8 +3288,8 @@ function rootFor(database, project, assetId) {
|
|
|
2998
3288
|
}
|
|
2999
3289
|
function assertCanonicalRoot(database, project, root) {
|
|
3000
3290
|
requireAsset2(database, project, root);
|
|
3001
|
-
const
|
|
3002
|
-
if (
|
|
3291
|
+
const canonicalRoot2 = rootFor(database, project, root);
|
|
3292
|
+
if (canonicalRoot2 !== root) throw new LineageError(`Asset ${root} is not a lineage root`, 400);
|
|
3003
3293
|
}
|
|
3004
3294
|
function explicitWorkspaceRoot(database, project, assetId) {
|
|
3005
3295
|
const row = database.prepare(`
|
|
@@ -3688,8 +3978,8 @@ function linkSelectedLineageChild(project, fields) {
|
|
|
3688
3978
|
|
|
3689
3979
|
// src/server/generationReceipts.ts
|
|
3690
3980
|
import { existsSync as existsSync8, realpathSync as realpathSync2, statSync as statSync4 } from "node:fs";
|
|
3691
|
-
import { relative as
|
|
3692
|
-
import { randomUUID as
|
|
3981
|
+
import { relative as relative3, resolve as resolve5 } from "node:path";
|
|
3982
|
+
import { randomUUID as randomUUID3 } from "node:crypto";
|
|
3693
3983
|
var adapterVersion = "generation-receipts-v1";
|
|
3694
3984
|
var provider = "codex-handoff";
|
|
3695
3985
|
var GenerationReceiptError = class extends Error {
|
|
@@ -3700,7 +3990,7 @@ var GenerationReceiptError = class extends Error {
|
|
|
3700
3990
|
status;
|
|
3701
3991
|
};
|
|
3702
3992
|
function jobId() {
|
|
3703
|
-
return `gen-${Date.now().toString(36)}-${
|
|
3993
|
+
return `gen-${Date.now().toString(36)}-${randomUUID3().slice(0, 8)}`;
|
|
3704
3994
|
}
|
|
3705
3995
|
function quote(value) {
|
|
3706
3996
|
return JSON.stringify(value);
|
|
@@ -3896,12 +4186,12 @@ function planImageReroll(project = defaultProject, fields) {
|
|
|
3896
4186
|
}
|
|
3897
4187
|
}
|
|
3898
4188
|
function isPathInside(child, parent) {
|
|
3899
|
-
const rel =
|
|
4189
|
+
const rel = relative3(parent, child);
|
|
3900
4190
|
return Boolean(rel) && !rel.startsWith("..") && !rel.startsWith("/");
|
|
3901
4191
|
}
|
|
3902
4192
|
function resolveScratchFile(file) {
|
|
3903
|
-
const scratchRoot =
|
|
3904
|
-
const candidate = file.startsWith(".asset-scratch/") ||
|
|
4193
|
+
const scratchRoot = resolve5(repoRoot, ".asset-scratch");
|
|
4194
|
+
const candidate = file.startsWith(".asset-scratch/") || resolve5(file).startsWith(scratchRoot) ? resolve5(repoRoot, file) : resolve5(scratchRoot, file);
|
|
3905
4195
|
if (!isPathInside(candidate, scratchRoot)) throw new GenerationReceiptError(`Import file must be under .asset-scratch: ${file}`);
|
|
3906
4196
|
if (!existsSync8(candidate)) throw new GenerationReceiptError(`Missing import file: ${file}`, 404);
|
|
3907
4197
|
const realScratchRoot = realpathSync2(scratchRoot);
|
|
@@ -3911,7 +4201,7 @@ function resolveScratchFile(file) {
|
|
|
3911
4201
|
if (!stats.isFile()) throw new GenerationReceiptError(`Import path is not a file: ${file}`);
|
|
3912
4202
|
const checksum = fileSha256(candidate);
|
|
3913
4203
|
return {
|
|
3914
|
-
relativePath:
|
|
4204
|
+
relativePath: relative3(scratchRoot, candidate),
|
|
3915
4205
|
checksum,
|
|
3916
4206
|
size: stats.size,
|
|
3917
4207
|
contentType: contentTypeFor(candidate),
|
|
@@ -3987,9 +4277,9 @@ function importImageRerollOutput(project = defaultProject, fields) {
|
|
|
3987
4277
|
}
|
|
3988
4278
|
|
|
3989
4279
|
// src/server/lineageSelectionPacket.ts
|
|
3990
|
-
import { createHash as
|
|
4280
|
+
import { createHash as createHash5 } from "node:crypto";
|
|
3991
4281
|
import { existsSync as existsSync9, statSync as statSync5 } from "node:fs";
|
|
3992
|
-
import { isAbsolute as isAbsolute2, resolve as
|
|
4282
|
+
import { isAbsolute as isAbsolute2, resolve as resolve6 } from "node:path";
|
|
3993
4283
|
var LineageSelectionPacketError = class extends Error {
|
|
3994
4284
|
constructor(message, warnings = [], errors = [], diagnostics = []) {
|
|
3995
4285
|
super(message);
|
|
@@ -4036,9 +4326,9 @@ function resolveWorkspace(project, options) {
|
|
|
4036
4326
|
function resolveLocalReference(reference) {
|
|
4037
4327
|
if (!reference) return void 0;
|
|
4038
4328
|
if (isAbsolute2(reference)) return reference;
|
|
4039
|
-
const repoRelative =
|
|
4329
|
+
const repoRelative = resolve6(repoRoot, reference);
|
|
4040
4330
|
if (existsSync9(repoRelative)) return repoRelative;
|
|
4041
|
-
return
|
|
4331
|
+
return resolve6(repoRoot, ".asset-scratch", reference);
|
|
4042
4332
|
}
|
|
4043
4333
|
function fileSize(path) {
|
|
4044
4334
|
if (!path || !existsSync9(path)) return void 0;
|
|
@@ -4114,7 +4404,7 @@ function assetForNode(node, catalogAsset, warnings) {
|
|
|
4114
4404
|
};
|
|
4115
4405
|
}
|
|
4116
4406
|
function packetId(packetIdentity) {
|
|
4117
|
-
const digest =
|
|
4407
|
+
const digest = createHash5("sha256").update(JSON.stringify(packetIdentity)).digest("hex").slice(0, 24);
|
|
4118
4408
|
return `lineage_packet_${digest}`;
|
|
4119
4409
|
}
|
|
4120
4410
|
var SHA256_PATTERN = /^[a-f0-9]{64}$/;
|
|
@@ -4185,7 +4475,7 @@ function lineageSelectionPacketV2IdentityProjection(packet) {
|
|
|
4185
4475
|
}
|
|
4186
4476
|
function lineageSelectionPacketV2IdentitySha256(packet) {
|
|
4187
4477
|
const projection = lineageSelectionPacketV2IdentityProjection(packet);
|
|
4188
|
-
return
|
|
4478
|
+
return createHash5("sha256").update(canonicalLineageSelectionPacketIdentityJson(projection)).digest("hex");
|
|
4189
4479
|
}
|
|
4190
4480
|
function addDiagnostic(diagnostics, diagnostic) {
|
|
4191
4481
|
if (diagnostics.some((item) => item.code === diagnostic.code && item.severity === diagnostic.severity && item.asset_id === diagnostic.asset_id)) return;
|
|
@@ -4490,14 +4780,49 @@ function getLineageSelectionPacket(project, options = {}) {
|
|
|
4490
4780
|
}
|
|
4491
4781
|
|
|
4492
4782
|
// src/server/runtimeInfo.ts
|
|
4783
|
+
import { createHash as createHash6 } from "node:crypto";
|
|
4493
4784
|
import { spawnSync as spawnSync2 } from "node:child_process";
|
|
4494
4785
|
import { createRequire as createRequire4 } from "node:module";
|
|
4495
|
-
import { existsSync as existsSync10, readFileSync as readFileSync5, statSync as statSync6 } from "node:fs";
|
|
4496
|
-
import { join as join9 } from "node:path";
|
|
4786
|
+
import { existsSync as existsSync10, lstatSync as lstatSync2, readFileSync as readFileSync5, readdirSync as readdirSync3, readlinkSync, realpathSync as realpathSync3, statSync as statSync6 } from "node:fs";
|
|
4787
|
+
import { dirname as dirname5, isAbsolute as isAbsolute3, join as join9, resolve as resolve7 } from "node:path";
|
|
4788
|
+
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
4789
|
+
|
|
4790
|
+
// src/shared/runtimeInfoTypes.ts
|
|
4791
|
+
var lineageRuntimeBuildSchemaVersion = "lineage.runtime_build.v1";
|
|
4792
|
+
var lineageRuntimeInstallSchemaVersion = "lineage.runtime_install.v1";
|
|
4793
|
+
|
|
4794
|
+
// src/server/runtimeInfo.ts
|
|
4497
4795
|
var require5 = createRequire4(import.meta.url);
|
|
4498
|
-
|
|
4796
|
+
var processStartedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
4797
|
+
function isLineagePackageRoot(root) {
|
|
4499
4798
|
try {
|
|
4500
|
-
const info = JSON.parse(readFileSync5(join9(
|
|
4799
|
+
const info = JSON.parse(readFileSync5(join9(root, "package.json"), "utf8"));
|
|
4800
|
+
return info.name === "@mean-weasel/lineage";
|
|
4801
|
+
} catch {
|
|
4802
|
+
return false;
|
|
4803
|
+
}
|
|
4804
|
+
}
|
|
4805
|
+
function executingCodeRoot() {
|
|
4806
|
+
const moduleDirectory = dirname5(fileURLToPath2(import.meta.url));
|
|
4807
|
+
const candidates = [resolve7(moduleDirectory, "../.."), resolve7(moduleDirectory, "..")];
|
|
4808
|
+
const root = candidates.find(isLineagePackageRoot);
|
|
4809
|
+
if (!root) throw new Error(`Unable to derive Lineage code root from executing module ${fileURLToPath2(import.meta.url)}`);
|
|
4810
|
+
return canonicalRoot(root);
|
|
4811
|
+
}
|
|
4812
|
+
var codeRoot = executingCodeRoot();
|
|
4813
|
+
function sha256(value) {
|
|
4814
|
+
return createHash6("sha256").update(value).digest("hex");
|
|
4815
|
+
}
|
|
4816
|
+
function canonicalRoot(root) {
|
|
4817
|
+
try {
|
|
4818
|
+
return realpathSync3(root);
|
|
4819
|
+
} catch {
|
|
4820
|
+
return resolve7(root);
|
|
4821
|
+
}
|
|
4822
|
+
}
|
|
4823
|
+
function packageInfo(root = codeRoot) {
|
|
4824
|
+
try {
|
|
4825
|
+
const info = JSON.parse(readFileSync5(join9(root, "package.json"), "utf8"));
|
|
4501
4826
|
return { name: info.name || "@mean-weasel/lineage", version: info.version || "0.0.0" };
|
|
4502
4827
|
} catch {
|
|
4503
4828
|
return { name: "@mean-weasel/lineage", version: "0.0.0" };
|
|
@@ -4510,12 +4835,199 @@ function normalizeRuntimeChannel(value) {
|
|
|
4510
4835
|
if (value === "development") return "dev";
|
|
4511
4836
|
return process.env.NODE_ENV === "production" ? "stable" : "dev";
|
|
4512
4837
|
}
|
|
4513
|
-
function
|
|
4514
|
-
const
|
|
4515
|
-
|
|
4516
|
-
|
|
4517
|
-
|
|
4518
|
-
|
|
4838
|
+
function git(root, args) {
|
|
4839
|
+
const result = spawnSync2("git", args, { cwd: root, encoding: "utf8", maxBuffer: 32 * 1024 * 1024 });
|
|
4840
|
+
return result.status === 0 ? result.stdout : void 0;
|
|
4841
|
+
}
|
|
4842
|
+
function untrackedFingerprint(root, status) {
|
|
4843
|
+
const hash = createHash6("sha256");
|
|
4844
|
+
hash.update(status);
|
|
4845
|
+
const listed = git(root, ["ls-files", "--others", "--exclude-standard", "-z"]);
|
|
4846
|
+
if (listed === void 0) return hash.digest("hex");
|
|
4847
|
+
for (const relativePath of listed.split("\0").filter(Boolean).sort()) {
|
|
4848
|
+
hash.update("\0");
|
|
4849
|
+
hash.update(relativePath);
|
|
4850
|
+
const path = join9(root, relativePath);
|
|
4851
|
+
try {
|
|
4852
|
+
const stat = lstatSync2(path);
|
|
4853
|
+
if (stat.isSymbolicLink()) hash.update(readlinkSync(path));
|
|
4854
|
+
else if (stat.isFile()) hash.update(readFileSync5(path));
|
|
4855
|
+
else hash.update(`[${stat.mode}:${stat.size}]`);
|
|
4856
|
+
} catch (error) {
|
|
4857
|
+
hash.update(`[unreadable:${error instanceof Error ? error.message : String(error)}]`);
|
|
4858
|
+
}
|
|
4859
|
+
}
|
|
4860
|
+
return hash.digest("hex");
|
|
4861
|
+
}
|
|
4862
|
+
function checkoutCodeIdentity(root, channel, version) {
|
|
4863
|
+
const errors = [];
|
|
4864
|
+
const gitRootRaw = git(root, ["rev-parse", "--show-toplevel"])?.trim();
|
|
4865
|
+
const gitSha = git(root, ["rev-parse", "HEAD"])?.trim();
|
|
4866
|
+
const status = git(root, ["status", "--porcelain=v1", "-z", "--untracked-files=all"]);
|
|
4867
|
+
const diff = git(root, ["diff", "--binary", "HEAD", "--"]);
|
|
4868
|
+
const canonicalGitRoot = gitRootRaw ? canonicalRoot(gitRootRaw) : void 0;
|
|
4869
|
+
if (!canonicalGitRoot || canonicalGitRoot !== root) errors.push("Code root is not the canonical root of a Git checkout/worktree");
|
|
4870
|
+
if (!gitSha || !/^[a-f0-9]{40}$/i.test(gitSha)) errors.push("Checkout Git revision is unavailable");
|
|
4871
|
+
if (status === void 0 || diff === void 0) errors.push("Checkout dirty state could not be inspected");
|
|
4872
|
+
if (channel !== "dev") errors.push(`Checkout code may run only as dev, not ${channel}`);
|
|
4873
|
+
const dirty = Boolean(status);
|
|
4874
|
+
const sourceFingerprint = sha256(`${gitSha || "unknown"}\0${sha256(diff || "")}\0${untrackedFingerprint(root, status || "")}`);
|
|
4875
|
+
return {
|
|
4876
|
+
channel,
|
|
4877
|
+
dirty,
|
|
4878
|
+
errors,
|
|
4879
|
+
fingerprint: sha256(JSON.stringify({ channel, dirty, git_sha: gitSha, origin: "checkout", root, source_fingerprint: sourceFingerprint, version })),
|
|
4880
|
+
git_sha: gitSha,
|
|
4881
|
+
origin: "checkout",
|
|
4882
|
+
package_version: version,
|
|
4883
|
+
root,
|
|
4884
|
+
source_fingerprint: sourceFingerprint,
|
|
4885
|
+
verified: errors.length === 0
|
|
4886
|
+
};
|
|
4887
|
+
}
|
|
4888
|
+
function buildFingerprint(build) {
|
|
4889
|
+
return sha256(JSON.stringify({
|
|
4890
|
+
package_name: build.package_name,
|
|
4891
|
+
package_version: build.package_version,
|
|
4892
|
+
schema_version: build.schema_version,
|
|
4893
|
+
source_dirty: build.source_dirty,
|
|
4894
|
+
source_fingerprint: build.source_fingerprint,
|
|
4895
|
+
source_git_sha: build.source_git_sha
|
|
4896
|
+
}));
|
|
4897
|
+
}
|
|
4898
|
+
function readBuildIdentity(root, errors) {
|
|
4899
|
+
const path = join9(root, "dist", "runtime-build.json");
|
|
4900
|
+
let build;
|
|
4901
|
+
try {
|
|
4902
|
+
build = JSON.parse(readFileSync5(path, "utf8"));
|
|
4903
|
+
} catch (error) {
|
|
4904
|
+
errors.push(`Embedded build identity is missing or unreadable at ${path}: ${error instanceof Error ? error.message : String(error)}`);
|
|
4905
|
+
return void 0;
|
|
4906
|
+
}
|
|
4907
|
+
if (build.schema_version !== lineageRuntimeBuildSchemaVersion) errors.push(`Unsupported build identity schema: ${String(build.schema_version)}`);
|
|
4908
|
+
if (!/^[a-f0-9]{40}$/i.test(build.source_git_sha || "")) errors.push("Embedded build Git revision is invalid");
|
|
4909
|
+
if (!/^[a-f0-9]{64}$/i.test(build.source_fingerprint || "")) errors.push("Embedded source fingerprint is invalid");
|
|
4910
|
+
const expected = buildFingerprint(build);
|
|
4911
|
+
if (build.build_fingerprint !== expected) errors.push("Embedded build fingerprint does not match its contents");
|
|
4912
|
+
return build;
|
|
4913
|
+
}
|
|
4914
|
+
function readInstallReceipt(path, errors) {
|
|
4915
|
+
if (!path) {
|
|
4916
|
+
errors.push("Packaged runtime was not launched through a channel install receipt");
|
|
4917
|
+
return void 0;
|
|
4918
|
+
}
|
|
4919
|
+
let receipt;
|
|
4920
|
+
const receiptPath = resolve7(path);
|
|
4921
|
+
try {
|
|
4922
|
+
receipt = JSON.parse(readFileSync5(receiptPath, "utf8"));
|
|
4923
|
+
} catch (error) {
|
|
4924
|
+
errors.push(`Runtime install receipt is missing or unreadable at ${receiptPath}: ${error instanceof Error ? error.message : String(error)}`);
|
|
4925
|
+
return void 0;
|
|
4926
|
+
}
|
|
4927
|
+
if (receipt.schema_version !== lineageRuntimeInstallSchemaVersion) errors.push(`Unsupported runtime install receipt schema: ${String(receipt.schema_version)}`);
|
|
4928
|
+
if (!isAbsolute3(receipt.package_root || "")) errors.push("Runtime install receipt package_root must be absolute");
|
|
4929
|
+
if (!receipt.package_integrity?.startsWith("sha512-")) errors.push("Runtime install receipt package integrity is invalid");
|
|
4930
|
+
if (receipt.package_source !== "registry" && receipt.package_source !== "local") errors.push("Runtime install receipt package source is invalid");
|
|
4931
|
+
if (typeof receipt.package_spec !== "string" || !receipt.package_spec) errors.push("Runtime install receipt package spec is invalid");
|
|
4932
|
+
if (!/^[a-f0-9]{64}$/i.test(receipt.package_tree_sha256 || "")) errors.push("Runtime install receipt package tree hash is invalid");
|
|
4933
|
+
if (Number.isNaN(Date.parse(receipt.installed_at))) errors.push("Runtime install receipt timestamp is invalid");
|
|
4934
|
+
return { ...receipt, receipt_path: receiptPath };
|
|
4935
|
+
}
|
|
4936
|
+
function lineagePackageTreeSha256(root) {
|
|
4937
|
+
const hash = createHash6("sha256");
|
|
4938
|
+
const visit = (directory, relativeDirectory = "") => {
|
|
4939
|
+
for (const entry of readdirSync3(directory, { withFileTypes: true }).sort((left, right) => left.name.localeCompare(right.name))) {
|
|
4940
|
+
const relativePath = relativeDirectory ? join9(relativeDirectory, entry.name) : entry.name;
|
|
4941
|
+
const path = join9(directory, entry.name);
|
|
4942
|
+
hash.update(relativePath.replaceAll("\\", "/"));
|
|
4943
|
+
hash.update("\0");
|
|
4944
|
+
if (entry.isDirectory()) {
|
|
4945
|
+
hash.update("directory\0");
|
|
4946
|
+
visit(path, relativePath);
|
|
4947
|
+
} else if (entry.isSymbolicLink()) {
|
|
4948
|
+
hash.update("symlink\0");
|
|
4949
|
+
hash.update(readlinkSync(path));
|
|
4950
|
+
} else if (entry.isFile()) {
|
|
4951
|
+
hash.update("file\0");
|
|
4952
|
+
hash.update(readFileSync5(path));
|
|
4953
|
+
} else {
|
|
4954
|
+
hash.update("other\0");
|
|
4955
|
+
}
|
|
4956
|
+
hash.update("\0");
|
|
4957
|
+
}
|
|
4958
|
+
};
|
|
4959
|
+
visit(root);
|
|
4960
|
+
return hash.digest("hex");
|
|
4961
|
+
}
|
|
4962
|
+
function packageCodeIdentity(root, channel, info, receiptPath) {
|
|
4963
|
+
const errors = [];
|
|
4964
|
+
const build = readBuildIdentity(root, errors);
|
|
4965
|
+
const install = readInstallReceipt(receiptPath, errors);
|
|
4966
|
+
if (channel === "dev") errors.push("Published package code cannot run as dev; use a Git checkout/worktree");
|
|
4967
|
+
if (build?.package_name !== info.name || build?.package_version !== info.version) {
|
|
4968
|
+
errors.push("Embedded build package identity does not match package.json");
|
|
4969
|
+
}
|
|
4970
|
+
if (build?.source_dirty) errors.push("Stable and preview package installs require a clean-source build");
|
|
4971
|
+
if (install) {
|
|
4972
|
+
if (install.channel !== channel) errors.push(`Install receipt channel ${install.channel} does not match requested ${channel}`);
|
|
4973
|
+
if (canonicalRoot(install.package_root) !== root) errors.push("Install receipt package root does not match the executing package root");
|
|
4974
|
+
if (install.package_name !== info.name || install.package_version !== info.version) errors.push("Install receipt package identity does not match package.json");
|
|
4975
|
+
if (build && install.build_fingerprint !== build.build_fingerprint) errors.push("Install receipt build fingerprint does not match the embedded build");
|
|
4976
|
+
try {
|
|
4977
|
+
if (lineagePackageTreeSha256(root) !== install.package_tree_sha256) errors.push("Installed package tree does not match the channel install receipt");
|
|
4978
|
+
} catch (error) {
|
|
4979
|
+
errors.push(`Installed package tree could not be verified: ${error instanceof Error ? error.message : String(error)}`);
|
|
4980
|
+
}
|
|
4981
|
+
}
|
|
4982
|
+
const gitSha = build?.source_git_sha;
|
|
4983
|
+
const sourceFingerprint = build?.source_fingerprint;
|
|
4984
|
+
return {
|
|
4985
|
+
build,
|
|
4986
|
+
channel,
|
|
4987
|
+
dirty: build?.source_dirty,
|
|
4988
|
+
errors,
|
|
4989
|
+
fingerprint: sha256(JSON.stringify({
|
|
4990
|
+
build_fingerprint: build?.build_fingerprint,
|
|
4991
|
+
channel,
|
|
4992
|
+
install_integrity: install?.package_integrity,
|
|
4993
|
+
origin: "package",
|
|
4994
|
+
root,
|
|
4995
|
+
version: info.version
|
|
4996
|
+
})),
|
|
4997
|
+
git_sha: gitSha,
|
|
4998
|
+
install,
|
|
4999
|
+
origin: "package",
|
|
5000
|
+
package_version: info.version,
|
|
5001
|
+
root,
|
|
5002
|
+
source_fingerprint: sourceFingerprint,
|
|
5003
|
+
verified: errors.length === 0
|
|
5004
|
+
};
|
|
5005
|
+
}
|
|
5006
|
+
function getLineageCodeIdentity(channel, options = {}) {
|
|
5007
|
+
const root = canonicalRoot(options.root || codeRoot);
|
|
5008
|
+
const info = packageInfo(root);
|
|
5009
|
+
if (existsSync10(join9(root, ".git"))) return checkoutCodeIdentity(root, channel, info.version);
|
|
5010
|
+
if (existsSync10(join9(root, "package.json"))) {
|
|
5011
|
+
return packageCodeIdentity(root, channel, info, options.receiptPath ?? process.env.LINEAGE_RUNTIME_RECEIPT);
|
|
5012
|
+
}
|
|
5013
|
+
const errors = [`Code root has neither checkout metadata nor package.json: ${root}`];
|
|
5014
|
+
return {
|
|
5015
|
+
channel,
|
|
5016
|
+
errors,
|
|
5017
|
+
fingerprint: sha256(JSON.stringify({ channel, origin: "unknown", root, version: info.version })),
|
|
5018
|
+
origin: "unknown",
|
|
5019
|
+
package_version: info.version,
|
|
5020
|
+
root,
|
|
5021
|
+
verified: false
|
|
5022
|
+
};
|
|
5023
|
+
}
|
|
5024
|
+
function assertLineageCodeOrigin(channel) {
|
|
5025
|
+
const identity = getLineageCodeIdentity(channel);
|
|
5026
|
+
if (!identity.verified) {
|
|
5027
|
+
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}.`;
|
|
5028
|
+
throw new Error(`Unverified ${channel} code origin: ${identity.errors.join("; ")}. ${migration}`);
|
|
5029
|
+
}
|
|
5030
|
+
return identity;
|
|
4519
5031
|
}
|
|
4520
5032
|
function tableExists2(database, table) {
|
|
4521
5033
|
return Boolean(database.prepare("select name from sqlite_master where type = 'table' and name = ?").get(table));
|
|
@@ -4533,6 +5045,7 @@ function getLineageRuntimeInfo(options = {}) {
|
|
|
4533
5045
|
const info = packageInfo();
|
|
4534
5046
|
const dbPath = options.dbPath || lineageDbPath();
|
|
4535
5047
|
const channel = normalizeRuntimeChannel(options.channel || process.env.LINEAGE_CHANNEL || process.env.LINEAGE_RELEASE_CHANNEL);
|
|
5048
|
+
const code = options.code || getLineageCodeIdentity(channel);
|
|
4536
5049
|
const databaseInfo = { exists: existsSync10(dbPath), path: dbPath };
|
|
4537
5050
|
const schema = { migration_keys: [] };
|
|
4538
5051
|
if (databaseInfo.exists) {
|
|
@@ -4547,11 +5060,13 @@ function getLineageRuntimeInfo(options = {}) {
|
|
|
4547
5060
|
databaseInfo.workspaces = tableCount(database, "lineage_workspaces");
|
|
4548
5061
|
schema.migration_keys = migrationKeys(database);
|
|
4549
5062
|
if (tableExists2(database, "lineage_profile_identity")) {
|
|
4550
|
-
const
|
|
4551
|
-
|
|
5063
|
+
const columns = new Set(database.prepare("pragma table_info(lineage_profile_identity)").all().map((row) => row.name));
|
|
5064
|
+
const fingerprintExpression = columns.has("profile_fingerprint") ? "profile_fingerprint" : "null as profile_fingerprint";
|
|
5065
|
+
const rows = database.prepare(`select profile_id, environment, ${fingerprintExpression} from lineage_profile_identity`).all();
|
|
4552
5066
|
if (rows.length === 1) {
|
|
4553
5067
|
schema.profile_id = rows[0].profile_id;
|
|
4554
5068
|
schema.profile_environment = rows[0].environment;
|
|
5069
|
+
if (rows[0].profile_fingerprint) schema.profile_fingerprint = rows[0].profile_fingerprint;
|
|
4555
5070
|
}
|
|
4556
5071
|
}
|
|
4557
5072
|
} finally {
|
|
@@ -4564,13 +5079,20 @@ function getLineageRuntimeInfo(options = {}) {
|
|
|
4564
5079
|
return {
|
|
4565
5080
|
asset_root: repoRoot,
|
|
4566
5081
|
channel,
|
|
5082
|
+
code,
|
|
4567
5083
|
database: databaseInfo,
|
|
4568
5084
|
fetchedAt: nowIso(),
|
|
4569
|
-
git_sha:
|
|
5085
|
+
git_sha: code.git_sha,
|
|
4570
5086
|
node_env: process.env.NODE_ENV,
|
|
4571
5087
|
package_name: info.name,
|
|
4572
5088
|
profile: runtimeProfileIdentity(channel),
|
|
4573
5089
|
schema,
|
|
5090
|
+
service: {
|
|
5091
|
+
instance_id: process.env.LINEAGE_SERVICE_INSTANCE_ID,
|
|
5092
|
+
launcher_pid: process.env.LINEAGE_LAUNCHER_PID ? Number(process.env.LINEAGE_LAUNCHER_PID) : void 0,
|
|
5093
|
+
pid: process.pid,
|
|
5094
|
+
started_at: processStartedAt
|
|
5095
|
+
},
|
|
4574
5096
|
version: info.version
|
|
4575
5097
|
};
|
|
4576
5098
|
}
|
|
@@ -4665,7 +5187,7 @@ var signalExitCodes = {
|
|
|
4665
5187
|
SIGTERM: 143
|
|
4666
5188
|
};
|
|
4667
5189
|
function packageRoot2() {
|
|
4668
|
-
return resolve8(
|
|
5190
|
+
return resolve8(dirname6(fileURLToPath3(import.meta.url)), "..", "..");
|
|
4669
5191
|
}
|
|
4670
5192
|
function packageVersion() {
|
|
4671
5193
|
try {
|
|
@@ -4710,9 +5232,8 @@ function resolveStartOptions(config, args) {
|
|
|
4710
5232
|
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
4711
5233
|
throw new Error(`Invalid port: ${rawPort}`);
|
|
4712
5234
|
}
|
|
4713
|
-
const
|
|
4714
|
-
|
|
4715
|
-
if (profile && serviceUrl && (host !== profileHost || port !== Number(serviceUrl.port || 80))) {
|
|
5235
|
+
const host = readOption(args, "--host") || process.env.HOST || serviceUrl?.hostname || config.defaultHost;
|
|
5236
|
+
if (profile && serviceUrl && (host !== serviceUrl.hostname || port !== Number(serviceUrl.port || 80))) {
|
|
4716
5237
|
throw new Error(`Profile ${profile.profile_id} service_origin ${profile.service_origin} conflicts with requested host/port ${host}:${port}`);
|
|
4717
5238
|
}
|
|
4718
5239
|
return {
|
|
@@ -4725,13 +5246,6 @@ function resolveStartOptions(config, args) {
|
|
|
4725
5246
|
profile
|
|
4726
5247
|
};
|
|
4727
5248
|
}
|
|
4728
|
-
function normalizeListenHost(host) {
|
|
4729
|
-
return host.startsWith("[") && host.endsWith("]") ? host.slice(1, -1) : host;
|
|
4730
|
-
}
|
|
4731
|
-
function lineageServiceUrl(host, port) {
|
|
4732
|
-
const urlHost = host.includes(":") ? `[${normalizeListenHost(host)}]` : host;
|
|
4733
|
-
return `http://${urlHost}:${port}`;
|
|
4734
|
-
}
|
|
4735
5249
|
function resolveCliAssetRoot(args) {
|
|
4736
5250
|
return resolve8(readOption(args, "--asset-root") || process.env.LINEAGE_ASSET_ROOT || packageRoot);
|
|
4737
5251
|
}
|
|
@@ -4748,15 +5262,8 @@ function hasOption(args, name) {
|
|
|
4748
5262
|
}
|
|
4749
5263
|
function profileSelector(args) {
|
|
4750
5264
|
const option = readOption(args, "--profile");
|
|
4751
|
-
if (
|
|
4752
|
-
throw new Error(
|
|
4753
|
-
}
|
|
4754
|
-
if (option && process.env.LINEAGE_PROFILE) {
|
|
4755
|
-
const optionProfile = resolveLineageProfile(option);
|
|
4756
|
-
const environmentProfile = resolveLineageProfile(process.env.LINEAGE_PROFILE);
|
|
4757
|
-
if (optionProfile.manifest_path !== environmentProfile.manifest_path) {
|
|
4758
|
-
throw new Error(`--profile ${option} conflicts with LINEAGE_PROFILE ${process.env.LINEAGE_PROFILE}`);
|
|
4759
|
-
}
|
|
5265
|
+
if (option && process.env.LINEAGE_PROFILE && option !== process.env.LINEAGE_PROFILE) {
|
|
5266
|
+
throw new Error(`--profile ${option} conflicts with LINEAGE_PROFILE ${process.env.LINEAGE_PROFILE}`);
|
|
4760
5267
|
}
|
|
4761
5268
|
return option || process.env.LINEAGE_PROFILE;
|
|
4762
5269
|
}
|
|
@@ -4780,12 +5287,13 @@ function prepareCliProfile(config, args) {
|
|
|
4780
5287
|
}
|
|
4781
5288
|
assertProfileChannel(profile, config.channel);
|
|
4782
5289
|
const runtime = getLineageRuntimeInfo({ channel: config.channel, dbPath: profile.database_path });
|
|
4783
|
-
const doctor = doctorLineageProfile(selector, { channel: config.channel, gitSha: runtime.git_sha, version: runtime.version });
|
|
5290
|
+
const doctor = doctorLineageProfile(selector, { channel: config.channel, code: runtime.code, gitSha: runtime.git_sha, version: runtime.version });
|
|
4784
5291
|
if (!doctor.ok) throw new Error(`Profile ${profile.profile_id} failed doctor: ${doctorFailures(doctor)}`);
|
|
4785
5292
|
process.env.LINEAGE_ASSET_ROOT = profile.asset_root;
|
|
4786
5293
|
process.env.LINEAGE_DB = profile.database_path;
|
|
4787
5294
|
process.env.LINEAGE_PROFILE = selector;
|
|
4788
5295
|
process.env.LINEAGE_PROFILE_ENVIRONMENT = profile.environment;
|
|
5296
|
+
process.env.LINEAGE_PROFILE_FINGERPRINT = profile.profile_fingerprint;
|
|
4789
5297
|
process.env.LINEAGE_PROFILE_ID = profile.profile_id;
|
|
4790
5298
|
process.env.LINEAGE_PROFILE_MANIFEST = profile.manifest_path;
|
|
4791
5299
|
process.env.LINEAGE_PROFILE_SERVICE_ORIGIN = profile.service_origin;
|
|
@@ -4798,7 +5306,11 @@ function formatLineageHelp(config) {
|
|
|
4798
5306
|
Usage:
|
|
4799
5307
|
${config.binName} start [--profile <id-or-manifest>] [--port <port>] [--host <host>] [--db <path>] [--asset-root <path>] [--open] [--json]
|
|
4800
5308
|
${config.binName} profile doctor --profile <id-or-manifest> [--json]
|
|
4801
|
-
${config.binName} profile bind --profile <id-or-manifest>
|
|
5309
|
+
${config.binName} profile bind --profile <id-or-manifest> --confirm-write [--json]
|
|
5310
|
+
${config.binName} profile clone --source-db <snapshot-source> --target-profile <id-or-manifest> --confirm-write [--json]
|
|
5311
|
+
${config.binName} profile clone-assets --source-asset-root <path> --target-profile <id-or-manifest> --confirm-write [--json]
|
|
5312
|
+
${config.binName} runtime info [--json]
|
|
5313
|
+
${config.binName} runtime doctor [--json]
|
|
4802
5314
|
${config.binName} next [--project <project>] [--root <asset-id>] [--db <path>] [--json]
|
|
4803
5315
|
${config.binName} brief [--project <project>] [--root <asset-id>] [--db <path>] [--json]
|
|
4804
5316
|
${config.binName} inspect --asset-id <asset-id> [--project <project>] [--db <path>] [--json]
|
|
@@ -4837,7 +5349,15 @@ Asset catalogs and local media default to the installed package root. Pass
|
|
|
4837
5349
|
All commands accept --profile <id-or-manifest> or LINEAGE_PROFILE. Named
|
|
4838
5350
|
profiles are authoritative for database, asset root, environment, and origin;
|
|
4839
5351
|
they cannot be combined with direct --db or --asset-root overrides. Commands
|
|
4840
|
-
without a profile run in legacy-unbound
|
|
5352
|
+
without a profile run in legacy-unbound diagnostic/read-only mode.
|
|
5353
|
+
|
|
5354
|
+
Operational commands also require an attested code origin. Stable and preview
|
|
5355
|
+
run from separate lineage-channel install receipts. Dev runs only from a Git
|
|
5356
|
+
checkout/worktree through npm run lineage:dev.
|
|
5357
|
+
|
|
5358
|
+
Server startup independently verifies code origin. When --open is requested,
|
|
5359
|
+
the browser opens only after /api/runtime matches the expected code, profile,
|
|
5360
|
+
database, and unique service instance.
|
|
4841
5361
|
|
|
4842
5362
|
Variation vs re-roll:
|
|
4843
5363
|
link-child creates a new visible child variation edge.
|
|
@@ -4852,6 +5372,48 @@ function openBrowser(url) {
|
|
|
4852
5372
|
const opener = spawn(command, args, { detached: true, stdio: "ignore" });
|
|
4853
5373
|
opener.unref();
|
|
4854
5374
|
}
|
|
5375
|
+
function lineageServiceIdentityErrors(runtime, expected) {
|
|
5376
|
+
const errors = [];
|
|
5377
|
+
if (runtime.channel !== expected.channel) errors.push(`channel ${runtime.channel} != ${expected.channel}`);
|
|
5378
|
+
if (!runtime.code?.verified) errors.push("service code identity is not verified");
|
|
5379
|
+
if (runtime.code?.fingerprint !== expected.code_fingerprint) errors.push(`code fingerprint ${runtime.code?.fingerprint || "missing"} != ${expected.code_fingerprint}`);
|
|
5380
|
+
if (resolve8(runtime.database.path) !== resolve8(expected.database_path)) errors.push(`database ${runtime.database.path} != ${expected.database_path}`);
|
|
5381
|
+
if (runtime.service?.instance_id !== expected.instance_id) errors.push(`service instance ${runtime.service?.instance_id || "missing"} != ${expected.instance_id}`);
|
|
5382
|
+
if (runtime.service?.launcher_pid !== expected.launcher_pid) errors.push(`launcher pid ${runtime.service?.launcher_pid || "missing"} != ${expected.launcher_pid}`);
|
|
5383
|
+
if (expected.profile) {
|
|
5384
|
+
if (!runtime.profile.bound) errors.push("service profile is unbound");
|
|
5385
|
+
if (runtime.profile.id !== expected.profile.profile_id) errors.push(`profile ${runtime.profile.id} != ${expected.profile.profile_id}`);
|
|
5386
|
+
if (runtime.profile.environment !== expected.profile.environment) errors.push(`environment ${runtime.profile.environment} != ${expected.profile.environment}`);
|
|
5387
|
+
if (runtime.profile.fingerprint !== expected.profile.profile_fingerprint) errors.push(`profile fingerprint ${runtime.profile.fingerprint || "missing"} != ${expected.profile.profile_fingerprint}`);
|
|
5388
|
+
if (runtime.schema.profile_id !== expected.profile.profile_id) errors.push(`database profile ${runtime.schema.profile_id || "missing"} != ${expected.profile.profile_id}`);
|
|
5389
|
+
if (runtime.schema.profile_fingerprint !== expected.profile.profile_fingerprint) errors.push(`database fingerprint ${runtime.schema.profile_fingerprint || "missing"} != ${expected.profile.profile_fingerprint}`);
|
|
5390
|
+
}
|
|
5391
|
+
return errors;
|
|
5392
|
+
}
|
|
5393
|
+
async function openBrowserAfterReadiness(url, expected, child) {
|
|
5394
|
+
const deadline = Date.now() + 15e3;
|
|
5395
|
+
let lastError = "service did not respond";
|
|
5396
|
+
while (Date.now() < deadline && child.exitCode === null) {
|
|
5397
|
+
try {
|
|
5398
|
+
const response = await fetch(`${url}/api/runtime`, { signal: AbortSignal.timeout(1e3) });
|
|
5399
|
+
if (response.ok) {
|
|
5400
|
+
const body = await response.json();
|
|
5401
|
+
if (body.runtime) {
|
|
5402
|
+
const errors = lineageServiceIdentityErrors(body.runtime, expected);
|
|
5403
|
+
if (errors.length === 0) {
|
|
5404
|
+
openBrowser(url);
|
|
5405
|
+
return;
|
|
5406
|
+
}
|
|
5407
|
+
lastError = errors.join("; ");
|
|
5408
|
+
}
|
|
5409
|
+
} else lastError = `HTTP ${response.status}`;
|
|
5410
|
+
} catch (error) {
|
|
5411
|
+
lastError = error instanceof Error ? error.message : String(error);
|
|
5412
|
+
}
|
|
5413
|
+
await new Promise((resolveDelay) => setTimeout(resolveDelay, 200));
|
|
5414
|
+
}
|
|
5415
|
+
throw new Error(`Service readiness failed; browser was not opened: ${lastError}`);
|
|
5416
|
+
}
|
|
4855
5417
|
function positionalArgs(args) {
|
|
4856
5418
|
const values = [];
|
|
4857
5419
|
for (let index = 0; index < args.length; index += 1) {
|
|
@@ -4910,8 +5472,8 @@ function runLineageDataCommand(command, args) {
|
|
|
4910
5472
|
const out = readOption(args, "--out");
|
|
4911
5473
|
if (out) {
|
|
4912
5474
|
const outPath = resolve8(out);
|
|
4913
|
-
mkdirSync6(
|
|
4914
|
-
|
|
5475
|
+
mkdirSync6(dirname6(outPath), { recursive: true });
|
|
5476
|
+
writeFileSync4(outPath, `${JSON.stringify(packet, null, 2)}
|
|
4915
5477
|
`);
|
|
4916
5478
|
}
|
|
4917
5479
|
return packet;
|
|
@@ -5122,41 +5684,90 @@ function runLineageDbCommand(config, command, args) {
|
|
|
5122
5684
|
throw new Error(`Unknown db command: ${command}`);
|
|
5123
5685
|
}
|
|
5124
5686
|
function runLineageProfileCommand(config, command, args) {
|
|
5125
|
-
|
|
5687
|
+
const runtime = getLineageRuntimeInfo({ channel: config.channel });
|
|
5688
|
+
const runtimeIdentity = { channel: config.channel, code: runtime.code, gitSha: runtime.git_sha, version: runtime.version };
|
|
5689
|
+
if (command === "clone") {
|
|
5690
|
+
const source = readOption(args, "--source-db");
|
|
5691
|
+
const target = readOption(args, "--target-profile");
|
|
5692
|
+
if (!source || !target) throw new Error("lineage profile clone requires --source-db and --target-profile");
|
|
5693
|
+
if (hasOption(args, "--profile")) throw new Error("Profile clone uses --target-profile, not --profile");
|
|
5694
|
+
if (!args.includes("--confirm-write")) throw new Error("Profile clone requires --confirm-write");
|
|
5695
|
+
const targetProfile = resolveLineageProfile(target);
|
|
5696
|
+
const writerLease = acquireProfileWriterLease(targetProfile, config.channel, "cli");
|
|
5697
|
+
return cloneLineageProfileDatabase(source, target, runtimeIdentity, true).finally(writerLease.release);
|
|
5698
|
+
}
|
|
5699
|
+
if (command === "clone-assets") {
|
|
5700
|
+
const source = readOption(args, "--source-asset-root");
|
|
5701
|
+
const target = readOption(args, "--target-profile");
|
|
5702
|
+
if (!source || !target) throw new Error("lineage profile clone-assets requires --source-asset-root and --target-profile");
|
|
5703
|
+
if (hasOption(args, "--profile")) throw new Error("Profile asset clone uses --target-profile, not --profile");
|
|
5704
|
+
if (!args.includes("--confirm-write")) throw new Error("Profile asset clone requires --confirm-write");
|
|
5705
|
+
const targetProfile = resolveLineageProfile(target);
|
|
5706
|
+
const writerLease = acquireProfileWriterLease(targetProfile, config.channel, "cli");
|
|
5707
|
+
try {
|
|
5708
|
+
return cloneLineageProfileAssets(source, target, runtimeIdentity, true);
|
|
5709
|
+
} finally {
|
|
5710
|
+
writerLease.release();
|
|
5711
|
+
}
|
|
5712
|
+
}
|
|
5126
5713
|
if (hasOption(args, "--db")) throw new Error(`Profile ${command} cannot be combined with --db`);
|
|
5127
5714
|
if (hasOption(args, "--asset-root")) throw new Error(`Profile ${command} cannot be combined with --asset-root`);
|
|
5128
5715
|
const selector = profileSelector(args);
|
|
5129
5716
|
if (!selector) throw new Error(`lineage profile ${command} requires --profile or LINEAGE_PROFILE`);
|
|
5717
|
+
if (command === "doctor") return doctorLineageProfile(selector, runtimeIdentity);
|
|
5130
5718
|
if (command === "bind") {
|
|
5719
|
+
if (!args.includes("--confirm-write")) throw new Error("Profile bind requires --confirm-write");
|
|
5131
5720
|
const profile = resolveLineageProfile(selector);
|
|
5132
|
-
assertProfileChannel(profile, config.channel);
|
|
5133
|
-
if (!args.includes("--confirm-write")) return bindLineageProfileDatabase(profile, false);
|
|
5134
|
-
mkdirSync6(dirname5(profile.database_path), { recursive: true });
|
|
5135
5721
|
const writerLease = acquireProfileWriterLease(profile, config.channel, "cli");
|
|
5136
5722
|
try {
|
|
5137
|
-
return bindLineageProfileDatabase(
|
|
5723
|
+
return bindLineageProfileDatabase(selector, runtimeIdentity, true);
|
|
5138
5724
|
} finally {
|
|
5139
5725
|
writerLease.release();
|
|
5140
5726
|
}
|
|
5141
5727
|
}
|
|
5142
|
-
|
|
5143
|
-
return doctorLineageProfile(selector, { channel: config.channel, gitSha: runtime.git_sha, version: runtime.version });
|
|
5728
|
+
throw new Error(`Unknown profile command: ${command}`);
|
|
5144
5729
|
}
|
|
5145
5730
|
function printProfileResult(result, json) {
|
|
5146
5731
|
if (json) {
|
|
5147
5732
|
console.log(JSON.stringify(result, null, 2));
|
|
5148
5733
|
return;
|
|
5149
5734
|
}
|
|
5150
|
-
if (result.schema_version === "lineage.
|
|
5151
|
-
console.log(
|
|
5152
|
-
|
|
5735
|
+
if (result.schema_version === "lineage.profile_doctor.v1") {
|
|
5736
|
+
console.log(`Profile doctor: ${result.ok ? "ok" : "failed"}`);
|
|
5737
|
+
for (const check of result.checks) console.log(`${check.status.toUpperCase()} ${check.id}: ${check.message}`);
|
|
5738
|
+
} else if (result.schema_version === "lineage.profile_bind.v1") {
|
|
5739
|
+
console.log(`${result.already_bound ? "Already bound" : "Bound"} ${result.database_path} to ${result.identity.profile_id}`);
|
|
5740
|
+
} else if (result.schema_version === "lineage.profile_clone_receipt.v1") {
|
|
5741
|
+
console.log(`Cloned ${result.source_database_path} to ${result.database_path} for ${result.target_identity.profile_id}`);
|
|
5742
|
+
console.log(`Receipt: ${result.receipt_path}`);
|
|
5743
|
+
} else {
|
|
5744
|
+
console.log(`Cloned ${result.files_copied} referenced asset file(s) into ${result.asset_root}`);
|
|
5745
|
+
console.log(`Receipt: ${result.receipt_path}`);
|
|
5153
5746
|
}
|
|
5154
|
-
console.log(`Profile doctor: ${result.ok ? "ok" : "failed"}`);
|
|
5155
|
-
for (const check of result.checks) console.log(`${check.status.toUpperCase()} ${check.id}: ${check.message}`);
|
|
5156
5747
|
}
|
|
5157
5748
|
function lineageProfileDoctorExitCode(result) {
|
|
5158
5749
|
return result.ok ? 0 : 1;
|
|
5159
5750
|
}
|
|
5751
|
+
function runLineageRuntimeCommand(config, command) {
|
|
5752
|
+
if (command === "info") return getLineageCodeIdentity(config.channel);
|
|
5753
|
+
if (command === "doctor") return assertLineageCodeOrigin(config.channel);
|
|
5754
|
+
throw new Error(`Unknown runtime command: ${command}`);
|
|
5755
|
+
}
|
|
5756
|
+
function printRuntimeResult(result, json) {
|
|
5757
|
+
if (json) {
|
|
5758
|
+
console.log(JSON.stringify(result, null, 2));
|
|
5759
|
+
return;
|
|
5760
|
+
}
|
|
5761
|
+
console.log(`Code origin: ${result.origin}`);
|
|
5762
|
+
console.log(`Code root: ${result.root}`);
|
|
5763
|
+
console.log(`Channel: ${result.channel}`);
|
|
5764
|
+
console.log(`Version: ${result.package_version}`);
|
|
5765
|
+
if (result.git_sha) console.log(`Git: ${result.git_sha}`);
|
|
5766
|
+
if (result.dirty !== void 0) console.log(`Dirty: ${result.dirty ? "yes" : "no"}`);
|
|
5767
|
+
console.log(`Fingerprint: ${result.fingerprint}`);
|
|
5768
|
+
console.log(`Verified: ${result.verified ? "yes" : "no"}`);
|
|
5769
|
+
for (const error of result.errors) console.log(`FAIL: ${error}`);
|
|
5770
|
+
}
|
|
5160
5771
|
function lineageCliRequiresWriterLease(command, args) {
|
|
5161
5772
|
if (command === "next" || command === "brief" || command === "inspect" || command === "selection") return false;
|
|
5162
5773
|
const subcommand = positionalArgs(args)[0] || "";
|
|
@@ -5188,6 +5799,10 @@ function printDbResult(command, result, json) {
|
|
|
5188
5799
|
const runtime = result;
|
|
5189
5800
|
console.log(`Channel: ${runtime.channel}`);
|
|
5190
5801
|
console.log(`Version: ${runtime.version}`);
|
|
5802
|
+
if (runtime.code) {
|
|
5803
|
+
console.log(`Code: ${runtime.code.origin} ${runtime.code.verified ? "verified" : "unverified"} ${runtime.code.fingerprint}`);
|
|
5804
|
+
console.log(`Code root: ${runtime.code.root}`);
|
|
5805
|
+
}
|
|
5191
5806
|
if (runtime.git_sha) console.log(`Git: ${runtime.git_sha}`);
|
|
5192
5807
|
if (runtime.node_env) console.log(`Node env: ${runtime.node_env}`);
|
|
5193
5808
|
console.log(`Assets: ${runtime.asset_root}`);
|
|
@@ -5336,8 +5951,8 @@ function start(config, args) {
|
|
|
5336
5951
|
else console.error(`${config.binName}: ${message}`);
|
|
5337
5952
|
process.exit(1);
|
|
5338
5953
|
}
|
|
5339
|
-
mkdirSync6(
|
|
5340
|
-
const url =
|
|
5954
|
+
mkdirSync6(dirname6(options.dbPath), { recursive: true });
|
|
5955
|
+
const url = `http://${options.host}:${options.port}`;
|
|
5341
5956
|
if (options.json) {
|
|
5342
5957
|
console.log(JSON.stringify({ assetRoot: options.assetRoot, channel: config.channel, dbPath: options.dbPath, host: options.host, port: options.port, profile: options.profile ? { environment: options.profile.environment, id: options.profile.profile_id } : { bound: false, id: "legacy-unbound" }, status: "starting", url }, null, 2));
|
|
5343
5958
|
} else {
|
|
@@ -5347,7 +5962,8 @@ function start(config, args) {
|
|
|
5347
5962
|
console.log(`SQLite: ${options.dbPath}`);
|
|
5348
5963
|
console.log(`Assets: ${options.assetRoot}`);
|
|
5349
5964
|
}
|
|
5350
|
-
|
|
5965
|
+
const code = getLineageCodeIdentity(config.channel);
|
|
5966
|
+
const serviceInstanceId = process.env.LINEAGE_SERVICE_INSTANCE_ID || randomUUID4();
|
|
5351
5967
|
const child = spawn(process.execPath, [serverPath], {
|
|
5352
5968
|
env: {
|
|
5353
5969
|
...process.env,
|
|
@@ -5357,15 +5973,31 @@ function start(config, args) {
|
|
|
5357
5973
|
LINEAGE_DB: options.dbPath,
|
|
5358
5974
|
LINEAGE_PROFILE: options.profile ? process.env.LINEAGE_PROFILE : void 0,
|
|
5359
5975
|
LINEAGE_PROFILE_ENVIRONMENT: options.profile?.environment,
|
|
5976
|
+
LINEAGE_PROFILE_FINGERPRINT: options.profile?.profile_fingerprint,
|
|
5360
5977
|
LINEAGE_PROFILE_ID: options.profile?.profile_id,
|
|
5361
5978
|
LINEAGE_PROFILE_MANIFEST: options.profile?.manifest_path,
|
|
5362
5979
|
LINEAGE_PROFILE_SERVICE_ORIGIN: options.profile?.service_origin,
|
|
5363
|
-
|
|
5980
|
+
LINEAGE_LAUNCHER_PID: String(process.pid),
|
|
5981
|
+
LINEAGE_SERVICE_INSTANCE_ID: serviceInstanceId,
|
|
5982
|
+
LINEAGE_DB_ACCESS: options.profile ? void 0 : "read-only",
|
|
5364
5983
|
NODE_ENV: "production",
|
|
5365
5984
|
PORT: String(options.port)
|
|
5366
5985
|
},
|
|
5367
5986
|
stdio: "inherit"
|
|
5368
5987
|
});
|
|
5988
|
+
if (options.open) {
|
|
5989
|
+
void openBrowserAfterReadiness(url, {
|
|
5990
|
+
channel: config.channel,
|
|
5991
|
+
code_fingerprint: code.fingerprint,
|
|
5992
|
+
database_path: options.dbPath,
|
|
5993
|
+
instance_id: serviceInstanceId,
|
|
5994
|
+
launcher_pid: process.pid,
|
|
5995
|
+
profile: options.profile
|
|
5996
|
+
}, child).catch((error) => {
|
|
5997
|
+
console.error(`${config.binName}: ${error instanceof Error ? error.message : String(error)}`);
|
|
5998
|
+
child.kill("SIGTERM");
|
|
5999
|
+
});
|
|
6000
|
+
}
|
|
5369
6001
|
let forwardedSignal;
|
|
5370
6002
|
const stop = (signal) => {
|
|
5371
6003
|
forwardedSignal = signal;
|
|
@@ -5373,7 +6005,7 @@ function start(config, args) {
|
|
|
5373
6005
|
};
|
|
5374
6006
|
process.once("SIGINT", stop);
|
|
5375
6007
|
process.once("SIGTERM", stop);
|
|
5376
|
-
child.on("exit", (
|
|
6008
|
+
child.on("exit", (code2) => process.exit(code2 ?? (forwardedSignal ? signalExitCodes[forwardedSignal] || 1 : 0)));
|
|
5377
6009
|
child.on("error", (error) => {
|
|
5378
6010
|
console.error(`${config.binName}: failed to start server: ${error.message}`);
|
|
5379
6011
|
process.exit(1);
|
|
@@ -5391,18 +6023,26 @@ async function runLineageCli(config, args = process.argv.slice(2)) {
|
|
|
5391
6023
|
}
|
|
5392
6024
|
const normalizedArgs = args[0] === "lineage" ? args.slice(1) : args;
|
|
5393
6025
|
const [command] = normalizedArgs;
|
|
5394
|
-
if (command === "start") {
|
|
5395
|
-
start(config, normalizedArgs.slice(1));
|
|
5396
|
-
return;
|
|
5397
|
-
}
|
|
5398
6026
|
if (command === "profile") {
|
|
5399
6027
|
const commandArgs = normalizedArgs.slice(2);
|
|
5400
6028
|
const profileCommand = normalizedArgs[1] || "";
|
|
5401
6029
|
const json2 = commandArgs.includes("--json");
|
|
5402
6030
|
try {
|
|
5403
6031
|
const result = runLineageProfileCommand(config, profileCommand, commandArgs);
|
|
6032
|
+
if (result instanceof Promise) {
|
|
6033
|
+
result.then((value) => {
|
|
6034
|
+
printProfileResult(value, json2);
|
|
6035
|
+
process.exit(0);
|
|
6036
|
+
}).catch((error) => {
|
|
6037
|
+
const message2 = error instanceof Error ? error.message : String(error);
|
|
6038
|
+
if (json2) console.error(JSON.stringify({ ok: false, command: `profile ${profileCommand}`, error: message2 }, null, 2));
|
|
6039
|
+
else console.error(`${config.binName}: ${message2}`);
|
|
6040
|
+
process.exit(1);
|
|
6041
|
+
});
|
|
6042
|
+
return;
|
|
6043
|
+
}
|
|
5404
6044
|
printProfileResult(result, json2);
|
|
5405
|
-
process.exit(lineageProfileDoctorExitCode(result));
|
|
6045
|
+
process.exit(result.schema_version === "lineage.profile_doctor.v1" ? lineageProfileDoctorExitCode(result) : 0);
|
|
5406
6046
|
} catch (error) {
|
|
5407
6047
|
const message2 = error instanceof Error ? error.message : String(error);
|
|
5408
6048
|
if (json2) console.error(JSON.stringify({ ok: false, command: `profile ${profileCommand}`, error: message2 }, null, 2));
|
|
@@ -5410,11 +6050,42 @@ async function runLineageCli(config, args = process.argv.slice(2)) {
|
|
|
5410
6050
|
process.exit(1);
|
|
5411
6051
|
}
|
|
5412
6052
|
}
|
|
6053
|
+
if (command === "runtime") {
|
|
6054
|
+
const runtimeCommand = normalizedArgs[1] || "";
|
|
6055
|
+
const commandArgs = normalizedArgs.slice(2);
|
|
6056
|
+
const json2 = commandArgs.includes("--json");
|
|
6057
|
+
try {
|
|
6058
|
+
printRuntimeResult(runLineageRuntimeCommand(config, runtimeCommand), json2);
|
|
6059
|
+
process.exit(0);
|
|
6060
|
+
} catch (error) {
|
|
6061
|
+
const message2 = error instanceof Error ? error.message : String(error);
|
|
6062
|
+
if (json2) console.error(JSON.stringify({ ok: false, command: `runtime ${runtimeCommand}`, error: message2 }, null, 2));
|
|
6063
|
+
else console.error(`${config.binName}: ${message2}`);
|
|
6064
|
+
process.exit(1);
|
|
6065
|
+
}
|
|
6066
|
+
}
|
|
6067
|
+
const originDiagnosticOnly = command === "db" && normalizedArgs[1] === "info";
|
|
6068
|
+
if (!originDiagnosticOnly) {
|
|
6069
|
+
try {
|
|
6070
|
+
assertLineageCodeOrigin(config.channel);
|
|
6071
|
+
} catch (error) {
|
|
6072
|
+
const message2 = error instanceof Error ? error.message : String(error);
|
|
6073
|
+
const json2 = normalizedArgs.includes("--json");
|
|
6074
|
+
if (json2) console.error(JSON.stringify({ ok: false, command, error: message2 }, null, 2));
|
|
6075
|
+
else console.error(`${config.binName}: ${message2}`);
|
|
6076
|
+
process.exit(1);
|
|
6077
|
+
}
|
|
6078
|
+
}
|
|
6079
|
+
if (command === "start") {
|
|
6080
|
+
start(config, normalizedArgs.slice(1));
|
|
6081
|
+
return;
|
|
6082
|
+
}
|
|
5413
6083
|
let managedWriterProfile;
|
|
5414
6084
|
try {
|
|
5415
6085
|
const profile = prepareCliProfile(config, normalizedArgs.slice(1));
|
|
6086
|
+
const requiresWriter = lineageCliRequiresWriterLease(command, normalizedArgs.slice(1));
|
|
5416
6087
|
if (profile) {
|
|
5417
|
-
if (
|
|
6088
|
+
if (requiresWriter) {
|
|
5418
6089
|
delete process.env.LINEAGE_DB_ACCESS;
|
|
5419
6090
|
try {
|
|
5420
6091
|
const writerLease = acquireProfileWriterLease(profile, config.channel, "cli");
|
|
@@ -5427,6 +6098,10 @@ async function runLineageCli(config, args = process.argv.slice(2)) {
|
|
|
5427
6098
|
} else {
|
|
5428
6099
|
process.env.LINEAGE_DB_ACCESS = "read-only";
|
|
5429
6100
|
}
|
|
6101
|
+
} else if (requiresWriter) {
|
|
6102
|
+
throw new Error("Persistent writes require --profile; legacy-unbound access is read-only");
|
|
6103
|
+
} else {
|
|
6104
|
+
process.env.LINEAGE_DB_ACCESS = "read-only";
|
|
5430
6105
|
}
|
|
5431
6106
|
} catch (error) {
|
|
5432
6107
|
const message2 = error instanceof Error ? error.message : String(error);
|