@hasna/recordings 0.3.1 → 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/dist/__tests__/helpers/installer-guard-execution.d.ts.map +1 -1
- package/dist/cli/index.js +18 -5
- package/dist/http/client.d.ts +9 -0
- package/dist/http/client.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/mcp/index.js +2 -2
- package/dist/server/index.js +2 -2
- package/dist/storage.js +2 -2
- package/dist/version.d.ts +1 -1
- package/package.json +1 -1
- package/scripts/install_macos_app.sh +54 -7
- package/scripts/macos_artifact.ts +118 -10
- package/scripts/smoke_macos_app.sh +62 -16
- 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 +2 -2
- package/src/native/Recordings/RecordingsLib/PermissionRequestLaunchPlan.swift +21 -1
- package/src/native/Recordings/RecordingsTests/BarOnlyLaunchPlanTests.swift +99 -0
- package/src/native/Recordings/build.sh +152 -12
|
@@ -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 (Recordings.app for the
|
|
187
|
+
/// full app, HasnaRecordings.app for 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 ?? "Recordings.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;
|
|
@@ -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 = "Recordings.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],
|
|
@@ -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),
|
|
@@ -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) {
|
|
@@ -5188,6 +5278,9 @@ function manifestGet(path: string, expectedManifestSha256: string, field: string
|
|
|
5188
5278
|
}
|
|
5189
5279
|
else if (field === "approved_target_identity_sha256") console.log(manifest.approved_target_identity_sha256);
|
|
5190
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));
|
|
5191
5284
|
else throw new Error(`unsupported manifest field: ${field}`);
|
|
5192
5285
|
}
|
|
5193
5286
|
|
|
@@ -5214,6 +5307,14 @@ function repeatedArguments(name: string): string[] {
|
|
|
5214
5307
|
return values;
|
|
5215
5308
|
}
|
|
5216
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
|
+
|
|
5217
5318
|
function artifactPolicyArgument(): ArtifactPolicy {
|
|
5218
5319
|
const value = argument("--artifact-policy");
|
|
5219
5320
|
if (value !== "release" && value !== "local_only") {
|
|
@@ -5276,6 +5377,7 @@ function main(): void {
|
|
|
5276
5377
|
argument("--approved-target-identity-sha256"),
|
|
5277
5378
|
builderIdentityKindArgument(policy),
|
|
5278
5379
|
argument("--builder-identity-sha256"),
|
|
5380
|
+
variantArgument("full"),
|
|
5279
5381
|
);
|
|
5280
5382
|
} else if (command === "finalize") {
|
|
5281
5383
|
const sourceSha = argument("--source-sha");
|
|
@@ -5290,6 +5392,7 @@ function main(): void {
|
|
|
5290
5392
|
argument("--notary-log"),
|
|
5291
5393
|
argument("--notary-submission-id"),
|
|
5292
5394
|
argument("--submitted-archive-sha256"),
|
|
5395
|
+
variantArgument("full"),
|
|
5293
5396
|
);
|
|
5294
5397
|
} else if (command === "finalize-local") {
|
|
5295
5398
|
const sourceSha = argument("--source-sha");
|
|
@@ -5303,6 +5406,7 @@ function main(): void {
|
|
|
5303
5406
|
targetIdentityKindArgument(undefined, true),
|
|
5304
5407
|
argument("--approved-target-identity-sha256"),
|
|
5305
5408
|
optionalArgument("--expected-team-id") ?? "ADHOC",
|
|
5409
|
+
variantArgument("full"),
|
|
5306
5410
|
);
|
|
5307
5411
|
} else if (command === "verify-archive") {
|
|
5308
5412
|
const teamId = argument("--team-id");
|
|
@@ -5318,6 +5422,7 @@ function main(): void {
|
|
|
5318
5422
|
argument("--approved-target"),
|
|
5319
5423
|
argument("--approved-target-identity-sha256"),
|
|
5320
5424
|
targetIdentityKindArgument(policy),
|
|
5425
|
+
variantArgument("full"),
|
|
5321
5426
|
);
|
|
5322
5427
|
} else if (command === "extract-verified-archive") {
|
|
5323
5428
|
const teamId = argument("--team-id");
|
|
@@ -5334,6 +5439,7 @@ function main(): void {
|
|
|
5334
5439
|
argument("--approved-target"),
|
|
5335
5440
|
argument("--approved-target-identity-sha256"),
|
|
5336
5441
|
targetIdentityKindArgument(policy),
|
|
5442
|
+
variantArgument("full"),
|
|
5337
5443
|
);
|
|
5338
5444
|
} else if (command === "verify-app") {
|
|
5339
5445
|
const teamId = argument("--team-id");
|
|
@@ -5347,6 +5453,7 @@ function main(): void {
|
|
|
5347
5453
|
argument("--approved-target"),
|
|
5348
5454
|
argument("--approved-target-identity-sha256"),
|
|
5349
5455
|
targetIdentityKindArgument(policy),
|
|
5456
|
+
variantArgument("full"),
|
|
5350
5457
|
);
|
|
5351
5458
|
} else if (command === "verify-active") {
|
|
5352
5459
|
const teamId = argument("--team-id");
|
|
@@ -5360,6 +5467,7 @@ function main(): void {
|
|
|
5360
5467
|
argument("--approved-target"),
|
|
5361
5468
|
argument("--approved-target-identity-sha256"),
|
|
5362
5469
|
targetIdentityKindArgument(policy),
|
|
5470
|
+
variantArgument("full"),
|
|
5363
5471
|
);
|
|
5364
5472
|
} else if (command === "assert-release") {
|
|
5365
5473
|
assertExpectedRelease(
|
|
@@ -2,10 +2,26 @@
|
|
|
2
2
|
set -euo pipefail
|
|
3
3
|
umask 077
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
SMOKE_VARIANT="full"
|
|
6
|
+
if [ "$#" -ge 3 ]; then
|
|
7
|
+
case "$3" in
|
|
8
|
+
--variant)
|
|
9
|
+
SMOKE_VARIANT="${4:-full}"
|
|
10
|
+
;;
|
|
11
|
+
*)
|
|
12
|
+
echo "Usage: $0 <Recordings.app> <explicit absolute Bun executable path> [--variant full|bar]" >&2
|
|
13
|
+
exit 2
|
|
14
|
+
;;
|
|
15
|
+
esac
|
|
16
|
+
case "$SMOKE_VARIANT" in
|
|
17
|
+
full|bar) ;;
|
|
18
|
+
*)
|
|
19
|
+
echo "SMOKE_VARIANT must be full or bar; got: $SMOKE_VARIANT" >&2
|
|
20
|
+
exit 2
|
|
21
|
+
;;
|
|
22
|
+
esac
|
|
8
23
|
fi
|
|
24
|
+
readonly SMOKE_VARIANT
|
|
9
25
|
|
|
10
26
|
HOST_UNAME_EXECUTABLE="/usr/bin/uname"
|
|
11
27
|
SYSTEM_DIRNAME_EXECUTABLE="/usr/bin/dirname"
|
|
@@ -345,6 +361,12 @@ run_smoke() {
|
|
|
345
361
|
if [ "$mode" = "permission-helper" ]; then
|
|
346
362
|
arguments=(--request-permissions "${arguments[@]}")
|
|
347
363
|
fi
|
|
364
|
+
if [ "$SMOKE_VARIANT" = "bar" ]; then
|
|
365
|
+
# Bar builds are bar-only by construction (compile-time RECORDINGS_BAR_ONLY); the
|
|
366
|
+
# explicit argument is a belt-and-suspenders self-describing launch record so the
|
|
367
|
+
# launch is attributable to the variant even without the build define.
|
|
368
|
+
arguments=(--bar-only "${arguments[@]}")
|
|
369
|
+
fi
|
|
348
370
|
|
|
349
371
|
SMOKE_PID=""
|
|
350
372
|
SMOKE_PID_START_IDENTITY=""
|
|
@@ -428,6 +450,7 @@ run_smoke() {
|
|
|
428
450
|
PATH="$SANITIZED_PATH" \
|
|
429
451
|
TMPDIR="$WORK_DIR" \
|
|
430
452
|
SMOKE_MODE="$mode" \
|
|
453
|
+
SMOKE_VARIANT="$SMOKE_VARIANT" \
|
|
431
454
|
SMOKE_FOCUS_EVIDENCE="$SMOKE_FOCUS_EVIDENCE" \
|
|
432
455
|
EXPECTED_HELPER="$APP_PATH/Contents/Helpers/recordings" \
|
|
433
456
|
"$BUN_EXECUTABLE" -e '
|
|
@@ -454,19 +477,38 @@ run_smoke() {
|
|
|
454
477
|
if (JSON.stringify(result.renderedStatusLabels) !== JSON.stringify(expected)) {
|
|
455
478
|
fail(`unexpected status labels ${JSON.stringify(result.renderedStatusLabels)}`);
|
|
456
479
|
}
|
|
457
|
-
if (
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
480
|
+
if (process.env.SMOKE_VARIANT === "bar") {
|
|
481
|
+
// The bar variant launches windowless by construction: no workspace window is
|
|
482
|
+
// ever created, activated, or reused, the app stays an accessory (LSUIElement),
|
|
483
|
+
// and no window is exposed. This is the runtime half of the P1 #2 regression —
|
|
484
|
+
// the full-build smoke below must keep creating the window.
|
|
485
|
+
if (result.windowCreationCount !== 0 || result.windowActivationCount !== 0) {
|
|
486
|
+
fail("bar-only launch created or activated a workspace window");
|
|
487
|
+
}
|
|
488
|
+
if (result.retainedWindowReused !== false) {
|
|
489
|
+
fail("bar-only launch reused a workspace window");
|
|
490
|
+
}
|
|
491
|
+
if (result.mainWindowIsVisible || result.mainWindowCanBecomeKey || result.mainWindowIsKey) {
|
|
492
|
+
fail("bar-only launch exposed a main window");
|
|
493
|
+
}
|
|
494
|
+
if (result.applicationActivationPolicy !== 1) {
|
|
495
|
+
fail("bar-only launch did not stay an accessory activation policy");
|
|
496
|
+
}
|
|
497
|
+
} else {
|
|
498
|
+
if (result.windowCreationCount !== 1 || result.windowActivationCount !== 2) {
|
|
499
|
+
fail("retained-window activation path was not exercised twice");
|
|
500
|
+
}
|
|
501
|
+
if (result.retainedWindowReused !== true) fail("main window was not retained");
|
|
502
|
+
if (result.applicationActivationPolicy !== 0) fail("main window did not set regular activation policy");
|
|
503
|
+
if (!result.mainWindowIsVisible || !result.mainWindowCanBecomeKey) {
|
|
504
|
+
fail("retained main window was not visible and capable of becoming key");
|
|
505
|
+
}
|
|
506
|
+
if (!result.applicationIsActive || !result.mainWindowIsKey) {
|
|
507
|
+
if (process.env.SMOKE_FOCUS_EVIDENCE !== "ssh-unavailable") {
|
|
508
|
+
fail("Open Recordings did not make the retained window active and key");
|
|
509
|
+
}
|
|
510
|
+
focusEvidenceStatus = "ssh-unavailable";
|
|
468
511
|
}
|
|
469
|
-
focusEvidenceStatus = "ssh-unavailable";
|
|
470
512
|
}
|
|
471
513
|
} else if (mode === "resolver") {
|
|
472
514
|
if (result.menuBarSurfaceCount !== 0 || result.globalHandlersInstalled !== false) {
|
|
@@ -570,4 +612,8 @@ run_smoke() {
|
|
|
570
612
|
run_smoke normal
|
|
571
613
|
run_smoke permission-helper
|
|
572
614
|
run_smoke resolver
|
|
573
|
-
|
|
615
|
+
if [ "$SMOKE_VARIANT" = "bar" ]; then
|
|
616
|
+
echo "Recordings.app runtime smoke passed (bar variant): menu bar, windowless launch, helper isolation, and packaged resolver."
|
|
617
|
+
else
|
|
618
|
+
echo "Recordings.app runtime smoke passed: menu bar, retained window, helper isolation, and packaged resolver."
|
|
619
|
+
fi
|
|
@@ -24,6 +24,9 @@ struct MenuBarStatusLabel: View {
|
|
|
24
24
|
struct MenuBarStatusView: View {
|
|
25
25
|
@ObservedObject var store: RecordingsStore
|
|
26
26
|
let openRecordings: () -> Void
|
|
27
|
+
/// Bar-only builds have no workspace window to open; the entry point is hidden so
|
|
28
|
+
/// the affordance does not exist where the surface cannot exist.
|
|
29
|
+
let barOnly: Bool
|
|
27
30
|
|
|
28
31
|
var body: some View {
|
|
29
32
|
VStack(alignment: .leading, spacing: 12) {
|
|
@@ -50,11 +53,13 @@ struct MenuBarStatusView: View {
|
|
|
50
53
|
|
|
51
54
|
Divider()
|
|
52
55
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
+
if !barOnly {
|
|
57
|
+
Button(action: openRecordings) {
|
|
58
|
+
Label("Open Recordings", systemImage: "macwindow")
|
|
59
|
+
.frame(maxWidth: .infinity, alignment: .leading)
|
|
60
|
+
}
|
|
61
|
+
.buttonStyle(.plain)
|
|
56
62
|
}
|
|
57
|
-
.buttonStyle(.plain)
|
|
58
63
|
|
|
59
64
|
SettingsLink {
|
|
60
65
|
Label("Settings", systemImage: "gearshape")
|
|
@@ -56,6 +56,8 @@ final class RecordingsAppDelegate: NSObject, NSApplicationDelegate {
|
|
|
56
56
|
final class RecordingsAppState: ObservableObject {
|
|
57
57
|
let store: RecordingsStore?
|
|
58
58
|
let declaresMenuBar: Bool
|
|
59
|
+
let barOnly: Bool
|
|
60
|
+
let declaresWindow: Bool
|
|
59
61
|
let runtimeSmokeProbe: RuntimeSmokeProbe?
|
|
60
62
|
private let runtimeSmokeMode: String?
|
|
61
63
|
private let runtimeSmokeOutputPath: String?
|
|
@@ -67,6 +69,8 @@ final class RecordingsAppState: ObservableObject {
|
|
|
67
69
|
|
|
68
70
|
init(plan: PermissionRequestLaunchPlan) {
|
|
69
71
|
declaresMenuBar = plan.declaresMenuBar
|
|
72
|
+
barOnly = plan.isBarOnly
|
|
73
|
+
declaresWindow = plan.declaresWindow
|
|
70
74
|
runtimeSmokeMode = plan.runtimeSmokeMode
|
|
71
75
|
runtimeSmokeOutputPath = plan.runtimeSmokeOutputPath
|
|
72
76
|
runtimeSmokeAcknowledgementPath = plan.runtimeSmokeAcknowledgementPath
|
|
@@ -86,6 +90,14 @@ final class RecordingsAppState: ObservableObject {
|
|
|
86
90
|
}
|
|
87
91
|
|
|
88
92
|
func openRecordings() {
|
|
93
|
+
// Bar launches never create the workspace window — on any path, including the
|
|
94
|
+
// reopen handler and the runtime smoke. Keyed on declaresWindow (NOT on
|
|
95
|
+
// declaresMainWindow, which excludes every runtime smoke): a full build's
|
|
96
|
+
// normal-mode smoke must keep exercising the window, and only bar builds finish
|
|
97
|
+
// windowless. This was the cycle-2 P1 of the terminated PR #269 lineage — a
|
|
98
|
+
// windowless branch keyed on declaresMainWindow made the FULL smoke fail
|
|
99
|
+
// deterministically on every build.
|
|
100
|
+
guard declaresWindow else { return }
|
|
89
101
|
if let store {
|
|
90
102
|
showWindow(contentView: NSHostingView(rootView: ContentView(store: store)))
|
|
91
103
|
} else if runtimeSmokeMode == "normal" {
|
|
@@ -181,6 +193,23 @@ final class RecordingsAppState: ObservableObject {
|
|
|
181
193
|
runtimeSmokeProbe.completed = { [weak self, weak runtimeSmokeProbe] in
|
|
182
194
|
guard let self, let runtimeSmokeProbe else { return }
|
|
183
195
|
let accessibility = RuntimeSmokeAccessibilitySnapshot.processMenuBarExtras()
|
|
196
|
+
if self.barOnly {
|
|
197
|
+
// Windowless by construction: the bar smoke must never create the
|
|
198
|
+
// workspace window, and it reports the windowless state directly. This
|
|
199
|
+
// branch keys on barOnly, NOT on declaresMainWindow — that property
|
|
200
|
+
// excludes every runtime smoke and made the FULL build's smoke fail
|
|
201
|
+
// deterministically in the terminated PR #269 lineage (cycle-2 P1). A
|
|
202
|
+
// nil-window comparison would also read as a retained window (nil ===
|
|
203
|
+
// nil), so the windowless result is finished here explicitly.
|
|
204
|
+
self.finishRuntimeSmoke(
|
|
205
|
+
mode: mode,
|
|
206
|
+
surfaceCount: runtimeSmokeProbe.surfaceAppearances,
|
|
207
|
+
labels: runtimeSmokeProbe.renderedLabels,
|
|
208
|
+
accessibility: accessibility,
|
|
209
|
+
retainedWindowReused: false
|
|
210
|
+
)
|
|
211
|
+
return
|
|
212
|
+
}
|
|
184
213
|
self.openRecordings()
|
|
185
214
|
let firstWindow = self.mainWindow
|
|
186
215
|
self.openRecordings()
|
|
@@ -202,6 +231,9 @@ final class RecordingsAppState: ObservableObject {
|
|
|
202
231
|
retainedWindowReused: Bool,
|
|
203
232
|
attempt: Int = 0
|
|
204
233
|
) {
|
|
234
|
+
// Only the non-bar smoke reaches this (bar launches finish windowless in the
|
|
235
|
+
// probe completion handler above), so the settled state is exactly the retained
|
|
236
|
+
// window being key, unchanged from the base contract.
|
|
205
237
|
let windowSettled = NSApplication.shared.isActive && (mainWindow?.isKeyWindow ?? false)
|
|
206
238
|
guard windowSettled || attempt >= 60 else {
|
|
207
239
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) { [weak self] in
|
|
@@ -316,7 +348,11 @@ struct RecordingsApp: App {
|
|
|
316
348
|
@SceneBuilder var body: some Scene {
|
|
317
349
|
MenuBarExtra(isInserted: menuBarInsertion) {
|
|
318
350
|
if let store = state.store {
|
|
319
|
-
MenuBarStatusView(
|
|
351
|
+
MenuBarStatusView(
|
|
352
|
+
store: store,
|
|
353
|
+
openRecordings: state.openRecordings,
|
|
354
|
+
barOnly: state.barOnly
|
|
355
|
+
)
|
|
320
356
|
} else if state.runtimeSmokeProbe != nil {
|
|
321
357
|
EmptyView()
|
|
322
358
|
}
|
|
@@ -17,11 +17,11 @@
|
|
|
17
17
|
<key>CFBundleIdentifier</key>
|
|
18
18
|
<string>com.hasna.recordings</string>
|
|
19
19
|
<key>CFBundleVersion</key>
|
|
20
|
-
<string>0.3.
|
|
20
|
+
<string>0.3.2</string>
|
|
21
21
|
<key>CFBundlePackageType</key>
|
|
22
22
|
<string>APPL</string>
|
|
23
23
|
<key>CFBundleShortVersionString</key>
|
|
24
|
-
<string>0.3.
|
|
24
|
+
<string>0.3.2</string>
|
|
25
25
|
<key>LSMinimumSystemVersion</key>
|
|
26
26
|
<string>26.0</string>
|
|
27
27
|
<key>NSMicrophoneUsageDescription</key>
|
|
@@ -7,10 +7,19 @@ public struct PermissionRequestLaunchPlan: Sendable, Equatable {
|
|
|
7
7
|
public let runtimeSmokeOutputPath: String?
|
|
8
8
|
public let runtimeSmokeAcknowledgementPath: String?
|
|
9
9
|
public let runtimeSmokeCompletionPath: String?
|
|
10
|
+
public let isBarOnly: Bool
|
|
10
11
|
|
|
11
12
|
public var isRuntimeSmoke: Bool { runtimeSmokeMode != nil }
|
|
12
13
|
public var installsGlobalHandlers: Bool { !isHelper && !isRuntimeSmoke }
|
|
13
|
-
|
|
14
|
+
/// The workspace window may be created only when this holds: never for the permission
|
|
15
|
+
/// helper, never for a bar launch, and regardless of runtime-smoke mode — a full
|
|
16
|
+
/// build's smoke keeps exercising the window deterministically, and only bar builds
|
|
17
|
+
/// finish windowless.
|
|
18
|
+
public var declaresWindow: Bool { !isHelper && !isBarOnly }
|
|
19
|
+
/// Controls the init auto-open and the reopen handler ONLY. Runtime smoke launches
|
|
20
|
+
/// are excluded so the smoke controls window creation deterministically, and bar
|
|
21
|
+
/// launches are excluded so a real bar launch never auto-opens the workspace window.
|
|
22
|
+
public var declaresMainWindow: Bool { !isHelper && !isRuntimeSmoke && !isBarOnly }
|
|
14
23
|
public var declaresMenuBar: Bool {
|
|
15
24
|
if isRuntimeSmoke { return runtimeSmokeMode == "normal" }
|
|
16
25
|
return !isHelper
|
|
@@ -31,6 +40,17 @@ public struct PermissionRequestLaunchPlan: Sendable, Equatable {
|
|
|
31
40
|
"--runtime-smoke-completion",
|
|
32
41
|
arguments: arguments
|
|
33
42
|
)
|
|
43
|
+
#if RECORDINGS_BAR_ONLY
|
|
44
|
+
// A bar build is bar-only by construction on EVERY launch path — installer
|
|
45
|
+
// relaunch, manual `open`, LaunchAgent, smoke. Relying on the argument alone left
|
|
46
|
+
// real launches of the installed bar behaviorally identical to the full app
|
|
47
|
+
// (review lineage hasna/apps#269, cycle-1 P1). The explicit --bar-only argument
|
|
48
|
+
// remains a belt-and-suspenders self-describing launch record; it is never the
|
|
49
|
+
// only carrier of the bar property.
|
|
50
|
+
isBarOnly = true
|
|
51
|
+
#else
|
|
52
|
+
isBarOnly = arguments.contains("--bar-only")
|
|
53
|
+
#endif
|
|
34
54
|
}
|
|
35
55
|
|
|
36
56
|
private static func optionValue(_ name: String, arguments: [String]) -> String? {
|