@hasna/recordings 0.2.10 → 0.2.13

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.
Files changed (167) hide show
  1. package/README.md +350 -35
  2. package/bun.lock +3 -0
  3. package/dist/__tests__/helpers/native-fs-guard.d.ts +2 -0
  4. package/dist/__tests__/helpers/native-fs-guard.d.ts.map +1 -0
  5. package/dist/cli/index.js +1728 -320
  6. package/dist/cli/macos-permissions.d.ts +13 -0
  7. package/dist/cli/macos-permissions.d.ts.map +1 -0
  8. package/dist/cli/options.d.ts +12 -0
  9. package/dist/cli/options.d.ts.map +1 -1
  10. package/dist/db/pg-migrations.d.ts.map +1 -1
  11. package/dist/db/recordings.d.ts +2 -1
  12. package/dist/db/recordings.d.ts.map +1 -1
  13. package/dist/db/remote-storage.d.ts +5 -1
  14. package/dist/db/remote-storage.d.ts.map +1 -1
  15. package/dist/index.d.ts +2 -2
  16. package/dist/index.d.ts.map +1 -1
  17. package/dist/index.js +628 -147
  18. package/dist/lib/bun-runtime.d.ts +11 -0
  19. package/dist/lib/bun-runtime.d.ts.map +1 -0
  20. package/dist/lib/config.d.ts +8 -1
  21. package/dist/lib/config.d.ts.map +1 -1
  22. package/dist/lib/enhancer.d.ts +10 -2
  23. package/dist/lib/enhancer.d.ts.map +1 -1
  24. package/dist/lib/install-maintenance.d.ts +24 -0
  25. package/dist/lib/install-maintenance.d.ts.map +1 -0
  26. package/dist/lib/machine.d.ts +2 -0
  27. package/dist/lib/machine.d.ts.map +1 -0
  28. package/dist/lib/recorder.d.ts +2 -1
  29. package/dist/lib/recorder.d.ts.map +1 -1
  30. package/dist/lib/recording-create-identity.d.ts +13 -0
  31. package/dist/lib/recording-create-identity.d.ts.map +1 -0
  32. package/dist/lib/release-install-policy.d.ts +31 -0
  33. package/dist/lib/release-install-policy.d.ts.map +1 -0
  34. package/dist/lib/transcriber.d.ts.map +1 -1
  35. package/dist/mcp/index.d.ts.map +1 -1
  36. package/dist/mcp/index.js +977 -194
  37. package/dist/sdk/index.js +5 -1
  38. package/dist/sdk/v1.generated.d.ts +4 -0
  39. package/dist/sdk/v1.generated.d.ts.map +1 -1
  40. package/dist/server/cloud-config.d.ts +9 -0
  41. package/dist/server/cloud-config.d.ts.map +1 -0
  42. package/dist/server/cloud-readiness.d.ts +10 -0
  43. package/dist/server/cloud-readiness.d.ts.map +1 -0
  44. package/dist/server/cloud.d.ts +9 -18
  45. package/dist/server/cloud.d.ts.map +1 -1
  46. package/dist/server/index.js +1654 -304
  47. package/dist/server/migrate-command.d.ts +11 -0
  48. package/dist/server/migrate-command.d.ts.map +1 -0
  49. package/dist/server/openapi.d.ts +22 -0
  50. package/dist/server/openapi.d.ts.map +1 -1
  51. package/dist/server/repo.d.ts +8 -1
  52. package/dist/server/repo.d.ts.map +1 -1
  53. package/dist/server/serve.d.ts +6 -1
  54. package/dist/server/serve.d.ts.map +1 -1
  55. package/dist/server/v1.d.ts.map +1 -1
  56. package/dist/storage.js +515 -80
  57. package/dist/store.d.ts +3 -1
  58. package/dist/store.d.ts.map +1 -1
  59. package/dist/types/index.d.ts +10 -0
  60. package/dist/types/index.d.ts.map +1 -1
  61. package/dist/version.d.ts +1 -1
  62. package/package.json +7 -4
  63. package/packaging/macos/Empty.entitlements +5 -0
  64. package/packaging/macos/Library/LaunchDaemons/com.hasna.recordings.updater.plist +34 -0
  65. package/packaging/macos/Verifier.entitlements +10 -0
  66. package/packaging/macos/artifact-verifier.sb +33 -0
  67. package/packaging/macos/build_release_pkg.sh +872 -0
  68. package/packaging/macos/managed_bootstrap.sh +623 -0
  69. package/packaging/macos/pkgutil_fingerprint.awk +37 -0
  70. package/packaging/macos/release_lifecycle.ts +463 -0
  71. package/packaging/macos/scripts/postinstall +565 -0
  72. package/packaging/macos/scripts/preinstall +297 -0
  73. package/scripts/build_companion_cli.sh +337 -0
  74. package/scripts/build_native_fs_guard.sh +59 -0
  75. package/scripts/generate-sdk.ts +19 -1
  76. package/scripts/install_macos_app.sh +1922 -77
  77. package/scripts/macos_artifact.ts +5316 -0
  78. package/scripts/migrate.ts +38 -10
  79. package/scripts/native/prebuilds/darwin-universal/recordings_fs_guard.node +0 -0
  80. package/scripts/native/recordings_fs_guard.c +1167 -0
  81. package/scripts/native_fs_guard.ts +158 -0
  82. package/scripts/release-guard.ts +20 -1
  83. package/scripts/resolve_tailscale_cli.sh +389 -0
  84. package/scripts/smoke_macos_app.sh +573 -0
  85. package/src/native/Recordings/App/ContentView.swift +83 -0
  86. package/src/native/Recordings/App/MenuBarStatusView.swift +102 -0
  87. package/src/native/Recordings/App/RecordWorkspaceView.swift +468 -0
  88. package/src/native/Recordings/App/RecordingDetailView.swift +138 -0
  89. package/src/native/Recordings/App/RecordingsApp.swift +367 -44
  90. package/src/native/Recordings/App/RecordingsListView.swift +130 -0
  91. package/src/native/Recordings/App/RecordingsStore.swift +206 -0
  92. package/src/native/Recordings/App/RuntimeSmoke.swift +158 -0
  93. package/src/native/Recordings/App/SidebarView.swift +212 -0
  94. package/src/native/Recordings/App/Theme.swift +87 -0
  95. package/src/native/Recordings/Package.resolved +5 -5
  96. package/src/native/Recordings/Package.swift +62 -2
  97. package/src/native/Recordings/RecordingsLib/AccessibilityPromptGate.swift +104 -0
  98. package/src/native/Recordings/RecordingsLib/BrandAssets.swift +0 -32
  99. package/src/native/Recordings/RecordingsLib/ChromeSurface.swift +17 -0
  100. package/src/native/Recordings/RecordingsLib/Info.plist +7 -3
  101. package/src/native/Recordings/RecordingsLib/MenuBarPresentation.swift +37 -0
  102. package/src/native/Recordings/RecordingsLib/NativeAppDiagnostics.swift +32 -2
  103. package/src/native/Recordings/RecordingsLib/NativeMachineIdentity.swift +24 -0
  104. package/src/native/Recordings/RecordingsLib/NativePCMRecorder.swift +262 -23
  105. package/src/native/Recordings/RecordingsLib/OpenAIAPIKeyStore.swift +56 -0
  106. package/src/native/Recordings/RecordingsLib/PermissionRequestLaunchPlan.swift +56 -0
  107. package/src/native/Recordings/RecordingsLib/ProjectStore.swift +321 -25
  108. package/src/native/Recordings/RecordingsLib/RealtimeTranscriptionClient.swift +605 -90
  109. package/src/native/Recordings/RecordingsLib/Recording.swift +141 -0
  110. package/src/native/Recordings/RecordingsLib/RecordingEngine.swift +3704 -376
  111. package/src/native/Recordings/RecordingsLib/RecordingStartControlPresentation.swift +29 -0
  112. package/src/native/Recordings/RecordingsLib/RecordingsCLI.entitlements +10 -0
  113. package/src/native/Recordings/RecordingsLib/RecordingsCLI.swift +222 -0
  114. package/src/native/Recordings/RecordingsLib/SettingsView.swift +45 -11
  115. package/src/native/Recordings/RecordingsLib/SpeechIntent.swift +331 -0
  116. package/src/native/Recordings/RecordingsLib/SpeechIntentClassifier.swift +232 -0
  117. package/src/native/Recordings/RecordingsLib/VoiceShortcuts.swift +17 -7
  118. package/src/native/Recordings/RecordingsTests/CLIRunnerTests.swift +1343 -0
  119. package/src/native/Recordings/RecordingsTests/MenuBarPresentationTests.swift +98 -0
  120. package/src/native/Recordings/RecordingsTests/NativeAppDiagnosticsTests.swift +20 -0
  121. package/src/native/Recordings/RecordingsTests/NativePCMRecorderTests.swift +370 -1
  122. package/src/native/Recordings/RecordingsTests/PasteTargetTests.swift +987 -1
  123. package/src/native/Recordings/RecordingsTests/ProjectStoreTests.swift +385 -0
  124. package/src/native/Recordings/RecordingsTests/RealtimeTranscriptionTests.swift +835 -8
  125. package/src/native/Recordings/RecordingsTests/RecordingBridgeTests.swift +180 -0
  126. package/src/native/Recordings/RecordingsTests/RecordingEngineDeliveryTests.swift +760 -0
  127. package/src/native/Recordings/RecordingsTests/RecordingStartControlPresentationTests.swift +42 -0
  128. package/src/native/Recordings/RecordingsTests/RecordingStartGateTests.swift +264 -0
  129. package/src/native/Recordings/RecordingsTests/RecordingStartTimingTests.swift +129 -0
  130. package/src/native/Recordings/RecordingsTests/SpeechIntentTests.swift +600 -0
  131. package/src/native/Recordings/RecordingsTests/TranscriptResolutionTests.swift +62 -1
  132. package/src/native/Recordings/RecordingsTests/TranscriptionResultIdentityTests.swift +21 -0
  133. package/src/native/Recordings/Updater/BootstrapPreflight/BootstrapPreflightMain.swift +739 -0
  134. package/src/native/Recordings/Updater/Broker/ActivationRecoveryPolicy.swift +254 -0
  135. package/src/native/Recordings/Updater/Broker/ApplicationNamespace.swift +463 -0
  136. package/src/native/Recordings/Updater/Broker/ApplicationProcessQuiescence.swift +55 -0
  137. package/src/native/Recordings/Updater/Broker/ArtifactIngest.swift +341 -0
  138. package/src/native/Recordings/Updater/Broker/AtomicActivation.swift +477 -0
  139. package/src/native/Recordings/Updater/Broker/BrokerMain.swift +624 -0
  140. package/src/native/Recordings/Updater/Broker/CanonicalTreeCopy.swift +63 -0
  141. package/src/native/Recordings/Updater/Broker/CodeValidation.swift +561 -0
  142. package/src/native/Recordings/Updater/Broker/DarwinACLValidator.swift +66 -0
  143. package/src/native/Recordings/Updater/Broker/HostOSProductVersion.swift +50 -0
  144. package/src/native/Recordings/Updater/Broker/InstallJournal.swift +362 -0
  145. package/src/native/Recordings/Updater/Broker/InstallRecovery.swift +662 -0
  146. package/src/native/Recordings/Updater/Broker/MonotonicState.swift +456 -0
  147. package/src/native/Recordings/Updater/Broker/PeerIdentity.swift +274 -0
  148. package/src/native/Recordings/Updater/Broker/VerifierRunner.swift +81 -0
  149. package/src/native/Recordings/Updater/BrokerTests/ActivationRecoveryPolicyTests.swift +259 -0
  150. package/src/native/Recordings/Updater/BrokerTests/CanonicalReleaseOrderTests.swift +21 -0
  151. package/src/native/Recordings/Updater/Client/ClientMain.swift +119 -0
  152. package/src/native/Recordings/Updater/Protocol/BoundedProcess.swift +159 -0
  153. package/src/native/Recordings/Updater/Protocol/CandidateMetadataPolicy.swift +214 -0
  154. package/src/native/Recordings/Updater/Protocol/HostOSVersionPolicy.swift +55 -0
  155. package/src/native/Recordings/Updater/Protocol/MonotonicReleasePolicy.swift +152 -0
  156. package/src/native/Recordings/Updater/Protocol/ReleaseEnvelope.swift +229 -0
  157. package/src/native/Recordings/Updater/Protocol/UpdateProtocol.swift +119 -0
  158. package/src/native/Recordings/Updater/ProtocolTests/BoundedProcessRunnerTests.swift +60 -0
  159. package/src/native/Recordings/Updater/ProtocolTests/CandidateMetadataPolicyTests.swift +168 -0
  160. package/src/native/Recordings/Updater/ProtocolTests/HostOSVersionPolicyTests.swift +62 -0
  161. package/src/native/Recordings/Updater/ProtocolTests/MonotonicReleasePolicyTests.swift +172 -0
  162. package/src/native/Recordings/Updater/ProtocolTests/ReleaseEnvelopeValidationTests.swift +65 -0
  163. package/src/native/Recordings/Updater/Signer/SignerMain.swift +210 -0
  164. package/src/native/Recordings/Updater/VerifierLauncher/RecordingsVerifierLauncher.c +725 -0
  165. package/src/native/Recordings/Updater/VerifierLauncher/include/RecordingsVerifierLauncher.h +33 -0
  166. package/src/native/Recordings/build.sh +1990 -29
  167. package/src/native/Recordings/RecordingsLib/MenuBarPopover.swift +0 -380
