@hasna/recordings 0.2.14 → 0.3.2
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/Dockerfile.package +4 -5
- package/README.md +113 -8
- package/bun.lock +2 -2
- package/dist/__tests__/helpers/installer-guard-execution.d.ts +22 -0
- package/dist/__tests__/helpers/installer-guard-execution.d.ts.map +1 -0
- package/dist/__tests__/helpers/installer-preflight.d.ts +22 -0
- package/dist/__tests__/helpers/installer-preflight.d.ts.map +1 -0
- package/dist/__tests__/helpers/native-fs-guard.d.ts.map +1 -1
- package/dist/__tests__/helpers/source-assertions.d.ts +169 -0
- package/dist/__tests__/helpers/source-assertions.d.ts.map +1 -0
- package/dist/__tests__/preload.d.ts +1 -4
- package/dist/__tests__/preload.d.ts.map +1 -1
- package/dist/cli/desktop-snapshot.d.ts +20 -0
- package/dist/cli/desktop-snapshot.d.ts.map +1 -0
- package/dist/cli/index.js +2094 -280
- package/dist/cli/macos-permissions.d.ts +72 -0
- package/dist/cli/macos-permissions.d.ts.map +1 -1
- package/dist/cli/macos-shortcut.d.ts +139 -0
- package/dist/cli/macos-shortcut.d.ts.map +1 -0
- package/dist/cli/trigger-probe.d.ts +172 -0
- package/dist/cli/trigger-probe.d.ts.map +1 -0
- package/dist/db/database.d.ts +9 -0
- package/dist/db/database.d.ts.map +1 -1
- package/dist/http/client.d.ts +19 -7
- package/dist/http/client.d.ts.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +635 -50
- package/dist/lib/capture-probe.d.ts +216 -0
- package/dist/lib/capture-probe.d.ts.map +1 -0
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/macos-bundle.d.ts +50 -0
- package/dist/lib/macos-bundle.d.ts.map +1 -0
- package/dist/lib/persistence-probe.d.ts +208 -0
- package/dist/lib/persistence-probe.d.ts.map +1 -0
- package/dist/lib/recorder.d.ts.map +1 -1
- package/dist/lib/retired-deployment-modes.d.ts +24 -0
- package/dist/lib/retired-deployment-modes.d.ts.map +1 -0
- package/dist/lib/transcriber.d.ts +60 -0
- package/dist/lib/transcriber.d.ts.map +1 -1
- package/dist/mcp/index.js +141 -57
- package/dist/server/cloud-config.d.ts +25 -3
- package/dist/server/cloud-config.d.ts.map +1 -1
- package/dist/server/cloud-readiness.d.ts.map +1 -1
- package/dist/server/cloud.d.ts +8 -7
- package/dist/server/cloud.d.ts.map +1 -1
- package/dist/server/index.js +188 -80
- package/dist/server/migrate-command.d.ts.map +1 -1
- package/dist/server/serve.d.ts.map +1 -1
- package/dist/storage.d.ts +2 -2
- package/dist/storage.d.ts.map +1 -1
- package/dist/storage.js +109 -50
- package/dist/store.d.ts +1 -1
- 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 +25 -6
- package/scripts/ci-linux-suite.ts +403 -0
- package/scripts/ci-native-build.ts +402 -0
- package/scripts/enforce_identity_migration.sh +110 -0
- package/scripts/generate-sdk.ts +1 -1
- package/scripts/install_macos_app.sh +192 -31
- package/scripts/macos_artifact.ts +323 -43
- package/scripts/native/realtime-settle-probe/README.md +39 -0
- package/scripts/native/realtime-settle-probe/main.swift +135 -0
- package/scripts/policy/local-only-approved-targets.txt +21 -0
- package/scripts/read_local_only_targets.sh +203 -0
- package/scripts/scan-artifact.ts +40 -0
- package/scripts/set-version.ts +145 -0
- package/scripts/smoke_macos_app.sh +62 -16
- package/scripts/vacuity-manifest-gen.ts +96 -0
- package/scripts/vacuity-manifests/enhancement-screen-mirror.tsv +44 -0
- package/scripts/vacuity-manifests/install-chain.tsv +13 -0
- package/scripts/vacuity-manifests/must-survive-controls.tsv +10 -0
- package/scripts/vacuity-manifests/reorder.tsv +4 -0
- package/scripts/vacuity-manifests/source-side.tsv +15 -0
- package/scripts/vacuity-manifests/variable-operands.tsv +16 -0
- package/scripts/vacuity-manifests/version-sites.tsv +61 -0
- package/scripts/vacuity-mutation-battery.ts +266 -0
- package/src/native/Recordings/App/MenuBarStatusView.swift +23 -11
- package/src/native/Recordings/App/RecordingsApp.swift +37 -1
- package/src/native/Recordings/App/RuntimeSmoke.swift +5 -1
- package/src/native/Recordings/App/SidebarView.swift +1 -1
- package/src/native/Recordings/Package.swift +24 -5
- package/src/native/Recordings/RecordingsLib/EnhancementScreen.swift +91 -0
- package/src/native/Recordings/RecordingsLib/FnKeyMonitor.swift +27 -4
- package/src/native/Recordings/RecordingsLib/Info.plist +2 -2
- package/src/native/Recordings/RecordingsLib/MenuBarPresentation.swift +56 -3
- package/src/native/Recordings/RecordingsLib/NativePCMRecorder.swift +13 -0
- package/src/native/Recordings/RecordingsLib/PasteDeliveryVerification.swift +460 -0
- package/src/native/Recordings/RecordingsLib/PermissionRequestLaunchPlan.swift +21 -1
- package/src/native/Recordings/RecordingsLib/RealtimeTranscriptionClient.swift +41 -7
- package/src/native/Recordings/RecordingsLib/RecordingAttemptAlert.swift +36 -0
- package/src/native/Recordings/RecordingsLib/RecordingEngine.swift +1071 -75
- package/src/native/Recordings/RecordingsLib/SettingsView.swift +47 -3
- package/src/native/Recordings/RecordingsLib/VoiceShortcuts.swift +11 -6
- package/src/native/Recordings/RecordingsTests/BarOnlyLaunchPlanTests.swift +99 -0
- package/src/native/Recordings/RecordingsTests/CLIRunnerTests.swift +3 -3
- package/src/native/Recordings/RecordingsTests/EnhancementScreenTests.swift +191 -0
- package/src/native/Recordings/RecordingsTests/MenuBarPresentationTests.swift +144 -4
- package/src/native/Recordings/RecordingsTests/PasteDeliveryVerificationTests.swift +463 -0
- package/src/native/Recordings/RecordingsTests/PasteTargetTests.swift +33 -20
- package/src/native/Recordings/RecordingsTests/ProjectStoreTests.swift +1 -1
- package/src/native/Recordings/RecordingsTests/RealtimeSettleBenchmark.swift +200 -0
- package/src/native/Recordings/RecordingsTests/RealtimeSettleBudgetTests.swift +80 -0
- package/src/native/Recordings/RecordingsTests/RealtimeTranscriptionTests.swift +22 -4
- package/src/native/Recordings/RecordingsTests/RecordingEngineDeliveryTests.swift +12 -6
- package/src/native/Recordings/RecordingsTests/RecordingEngineHomeIsolationTests.swift +67 -0
- package/src/native/Recordings/RecordingsTests/RecordingStartGateTests.swift +12 -3
- package/src/native/Recordings/RecordingsTests/RecordingStartTimingTests.swift +207 -11
- package/src/native/Recordings/RecordingsTests/SpeechIntentTests.swift +17 -6
- package/src/native/Recordings/RecordingsTests/TestHomeDirectory.swift +23 -0
- package/src/native/Recordings/Updater/Broker/ApplicationProcessQuiescence.swift +7 -1
- package/src/native/Recordings/Updater/Broker/AtomicActivation.swift +10 -10
- package/src/native/Recordings/Updater/Broker/BrokerMain.swift +5 -1
- package/src/native/Recordings/Updater/Broker/HostOSProductVersion.swift +1 -0
- package/src/native/Recordings/Updater/Broker/PeerIdentity.swift +59 -3
- package/src/native/Recordings/Updater/Protocol/UpdateProtocol.swift +21 -2
- package/src/native/Recordings/Updater/ProtocolTests/ExpectationIntegrityTests.swift +60 -0
- package/src/native/Recordings/Updater/ProtocolTests/UpdateXPCInterfaceTests.swift +113 -0
- package/src/native/Recordings/Updater/VerifierLauncher/RecordingsVerifierLauncher.c +106 -5
- package/src/native/Recordings/build.sh +226 -20
- package/scripts/native/prebuilds/darwin-universal/recordings_fs_guard.node +0 -0
|
@@ -31,6 +31,7 @@ import {
|
|
|
31
31
|
unlinkSync,
|
|
32
32
|
} from "node:fs";
|
|
33
33
|
import { tmpdir } from "node:os";
|
|
34
|
+
import { fileURLToPath } from "node:url";
|
|
34
35
|
import { crc32, inflateRawSync } from "node:zlib";
|
|
35
36
|
import {
|
|
36
37
|
nativeFsGuard,
|
|
@@ -57,8 +58,100 @@ export const BUNDLE_ID = "com.hasna.recordings";
|
|
|
57
58
|
export const PROVENANCE_FILENAME = "recordings-build-provenance.json";
|
|
58
59
|
export const RELEASE_APPROVED_TARGET = "fleet";
|
|
59
60
|
export const LEGACY_LOCAL_TARGET_IDENTITY_KIND = "hardware_uuid_sha256";
|
|
61
|
+
export const LOCAL_ONLY_APPROVED_TARGETS_POLICY_PATH = join(
|
|
62
|
+
dirname(fileURLToPath(import.meta.url)),
|
|
63
|
+
"policy",
|
|
64
|
+
"local-only-approved-targets.txt",
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
// The approved local-only targets are policy data, not code: one file is read by
|
|
68
|
+
// this validator and, through scripts/read_local_only_targets.sh, by both shell entry
|
|
69
|
+
// points, so a target is declared once. The shell reader is a separate implementation
|
|
70
|
+
// of these rules; equivalence is enforced by an executing contract test, not by shared
|
|
71
|
+
// code. Parsing stays deliberately strict — a malformed policy fails closed rather than
|
|
72
|
+
// silently widening the allowlist.
|
|
73
|
+
//
|
|
74
|
+
// This is an operator-integrity control, NOT an authorization boundary. The file sits in
|
|
75
|
+
// the same package root as the guards that read it, so anyone able to edit it can edit
|
|
76
|
+
// those guards. Widening it still grants nothing on its own: the install additionally
|
|
77
|
+
// requires a matching `hostname -s`, a matching authenticated Tailscale node digest, and
|
|
78
|
+
// a matching authenticated manifest SHA-256.
|
|
79
|
+
export function localOnlyApprovedTargets(
|
|
80
|
+
policyPath: string = LOCAL_ONLY_APPROVED_TARGETS_POLICY_PATH,
|
|
81
|
+
): string[] {
|
|
82
|
+
// lstat, not readFileSync alone: readFileSync FOLLOWS a symlink, and the shell reader
|
|
83
|
+
// refuses one outright (`[ -L ]`). That divergence was real and it favoured the
|
|
84
|
+
// attacker — a policy symlinked at a widened allowlist was rejected by the shell gate
|
|
85
|
+
// and silently honoured here. Refusing every non-regular file matches the shell reader
|
|
86
|
+
// and keeps the wording it prints, so both sides fail closed identically.
|
|
87
|
+
let policyStats;
|
|
88
|
+
try {
|
|
89
|
+
policyStats = lstatSync(policyPath);
|
|
90
|
+
} catch {
|
|
91
|
+
throw new Error("local-only approved target policy is missing");
|
|
92
|
+
}
|
|
93
|
+
if (!policyStats.isFile()) {
|
|
94
|
+
throw new Error("local-only approved target policy is missing");
|
|
95
|
+
}
|
|
96
|
+
// An unreadable (mode 000) policy passes the lstat above and then threw a raw EACCES from
|
|
97
|
+
// here, while the shell reader emitted bash's own "Permission denied" and no reader
|
|
98
|
+
// message. Same fail-closed outcome, two unrecognizable errors; both now say this.
|
|
99
|
+
let rawContents: string;
|
|
100
|
+
try {
|
|
101
|
+
rawContents = readFileSync(policyPath, "utf8");
|
|
102
|
+
} catch {
|
|
103
|
+
throw new Error("local-only approved target policy is not readable");
|
|
104
|
+
}
|
|
105
|
+
// Reject a NUL anywhere, in the same order and with the same wording as the shell
|
|
106
|
+
// reader's pre-scan. This reader had no NUL check at all and only ever refused one that
|
|
107
|
+
// happened to land inside a would-be hostname: "# comment\0" was dropped as a comment,
|
|
108
|
+
// so "# comment\0\nstation03\n" was refused by the shell gate and ACCEPTED here.
|
|
109
|
+
if (rawContents.includes("\u0000")) {
|
|
110
|
+
throw new Error("local-only approved target policy contains a NUL byte");
|
|
111
|
+
}
|
|
112
|
+
// Strip a BOM at offset 0 only. A U+FEFF anywhere else is a zero-width no-break space,
|
|
113
|
+
// not a byte-order mark; the shell reader used to strip one per line and therefore
|
|
114
|
+
// accepted a BOM on line 2 that this reader rejected.
|
|
115
|
+
const contents = rawContents.replace(/^/, "");
|
|
116
|
+
const targets = contents
|
|
117
|
+
.split("\n")
|
|
118
|
+
// Deliberately NOT String.trim(): trim() also strips U+FEFF and U+00A0, and the
|
|
119
|
+
// shell reader trims neither, so a trailing NBSP or BOM would be accepted here and
|
|
120
|
+
// rejected there. ASCII space and tab only, matching read_local_only_targets.sh
|
|
121
|
+
// under its pinned LC_ALL=C; everything else is left for the hostname shape below.
|
|
122
|
+
.map((line) => line.replace(/\r$/, "").replace(/^[\t ]+|[\t ]+$/g, ""))
|
|
123
|
+
.filter((line) => line.length > 0 && !line.startsWith("#"));
|
|
124
|
+
if (targets.length === 0) {
|
|
125
|
+
throw new Error("local-only approved target policy lists no targets");
|
|
126
|
+
}
|
|
127
|
+
for (const target of targets) {
|
|
128
|
+
if (!/^[a-z][a-z0-9-]{1,30}[a-z0-9]$/.test(target)) {
|
|
129
|
+
throw new Error(`local-only approved target policy has an invalid target name: ${target}`);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
if (new Set(targets).size !== targets.length) {
|
|
133
|
+
throw new Error("local-only approved target policy has duplicate targets");
|
|
134
|
+
}
|
|
135
|
+
if (targets.includes(RELEASE_APPROVED_TARGET)) {
|
|
136
|
+
throw new Error("local-only approved target policy must not list the release fleet target");
|
|
137
|
+
}
|
|
138
|
+
return targets;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export function isLocalOnlyApprovedTarget(
|
|
142
|
+
target: string | undefined,
|
|
143
|
+
policyPath?: string,
|
|
144
|
+
): boolean {
|
|
145
|
+
if (target === undefined) return false;
|
|
146
|
+
return localOnlyApprovedTargets(policyPath).includes(target);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function localOnlyApprovedTargetsMessage(policyPath?: string): string {
|
|
150
|
+
return localOnlyApprovedTargets(policyPath).join(", ");
|
|
151
|
+
}
|
|
60
152
|
|
|
61
153
|
export type ArtifactPolicy = "release" | "local_only";
|
|
154
|
+
export type ArtifactVariant = "full" | "bar";
|
|
62
155
|
export type TargetIdentityKind =
|
|
63
156
|
| typeof LEGACY_LOCAL_TARGET_IDENTITY_KIND
|
|
64
157
|
| "tailscale_node_id_sha256";
|
|
@@ -67,6 +160,7 @@ type OperatorTargetIdentityKind = TargetIdentityKind | "none";
|
|
|
67
160
|
export type BuildProvenance = {
|
|
68
161
|
schema_version: 3 | 4;
|
|
69
162
|
artifact_policy?: "local_only";
|
|
163
|
+
variant?: ArtifactVariant;
|
|
70
164
|
approved_target?: string;
|
|
71
165
|
approved_target_identity_kind?: TargetIdentityKind;
|
|
72
166
|
approved_target_identity_sha256?: string;
|
|
@@ -89,13 +183,16 @@ export type BuildProvenance = {
|
|
|
89
183
|
|
|
90
184
|
export type MacOSArtifactManifest = BuildProvenance & {
|
|
91
185
|
artifact_type: "recordings-macos-app";
|
|
186
|
+
/// The top-level bundle directory name inside the artifact (Recordings.app for the
|
|
187
|
+
/// full app, HasnaRecordings.app for the bar variant per the fleet naming rule).
|
|
188
|
+
bundle_name: string;
|
|
92
189
|
app_sha256: string;
|
|
93
190
|
binding: {
|
|
94
191
|
bundle_tree_sha256: string;
|
|
95
192
|
};
|
|
96
193
|
provenance_sha256: string;
|
|
97
194
|
signing: {
|
|
98
|
-
mode?: "ad_hoc";
|
|
195
|
+
mode?: "ad_hoc" | "developer_id";
|
|
99
196
|
authority: string;
|
|
100
197
|
team_id: string;
|
|
101
198
|
trusted_timestamp: string;
|
|
@@ -118,7 +215,7 @@ export type MacOSArtifactManifest = BuildProvenance & {
|
|
|
118
215
|
};
|
|
119
216
|
container: {
|
|
120
217
|
type: "zip";
|
|
121
|
-
install_locations: [
|
|
218
|
+
install_locations: [string];
|
|
122
219
|
};
|
|
123
220
|
nested_code_policy: {
|
|
124
221
|
allowlist_sha256: string;
|
|
@@ -460,14 +557,43 @@ export function parseDesignatedRequirement(output: string): string {
|
|
|
460
557
|
return requirement;
|
|
461
558
|
}
|
|
462
559
|
|
|
560
|
+
/**
|
|
561
|
+
* A local-station artifact is signed one of two ways, and the expected team identifier is
|
|
562
|
+
* what says which: the sentinel `ADHOC`, or a real 10-character Apple team.
|
|
563
|
+
*
|
|
564
|
+
* Developer ID signing exists on this path because ad-hoc signing cannot hold a macOS TCC
|
|
565
|
+
* grant. TCC keys Accessibility (and Microphone) to bundle id plus signing identity, and an
|
|
566
|
+
* ad-hoc signature offers only a CDHash that changes on every rebuild, so the hold-to-talk
|
|
567
|
+
* CGEventTap in FnKeyMonitor.swift lost its permission on each reinstall. Ad-hoc remains
|
|
568
|
+
* supported for builders that hold no signing key.
|
|
569
|
+
*/
|
|
570
|
+
export function isLocalOnlySigningTeam(expectedTeamId: string): boolean {
|
|
571
|
+
return expectedTeamId === "ADHOC" || /^[A-Z0-9]{10}$/.test(expectedTeamId);
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
export function localOnlySigningMode(expectedTeamId: string): "ad_hoc" | "developer_id" {
|
|
575
|
+
if (expectedTeamId === "ADHOC") return "ad_hoc";
|
|
576
|
+
if (!/^[A-Z0-9]{10}$/.test(expectedTeamId)) {
|
|
577
|
+
throw new Error(
|
|
578
|
+
"local-only Developer ID signing requires ADHOC or a 10-character Apple team identifier",
|
|
579
|
+
);
|
|
580
|
+
}
|
|
581
|
+
return "developer_id";
|
|
582
|
+
}
|
|
583
|
+
|
|
463
584
|
export function designatedRequirementForPolicy(
|
|
464
585
|
output: string,
|
|
465
586
|
artifactPolicy: ArtifactPolicy,
|
|
466
587
|
adHocSignatureVerified = false,
|
|
588
|
+
developerIdSignatureVerified = false,
|
|
467
589
|
): string {
|
|
468
590
|
if (artifactPolicy === "release") return parseDesignatedRequirement(output);
|
|
591
|
+
// A Developer ID local-station artifact always carries a real designated requirement, and
|
|
592
|
+
// that requirement is the whole point of signing it: it is the stable identity a TCC grant
|
|
593
|
+
// attaches to. Never let it degrade to the ad-hoc sentinel.
|
|
594
|
+
if (developerIdSignatureVerified) return parseDesignatedRequirement(output);
|
|
469
595
|
if (!adHocSignatureVerified) {
|
|
470
|
-
throw new Error("local-only designated requirement evidence requires verified
|
|
596
|
+
throw new Error("local-only designated requirement evidence requires verified signing");
|
|
471
597
|
}
|
|
472
598
|
try {
|
|
473
599
|
return parseDesignatedRequirement(output);
|
|
@@ -558,11 +684,24 @@ function signingEvidence(
|
|
|
558
684
|
if (!timestamp || timestamp.toLowerCase() === "none") {
|
|
559
685
|
throw new Error(`${codePath} is missing a trusted signing timestamp`);
|
|
560
686
|
}
|
|
687
|
+
} else if (localOnlySigningMode(expectedTeamId) === "developer_id") {
|
|
688
|
+
// Developer ID signed but deliberately NOT notarized: the notarization, single-target
|
|
689
|
+
// binding and builder attestation are what still separate this from a release. Only the
|
|
690
|
+
// signing identity is shared, and only because TCC cannot key a durable Accessibility
|
|
691
|
+
// grant to anything weaker (see localOnlySigningMode).
|
|
692
|
+
if (!authority.startsWith("Developer ID Application:")) {
|
|
693
|
+
throw new Error(`${codePath} is not signed by a Developer ID Application authority`);
|
|
694
|
+
}
|
|
695
|
+
if (teamId !== expectedTeamId) {
|
|
696
|
+
throw new Error(`${codePath} TeamIdentifier ${teamId || "missing"} does not match ${expectedTeamId}`);
|
|
697
|
+
}
|
|
698
|
+
if (rawSignature.toLowerCase() === "adhoc") {
|
|
699
|
+
throw new Error(`${codePath} is ad-hoc signed but a Developer ID team was pinned`);
|
|
700
|
+
}
|
|
701
|
+
// A trusted timestamp is a release-only claim; local-station builds never pass --timestamp.
|
|
702
|
+
timestamp = rawTimestamp && rawTimestamp.toLowerCase() !== "none" ? rawTimestamp : "none";
|
|
561
703
|
} else {
|
|
562
704
|
mode = "ad_hoc";
|
|
563
|
-
if (expectedTeamId !== "ADHOC") {
|
|
564
|
-
throw new Error("local-only code verification requires the ADHOC signing identity");
|
|
565
|
-
}
|
|
566
705
|
if (rawSignature.toLowerCase() !== "adhoc" || authority) {
|
|
567
706
|
throw new Error(`${codePath} is not consistently ad-hoc signed for local-only use`);
|
|
568
707
|
}
|
|
@@ -587,6 +726,7 @@ function signingEvidence(
|
|
|
587
726
|
requirementOutput,
|
|
588
727
|
artifactPolicy,
|
|
589
728
|
mode === "ad_hoc",
|
|
729
|
+
artifactPolicy !== "release" && mode === "developer_id",
|
|
590
730
|
);
|
|
591
731
|
const entitlements = canonicalEntitlements(codePath);
|
|
592
732
|
if (entitlements !== canonicalJson(expectedEntitlements)) {
|
|
@@ -690,6 +830,22 @@ function manifestPolicy(manifest: MacOSArtifactManifest): ArtifactPolicy {
|
|
|
690
830
|
return manifest.schema_version === LOCAL_ARTIFACT_SCHEMA_VERSION ? "local_only" : "release";
|
|
691
831
|
}
|
|
692
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 ?? "Recordings.app";
|
|
847
|
+
}
|
|
848
|
+
|
|
693
849
|
function manifestTargetIdentityKind(manifest: MacOSArtifactManifest): OperatorTargetIdentityKind {
|
|
694
850
|
if (manifestPolicy(manifest) === "release") return "none";
|
|
695
851
|
return manifest.approved_target_identity_kind ?? LEGACY_LOCAL_TARGET_IDENTITY_KIND;
|
|
@@ -1270,6 +1426,7 @@ export function withPrivatelyExtractedArchiveApp<T>(
|
|
|
1270
1426
|
operation: (appPath: string) => T,
|
|
1271
1427
|
platformArchiveTool = "/usr/bin/ditto",
|
|
1272
1428
|
expectedArchiveSha256?: string,
|
|
1429
|
+
expectedBundleName = "Recordings.app",
|
|
1273
1430
|
): T {
|
|
1274
1431
|
const privateRoot = mkdtempSync(join(tmpdir(), "recordings-artifact-extract-"));
|
|
1275
1432
|
chmodSync(privateRoot, 0o700);
|
|
@@ -1314,13 +1471,17 @@ export function withPrivatelyExtractedArchiveApp<T>(
|
|
|
1314
1471
|
inspectZipArchive(pinnedArchivePath);
|
|
1315
1472
|
run(platformArchiveTool, ["-x", "-k", pinnedArchivePath, extractionRoot]);
|
|
1316
1473
|
const rootEntries = readdirSync(extractionRoot);
|
|
1317
|
-
if (rootEntries.length !== 1 || rootEntries[0] !==
|
|
1318
|
-
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
|
+
);
|
|
1319
1478
|
}
|
|
1320
|
-
const extractedAppPath = join(extractionRoot,
|
|
1479
|
+
const extractedAppPath = join(extractionRoot, expectedBundleName);
|
|
1321
1480
|
const appDetails = lstatSync(extractedAppPath);
|
|
1322
1481
|
if (appDetails.isSymbolicLink() || !appDetails.isDirectory()) {
|
|
1323
|
-
throw new Error(
|
|
1482
|
+
throw new Error(
|
|
1483
|
+
`release archive top-level ${expectedBundleName} must be a regular directory`,
|
|
1484
|
+
);
|
|
1324
1485
|
}
|
|
1325
1486
|
assertRegularArchiveTree(extractedAppPath);
|
|
1326
1487
|
return operation(extractedAppPath);
|
|
@@ -1343,6 +1504,7 @@ export function extractVerifiedArchiveToStaging(
|
|
|
1343
1504
|
expectedApprovedTarget: string,
|
|
1344
1505
|
expectedApprovedTargetIdentitySha256: string,
|
|
1345
1506
|
expectedApprovedTargetIdentityKind: OperatorTargetIdentityKind,
|
|
1507
|
+
expectedVariant: ArtifactVariant = "full",
|
|
1346
1508
|
platformArchiveTool = "/usr/bin/ditto",
|
|
1347
1509
|
): void {
|
|
1348
1510
|
if (resolve(stagingTarget) !== stagingTarget) {
|
|
@@ -1375,16 +1537,19 @@ export function extractVerifiedArchiveToStaging(
|
|
|
1375
1537
|
withPrivatelyExtractedArchiveApp(
|
|
1376
1538
|
archivePath,
|
|
1377
1539
|
(appPath) => {
|
|
1378
|
-
const targetAppPath = join(stagingTarget,
|
|
1540
|
+
const targetAppPath = join(stagingTarget, manifestBundleName(manifest));
|
|
1379
1541
|
renameSync(appPath, targetAppPath);
|
|
1380
1542
|
assertRegularArchiveTree(targetAppPath);
|
|
1381
1543
|
const entries = readdirSync(stagingTarget);
|
|
1382
|
-
if (entries.length !== 1 || entries[0] !==
|
|
1383
|
-
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
|
+
);
|
|
1384
1548
|
}
|
|
1385
1549
|
},
|
|
1386
1550
|
platformArchiveTool,
|
|
1387
1551
|
manifest.archive.sha256,
|
|
1552
|
+
manifestBundleName(manifest),
|
|
1388
1553
|
);
|
|
1389
1554
|
}
|
|
1390
1555
|
|
|
@@ -1412,6 +1577,20 @@ export function assertManifestShape(manifest: MacOSArtifactManifest): void {
|
|
|
1412
1577
|
}
|
|
1413
1578
|
if (manifest.artifact_type !== "recordings-macos-app") throw new Error("unexpected artifact type");
|
|
1414
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
|
+
}
|
|
1415
1594
|
const artifactPolicy = manifestPolicy(manifest);
|
|
1416
1595
|
for (const [label, value] of [
|
|
1417
1596
|
["bundle version", manifest.bundle_version],
|
|
@@ -1510,7 +1689,7 @@ export function assertManifestShape(manifest: MacOSArtifactManifest): void {
|
|
|
1510
1689
|
} else {
|
|
1511
1690
|
if (
|
|
1512
1691
|
manifest.artifact_policy !== "local_only" ||
|
|
1513
|
-
manifest.approved_target
|
|
1692
|
+
!isLocalOnlyApprovedTarget(manifest.approved_target) ||
|
|
1514
1693
|
(manifest.approved_target_identity_kind !== undefined &&
|
|
1515
1694
|
manifest.approved_target_identity_kind !== "tailscale_node_id_sha256") ||
|
|
1516
1695
|
(manifest.approved_target_identity_kind === undefined) !==
|
|
@@ -1523,20 +1702,38 @@ export function assertManifestShape(manifest: MacOSArtifactManifest): void {
|
|
|
1523
1702
|
!isHex(manifest.builder_identity_sha256, 64) ||
|
|
1524
1703
|
manifest.builder_identity_sha256 === manifest.approved_target_identity_sha256
|
|
1525
1704
|
) {
|
|
1526
|
-
throw new Error(
|
|
1705
|
+
throw new Error(
|
|
1706
|
+
"local-only schema v3 requires an approved target name " +
|
|
1707
|
+
`(${localOnlyApprovedTargetsMessage()}) and machine identity`,
|
|
1708
|
+
);
|
|
1527
1709
|
}
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
manifest
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1710
|
+
// A local-station manifest is internally consistent in exactly one of two shapes: fully
|
|
1711
|
+
// ad-hoc, or fully Developer ID under one pinned team. Mixing them (e.g. a Developer ID
|
|
1712
|
+
// app with an ad-hoc helper) would leave the bundle without one stable identity for TCC
|
|
1713
|
+
// to key on, which is the failure this whole path exists to prevent.
|
|
1714
|
+
if (manifest.non_notarized !== true || !isLocalOnlySigningTeam(manifest.team_id)) {
|
|
1715
|
+
throw new Error("local-only manifest requires a non-notarized ADHOC or Developer ID team");
|
|
1716
|
+
}
|
|
1717
|
+
if (manifest.team_id === "ADHOC") {
|
|
1718
|
+
if (
|
|
1719
|
+
manifest.signing.mode !== "ad_hoc" ||
|
|
1720
|
+
manifest.signing.authority !== "adhoc" ||
|
|
1721
|
+
manifest.signing.helper_authority !== "adhoc" ||
|
|
1722
|
+
manifest.signing.team_id !== "ADHOC" ||
|
|
1723
|
+
manifest.signing.helper_team_id !== "ADHOC" ||
|
|
1724
|
+
manifest.signing.trusted_timestamp !== "none" ||
|
|
1725
|
+
manifest.signing.helper_trusted_timestamp !== "none"
|
|
1726
|
+
) {
|
|
1727
|
+
throw new Error("local-only manifest requires consistent ad-hoc signing evidence");
|
|
1728
|
+
}
|
|
1729
|
+
} else if (
|
|
1730
|
+
manifest.signing.mode !== "developer_id" ||
|
|
1731
|
+
!manifest.signing.authority.startsWith("Developer ID Application:") ||
|
|
1732
|
+
!manifest.signing.helper_authority.startsWith("Developer ID Application:") ||
|
|
1733
|
+
manifest.signing.team_id !== manifest.team_id ||
|
|
1734
|
+
manifest.signing.helper_team_id !== manifest.team_id
|
|
1538
1735
|
) {
|
|
1539
|
-
throw new Error("local-only manifest requires consistent
|
|
1736
|
+
throw new Error("local-only manifest requires consistent Developer ID signing evidence");
|
|
1540
1737
|
}
|
|
1541
1738
|
if (
|
|
1542
1739
|
manifest.notarization.status !== "Not Submitted" ||
|
|
@@ -1602,6 +1799,7 @@ export function verifyArchiveManifest(
|
|
|
1602
1799
|
expectedApprovedTarget: string = RELEASE_APPROVED_TARGET,
|
|
1603
1800
|
expectedApprovedTargetIdentitySha256: string = "none",
|
|
1604
1801
|
expectedApprovedTargetIdentityKind: OperatorTargetIdentityKind = LEGACY_LOCAL_TARGET_IDENTITY_KIND,
|
|
1802
|
+
expectedVariant: ArtifactVariant = "full",
|
|
1605
1803
|
): MacOSArtifactManifest {
|
|
1606
1804
|
if (!expectedTeamId) throw new Error("expected Team ID is required");
|
|
1607
1805
|
const manifest = readAuthenticatedManifest<MacOSArtifactManifest>(
|
|
@@ -1612,6 +1810,11 @@ export function verifyArchiveManifest(
|
|
|
1612
1810
|
if (manifestPolicy(manifest) !== expectedPolicy) {
|
|
1613
1811
|
throw new Error("manifest artifact policy does not match the explicit operator selection");
|
|
1614
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
|
+
}
|
|
1615
1818
|
const actualApprovedTarget = manifest.approved_target ?? RELEASE_APPROVED_TARGET;
|
|
1616
1819
|
if (actualApprovedTarget !== expectedApprovedTarget) {
|
|
1617
1820
|
throw new Error("manifest approved target does not match the exact operator-approved target");
|
|
@@ -1664,6 +1867,7 @@ function assertProvenanceMatchesManifest(
|
|
|
1664
1867
|
"builder_identity_kind",
|
|
1665
1868
|
"builder_identity_sha256",
|
|
1666
1869
|
"non_notarized",
|
|
1870
|
+
"variant",
|
|
1667
1871
|
"bundle_id",
|
|
1668
1872
|
"bundle_version",
|
|
1669
1873
|
"git_sha",
|
|
@@ -1836,11 +2040,17 @@ export function verifyExtractedApp(
|
|
|
1836
2040
|
expectedApprovedTarget: string = RELEASE_APPROVED_TARGET,
|
|
1837
2041
|
expectedApprovedTargetIdentitySha256: string = "none",
|
|
1838
2042
|
expectedApprovedTargetIdentityKind: OperatorTargetIdentityKind = LEGACY_LOCAL_TARGET_IDENTITY_KIND,
|
|
2043
|
+
expectedVariant: ArtifactVariant = "full",
|
|
1839
2044
|
): MacOSArtifactManifest {
|
|
1840
2045
|
const manifest = readAuthenticatedManifest<MacOSArtifactManifest>(
|
|
1841
2046
|
manifestPath,
|
|
1842
2047
|
expectedManifestSha256,
|
|
1843
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
|
+
}
|
|
1844
2054
|
verifyAppAgainstManifest(
|
|
1845
2055
|
appPath,
|
|
1846
2056
|
manifest,
|
|
@@ -1910,6 +2120,7 @@ function verifySuppliedAndArchivedApps(
|
|
|
1910
2120
|
},
|
|
1911
2121
|
"/usr/bin/ditto",
|
|
1912
2122
|
manifest.archive.sha256,
|
|
2123
|
+
manifestBundleName(manifest),
|
|
1913
2124
|
);
|
|
1914
2125
|
if (sha256ArchiveFile(archivePath) !== manifest.archive.sha256) {
|
|
1915
2126
|
throw new Error("release archive changed during exact-byte verification");
|
|
@@ -1927,6 +2138,7 @@ function writeProvenance(
|
|
|
1927
2138
|
approvedTargetIdentitySha256: string,
|
|
1928
2139
|
builderIdentityKind: OperatorTargetIdentityKind,
|
|
1929
2140
|
builderIdentitySha256: string,
|
|
2141
|
+
variant: ArtifactVariant,
|
|
1930
2142
|
): void {
|
|
1931
2143
|
const executablePath = join(appPath, "Contents", "MacOS", "Recordings");
|
|
1932
2144
|
const helperPath = join(appPath, "Contents", "Helpers", "recordings");
|
|
@@ -1936,6 +2148,7 @@ function writeProvenance(
|
|
|
1936
2148
|
artifactPolicy === "release"
|
|
1937
2149
|
? RELEASE_ARTIFACT_SCHEMA_VERSION
|
|
1938
2150
|
: LOCAL_ARTIFACT_SCHEMA_VERSION,
|
|
2151
|
+
variant,
|
|
1939
2152
|
bundle_id: plistValue(appPath, "CFBundleIdentifier"),
|
|
1940
2153
|
bundle_version: plistValue(appPath, "CFBundleShortVersionString"),
|
|
1941
2154
|
bundle_build_version: plistValue(appPath, "CFBundleVersion"),
|
|
@@ -1962,15 +2175,19 @@ function writeProvenance(
|
|
|
1962
2175
|
throw new Error("release provenance has an invalid target or Team ID");
|
|
1963
2176
|
}
|
|
1964
2177
|
} else if (
|
|
1965
|
-
|
|
1966
|
-
|
|
2178
|
+
// ADHOC or a pinned Developer ID team; localOnlySigningMode rejects anything else.
|
|
2179
|
+
!isLocalOnlySigningTeam(expectedTeamId) ||
|
|
2180
|
+
!isLocalOnlyApprovedTarget(approvedTarget) ||
|
|
1967
2181
|
approvedTargetIdentityKind !== "tailscale_node_id_sha256" ||
|
|
1968
2182
|
builderIdentityKind !== "tailscale_node_id_sha256" ||
|
|
1969
2183
|
!isHex(approvedTargetIdentitySha256, 64) ||
|
|
1970
2184
|
!isHex(builderIdentitySha256, 64) ||
|
|
1971
2185
|
approvedTargetIdentitySha256 === builderIdentitySha256
|
|
1972
2186
|
) {
|
|
1973
|
-
throw new Error(
|
|
2187
|
+
throw new Error(
|
|
2188
|
+
"new local-only provenance requires ADHOC or a pinned Developer ID team and a Tailscale " +
|
|
2189
|
+
`node-bound approved-target identity (${localOnlyApprovedTargetsMessage()})`,
|
|
2190
|
+
);
|
|
1974
2191
|
} else {
|
|
1975
2192
|
provenance.artifact_policy = "local_only";
|
|
1976
2193
|
provenance.approved_target = approvedTarget;
|
|
@@ -1994,6 +2211,7 @@ function finalizeArtifact(
|
|
|
1994
2211
|
notaryLogPath: string,
|
|
1995
2212
|
notarySubmissionId: string,
|
|
1996
2213
|
submittedArchiveSha256: string,
|
|
2214
|
+
expectedVariant: ArtifactVariant = "full",
|
|
1997
2215
|
): void {
|
|
1998
2216
|
assertCurrentSourceRevision(packageRoot, expectedSourceSha);
|
|
1999
2217
|
const executablePath = join(appPath, "Contents", "MacOS", "Recordings");
|
|
@@ -2003,6 +2221,14 @@ function finalizeArtifact(
|
|
|
2003
2221
|
if (provenance.git_sha !== expectedSourceSha) {
|
|
2004
2222
|
throw new Error("embedded provenance does not match the pinned source SHA");
|
|
2005
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
|
+
}
|
|
2006
2232
|
assertExpectedCodeLayout(appPath);
|
|
2007
2233
|
const outerSigning = signingEvidence(appPath, expectedTeamId, APP_ENTITLEMENTS, executablePath);
|
|
2008
2234
|
const helperSigning = signingEvidence(helperPath, expectedTeamId, HELPER_ENTITLEMENTS);
|
|
@@ -2015,9 +2241,14 @@ function finalizeArtifact(
|
|
|
2015
2241
|
const notaryLog = JSON.parse(notaryLogSnapshot.toString("utf8")) as unknown;
|
|
2016
2242
|
assertAcceptedNotaryLog(notaryLog, notarySubmissionId, submittedArchiveSha256);
|
|
2017
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
|
+
}
|
|
2018
2248
|
const manifest: MacOSArtifactManifest = {
|
|
2019
2249
|
...provenance,
|
|
2020
2250
|
artifact_type: "recordings-macos-app",
|
|
2251
|
+
bundle_name: bundleName,
|
|
2021
2252
|
app_sha256: sha256File(executablePath),
|
|
2022
2253
|
binding: { bundle_tree_sha256: treeDigest(appPath) },
|
|
2023
2254
|
provenance_sha256: sha256File(embeddedPath),
|
|
@@ -2044,7 +2275,7 @@ function finalizeArtifact(
|
|
|
2044
2275
|
},
|
|
2045
2276
|
container: {
|
|
2046
2277
|
type: "zip",
|
|
2047
|
-
install_locations: [
|
|
2278
|
+
install_locations: [`/Applications/${bundleName}`],
|
|
2048
2279
|
},
|
|
2049
2280
|
nested_code_policy: {
|
|
2050
2281
|
allowlist_sha256: nestedPolicyDigest(items),
|
|
@@ -2120,11 +2351,17 @@ function finalizeLocalArtifact(
|
|
|
2120
2351
|
approvedTarget: string,
|
|
2121
2352
|
approvedTargetIdentityKind: TargetIdentityKind,
|
|
2122
2353
|
approvedTargetIdentitySha256: string,
|
|
2354
|
+
expectedTeamId: string = "ADHOC",
|
|
2355
|
+
expectedVariant: ArtifactVariant = "full",
|
|
2123
2356
|
): void {
|
|
2124
2357
|
assertCurrentSourceRevision(packageRoot, expectedSourceSha);
|
|
2125
2358
|
if (approvedTargetIdentityKind !== "tailscale_node_id_sha256") {
|
|
2126
2359
|
throw new Error("new local-only artifacts require a Tailscale node ID identity hash");
|
|
2127
2360
|
}
|
|
2361
|
+
if (expectedVariant !== "full" && expectedVariant !== "bar") {
|
|
2362
|
+
throw new Error(`finalize variant is unsupported: ${expectedVariant}`);
|
|
2363
|
+
}
|
|
2364
|
+
const localSigningMode = localOnlySigningMode(expectedTeamId);
|
|
2128
2365
|
const executablePath = join(appPath, "Contents", "MacOS", "Recordings");
|
|
2129
2366
|
const helperPath = join(appPath, "Contents", "Helpers", "recordings");
|
|
2130
2367
|
const embeddedPath = provenancePath(appPath);
|
|
@@ -2137,34 +2374,44 @@ function finalizeLocalArtifact(
|
|
|
2137
2374
|
provenance.approved_target_identity_sha256 !== approvedTargetIdentitySha256 ||
|
|
2138
2375
|
provenance.builder_identity_kind !== "tailscale_node_id_sha256" ||
|
|
2139
2376
|
provenance.non_notarized !== true ||
|
|
2140
|
-
provenance.team_id !==
|
|
2377
|
+
provenance.team_id !== expectedTeamId
|
|
2141
2378
|
) {
|
|
2142
2379
|
throw new Error("embedded provenance is not approved for this local-only target");
|
|
2143
2380
|
}
|
|
2381
|
+
if ((provenance.variant ?? "full") !== expectedVariant) {
|
|
2382
|
+
throw new Error(
|
|
2383
|
+
`finalize variant ${expectedVariant} does not match the embedded provenance variant`,
|
|
2384
|
+
);
|
|
2385
|
+
}
|
|
2144
2386
|
assertExpectedCodeLayout(appPath);
|
|
2145
2387
|
const outerSigning = signingEvidence(
|
|
2146
2388
|
appPath,
|
|
2147
|
-
|
|
2389
|
+
expectedTeamId,
|
|
2148
2390
|
APP_ENTITLEMENTS,
|
|
2149
2391
|
executablePath,
|
|
2150
2392
|
"local_only",
|
|
2151
2393
|
);
|
|
2152
2394
|
const helperSigning = signingEvidence(
|
|
2153
2395
|
helperPath,
|
|
2154
|
-
|
|
2396
|
+
expectedTeamId,
|
|
2155
2397
|
HELPER_ENTITLEMENTS,
|
|
2156
2398
|
helperPath,
|
|
2157
2399
|
"local_only",
|
|
2158
2400
|
);
|
|
2159
|
-
const items = nestedItems(appPath,
|
|
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
|
+
}
|
|
2160
2406
|
const manifest: MacOSArtifactManifest = {
|
|
2161
2407
|
...provenance,
|
|
2162
2408
|
artifact_type: "recordings-macos-app",
|
|
2409
|
+
bundle_name: bundleName,
|
|
2163
2410
|
app_sha256: sha256File(executablePath),
|
|
2164
2411
|
binding: { bundle_tree_sha256: treeDigest(appPath) },
|
|
2165
2412
|
provenance_sha256: sha256File(embeddedPath),
|
|
2166
2413
|
signing: {
|
|
2167
|
-
mode:
|
|
2414
|
+
mode: localSigningMode,
|
|
2168
2415
|
authority: outerSigning.authority,
|
|
2169
2416
|
team_id: outerSigning.teamId,
|
|
2170
2417
|
trusted_timestamp: outerSigning.timestamp,
|
|
@@ -2187,7 +2434,7 @@ function finalizeLocalArtifact(
|
|
|
2187
2434
|
},
|
|
2188
2435
|
container: {
|
|
2189
2436
|
type: "zip",
|
|
2190
|
-
install_locations: [
|
|
2437
|
+
install_locations: [`~/Applications/${bundleName}`],
|
|
2191
2438
|
},
|
|
2192
2439
|
nested_code_policy: {
|
|
2193
2440
|
allowlist_sha256: nestedPolicyDigest(items),
|
|
@@ -2208,7 +2455,7 @@ function finalizeLocalArtifact(
|
|
|
2208
2455
|
appPath,
|
|
2209
2456
|
archivePath,
|
|
2210
2457
|
manifest,
|
|
2211
|
-
|
|
2458
|
+
expectedTeamId,
|
|
2212
2459
|
"local_only",
|
|
2213
2460
|
approvedTarget,
|
|
2214
2461
|
approvedTargetIdentitySha256,
|
|
@@ -2301,11 +2548,19 @@ function assertInstallTransition(
|
|
|
2301
2548
|
assertVersionTransition(installedVersion, installedSource, manifest);
|
|
2302
2549
|
}
|
|
2303
2550
|
|
|
2304
|
-
function requirementDigest(
|
|
2551
|
+
function requirementDigest(
|
|
2552
|
+
appPath: string,
|
|
2553
|
+
artifactPolicy: ArtifactPolicy,
|
|
2554
|
+
expectedTeamId: string = "ADHOC",
|
|
2555
|
+
): void {
|
|
2305
2556
|
if (artifactPolicy === "local_only") {
|
|
2557
|
+
// The pinned team decides which local signing shape is verified. Hard-coding "ADHOC" here
|
|
2558
|
+
// rejected every correctly Developer-ID-signed local-station bundle, and because
|
|
2559
|
+
// install_macos_app.sh calls this for EVERY local install under `set -euo pipefail`, that
|
|
2560
|
+
// rejection aborted the install outright — making a properly signed build uninstallable.
|
|
2306
2561
|
const evidence = signingEvidence(
|
|
2307
2562
|
appPath,
|
|
2308
|
-
|
|
2563
|
+
expectedTeamId,
|
|
2309
2564
|
APP_ENTITLEMENTS,
|
|
2310
2565
|
join(appPath, "Contents", "MacOS", "Recordings"),
|
|
2311
2566
|
"local_only",
|
|
@@ -2349,6 +2604,7 @@ function verifyActiveApp(
|
|
|
2349
2604
|
expectedApprovedTarget: string,
|
|
2350
2605
|
expectedApprovedTargetIdentitySha256: string,
|
|
2351
2606
|
expectedApprovedTargetIdentityKind: OperatorTargetIdentityKind,
|
|
2607
|
+
expectedVariant: ArtifactVariant = "full",
|
|
2352
2608
|
): void {
|
|
2353
2609
|
const manifest = verifyExtractedApp(
|
|
2354
2610
|
appPath,
|
|
@@ -2359,6 +2615,7 @@ function verifyActiveApp(
|
|
|
2359
2615
|
expectedApprovedTarget,
|
|
2360
2616
|
expectedApprovedTargetIdentitySha256,
|
|
2361
2617
|
expectedApprovedTargetIdentityKind,
|
|
2618
|
+
expectedVariant,
|
|
2362
2619
|
);
|
|
2363
2620
|
const helperPath = join(appPath, "Contents", "Helpers", "recordings");
|
|
2364
2621
|
if (companionVersion(helperPath) !== manifest.companion.version) {
|
|
@@ -2369,8 +2626,8 @@ function verifyActiveApp(
|
|
|
2369
2626
|
const environment: NodeJS.ProcessEnv = {
|
|
2370
2627
|
HOME: contractHome,
|
|
2371
2628
|
PATH: "/usr/bin:/bin:/usr/sbin:/sbin",
|
|
2372
|
-
|
|
2373
|
-
|
|
2629
|
+
HASNA_RECORDINGS_CLIENT_STORE: "sqlite",
|
|
2630
|
+
RECORDINGS_CLIENT_STORE: "sqlite",
|
|
2374
2631
|
HASNA_RECORDINGS_DB_PATH: join(contractHome, "recordings.db"),
|
|
2375
2632
|
RECORDINGS_AUDIO_DIR: join(contractHome, "audio"),
|
|
2376
2633
|
};
|
|
@@ -2702,7 +2959,7 @@ function readJournal(path: string): InstallJournal {
|
|
|
2702
2959
|
journal.approved_target_identity_sha256 !== "none" ||
|
|
2703
2960
|
journal.builder_identity_kind !== "none")) ||
|
|
2704
2961
|
(journal.artifact_policy === "local_only" &&
|
|
2705
|
-
(journal.approved_target
|
|
2962
|
+
(!isLocalOnlyApprovedTarget(journal.approved_target) ||
|
|
2706
2963
|
!isTargetIdentityKind(journal.approved_target_identity_kind) ||
|
|
2707
2964
|
!isTargetIdentityKind(journal.builder_identity_kind) ||
|
|
2708
2965
|
journal.approved_target_identity_kind !== journal.builder_identity_kind ||
|
|
@@ -5021,6 +5278,9 @@ function manifestGet(path: string, expectedManifestSha256: string, field: string
|
|
|
5021
5278
|
}
|
|
5022
5279
|
else if (field === "approved_target_identity_sha256") console.log(manifest.approved_target_identity_sha256);
|
|
5023
5280
|
else if (field === "builder_identity_kind") console.log(manifestBuilderIdentityKind(manifest));
|
|
5281
|
+
else if (field === "variant") console.log(manifestVariant(manifest));
|
|
5282
|
+
// Pre-variant manifests carry no bundle_name; they are full builds by definition.
|
|
5283
|
+
else if (field === "bundle_name") console.log(manifestBundleName(manifest));
|
|
5024
5284
|
else throw new Error(`unsupported manifest field: ${field}`);
|
|
5025
5285
|
}
|
|
5026
5286
|
|
|
@@ -5047,6 +5307,14 @@ function repeatedArguments(name: string): string[] {
|
|
|
5047
5307
|
return values;
|
|
5048
5308
|
}
|
|
5049
5309
|
|
|
5310
|
+
function variantArgument(defaultVariant: ArtifactVariant): ArtifactVariant {
|
|
5311
|
+
const value = optionalArgument("--variant") ?? defaultVariant;
|
|
5312
|
+
if (value !== "full" && value !== "bar") {
|
|
5313
|
+
throw new Error(`artifact variant must be full or bar; got: ${value}`);
|
|
5314
|
+
}
|
|
5315
|
+
return value;
|
|
5316
|
+
}
|
|
5317
|
+
|
|
5050
5318
|
function artifactPolicyArgument(): ArtifactPolicy {
|
|
5051
5319
|
const value = argument("--artifact-policy");
|
|
5052
5320
|
if (value !== "release" && value !== "local_only") {
|
|
@@ -5109,6 +5377,7 @@ function main(): void {
|
|
|
5109
5377
|
argument("--approved-target-identity-sha256"),
|
|
5110
5378
|
builderIdentityKindArgument(policy),
|
|
5111
5379
|
argument("--builder-identity-sha256"),
|
|
5380
|
+
variantArgument("full"),
|
|
5112
5381
|
);
|
|
5113
5382
|
} else if (command === "finalize") {
|
|
5114
5383
|
const sourceSha = argument("--source-sha");
|
|
@@ -5123,6 +5392,7 @@ function main(): void {
|
|
|
5123
5392
|
argument("--notary-log"),
|
|
5124
5393
|
argument("--notary-submission-id"),
|
|
5125
5394
|
argument("--submitted-archive-sha256"),
|
|
5395
|
+
variantArgument("full"),
|
|
5126
5396
|
);
|
|
5127
5397
|
} else if (command === "finalize-local") {
|
|
5128
5398
|
const sourceSha = argument("--source-sha");
|
|
@@ -5135,6 +5405,8 @@ function main(): void {
|
|
|
5135
5405
|
argument("--approved-target"),
|
|
5136
5406
|
targetIdentityKindArgument(undefined, true),
|
|
5137
5407
|
argument("--approved-target-identity-sha256"),
|
|
5408
|
+
optionalArgument("--expected-team-id") ?? "ADHOC",
|
|
5409
|
+
variantArgument("full"),
|
|
5138
5410
|
);
|
|
5139
5411
|
} else if (command === "verify-archive") {
|
|
5140
5412
|
const teamId = argument("--team-id");
|
|
@@ -5150,6 +5422,7 @@ function main(): void {
|
|
|
5150
5422
|
argument("--approved-target"),
|
|
5151
5423
|
argument("--approved-target-identity-sha256"),
|
|
5152
5424
|
targetIdentityKindArgument(policy),
|
|
5425
|
+
variantArgument("full"),
|
|
5153
5426
|
);
|
|
5154
5427
|
} else if (command === "extract-verified-archive") {
|
|
5155
5428
|
const teamId = argument("--team-id");
|
|
@@ -5166,6 +5439,7 @@ function main(): void {
|
|
|
5166
5439
|
argument("--approved-target"),
|
|
5167
5440
|
argument("--approved-target-identity-sha256"),
|
|
5168
5441
|
targetIdentityKindArgument(policy),
|
|
5442
|
+
variantArgument("full"),
|
|
5169
5443
|
);
|
|
5170
5444
|
} else if (command === "verify-app") {
|
|
5171
5445
|
const teamId = argument("--team-id");
|
|
@@ -5179,6 +5453,7 @@ function main(): void {
|
|
|
5179
5453
|
argument("--approved-target"),
|
|
5180
5454
|
argument("--approved-target-identity-sha256"),
|
|
5181
5455
|
targetIdentityKindArgument(policy),
|
|
5456
|
+
variantArgument("full"),
|
|
5182
5457
|
);
|
|
5183
5458
|
} else if (command === "verify-active") {
|
|
5184
5459
|
const teamId = argument("--team-id");
|
|
@@ -5192,6 +5467,7 @@ function main(): void {
|
|
|
5192
5467
|
argument("--approved-target"),
|
|
5193
5468
|
argument("--approved-target-identity-sha256"),
|
|
5194
5469
|
targetIdentityKindArgument(policy),
|
|
5470
|
+
variantArgument("full"),
|
|
5195
5471
|
);
|
|
5196
5472
|
} else if (command === "assert-release") {
|
|
5197
5473
|
assertExpectedRelease(
|
|
@@ -5207,7 +5483,11 @@ function main(): void {
|
|
|
5207
5483
|
argument("--manifest-sha256"),
|
|
5208
5484
|
);
|
|
5209
5485
|
} else if (command === "requirement-digest") {
|
|
5210
|
-
requirementDigest(
|
|
5486
|
+
requirementDigest(
|
|
5487
|
+
argument("--app"),
|
|
5488
|
+
artifactPolicyArgument(),
|
|
5489
|
+
optionalArgument("--expected-team-id") ?? "ADHOC",
|
|
5490
|
+
);
|
|
5211
5491
|
} else if (command === "tailscale-node-id-sha256") {
|
|
5212
5492
|
console.log(tailscaleNodeIdSha256(readFileSync(0, "utf8"), argument("--expected-hostname")));
|
|
5213
5493
|
} else if (command === "verify-filesystem-tree") {
|