@hasna/recordings 0.3.1 → 0.3.8
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/README.md +38 -34
- package/bun.lock +96 -36
- package/dist/__tests__/helpers/installer-guard-execution.d.ts.map +1 -1
- package/dist/__tests__/helpers/source-assertions.d.ts +6 -4
- package/dist/__tests__/helpers/source-assertions.d.ts.map +1 -1
- package/dist/cli/index.js +197 -95
- package/dist/cli/macos-shortcut.d.ts +2 -2
- package/dist/db/pg-migrations.d.ts +1 -1
- package/dist/http/client.d.ts +0 -1
- package/dist/http/client.d.ts.map +1 -1
- package/dist/index.js +162 -73
- package/dist/lib/capture-probe.d.ts +3 -3
- package/dist/lib/capture-probe.d.ts.map +1 -1
- package/dist/lib/macos-bundle.d.ts +1 -1
- package/dist/lib/release-install-policy.d.ts +21 -0
- package/dist/lib/release-install-policy.d.ts.map +1 -1
- package/dist/mcp/index.js +144 -59
- package/dist/sdk/index.d.ts +2 -2
- package/dist/sdk/index.js +7 -3
- package/dist/sdk/v1.generated.d.ts.map +1 -1
- package/dist/server/cloud-config.d.ts +4 -19
- package/dist/server/cloud-config.d.ts.map +1 -1
- package/dist/server/cloud.d.ts +1 -1
- package/dist/server/cloud.d.ts.map +1 -1
- package/dist/server/index.js +168 -110
- package/dist/server/serve.d.ts.map +1 -1
- package/dist/storage.d.ts +1 -1
- package/dist/storage.d.ts.map +1 -1
- package/dist/storage.js +144 -60
- package/dist/store.d.ts.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.d.ts.map +1 -1
- package/package.json +4 -3
- package/packaging/macos/build_release_pkg.sh +10 -4
- package/packaging/macos/managed_bootstrap.sh +4 -4
- package/packaging/macos/scripts/postinstall +2 -2
- package/packaging/macos/scripts/preinstall +2 -0
- package/scripts/generate-sdk.ts +17 -16
- package/scripts/install_macos_app.sh +75 -28
- package/scripts/macos_artifact.ts +162 -45
- package/scripts/migrate.ts +2 -2
- package/scripts/release-suite-gate.ts +181 -0
- package/scripts/set-version.ts +5 -5
- package/scripts/smoke_macos_app.sh +80 -34
- package/scripts/vacuity-manifests/version-sites.tsv +8 -4
- package/src/native/Recordings/App/MenuBarStatusView.swift +9 -4
- package/src/native/Recordings/App/RecordingsApp.swift +37 -1
- package/src/native/Recordings/RecordingsLib/Info.plist +3 -3
- package/src/native/Recordings/RecordingsLib/PermissionRequestLaunchPlan.swift +21 -1
- package/src/native/Recordings/RecordingsTests/BarOnlyLaunchPlanTests.swift +99 -0
- package/src/native/Recordings/RecordingsTests/CLIRunnerTests.swift +2 -2
- package/src/native/Recordings/RecordingsTests/RecordingStartGateTests.swift +1 -1
- package/src/native/Recordings/Updater/BootstrapPreflight/BootstrapPreflightMain.swift +1 -1
- package/src/native/Recordings/Updater/Broker/ApplicationNamespace.swift +2 -2
- package/src/native/Recordings/Updater/Broker/BrokerMain.swift +1 -1
- package/src/native/Recordings/Updater/Broker/CanonicalTreeCopy.swift +1 -1
- package/src/native/Recordings/Updater/Broker/InstallJournal.swift +1 -1
- package/src/native/Recordings/Updater/BrokerTests/ActivationRecoveryPolicyTests.swift +2 -2
- package/src/native/Recordings/Updater/Protocol/UpdateProtocol.swift +1 -1
- package/src/native/Recordings/Updater/VerifierLauncher/RecordingsVerifierLauncher.c +2 -2
- package/src/native/Recordings/build.sh +152 -18
- package/dist/lib/retired-deployment-modes.d.ts +0 -24
- package/dist/lib/retired-deployment-modes.d.ts.map +0 -1
|
@@ -151,6 +151,7 @@ function localOnlyApprovedTargetsMessage(policyPath?: string): string {
|
|
|
151
151
|
}
|
|
152
152
|
|
|
153
153
|
export type ArtifactPolicy = "release" | "local_only";
|
|
154
|
+
export type ArtifactVariant = "full" | "bar";
|
|
154
155
|
export type TargetIdentityKind =
|
|
155
156
|
| typeof LEGACY_LOCAL_TARGET_IDENTITY_KIND
|
|
156
157
|
| "tailscale_node_id_sha256";
|
|
@@ -159,6 +160,7 @@ type OperatorTargetIdentityKind = TargetIdentityKind | "none";
|
|
|
159
160
|
export type BuildProvenance = {
|
|
160
161
|
schema_version: 3 | 4;
|
|
161
162
|
artifact_policy?: "local_only";
|
|
163
|
+
variant?: ArtifactVariant;
|
|
162
164
|
approved_target?: string;
|
|
163
165
|
approved_target_identity_kind?: TargetIdentityKind;
|
|
164
166
|
approved_target_identity_sha256?: string;
|
|
@@ -181,6 +183,9 @@ export type BuildProvenance = {
|
|
|
181
183
|
|
|
182
184
|
export type MacOSArtifactManifest = BuildProvenance & {
|
|
183
185
|
artifact_type: "recordings-macos-app";
|
|
186
|
+
/// The top-level bundle directory name inside the artifact (HasnaRecordings.app for
|
|
187
|
+
/// both the full app and the bar variant per the fleet naming rule).
|
|
188
|
+
bundle_name: string;
|
|
184
189
|
app_sha256: string;
|
|
185
190
|
binding: {
|
|
186
191
|
bundle_tree_sha256: string;
|
|
@@ -210,7 +215,7 @@ export type MacOSArtifactManifest = BuildProvenance & {
|
|
|
210
215
|
};
|
|
211
216
|
container: {
|
|
212
217
|
type: "zip";
|
|
213
|
-
install_locations: [
|
|
218
|
+
install_locations: [string];
|
|
214
219
|
};
|
|
215
220
|
nested_code_policy: {
|
|
216
221
|
allowlist_sha256: string;
|
|
@@ -825,6 +830,22 @@ function manifestPolicy(manifest: MacOSArtifactManifest): ArtifactPolicy {
|
|
|
825
830
|
return manifest.schema_version === LOCAL_ARTIFACT_SCHEMA_VERSION ? "local_only" : "release";
|
|
826
831
|
}
|
|
827
832
|
|
|
833
|
+
/**
|
|
834
|
+
* The variant an artifact was built as. Older manifests predate the field; they are
|
|
835
|
+
* full artifacts by definition ("full" was the only variant that existed).
|
|
836
|
+
*/
|
|
837
|
+
function manifestVariant(manifest: MacOSArtifactManifest): ArtifactVariant {
|
|
838
|
+
const value = manifest.variant ?? "full";
|
|
839
|
+
if (value !== "full" && value !== "bar") {
|
|
840
|
+
throw new Error(`manifest variant is unsupported: ${value}`);
|
|
841
|
+
}
|
|
842
|
+
return value;
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
function manifestBundleName(manifest: MacOSArtifactManifest): string {
|
|
846
|
+
return manifest.bundle_name ?? "HasnaRecordings.app";
|
|
847
|
+
}
|
|
848
|
+
|
|
828
849
|
function manifestTargetIdentityKind(manifest: MacOSArtifactManifest): OperatorTargetIdentityKind {
|
|
829
850
|
if (manifestPolicy(manifest) === "release") return "none";
|
|
830
851
|
return manifest.approved_target_identity_kind ?? LEGACY_LOCAL_TARGET_IDENTITY_KIND;
|
|
@@ -1073,23 +1094,23 @@ function assertCanonicalZipEntries(entries: Array<{ name: string; isDirectory: b
|
|
|
1073
1094
|
const components = logicalPath.split("/");
|
|
1074
1095
|
if (
|
|
1075
1096
|
!logicalPath ||
|
|
1076
|
-
components[0] !== "
|
|
1097
|
+
components[0] !== "HasnaRecordings.app" ||
|
|
1077
1098
|
components.some((component) => !component || component === "." || component === "..")
|
|
1078
1099
|
) {
|
|
1079
|
-
throw new Error("release ZIP contains an entry outside the canonical
|
|
1100
|
+
throw new Error("release ZIP contains an entry outside the canonical HasnaRecordings.app tree");
|
|
1080
1101
|
}
|
|
1081
1102
|
const collisionKey = canonicalZipCollisionKey(logicalPath);
|
|
1082
1103
|
if (logicalPaths.has(collisionKey)) {
|
|
1083
1104
|
throw new Error("release ZIP contains duplicate, file/directory, case-fold, or Unicode-colliding entries");
|
|
1084
1105
|
}
|
|
1085
1106
|
logicalPaths.set(collisionKey, { name: logicalPath, isDirectory: entry.isDirectory });
|
|
1086
|
-
if (logicalPath === "
|
|
1087
|
-
if (!entry.isDirectory) throw new Error("release ZIP
|
|
1107
|
+
if (logicalPath === "HasnaRecordings.app") {
|
|
1108
|
+
if (!entry.isDirectory) throw new Error("release ZIP HasnaRecordings.app root is not a directory entry");
|
|
1088
1109
|
rootDirectoryCount += 1;
|
|
1089
1110
|
}
|
|
1090
1111
|
}
|
|
1091
1112
|
if (rootDirectoryCount !== 1) {
|
|
1092
|
-
throw new Error("release ZIP must contain exactly one canonical
|
|
1113
|
+
throw new Error("release ZIP must contain exactly one canonical HasnaRecordings.app root entry");
|
|
1093
1114
|
}
|
|
1094
1115
|
for (const { name } of logicalPaths.values()) {
|
|
1095
1116
|
const components = name.split("/");
|
|
@@ -1405,6 +1426,7 @@ export function withPrivatelyExtractedArchiveApp<T>(
|
|
|
1405
1426
|
operation: (appPath: string) => T,
|
|
1406
1427
|
platformArchiveTool = "/usr/bin/ditto",
|
|
1407
1428
|
expectedArchiveSha256?: string,
|
|
1429
|
+
expectedBundleName = "HasnaRecordings.app",
|
|
1408
1430
|
): T {
|
|
1409
1431
|
const privateRoot = mkdtempSync(join(tmpdir(), "recordings-artifact-extract-"));
|
|
1410
1432
|
chmodSync(privateRoot, 0o700);
|
|
@@ -1449,13 +1471,17 @@ export function withPrivatelyExtractedArchiveApp<T>(
|
|
|
1449
1471
|
inspectZipArchive(pinnedArchivePath);
|
|
1450
1472
|
run(platformArchiveTool, ["-x", "-k", pinnedArchivePath, extractionRoot]);
|
|
1451
1473
|
const rootEntries = readdirSync(extractionRoot);
|
|
1452
|
-
if (rootEntries.length !== 1 || rootEntries[0] !==
|
|
1453
|
-
throw new Error(
|
|
1474
|
+
if (rootEntries.length !== 1 || rootEntries[0] !== expectedBundleName) {
|
|
1475
|
+
throw new Error(
|
|
1476
|
+
`release archive must contain exactly one top-level ${expectedBundleName}`,
|
|
1477
|
+
);
|
|
1454
1478
|
}
|
|
1455
|
-
const extractedAppPath = join(extractionRoot,
|
|
1479
|
+
const extractedAppPath = join(extractionRoot, expectedBundleName);
|
|
1456
1480
|
const appDetails = lstatSync(extractedAppPath);
|
|
1457
1481
|
if (appDetails.isSymbolicLink() || !appDetails.isDirectory()) {
|
|
1458
|
-
throw new Error(
|
|
1482
|
+
throw new Error(
|
|
1483
|
+
`release archive top-level ${expectedBundleName} must be a regular directory`,
|
|
1484
|
+
);
|
|
1459
1485
|
}
|
|
1460
1486
|
assertRegularArchiveTree(extractedAppPath);
|
|
1461
1487
|
return operation(extractedAppPath);
|
|
@@ -1478,6 +1504,7 @@ export function extractVerifiedArchiveToStaging(
|
|
|
1478
1504
|
expectedApprovedTarget: string,
|
|
1479
1505
|
expectedApprovedTargetIdentitySha256: string,
|
|
1480
1506
|
expectedApprovedTargetIdentityKind: OperatorTargetIdentityKind,
|
|
1507
|
+
expectedVariant: ArtifactVariant = "full",
|
|
1481
1508
|
platformArchiveTool = "/usr/bin/ditto",
|
|
1482
1509
|
): void {
|
|
1483
1510
|
if (resolve(stagingTarget) !== stagingTarget) {
|
|
@@ -1510,16 +1537,19 @@ export function extractVerifiedArchiveToStaging(
|
|
|
1510
1537
|
withPrivatelyExtractedArchiveApp(
|
|
1511
1538
|
archivePath,
|
|
1512
1539
|
(appPath) => {
|
|
1513
|
-
const targetAppPath = join(stagingTarget,
|
|
1540
|
+
const targetAppPath = join(stagingTarget, manifestBundleName(manifest));
|
|
1514
1541
|
renameSync(appPath, targetAppPath);
|
|
1515
1542
|
assertRegularArchiveTree(targetAppPath);
|
|
1516
1543
|
const entries = readdirSync(stagingTarget);
|
|
1517
|
-
if (entries.length !== 1 || entries[0] !==
|
|
1518
|
-
throw new Error(
|
|
1544
|
+
if (entries.length !== 1 || entries[0] !== manifestBundleName(manifest)) {
|
|
1545
|
+
throw new Error(
|
|
1546
|
+
`archive extraction did not produce exactly ${manifestBundleName(manifest)} in staging`,
|
|
1547
|
+
);
|
|
1519
1548
|
}
|
|
1520
1549
|
},
|
|
1521
1550
|
platformArchiveTool,
|
|
1522
1551
|
manifest.archive.sha256,
|
|
1552
|
+
manifestBundleName(manifest),
|
|
1523
1553
|
);
|
|
1524
1554
|
}
|
|
1525
1555
|
|
|
@@ -1547,6 +1577,20 @@ export function assertManifestShape(manifest: MacOSArtifactManifest): void {
|
|
|
1547
1577
|
}
|
|
1548
1578
|
if (manifest.artifact_type !== "recordings-macos-app") throw new Error("unexpected artifact type");
|
|
1549
1579
|
if (manifest.bundle_id !== BUNDLE_ID) throw new Error("unexpected bundle identifier");
|
|
1580
|
+
if (
|
|
1581
|
+
manifest.bundle_name !== undefined &&
|
|
1582
|
+
(typeof manifest.bundle_name !== "string" ||
|
|
1583
|
+
!/^[A-Za-z0-9][A-Za-z0-9 .-]*\.app$/.test(manifest.bundle_name))
|
|
1584
|
+
) {
|
|
1585
|
+
throw new Error("manifest bundle name must be a *.app bundle directory name");
|
|
1586
|
+
}
|
|
1587
|
+
if (
|
|
1588
|
+
manifest.variant !== undefined &&
|
|
1589
|
+
manifest.variant !== "full" &&
|
|
1590
|
+
manifest.variant !== "bar"
|
|
1591
|
+
) {
|
|
1592
|
+
throw new Error(`manifest variant is unsupported: ${manifest.variant}`);
|
|
1593
|
+
}
|
|
1550
1594
|
const artifactPolicy = manifestPolicy(manifest);
|
|
1551
1595
|
for (const [label, value] of [
|
|
1552
1596
|
["bundle version", manifest.bundle_version],
|
|
@@ -1707,8 +1751,8 @@ export function assertManifestShape(manifest: MacOSArtifactManifest): void {
|
|
|
1707
1751
|
manifest.container?.type !== "zip" ||
|
|
1708
1752
|
JSON.stringify(manifest.container.install_locations) !== JSON.stringify(
|
|
1709
1753
|
artifactPolicy === "release"
|
|
1710
|
-
? ["/Applications/
|
|
1711
|
-
: ["~/Applications/
|
|
1754
|
+
? ["/Applications/HasnaRecordings.app"]
|
|
1755
|
+
: ["~/Applications/HasnaRecordings.app"],
|
|
1712
1756
|
)
|
|
1713
1757
|
) {
|
|
1714
1758
|
throw new Error("manifest has an unexpected container install policy");
|
|
@@ -1755,6 +1799,7 @@ export function verifyArchiveManifest(
|
|
|
1755
1799
|
expectedApprovedTarget: string = RELEASE_APPROVED_TARGET,
|
|
1756
1800
|
expectedApprovedTargetIdentitySha256: string = "none",
|
|
1757
1801
|
expectedApprovedTargetIdentityKind: OperatorTargetIdentityKind = LEGACY_LOCAL_TARGET_IDENTITY_KIND,
|
|
1802
|
+
expectedVariant: ArtifactVariant = "full",
|
|
1758
1803
|
): MacOSArtifactManifest {
|
|
1759
1804
|
if (!expectedTeamId) throw new Error("expected Team ID is required");
|
|
1760
1805
|
const manifest = readAuthenticatedManifest<MacOSArtifactManifest>(
|
|
@@ -1765,6 +1810,11 @@ export function verifyArchiveManifest(
|
|
|
1765
1810
|
if (manifestPolicy(manifest) !== expectedPolicy) {
|
|
1766
1811
|
throw new Error("manifest artifact policy does not match the explicit operator selection");
|
|
1767
1812
|
}
|
|
1813
|
+
if (manifestVariant(manifest) !== expectedVariant) {
|
|
1814
|
+
throw new Error(
|
|
1815
|
+
`manifest variant ${manifestVariant(manifest)} does not match the explicit operator selection ${expectedVariant}`,
|
|
1816
|
+
);
|
|
1817
|
+
}
|
|
1768
1818
|
const actualApprovedTarget = manifest.approved_target ?? RELEASE_APPROVED_TARGET;
|
|
1769
1819
|
if (actualApprovedTarget !== expectedApprovedTarget) {
|
|
1770
1820
|
throw new Error("manifest approved target does not match the exact operator-approved target");
|
|
@@ -1817,6 +1867,7 @@ function assertProvenanceMatchesManifest(
|
|
|
1817
1867
|
"builder_identity_kind",
|
|
1818
1868
|
"builder_identity_sha256",
|
|
1819
1869
|
"non_notarized",
|
|
1870
|
+
"variant",
|
|
1820
1871
|
"bundle_id",
|
|
1821
1872
|
"bundle_version",
|
|
1822
1873
|
"git_sha",
|
|
@@ -1989,11 +2040,17 @@ export function verifyExtractedApp(
|
|
|
1989
2040
|
expectedApprovedTarget: string = RELEASE_APPROVED_TARGET,
|
|
1990
2041
|
expectedApprovedTargetIdentitySha256: string = "none",
|
|
1991
2042
|
expectedApprovedTargetIdentityKind: OperatorTargetIdentityKind = LEGACY_LOCAL_TARGET_IDENTITY_KIND,
|
|
2043
|
+
expectedVariant: ArtifactVariant = "full",
|
|
1992
2044
|
): MacOSArtifactManifest {
|
|
1993
2045
|
const manifest = readAuthenticatedManifest<MacOSArtifactManifest>(
|
|
1994
2046
|
manifestPath,
|
|
1995
2047
|
expectedManifestSha256,
|
|
1996
2048
|
);
|
|
2049
|
+
if (manifestVariant(manifest) !== expectedVariant) {
|
|
2050
|
+
throw new Error(
|
|
2051
|
+
`manifest variant ${manifestVariant(manifest)} does not match the explicit operator selection ${expectedVariant}`,
|
|
2052
|
+
);
|
|
2053
|
+
}
|
|
1997
2054
|
verifyAppAgainstManifest(
|
|
1998
2055
|
appPath,
|
|
1999
2056
|
manifest,
|
|
@@ -2063,6 +2120,7 @@ function verifySuppliedAndArchivedApps(
|
|
|
2063
2120
|
},
|
|
2064
2121
|
"/usr/bin/ditto",
|
|
2065
2122
|
manifest.archive.sha256,
|
|
2123
|
+
manifestBundleName(manifest),
|
|
2066
2124
|
);
|
|
2067
2125
|
if (sha256ArchiveFile(archivePath) !== manifest.archive.sha256) {
|
|
2068
2126
|
throw new Error("release archive changed during exact-byte verification");
|
|
@@ -2080,6 +2138,7 @@ function writeProvenance(
|
|
|
2080
2138
|
approvedTargetIdentitySha256: string,
|
|
2081
2139
|
builderIdentityKind: OperatorTargetIdentityKind,
|
|
2082
2140
|
builderIdentitySha256: string,
|
|
2141
|
+
variant: ArtifactVariant,
|
|
2083
2142
|
): void {
|
|
2084
2143
|
const executablePath = join(appPath, "Contents", "MacOS", "Recordings");
|
|
2085
2144
|
const helperPath = join(appPath, "Contents", "Helpers", "recordings");
|
|
@@ -2089,6 +2148,7 @@ function writeProvenance(
|
|
|
2089
2148
|
artifactPolicy === "release"
|
|
2090
2149
|
? RELEASE_ARTIFACT_SCHEMA_VERSION
|
|
2091
2150
|
: LOCAL_ARTIFACT_SCHEMA_VERSION,
|
|
2151
|
+
variant,
|
|
2092
2152
|
bundle_id: plistValue(appPath, "CFBundleIdentifier"),
|
|
2093
2153
|
bundle_version: plistValue(appPath, "CFBundleShortVersionString"),
|
|
2094
2154
|
bundle_build_version: plistValue(appPath, "CFBundleVersion"),
|
|
@@ -2151,6 +2211,7 @@ function finalizeArtifact(
|
|
|
2151
2211
|
notaryLogPath: string,
|
|
2152
2212
|
notarySubmissionId: string,
|
|
2153
2213
|
submittedArchiveSha256: string,
|
|
2214
|
+
expectedVariant: ArtifactVariant = "full",
|
|
2154
2215
|
): void {
|
|
2155
2216
|
assertCurrentSourceRevision(packageRoot, expectedSourceSha);
|
|
2156
2217
|
const executablePath = join(appPath, "Contents", "MacOS", "Recordings");
|
|
@@ -2160,6 +2221,14 @@ function finalizeArtifact(
|
|
|
2160
2221
|
if (provenance.git_sha !== expectedSourceSha) {
|
|
2161
2222
|
throw new Error("embedded provenance does not match the pinned source SHA");
|
|
2162
2223
|
}
|
|
2224
|
+
if (expectedVariant !== "full" && expectedVariant !== "bar") {
|
|
2225
|
+
throw new Error(`finalize variant is unsupported: ${expectedVariant}`);
|
|
2226
|
+
}
|
|
2227
|
+
if ((provenance.variant ?? "full") !== expectedVariant) {
|
|
2228
|
+
throw new Error(
|
|
2229
|
+
`finalize variant ${expectedVariant} does not match the embedded provenance variant`,
|
|
2230
|
+
);
|
|
2231
|
+
}
|
|
2163
2232
|
assertExpectedCodeLayout(appPath);
|
|
2164
2233
|
const outerSigning = signingEvidence(appPath, expectedTeamId, APP_ENTITLEMENTS, executablePath);
|
|
2165
2234
|
const helperSigning = signingEvidence(helperPath, expectedTeamId, HELPER_ENTITLEMENTS);
|
|
@@ -2172,9 +2241,14 @@ function finalizeArtifact(
|
|
|
2172
2241
|
const notaryLog = JSON.parse(notaryLogSnapshot.toString("utf8")) as unknown;
|
|
2173
2242
|
assertAcceptedNotaryLog(notaryLog, notarySubmissionId, submittedArchiveSha256);
|
|
2174
2243
|
const items = nestedItems(appPath, expectedTeamId, "release", outerSigning, helperSigning);
|
|
2244
|
+
const bundleName = basename(appPath);
|
|
2245
|
+
if (!bundleName.endsWith(".app")) {
|
|
2246
|
+
throw new Error(`release app bundle must be named *.app: ${appPath}`);
|
|
2247
|
+
}
|
|
2175
2248
|
const manifest: MacOSArtifactManifest = {
|
|
2176
2249
|
...provenance,
|
|
2177
2250
|
artifact_type: "recordings-macos-app",
|
|
2251
|
+
bundle_name: bundleName,
|
|
2178
2252
|
app_sha256: sha256File(executablePath),
|
|
2179
2253
|
binding: { bundle_tree_sha256: treeDigest(appPath) },
|
|
2180
2254
|
provenance_sha256: sha256File(embeddedPath),
|
|
@@ -2201,7 +2275,7 @@ function finalizeArtifact(
|
|
|
2201
2275
|
},
|
|
2202
2276
|
container: {
|
|
2203
2277
|
type: "zip",
|
|
2204
|
-
install_locations: [
|
|
2278
|
+
install_locations: [`/Applications/${bundleName}`],
|
|
2205
2279
|
},
|
|
2206
2280
|
nested_code_policy: {
|
|
2207
2281
|
allowlist_sha256: nestedPolicyDigest(items),
|
|
@@ -2278,11 +2352,15 @@ function finalizeLocalArtifact(
|
|
|
2278
2352
|
approvedTargetIdentityKind: TargetIdentityKind,
|
|
2279
2353
|
approvedTargetIdentitySha256: string,
|
|
2280
2354
|
expectedTeamId: string = "ADHOC",
|
|
2355
|
+
expectedVariant: ArtifactVariant = "full",
|
|
2281
2356
|
): void {
|
|
2282
2357
|
assertCurrentSourceRevision(packageRoot, expectedSourceSha);
|
|
2283
2358
|
if (approvedTargetIdentityKind !== "tailscale_node_id_sha256") {
|
|
2284
2359
|
throw new Error("new local-only artifacts require a Tailscale node ID identity hash");
|
|
2285
2360
|
}
|
|
2361
|
+
if (expectedVariant !== "full" && expectedVariant !== "bar") {
|
|
2362
|
+
throw new Error(`finalize variant is unsupported: ${expectedVariant}`);
|
|
2363
|
+
}
|
|
2286
2364
|
const localSigningMode = localOnlySigningMode(expectedTeamId);
|
|
2287
2365
|
const executablePath = join(appPath, "Contents", "MacOS", "Recordings");
|
|
2288
2366
|
const helperPath = join(appPath, "Contents", "Helpers", "recordings");
|
|
@@ -2300,6 +2378,11 @@ function finalizeLocalArtifact(
|
|
|
2300
2378
|
) {
|
|
2301
2379
|
throw new Error("embedded provenance is not approved for this local-only target");
|
|
2302
2380
|
}
|
|
2381
|
+
if ((provenance.variant ?? "full") !== expectedVariant) {
|
|
2382
|
+
throw new Error(
|
|
2383
|
+
`finalize variant ${expectedVariant} does not match the embedded provenance variant`,
|
|
2384
|
+
);
|
|
2385
|
+
}
|
|
2303
2386
|
assertExpectedCodeLayout(appPath);
|
|
2304
2387
|
const outerSigning = signingEvidence(
|
|
2305
2388
|
appPath,
|
|
@@ -2316,9 +2399,14 @@ function finalizeLocalArtifact(
|
|
|
2316
2399
|
"local_only",
|
|
2317
2400
|
);
|
|
2318
2401
|
const items = nestedItems(appPath, expectedTeamId, "local_only", outerSigning, helperSigning);
|
|
2402
|
+
const bundleName = basename(appPath);
|
|
2403
|
+
if (!bundleName.endsWith(".app")) {
|
|
2404
|
+
throw new Error(`local app bundle must be named *.app: ${appPath}`);
|
|
2405
|
+
}
|
|
2319
2406
|
const manifest: MacOSArtifactManifest = {
|
|
2320
2407
|
...provenance,
|
|
2321
2408
|
artifact_type: "recordings-macos-app",
|
|
2409
|
+
bundle_name: bundleName,
|
|
2322
2410
|
app_sha256: sha256File(executablePath),
|
|
2323
2411
|
binding: { bundle_tree_sha256: treeDigest(appPath) },
|
|
2324
2412
|
provenance_sha256: sha256File(embeddedPath),
|
|
@@ -2346,7 +2434,7 @@ function finalizeLocalArtifact(
|
|
|
2346
2434
|
},
|
|
2347
2435
|
container: {
|
|
2348
2436
|
type: "zip",
|
|
2349
|
-
install_locations: [
|
|
2437
|
+
install_locations: [`~/Applications/${bundleName}`],
|
|
2350
2438
|
},
|
|
2351
2439
|
nested_code_policy: {
|
|
2352
2440
|
allowlist_sha256: nestedPolicyDigest(items),
|
|
@@ -2428,7 +2516,7 @@ export function assertVersionTransition(
|
|
|
2428
2516
|
assertManifestShape(manifest);
|
|
2429
2517
|
if (compareVersions(manifest.bundle_version, installedVersion) < 0) {
|
|
2430
2518
|
throw new Error(
|
|
2431
|
-
`refusing to downgrade
|
|
2519
|
+
`refusing to downgrade HasnaRecordings.app from ${installedVersion} to ${manifest.bundle_version}`,
|
|
2432
2520
|
);
|
|
2433
2521
|
}
|
|
2434
2522
|
if (compareVersions(manifest.bundle_version, installedVersion) === 0) {
|
|
@@ -2516,6 +2604,7 @@ function verifyActiveApp(
|
|
|
2516
2604
|
expectedApprovedTarget: string,
|
|
2517
2605
|
expectedApprovedTargetIdentitySha256: string,
|
|
2518
2606
|
expectedApprovedTargetIdentityKind: OperatorTargetIdentityKind,
|
|
2607
|
+
expectedVariant: ArtifactVariant = "full",
|
|
2519
2608
|
): void {
|
|
2520
2609
|
const manifest = verifyExtractedApp(
|
|
2521
2610
|
appPath,
|
|
@@ -2526,6 +2615,7 @@ function verifyActiveApp(
|
|
|
2526
2615
|
expectedApprovedTarget,
|
|
2527
2616
|
expectedApprovedTargetIdentitySha256,
|
|
2528
2617
|
expectedApprovedTargetIdentityKind,
|
|
2618
|
+
expectedVariant,
|
|
2529
2619
|
);
|
|
2530
2620
|
const helperPath = join(appPath, "Contents", "Helpers", "recordings");
|
|
2531
2621
|
if (companionVersion(helperPath) !== manifest.companion.version) {
|
|
@@ -2536,8 +2626,6 @@ function verifyActiveApp(
|
|
|
2536
2626
|
const environment: NodeJS.ProcessEnv = {
|
|
2537
2627
|
HOME: contractHome,
|
|
2538
2628
|
PATH: "/usr/bin:/bin:/usr/sbin:/sbin",
|
|
2539
|
-
HASNA_RECORDINGS_CLIENT_STORE: "sqlite",
|
|
2540
|
-
RECORDINGS_CLIENT_STORE: "sqlite",
|
|
2541
2629
|
HASNA_RECORDINGS_DB_PATH: join(contractHome, "recordings.db"),
|
|
2542
2630
|
RECORDINGS_AUDIO_DIR: join(contractHome, "audio"),
|
|
2543
2631
|
};
|
|
@@ -2893,7 +2981,7 @@ function readJournal(path: string): InstallJournal {
|
|
|
2893
2981
|
if (resolve(path) !== resolve(join(expectedParent, ".Recordings-install-transaction.json"))) {
|
|
2894
2982
|
throw new Error("install transaction journal is outside the expected app parent");
|
|
2895
2983
|
}
|
|
2896
|
-
if (resolve(journal.app_destination) !== resolve(join(expectedParent, "
|
|
2984
|
+
if (resolve(journal.app_destination) !== resolve(join(expectedParent, "HasnaRecordings.app"))) {
|
|
2897
2985
|
throw new Error("install transaction journal has an unexpected app destination");
|
|
2898
2986
|
}
|
|
2899
2987
|
const expectedDataDir = resolve(join(dirname(expectedParent), ".hasna", "recordings"));
|
|
@@ -2911,7 +2999,9 @@ function readJournal(path: string): InstallJournal {
|
|
|
2911
2999
|
return (
|
|
2912
3000
|
resolved === resolve(journal.app_destination) ||
|
|
2913
3001
|
resolved === resolve(join(journal.data_dir, "Recordings.app")) ||
|
|
2914
|
-
resolved
|
|
3002
|
+
resolved === resolve(join(journal.data_dir, "HasnaRecordings.app")) ||
|
|
3003
|
+
resolved.startsWith(`${resolve(journal.app_parent)}/Recordings.app.`) ||
|
|
3004
|
+
resolved.startsWith(`${resolve(journal.app_parent)}/HasnaRecordings.app.`)
|
|
2915
3005
|
);
|
|
2916
3006
|
};
|
|
2917
3007
|
if (!journal.originals.every((entry) => allowedOriginal(entry.path))) {
|
|
@@ -3849,7 +3939,7 @@ function openRecoveryCapabilities(
|
|
|
3849
3939
|
const homePath = resolve(dirname(appParent));
|
|
3850
3940
|
if (
|
|
3851
3941
|
basename(appParent) !== "Applications" ||
|
|
3852
|
-
resolve(journal.app_destination) !== resolve(join(appParent, "
|
|
3942
|
+
resolve(journal.app_destination) !== resolve(join(appParent, "HasnaRecordings.app")) ||
|
|
3853
3943
|
resolve(journal.data_dir) !== resolve(join(homePath, ".hasna", "recordings")) ||
|
|
3854
3944
|
dirname(resolve(journal.transaction_dir)) !== appParent
|
|
3855
3945
|
) {
|
|
@@ -4252,9 +4342,10 @@ export function publishInstallCandidate(
|
|
|
4252
4342
|
const destination = resolve(destinationPath);
|
|
4253
4343
|
const stagingParentPath = dirname(staging);
|
|
4254
4344
|
const stagingParentLeaf = basename(stagingParentPath);
|
|
4345
|
+
const bundleLeaf = basename(journal.app_destination);
|
|
4255
4346
|
if (
|
|
4256
4347
|
destination !== resolve(journal.app_destination) ||
|
|
4257
|
-
basename(staging) !==
|
|
4348
|
+
basename(staging) !== bundleLeaf ||
|
|
4258
4349
|
dirname(stagingParentPath) !== resolve(journal.app_parent) ||
|
|
4259
4350
|
!/^\.Recordings-install\.[A-Za-z0-9]+$/.test(stagingParentLeaf)
|
|
4260
4351
|
) {
|
|
@@ -4277,12 +4368,12 @@ export function publishInstallCandidate(
|
|
|
4277
4368
|
stagingParent,
|
|
4278
4369
|
"candidate staging parent",
|
|
4279
4370
|
);
|
|
4280
|
-
retained = guard.openDirAt(stagingParent,
|
|
4281
|
-
assertNativeBinding(guard, stagingParent,
|
|
4371
|
+
retained = guard.openDirAt(stagingParent, bundleLeaf);
|
|
4372
|
+
assertNativeBinding(guard, stagingParent, bundleLeaf, retained, "staged candidate");
|
|
4282
4373
|
if (nativeTreeDigest(guard, retained) !== expectedDigest) {
|
|
4283
4374
|
throw new Error("staged candidate does not match durable journal evidence");
|
|
4284
4375
|
}
|
|
4285
|
-
if (guard.statAt(capabilities.applications,
|
|
4376
|
+
if (guard.statAt(capabilities.applications, bundleLeaf) !== null) {
|
|
4286
4377
|
throw new Error("candidate destination already exists");
|
|
4287
4378
|
}
|
|
4288
4379
|
fsyncRetainedTree(guard, retained, uid, "staged candidate");
|
|
@@ -4294,8 +4385,8 @@ export function publishInstallCandidate(
|
|
|
4294
4385
|
stagingParent,
|
|
4295
4386
|
"candidate staging parent",
|
|
4296
4387
|
);
|
|
4297
|
-
assertNativeBinding(guard, stagingParent,
|
|
4298
|
-
if (guard.statAt(capabilities.applications,
|
|
4388
|
+
assertNativeBinding(guard, stagingParent, bundleLeaf, retained, "staged candidate");
|
|
4389
|
+
if (guard.statAt(capabilities.applications, bundleLeaf) !== null) {
|
|
4299
4390
|
throw new Error("candidate destination appeared before publication");
|
|
4300
4391
|
}
|
|
4301
4392
|
installTransitionTestPoint(
|
|
@@ -4305,10 +4396,10 @@ export function publishInstallCandidate(
|
|
|
4305
4396
|
);
|
|
4306
4397
|
guard.renameHandleNoReplaceAt(
|
|
4307
4398
|
stagingParent,
|
|
4308
|
-
|
|
4399
|
+
bundleLeaf,
|
|
4309
4400
|
retained,
|
|
4310
4401
|
capabilities.applications,
|
|
4311
|
-
|
|
4402
|
+
bundleLeaf,
|
|
4312
4403
|
);
|
|
4313
4404
|
installTransitionTestPoint(
|
|
4314
4405
|
"publish-candidate",
|
|
@@ -4319,11 +4410,11 @@ export function publishInstallCandidate(
|
|
|
4319
4410
|
assertNativeBinding(
|
|
4320
4411
|
guard,
|
|
4321
4412
|
capabilities.applications,
|
|
4322
|
-
|
|
4413
|
+
bundleLeaf,
|
|
4323
4414
|
retained,
|
|
4324
4415
|
"published candidate",
|
|
4325
4416
|
);
|
|
4326
|
-
if (guard.statAt(stagingParent,
|
|
4417
|
+
if (guard.statAt(stagingParent, bundleLeaf) !== null) {
|
|
4327
4418
|
throw new Error("candidate staging leaf was recreated during publication");
|
|
4328
4419
|
}
|
|
4329
4420
|
guard.fsyncHandle(capabilities.applications);
|
|
@@ -4369,7 +4460,8 @@ function cleanupInstallCandidateStaging(
|
|
|
4369
4460
|
staging,
|
|
4370
4461
|
"candidate staging root",
|
|
4371
4462
|
);
|
|
4372
|
-
const
|
|
4463
|
+
const bundleLeaf = basename(journal.app_destination);
|
|
4464
|
+
const candidateMetadata = guard.statAt(staging, bundleLeaf);
|
|
4373
4465
|
if (candidateMetadata !== null) {
|
|
4374
4466
|
if (journal.schema_version !== 9 || !journal.candidate_tree_sha256) {
|
|
4375
4467
|
throw new Error("legacy journal cannot authenticate candidate staging cleanup");
|
|
@@ -4377,7 +4469,7 @@ function cleanupInstallCandidateStaging(
|
|
|
4377
4469
|
const candidate = openProvenDirectoryAt(
|
|
4378
4470
|
guard,
|
|
4379
4471
|
staging,
|
|
4380
|
-
|
|
4472
|
+
bundleLeaf,
|
|
4381
4473
|
journal.candidate_tree_sha256,
|
|
4382
4474
|
"staged candidate",
|
|
4383
4475
|
);
|
|
@@ -4385,7 +4477,7 @@ function cleanupInstallCandidateStaging(
|
|
|
4385
4477
|
quarantineRemoveRetainedAt(
|
|
4386
4478
|
guard,
|
|
4387
4479
|
staging,
|
|
4388
|
-
|
|
4480
|
+
bundleLeaf,
|
|
4389
4481
|
candidate,
|
|
4390
4482
|
"staged candidate",
|
|
4391
4483
|
);
|
|
@@ -4477,14 +4569,20 @@ function originalDestinationCapability(
|
|
|
4477
4569
|
): { parent: NativeHandle; leaf: string } {
|
|
4478
4570
|
const resolved = resolve(path);
|
|
4479
4571
|
if (resolved === resolve(journal.app_destination)) {
|
|
4480
|
-
return { parent: capabilities.applications, leaf:
|
|
4572
|
+
return { parent: capabilities.applications, leaf: basename(journal.app_destination) };
|
|
4481
4573
|
}
|
|
4482
|
-
if (
|
|
4483
|
-
|
|
4574
|
+
if (
|
|
4575
|
+
dirname(resolved) === resolve(journal.data_dir) &&
|
|
4576
|
+
(basename(resolved) === "Recordings.app" || basename(resolved) === "HasnaRecordings.app")
|
|
4577
|
+
) {
|
|
4578
|
+
// Install site inside the state data directory: the legacy pre-rename
|
|
4579
|
+
// Recordings.app and the renamed HasnaRecordings.app are both recognized and
|
|
4580
|
+
// preserved, never treated as the canonical Applications destination.
|
|
4581
|
+
return { parent: capabilities.data, leaf: basename(resolved) };
|
|
4484
4582
|
}
|
|
4485
4583
|
if (
|
|
4486
4584
|
dirname(resolved) === resolve(journal.app_parent) &&
|
|
4487
|
-
/^Recordings\.app\.[A-Za-z0-9._-]+$/.test(basename(resolved))
|
|
4585
|
+
/^(?:Hasna)?Recordings\.app\.[A-Za-z0-9._-]+$/.test(basename(resolved))
|
|
4488
4586
|
) {
|
|
4489
4587
|
return { parent: capabilities.applications, leaf: basename(resolved) };
|
|
4490
4588
|
}
|
|
@@ -4915,24 +5013,25 @@ function recoverJournal(path: string): void {
|
|
|
4915
5013
|
const canonicalOriginal = journal.originals.find(
|
|
4916
5014
|
(entry) => resolve(entry.path) === resolve(journal.app_destination),
|
|
4917
5015
|
);
|
|
5016
|
+
const destinationLeaf = basename(journal.app_destination);
|
|
4918
5017
|
const canonicalDestinationDigest = canonicalOriginal
|
|
4919
|
-
? nativeTreeDigestAt(guard, applications,
|
|
5018
|
+
? nativeTreeDigestAt(guard, applications, destinationLeaf)
|
|
4920
5019
|
: null;
|
|
4921
5020
|
const canonicalAlreadyRestored = canonicalOriginal !== undefined &&
|
|
4922
5021
|
canonicalDestinationDigest === canonicalOriginal.sha256;
|
|
4923
5022
|
if (
|
|
4924
5023
|
["candidate-moving", "candidate-installed", "activated", "launching"].includes(journal.phase) &&
|
|
4925
|
-
!canonicalAlreadyRestored && guard.statAt(applications,
|
|
5024
|
+
!canonicalAlreadyRestored && guard.statAt(applications, destinationLeaf) !== null
|
|
4926
5025
|
) {
|
|
4927
5026
|
if (journal.schema_version !== 9 || !journal.candidate_tree_sha256) {
|
|
4928
5027
|
throw new Error("legacy recovery journal cannot prove the uncommitted candidate tree");
|
|
4929
5028
|
}
|
|
4930
|
-
recoveryTestBarrier("before-candidate-remove",
|
|
5029
|
+
recoveryTestBarrier("before-candidate-remove", destinationLeaf);
|
|
4931
5030
|
assertNativeBinding(guard, capabilities.home, "Applications", applications, "Applications");
|
|
4932
5031
|
const candidate = openProvenDirectoryAt(
|
|
4933
5032
|
guard,
|
|
4934
5033
|
applications,
|
|
4935
|
-
|
|
5034
|
+
destinationLeaf,
|
|
4936
5035
|
journal.candidate_tree_sha256,
|
|
4937
5036
|
"uncommitted candidate",
|
|
4938
5037
|
);
|
|
@@ -4940,7 +5039,7 @@ function recoverJournal(path: string): void {
|
|
|
4940
5039
|
quarantineRemoveRetainedAt(
|
|
4941
5040
|
guard,
|
|
4942
5041
|
applications,
|
|
4943
|
-
|
|
5042
|
+
destinationLeaf,
|
|
4944
5043
|
candidate,
|
|
4945
5044
|
"uncommitted candidate",
|
|
4946
5045
|
);
|
|
@@ -5188,6 +5287,9 @@ function manifestGet(path: string, expectedManifestSha256: string, field: string
|
|
|
5188
5287
|
}
|
|
5189
5288
|
else if (field === "approved_target_identity_sha256") console.log(manifest.approved_target_identity_sha256);
|
|
5190
5289
|
else if (field === "builder_identity_kind") console.log(manifestBuilderIdentityKind(manifest));
|
|
5290
|
+
else if (field === "variant") console.log(manifestVariant(manifest));
|
|
5291
|
+
// Pre-variant manifests carry no bundle_name; they are full builds by definition.
|
|
5292
|
+
else if (field === "bundle_name") console.log(manifestBundleName(manifest));
|
|
5191
5293
|
else throw new Error(`unsupported manifest field: ${field}`);
|
|
5192
5294
|
}
|
|
5193
5295
|
|
|
@@ -5214,6 +5316,14 @@ function repeatedArguments(name: string): string[] {
|
|
|
5214
5316
|
return values;
|
|
5215
5317
|
}
|
|
5216
5318
|
|
|
5319
|
+
function variantArgument(defaultVariant: ArtifactVariant): ArtifactVariant {
|
|
5320
|
+
const value = optionalArgument("--variant") ?? defaultVariant;
|
|
5321
|
+
if (value !== "full" && value !== "bar") {
|
|
5322
|
+
throw new Error(`artifact variant must be full or bar; got: ${value}`);
|
|
5323
|
+
}
|
|
5324
|
+
return value;
|
|
5325
|
+
}
|
|
5326
|
+
|
|
5217
5327
|
function artifactPolicyArgument(): ArtifactPolicy {
|
|
5218
5328
|
const value = argument("--artifact-policy");
|
|
5219
5329
|
if (value !== "release" && value !== "local_only") {
|
|
@@ -5276,6 +5386,7 @@ function main(): void {
|
|
|
5276
5386
|
argument("--approved-target-identity-sha256"),
|
|
5277
5387
|
builderIdentityKindArgument(policy),
|
|
5278
5388
|
argument("--builder-identity-sha256"),
|
|
5389
|
+
variantArgument("full"),
|
|
5279
5390
|
);
|
|
5280
5391
|
} else if (command === "finalize") {
|
|
5281
5392
|
const sourceSha = argument("--source-sha");
|
|
@@ -5290,6 +5401,7 @@ function main(): void {
|
|
|
5290
5401
|
argument("--notary-log"),
|
|
5291
5402
|
argument("--notary-submission-id"),
|
|
5292
5403
|
argument("--submitted-archive-sha256"),
|
|
5404
|
+
variantArgument("full"),
|
|
5293
5405
|
);
|
|
5294
5406
|
} else if (command === "finalize-local") {
|
|
5295
5407
|
const sourceSha = argument("--source-sha");
|
|
@@ -5303,6 +5415,7 @@ function main(): void {
|
|
|
5303
5415
|
targetIdentityKindArgument(undefined, true),
|
|
5304
5416
|
argument("--approved-target-identity-sha256"),
|
|
5305
5417
|
optionalArgument("--expected-team-id") ?? "ADHOC",
|
|
5418
|
+
variantArgument("full"),
|
|
5306
5419
|
);
|
|
5307
5420
|
} else if (command === "verify-archive") {
|
|
5308
5421
|
const teamId = argument("--team-id");
|
|
@@ -5318,6 +5431,7 @@ function main(): void {
|
|
|
5318
5431
|
argument("--approved-target"),
|
|
5319
5432
|
argument("--approved-target-identity-sha256"),
|
|
5320
5433
|
targetIdentityKindArgument(policy),
|
|
5434
|
+
variantArgument("full"),
|
|
5321
5435
|
);
|
|
5322
5436
|
} else if (command === "extract-verified-archive") {
|
|
5323
5437
|
const teamId = argument("--team-id");
|
|
@@ -5334,6 +5448,7 @@ function main(): void {
|
|
|
5334
5448
|
argument("--approved-target"),
|
|
5335
5449
|
argument("--approved-target-identity-sha256"),
|
|
5336
5450
|
targetIdentityKindArgument(policy),
|
|
5451
|
+
variantArgument("full"),
|
|
5337
5452
|
);
|
|
5338
5453
|
} else if (command === "verify-app") {
|
|
5339
5454
|
const teamId = argument("--team-id");
|
|
@@ -5347,6 +5462,7 @@ function main(): void {
|
|
|
5347
5462
|
argument("--approved-target"),
|
|
5348
5463
|
argument("--approved-target-identity-sha256"),
|
|
5349
5464
|
targetIdentityKindArgument(policy),
|
|
5465
|
+
variantArgument("full"),
|
|
5350
5466
|
);
|
|
5351
5467
|
} else if (command === "verify-active") {
|
|
5352
5468
|
const teamId = argument("--team-id");
|
|
@@ -5360,6 +5476,7 @@ function main(): void {
|
|
|
5360
5476
|
argument("--approved-target"),
|
|
5361
5477
|
argument("--approved-target-identity-sha256"),
|
|
5362
5478
|
targetIdentityKindArgument(policy),
|
|
5479
|
+
variantArgument("full"),
|
|
5363
5480
|
);
|
|
5364
5481
|
} else if (command === "assert-release") {
|
|
5365
5482
|
assertExpectedRelease(
|
package/scripts/migrate.ts
CHANGED
|
@@ -34,11 +34,11 @@ async function main() {
|
|
|
34
34
|
getCloudPg,
|
|
35
35
|
migrateCloudSchema,
|
|
36
36
|
pingCloudConnectivity,
|
|
37
|
-
|
|
37
|
+
resolveDatabaseUrl,
|
|
38
38
|
} = await import("../src/server/cloud.js");
|
|
39
39
|
const { assertCloudSchemaContract } = await import("../src/server/cloud-readiness.js");
|
|
40
40
|
const { runCloudMigration } = await import("../src/server/migrate-command.js");
|
|
41
|
-
const url =
|
|
41
|
+
const url = resolveDatabaseUrl();
|
|
42
42
|
if (!url) {
|
|
43
43
|
console.error(
|
|
44
44
|
"migrate: no database URL (HASNA_RECORDINGS_DATABASE_URL / RECORDINGS_DATABASE_URL / DATABASE_URL)",
|