@@ -0,0 +1,463 @@
1
+ import { createHash, randomUUID } from "node:crypto";
2
+ import { createReadStream, readFileSync, writeFileSync } from "node:fs";
3
+ import { isAbsolute } from "node:path";
4
+
5
+ const LIFECYCLE = "bootstrap-v1-app-updates-only" as const;
6
+ const PROTOCOL_VERSION = 1 as const;
7
+ const COMPATIBLE_COHORT_SCHEMA_VERSION = 2 as const;
8
+ const JSON_INPUT_LIMIT = 64 * 1024;
9
+ const REQUIREMENT_LIMIT = 8 * 1024;
10
+ const DIGEST_PATTERN = /^[a-f0-9]{64}$/;
11
+ const TEAM_PATTERN = /^[A-Z0-9]{10}$/;
12
+ const VERSION_PATTERN = /^[0-9]+\.[0-9]+\.[0-9]+(?:-[0-9A-Za-z.-]+)?$/;
13
+
14
+ export const COMPATIBLE_COHORT_KEYS = [
15
+ "artifact_verifier_designated_requirement",
16
+ "artifact_verifier_sha256",
17
+ "bootstrap_marker_sha256",
18
+ "envelope_public_key_sha256",
19
+ "installer_certificate_sha256",
20
+ "key_epoch",
21
+ "key_rotation_supported",
22
+ "lifecycle",
23
+ "minimum_broker_version",
24
+ "package_sha256",
25
+ "protocol_version",
26
+ "root_maintenance_supported",
27
+ "schema_version",
28
+ "signing_team_identifier",
29
+ "update_broker_designated_requirement",
30
+ "update_broker_sha256",
31
+ ] as const;
32
+
33
+ export type CompatibleCohortManifest = {
34
+ artifact_verifier_designated_requirement: string;
35
+ artifact_verifier_sha256: string;
36
+ bootstrap_marker_sha256: string;
37
+ envelope_public_key_sha256: string;
38
+ installer_certificate_sha256: string;
39
+ key_epoch: number;
40
+ key_rotation_supported: false;
41
+ lifecycle: typeof LIFECYCLE;
42
+ minimum_broker_version: string;
43
+ package_sha256: string;
44
+ protocol_version: typeof PROTOCOL_VERSION;
45
+ root_maintenance_supported: false;
46
+ schema_version: typeof COMPATIBLE_COHORT_SCHEMA_VERSION;
47
+ signing_team_identifier: string;
48
+ update_broker_designated_requirement: string;
49
+ update_broker_sha256: string;
50
+ };
51
+
52
+ type CompatibleCohortExpectations = {
53
+ teamIdentifier: string;
54
+ keyEpoch: number;
55
+ envelopePublicKeySHA256: string;
56
+ };
57
+
58
+ function isRecord(value: unknown): value is Record<string, unknown> {
59
+ return value !== null && typeof value === "object" && !Array.isArray(value);
60
+ }
61
+
62
+ function sha256(value: string | Uint8Array): string {
63
+ return createHash("sha256").update(value).digest("hex");
64
+ }
65
+
66
+ function canonicalJson(value: Record<string, unknown>): string {
67
+ const sorted = Object.fromEntries(Object.keys(value).sort().map((key) => [key, value[key]]));
68
+ return `${JSON.stringify(sorted)}\n`;
69
+ }
70
+
71
+ function requireDigest(value: unknown, label: string): asserts value is string {
72
+ if (typeof value !== "string" || !DIGEST_PATTERN.test(value)) {
73
+ throw new Error(`compatible-cohort ${label} must be a lowercase SHA-256 digest`);
74
+ }
75
+ }
76
+
77
+ function requireRequirement(value: unknown, label: string): asserts value is string {
78
+ if (
79
+ typeof value !== "string" ||
80
+ value.length === 0 ||
81
+ Buffer.byteLength(value, "utf8") > REQUIREMENT_LIMIT ||
82
+ value.includes("\0") ||
83
+ value.includes("\n") ||
84
+ value.includes("\r")
85
+ ) {
86
+ throw new Error(`compatible-cohort ${label} is missing or invalid`);
87
+ }
88
+ }
89
+
90
+ export function parseCompatibleCohortManifest(
91
+ value: unknown,
92
+ expected: CompatibleCohortExpectations,
93
+ ): CompatibleCohortManifest {
94
+ if (!isRecord(value)) throw new Error("compatible-cohort manifest must be a JSON object");
95
+ const actualKeys = Object.keys(value).sort();
96
+ const expectedKeys = [...COMPATIBLE_COHORT_KEYS].sort();
97
+ if (JSON.stringify(actualKeys) !== JSON.stringify(expectedKeys)) {
98
+ throw new Error("compatible-cohort manifest does not have the exact schema-v2 key set");
99
+ }
100
+ if (value.schema_version !== COMPATIBLE_COHORT_SCHEMA_VERSION) {
101
+ throw new Error("compatible-cohort schema_version must be 2");
102
+ }
103
+ if (value.protocol_version !== PROTOCOL_VERSION) {
104
+ throw new Error("compatible-cohort protocol_version must be 1");
105
+ }
106
+ if (!Number.isSafeInteger(value.key_epoch) || (value.key_epoch as number) <= 0) {
107
+ throw new Error("compatible-cohort key_epoch must be a positive integer");
108
+ }
109
+ if (value.key_epoch !== expected.keyEpoch) {
110
+ throw new Error("compatible-cohort key_epoch does not match the selected release key");
111
+ }
112
+ if (
113
+ typeof value.signing_team_identifier !== "string" ||
114
+ !TEAM_PATTERN.test(value.signing_team_identifier) ||
115
+ value.signing_team_identifier !== expected.teamIdentifier
116
+ ) {
117
+ throw new Error("compatible-cohort signing TeamIdentifier is invalid or mismatched");
118
+ }
119
+ if (
120
+ value.lifecycle !== LIFECYCLE ||
121
+ value.root_maintenance_supported !== false ||
122
+ value.key_rotation_supported !== false
123
+ ) {
124
+ throw new Error("compatible-cohort does not authorize the immutable app-only updater lifecycle");
125
+ }
126
+ for (const key of [
127
+ "artifact_verifier_sha256",
128
+ "bootstrap_marker_sha256",
129
+ "envelope_public_key_sha256",
130
+ "installer_certificate_sha256",
131
+ "package_sha256",
132
+ "update_broker_sha256",
133
+ ] as const) {
134
+ requireDigest(value[key], key);
135
+ }
136
+ if (value.envelope_public_key_sha256 !== expected.envelopePublicKeySHA256) {
137
+ throw new Error("compatible-cohort does not bind the selected release-envelope public key");
138
+ }
139
+ requireRequirement(
140
+ value.update_broker_designated_requirement,
141
+ "update_broker_designated_requirement",
142
+ );
143
+ requireRequirement(
144
+ value.artifact_verifier_designated_requirement,
145
+ "artifact_verifier_designated_requirement",
146
+ );
147
+ if (
148
+ typeof value.minimum_broker_version !== "string" ||
149
+ !VERSION_PATTERN.test(value.minimum_broker_version)
150
+ ) {
151
+ throw new Error("compatible-cohort minimum_broker_version is invalid");
152
+ }
153
+ return value as CompatibleCohortManifest;
154
+ }
155
+
156
+ export function createCompatibleCohortManifest(
157
+ values: Omit<CompatibleCohortManifest, "schema_version" | "protocol_version" | "lifecycle" |
158
+ "root_maintenance_supported" | "key_rotation_supported">,
159
+ ): CompatibleCohortManifest {
160
+ const manifest: CompatibleCohortManifest = {
161
+ ...values,
162
+ schema_version: COMPATIBLE_COHORT_SCHEMA_VERSION,
163
+ protocol_version: PROTOCOL_VERSION,
164
+ lifecycle: LIFECYCLE,
165
+ root_maintenance_supported: false,
166
+ key_rotation_supported: false,
167
+ };
168
+ return parseCompatibleCohortManifest(manifest, {
169
+ teamIdentifier: values.signing_team_identifier,
170
+ keyEpoch: values.key_epoch,
171
+ envelopePublicKeySHA256: values.envelope_public_key_sha256,
172
+ });
173
+ }
174
+
175
+ type UpdateManifestView = {
176
+ archiveSHA256: string;
177
+ build: string;
178
+ candidateTreeSHA256: string;
179
+ minimumOSVersion: string;
180
+ sourceCommit: string;
181
+ teamIdentifier: string;
182
+ version: string;
183
+ architectures: ["arm64", "x86_64"];
184
+ applicationDesignatedRequirementSHA256: string;
185
+ };
186
+
187
+ type UpdatePayloadInputs = {
188
+ cohort: CompatibleCohortManifest;
189
+ manifest: UpdateManifestView;
190
+ releaseSequence: number;
191
+ appArchiveSHA256: string;
192
+ appArchiveByteCount: number;
193
+ manifestSHA256: string;
194
+ manifestByteCount: number;
195
+ updateClientSHA256: string;
196
+ applicationDesignatedRequirement: string;
197
+ updateClientDesignatedRequirement: string;
198
+ expiresAtUTC: string;
199
+ now?: Date;
200
+ releaseID?: string;
201
+ };
202
+
203
+ export function createUpdateEnvelopePayload(inputs: UpdatePayloadInputs): Record<string, unknown> {
204
+ const { cohort, manifest } = inputs;
205
+ if (inputs.appArchiveSHA256 !== manifest.archiveSHA256) {
206
+ throw new Error("final app manifest does not bind the app-update archive");
207
+ }
208
+ for (const [label, value] of [
209
+ ["app archive", inputs.appArchiveSHA256],
210
+ ["manifest", inputs.manifestSHA256],
211
+ ["update client", inputs.updateClientSHA256],
212
+ ] as const) requireDigest(value, label);
213
+ if (
214
+ !Number.isSafeInteger(inputs.releaseSequence) ||
215
+ inputs.releaseSequence <= 0 ||
216
+ !Number.isSafeInteger(inputs.appArchiveByteCount) ||
217
+ inputs.appArchiveByteCount <= 0 ||
218
+ !Number.isSafeInteger(inputs.manifestByteCount) ||
219
+ inputs.manifestByteCount <= 0
220
+ ) {
221
+ throw new Error("app-update sequence or artifact sizes are invalid");
222
+ }
223
+ requireRequirement(inputs.applicationDesignatedRequirement, "application designated requirement");
224
+ requireRequirement(inputs.updateClientDesignatedRequirement, "update-client designated requirement");
225
+ if (sha256(inputs.applicationDesignatedRequirement) !== manifest.applicationDesignatedRequirementSHA256) {
226
+ throw new Error("final app manifest does not bind the app designated requirement");
227
+ }
228
+ if (JSON.stringify(manifest.architectures) !== '["arm64","x86_64"]') {
229
+ throw new Error("release app manifest must bind exactly arm64 and x86_64");
230
+ }
231
+ const now = inputs.now ?? new Date();
232
+ if (Number.isNaN(now.getTime())) throw new Error("issued-at timestamp is invalid");
233
+ return {
234
+ schema_version: PROTOCOL_VERSION,
235
+ purpose: "update",
236
+ key_epoch: cohort.key_epoch,
237
+ release_sequence: inputs.releaseSequence,
238
+ release_id: inputs.releaseID ?? randomUUID(),
239
+ version: manifest.version,
240
+ build: manifest.build,
241
+ source_commit: manifest.sourceCommit,
242
+ artifact_sha256: inputs.appArchiveSHA256,
243
+ artifact_byte_count: inputs.appArchiveByteCount,
244
+ manifest_sha256: inputs.manifestSHA256,
245
+ manifest_byte_count: inputs.manifestByteCount,
246
+ candidate_tree_sha256: manifest.candidateTreeSHA256,
247
+ package_sha256: cohort.package_sha256,
248
+ update_client_sha256: inputs.updateClientSHA256,
249
+ update_broker_sha256: cohort.update_broker_sha256,
250
+ artifact_verifier_sha256: cohort.artifact_verifier_sha256,
251
+ bootstrap_marker_sha256: cohort.bootstrap_marker_sha256,
252
+ architectures: manifest.architectures,
253
+ minimum_os_version: manifest.minimumOSVersion,
254
+ minimum_broker_version: cohort.minimum_broker_version,
255
+ signing_team_identifier: cohort.signing_team_identifier,
256
+ application_designated_requirement: inputs.applicationDesignatedRequirement,
257
+ update_client_designated_requirement: inputs.updateClientDesignatedRequirement,
258
+ update_broker_designated_requirement: cohort.update_broker_designated_requirement,
259
+ artifact_verifier_designated_requirement: cohort.artifact_verifier_designated_requirement,
260
+ installer_certificate_sha256: cohort.installer_certificate_sha256,
261
+ issued_at_utc: now.toISOString(),
262
+ expires_at_utc: inputs.expiresAtUTC,
263
+ };
264
+ }
265
+
266
+ function readJson(path: string, label: string): unknown {
267
+ const bytes = readFileSync(path);
268
+ if (bytes.length === 0 || bytes.length > JSON_INPUT_LIMIT) {
269
+ throw new Error(`${label} is empty or exceeds ${JSON_INPUT_LIMIT} bytes`);
270
+ }
271
+ return JSON.parse(bytes.toString("utf8")) as unknown;
272
+ }
273
+
274
+ async function sha256File(path: string): Promise<string> {
275
+ const hash = createHash("sha256");
276
+ for await (const chunk of createReadStream(path)) hash.update(chunk);
277
+ return hash.digest("hex");
278
+ }
279
+
280
+ function parsePositiveInteger(value: string, label: string): number {
281
+ if (!/^[1-9][0-9]*$/.test(value)) throw new Error(`${label} must be a positive integer`);
282
+ const parsed = Number(value);
283
+ if (!Number.isSafeInteger(parsed)) throw new Error(`${label} exceeds the safe integer range`);
284
+ return parsed;
285
+ }
286
+
287
+ function parseArguments(allowed: readonly string[]): Map<string, string> {
288
+ const values = new Map<string, string>();
289
+ for (let index = 3; index < Bun.argv.length; index += 2) {
290
+ const key = Bun.argv[index];
291
+ const value = Bun.argv[index + 1];
292
+ if (!key || !allowed.includes(key) || !value || values.has(key)) {
293
+ throw new Error(`unknown, duplicate, or incomplete release-lifecycle argument: ${key ?? "missing"}`);
294
+ }
295
+ values.set(key, value);
296
+ }
297
+ for (const key of allowed) {
298
+ if (!values.has(key)) throw new Error(`missing required release-lifecycle argument ${key}`);
299
+ }
300
+ return values;
301
+ }
302
+
303
+ function required(values: Map<string, string>, key: string): string {
304
+ const value = values.get(key);
305
+ if (!value) throw new Error(`missing required release-lifecycle argument ${key}`);
306
+ return value;
307
+ }
308
+
309
+ function writeExclusiveJson(path: string, value: Record<string, unknown>): void {
310
+ if (!isAbsolute(path)) throw new Error("release-lifecycle output path must be absolute");
311
+ writeFileSync(path, canonicalJson(value), { encoding: "utf8", flag: "wx", mode: 0o400 });
312
+ }
313
+
314
+ function manifestView(value: unknown): UpdateManifestView {
315
+ if (!isRecord(value)) throw new Error("final app manifest must be a JSON object");
316
+ const archive = value.archive;
317
+ const binding = value.binding;
318
+ const signing = value.signing;
319
+ if (!isRecord(archive) || !isRecord(binding) || !isRecord(signing)) {
320
+ throw new Error("final app manifest is missing release bindings");
321
+ }
322
+ const architectures = value.architectures;
323
+ const view = {
324
+ archiveSHA256: archive.sha256,
325
+ build: value.bundle_build_version,
326
+ candidateTreeSHA256: binding.bundle_tree_sha256,
327
+ minimumOSVersion: value.minimum_macos,
328
+ sourceCommit: value.git_sha,
329
+ teamIdentifier: value.team_id,
330
+ version: value.bundle_version,
331
+ architectures,
332
+ applicationDesignatedRequirementSHA256: signing.designated_requirement_sha256,
333
+ };
334
+ if (
335
+ typeof view.archiveSHA256 !== "string" || !DIGEST_PATTERN.test(view.archiveSHA256) ||
336
+ typeof view.candidateTreeSHA256 !== "string" || !DIGEST_PATTERN.test(view.candidateTreeSHA256) ||
337
+ typeof view.applicationDesignatedRequirementSHA256 !== "string" ||
338
+ !DIGEST_PATTERN.test(view.applicationDesignatedRequirementSHA256) ||
339
+ typeof view.build !== "string" || !/^[0-9]+(?:\.[0-9]+){0,2}$/.test(view.build) ||
340
+ typeof view.minimumOSVersion !== "string" || !/^\d+(?:\.\d+){1,2}$/.test(view.minimumOSVersion) ||
341
+ typeof view.sourceCommit !== "string" || !/^[a-f0-9]{40}$/.test(view.sourceCommit) ||
342
+ typeof view.teamIdentifier !== "string" || !TEAM_PATTERN.test(view.teamIdentifier) ||
343
+ typeof view.version !== "string" || !VERSION_PATTERN.test(view.version) ||
344
+ !Array.isArray(architectures) || JSON.stringify(architectures) !== '["arm64","x86_64"]'
345
+ ) {
346
+ throw new Error("final app manifest contains invalid release bindings");
347
+ }
348
+ return view as UpdateManifestView;
349
+ }
350
+
351
+ async function main(): Promise<void> {
352
+ const command = Bun.argv[2];
353
+ if (command === "write-compatible-cohort") {
354
+ const allowed = [
355
+ "--artifact-verifier-designated-requirement",
356
+ "--artifact-verifier-sha256",
357
+ "--bootstrap-marker-sha256",
358
+ "--envelope-public-key-sha256",
359
+ "--installer-certificate-sha256",
360
+ "--key-epoch",
361
+ "--minimum-broker-version",
362
+ "--output",
363
+ "--package-sha256",
364
+ "--team-id",
365
+ "--update-broker-designated-requirement",
366
+ "--update-broker-sha256",
367
+ ] as const;
368
+ const values = parseArguments(allowed);
369
+ const manifest = createCompatibleCohortManifest({
370
+ artifact_verifier_designated_requirement: required(values, "--artifact-verifier-designated-requirement"),
371
+ artifact_verifier_sha256: required(values, "--artifact-verifier-sha256"),
372
+ bootstrap_marker_sha256: required(values, "--bootstrap-marker-sha256"),
373
+ envelope_public_key_sha256: required(values, "--envelope-public-key-sha256"),
374
+ installer_certificate_sha256: required(values, "--installer-certificate-sha256"),
375
+ key_epoch: parsePositiveInteger(required(values, "--key-epoch"), "key epoch"),
376
+ minimum_broker_version: required(values, "--minimum-broker-version"),
377
+ package_sha256: required(values, "--package-sha256"),
378
+ signing_team_identifier: required(values, "--team-id"),
379
+ update_broker_designated_requirement: required(values, "--update-broker-designated-requirement"),
380
+ update_broker_sha256: required(values, "--update-broker-sha256"),
381
+ });
382
+ writeExclusiveJson(required(values, "--output"), manifest);
383
+ return;
384
+ }
385
+ if (command === "validate-compatible-cohort") {
386
+ const allowed = ["--manifest", "--public-key", "--team-id", "--key-epoch"] as const;
387
+ const values = parseArguments(allowed);
388
+ parseCompatibleCohortManifest(readJson(required(values, "--manifest"), "compatible-cohort manifest"), {
389
+ teamIdentifier: required(values, "--team-id"),
390
+ keyEpoch: parsePositiveInteger(required(values, "--key-epoch"), "key epoch"),
391
+ envelopePublicKeySHA256: await sha256File(required(values, "--public-key")),
392
+ });
393
+ return;
394
+ }
395
+ if (command === "write-update-payload") {
396
+ const allowed = [
397
+ "--app-archive",
398
+ "--application-designated-requirement",
399
+ "--compatible-cohort-manifest",
400
+ "--envelope-public-key",
401
+ "--expires-at-utc",
402
+ "--key-epoch",
403
+ "--manifest",
404
+ "--output",
405
+ "--release-sequence",
406
+ "--source-sha",
407
+ "--team-id",
408
+ "--update-client",
409
+ "--update-client-designated-requirement",
410
+ "--version",
411
+ ] as const;
412
+ const values = parseArguments(allowed);
413
+ const manifestPath = required(values, "--manifest");
414
+ const archivePath = required(values, "--app-archive");
415
+ const publicKeyPath = required(values, "--envelope-public-key");
416
+ const keyEpoch = parsePositiveInteger(required(values, "--key-epoch"), "key epoch");
417
+ const teamIdentifier = required(values, "--team-id");
418
+ const cohort = parseCompatibleCohortManifest(
419
+ readJson(required(values, "--compatible-cohort-manifest"), "compatible-cohort manifest"),
420
+ {
421
+ teamIdentifier,
422
+ keyEpoch,
423
+ envelopePublicKeySHA256: await sha256File(publicKeyPath),
424
+ },
425
+ );
426
+ const manifestBytes = readFileSync(manifestPath);
427
+ if (manifestBytes.length === 0 || manifestBytes.length > 16 * 1024 * 1024) {
428
+ throw new Error("final app manifest is empty or too large");
429
+ }
430
+ const view = manifestView(JSON.parse(manifestBytes.toString("utf8")) as unknown);
431
+ if (
432
+ view.teamIdentifier !== teamIdentifier ||
433
+ view.sourceCommit !== required(values, "--source-sha") ||
434
+ view.version !== required(values, "--version")
435
+ ) {
436
+ throw new Error("final app manifest does not match the selected release identity");
437
+ }
438
+ const archive = Bun.file(archivePath);
439
+ const payload = createUpdateEnvelopePayload({
440
+ cohort,
441
+ manifest: view,
442
+ releaseSequence: parsePositiveInteger(required(values, "--release-sequence"), "release sequence"),
443
+ appArchiveSHA256: await sha256File(archivePath),
444
+ appArchiveByteCount: archive.size,
445
+ manifestSHA256: sha256(manifestBytes),
446
+ manifestByteCount: manifestBytes.length,
447
+ updateClientSHA256: await sha256File(required(values, "--update-client")),
448
+ applicationDesignatedRequirement: required(values, "--application-designated-requirement"),
449
+ updateClientDesignatedRequirement: required(values, "--update-client-designated-requirement"),
450
+ expiresAtUTC: required(values, "--expires-at-utc"),
451
+ });
452
+ writeExclusiveJson(required(values, "--output"), payload);
453
+ return;
454
+ }
455
+ throw new Error("release-lifecycle command must be write-compatible-cohort, validate-compatible-cohort, or write-update-payload");
456
+ }
457
+
458
+ if (import.meta.main) {
459
+ main().catch((error) => {
460
+ console.error(error instanceof Error ? error.message : String(error));
461
+ process.exit(1);
462
+ });
463
+ }