@hasna/recordings 0.3.2 → 0.3.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +38 -34
- package/bun.lock +96 -36
- package/dist/__tests__/helpers/source-assertions.d.ts +6 -4
- package/dist/__tests__/helpers/source-assertions.d.ts.map +1 -1
- package/dist/cli/index.js +1059 -219
- package/dist/cli/macos-shortcut.d.ts +2 -2
- package/dist/db/pg-migrations.d.ts +1 -1
- package/dist/http/client.d.ts +6 -14
- package/dist/http/client.d.ts.map +1 -1
- package/dist/index.js +1971 -175
- package/dist/lib/capture-probe.d.ts +3 -3
- package/dist/lib/capture-probe.d.ts.map +1 -1
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/macos-bundle.d.ts +1 -1
- package/dist/lib/release-install-policy.d.ts +21 -0
- package/dist/lib/release-install-policy.d.ts.map +1 -1
- package/dist/mcp/index.js +978 -142
- package/dist/sdk/index.d.ts +2 -2
- package/dist/sdk/index.js +7 -3
- package/dist/sdk/v1.generated.d.ts.map +1 -1
- package/dist/server/cloud-config.d.ts +4 -19
- package/dist/server/cloud-config.d.ts.map +1 -1
- package/dist/server/cloud.d.ts +1 -1
- package/dist/server/cloud.d.ts.map +1 -1
- package/dist/server/index.js +217 -299
- package/dist/server/serve.d.ts.map +1 -1
- package/dist/storage.d.ts +1 -1
- package/dist/storage.d.ts.map +1 -1
- package/dist/storage.js +1881 -90
- package/dist/store.d.ts.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.d.ts.map +1 -1
- package/package.json +4 -3
- package/packaging/macos/build_release_pkg.sh +10 -4
- package/packaging/macos/managed_bootstrap.sh +4 -4
- package/packaging/macos/scripts/postinstall +2 -2
- package/packaging/macos/scripts/preinstall +2 -0
- package/scripts/generate-sdk.ts +17 -16
- package/scripts/install_macos_app.sh +22 -22
- package/scripts/macos_artifact.ts +48 -39
- package/scripts/migrate.ts +2 -2
- package/scripts/release-suite-gate.ts +174 -0
- package/scripts/set-version.ts +5 -5
- package/scripts/smoke_macos_app.sh +21 -21
- package/scripts/vacuity-manifests/version-sites.tsv +8 -4
- package/src/native/Recordings/RecordingsLib/Info.plist +3 -3
- package/src/native/Recordings/RecordingsTests/CLIRunnerTests.swift +2 -2
- package/src/native/Recordings/RecordingsTests/RecordingStartGateTests.swift +1 -1
- package/src/native/Recordings/Updater/BootstrapPreflight/BootstrapPreflightMain.swift +1 -1
- package/src/native/Recordings/Updater/Broker/ApplicationNamespace.swift +2 -2
- package/src/native/Recordings/Updater/Broker/BrokerMain.swift +1 -1
- package/src/native/Recordings/Updater/Broker/CanonicalTreeCopy.swift +1 -1
- package/src/native/Recordings/Updater/Broker/InstallJournal.swift +1 -1
- package/src/native/Recordings/Updater/BrokerTests/ActivationRecoveryPolicyTests.swift +2 -2
- package/src/native/Recordings/Updater/Protocol/UpdateProtocol.swift +1 -1
- package/src/native/Recordings/Updater/VerifierLauncher/RecordingsVerifierLauncher.c +2 -2
- package/src/native/Recordings/build.sh +14 -20
- package/dist/lib/retired-deployment-modes.d.ts +0 -24
- package/dist/lib/retired-deployment-modes.d.ts.map +0 -1
package/scripts/migrate.ts
CHANGED
|
@@ -34,11 +34,11 @@ async function main() {
|
|
|
34
34
|
getCloudPg,
|
|
35
35
|
migrateCloudSchema,
|
|
36
36
|
pingCloudConnectivity,
|
|
37
|
-
|
|
37
|
+
resolveDatabaseUrl,
|
|
38
38
|
} = await import("../src/server/cloud.js");
|
|
39
39
|
const { assertCloudSchemaContract } = await import("../src/server/cloud-readiness.js");
|
|
40
40
|
const { runCloudMigration } = await import("../src/server/migrate-command.js");
|
|
41
|
-
const url =
|
|
41
|
+
const url = resolveDatabaseUrl();
|
|
42
42
|
if (!url) {
|
|
43
43
|
console.error(
|
|
44
44
|
"migrate: no database URL (HASNA_RECORDINGS_DATABASE_URL / RECORDINGS_DATABASE_URL / DATABASE_URL)",
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* Release-suite gate (wired into `prepublishOnly`).
|
|
4
|
+
*
|
|
5
|
+
* Runs the app's own Linux-CI-partitioned suite — `test:gated`, which covers
|
|
6
|
+
* every discovered `*.test.ts` (the quarantine file is empty by design) — and
|
|
7
|
+
* fails on ANY failure.
|
|
8
|
+
*
|
|
9
|
+
* The allowlist is SELF-INVALIDATING, the same property as
|
|
10
|
+
* `.github/linux-quarantine.txt`: if an allowlisted failure stops failing
|
|
11
|
+
* (the owning lane lands its fix, or a quarantined package ages out of the
|
|
12
|
+
* 7-day window), the gate exits 1 and demands the entry be removed. Any
|
|
13
|
+
* failure outside the allowlist fails the gate.
|
|
14
|
+
*
|
|
15
|
+
* Allowlist history (both entries removed 2026-08-23 by the 0.3.9 release
|
|
16
|
+
* lane, publish-all-recordings, per this gate's own self-invalidation
|
|
17
|
+
* contract — each failure stopped failing at head 15b6181c6):
|
|
18
|
+
* - service-contract-manifest.test.ts (credential_seam_compliance): the
|
|
19
|
+
* contracts-surface migration landed in hasna/apps PR #957
|
|
20
|
+
* (resolveStorageClient now imported from @hasna/contracts/client), so
|
|
21
|
+
* the conformance check passes; the entry's own reason said "Remove this
|
|
22
|
+
* entry when the lane lands."
|
|
23
|
+
* - native-app-companion-contract.test.ts (release supply-chain policy):
|
|
24
|
+
* the companion build's --minimum-release-age=604800 quarantine no longer
|
|
25
|
+
* bites because @hasna/contracts is on the fleet minimumReleaseAgeExcludes
|
|
26
|
+
* list (the sanctioned mechanism for fresh publishes); the test passes.
|
|
27
|
+
* Both removals are the gate's designed behaviour, not a weakening: any
|
|
28
|
+
* future failure now fails the gate with zero allowlisted exceptions.
|
|
29
|
+
*
|
|
30
|
+
* The gate FAILS CLOSED on abnormal termination: a non-zero suite exit is
|
|
31
|
+
* acceptable only when it is fully explained by allowlisted failures — the
|
|
32
|
+
* runner completed (its summary line is present), every parsed failure is
|
|
33
|
+
* allowlisted, at least one allowlisted failure fired, and the runner's
|
|
34
|
+
* reported failure count equals the parsed count. A crash, signal kill,
|
|
35
|
+
* timeout, or runner error that produces a non-zero exit without a complete
|
|
36
|
+
* summary fails the gate (remediation cycle 2 of PR hasna/apps#826).
|
|
37
|
+
*/
|
|
38
|
+
import { spawnSync } from "node:child_process";
|
|
39
|
+
import { mkdtempSync, writeFileSync } from "node:fs";
|
|
40
|
+
import { tmpdir } from "node:os";
|
|
41
|
+
import { join } from "node:path";
|
|
42
|
+
|
|
43
|
+
const SEP = "::";
|
|
44
|
+
|
|
45
|
+
const GATED_KEYS: Array<{ file: string; test: string; owner: string; reason: string }> = [];
|
|
46
|
+
|
|
47
|
+
function run(argv: string[], env: Record<string, string>): { status: number; stdout: string; stderr: string } {
|
|
48
|
+
const res = spawnSync(process.execPath, argv, { encoding: "utf8", env: { ...process.env, ...env } });
|
|
49
|
+
return { status: res.status ?? -1, stdout: res.stdout ?? "", stderr: res.stderr ?? "" };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// 1. Validate the partition (quarantine file must be consistent).
|
|
53
|
+
const check = run(["scripts/ci-linux-suite.ts", "--check"], {});
|
|
54
|
+
if (check.status !== 0) {
|
|
55
|
+
console.error("release-suite-gate: partition check failed:\n" + (check.stderr || check.stdout));
|
|
56
|
+
process.exit(1);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// 2. Collect the gated file list.
|
|
60
|
+
const gated = run(["scripts/ci-linux-suite.ts", "--gated"], {});
|
|
61
|
+
if (gated.status !== 0) {
|
|
62
|
+
console.error("release-suite-gate: could not enumerate gated files:\n" + (gated.stderr || gated.stdout));
|
|
63
|
+
process.exit(1);
|
|
64
|
+
}
|
|
65
|
+
const gatedFiles = gated.stdout.split("\n").filter(Boolean);
|
|
66
|
+
if (gatedFiles.length === 0) {
|
|
67
|
+
console.error("release-suite-gate: zero gated files - refusing to pass on an empty suite.");
|
|
68
|
+
process.exit(1);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// 3. Run the gated suite exactly like `test:gated`.
|
|
72
|
+
const logDir = mkdtempSync(join(tmpdir(), "recordings-release-gate-"));
|
|
73
|
+
const logPath = join(logDir, "suite.log");
|
|
74
|
+
const suite = spawnSync(
|
|
75
|
+
process.execPath,
|
|
76
|
+
["test", "--timeout", "120000", ...gatedFiles],
|
|
77
|
+
{
|
|
78
|
+
encoding: "utf8",
|
|
79
|
+
env: { ...process.env, RECORDINGS_TEST_TIMEOUT_MS: "120000" },
|
|
80
|
+
maxBuffer: 64 * 1024 * 1024,
|
|
81
|
+
},
|
|
82
|
+
);
|
|
83
|
+
const output = (suite.stdout ?? "") + (suite.stderr ?? "");
|
|
84
|
+
try {
|
|
85
|
+
writeFileSync(logPath, output);
|
|
86
|
+
} catch {
|
|
87
|
+
/* log capture is best-effort */
|
|
88
|
+
}
|
|
89
|
+
const suiteStatus = suite.status ?? -1;
|
|
90
|
+
|
|
91
|
+
// 4. Parse failing tests as (file, description) pairs, in output order.
|
|
92
|
+
const failPairs: Array<{ file: string; test: string }> = [];
|
|
93
|
+
let currentFile = "";
|
|
94
|
+
for (const line of output.split("\n")) {
|
|
95
|
+
const fileMatch = /^(?:\.\/)?(src\/[^\s:]+\.test\.ts):$/.exec(line);
|
|
96
|
+
if (fileMatch) {
|
|
97
|
+
currentFile = fileMatch[1];
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
const failMatch = /^\(fail\) (.+) \[\d+.*\]$/.exec(line);
|
|
101
|
+
if (failMatch) {
|
|
102
|
+
failPairs.push({ file: currentFile, test: failMatch[1].trim() });
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// 5. Classify.
|
|
107
|
+
const failuresByKey = new Map<string, number>();
|
|
108
|
+
for (const key of GATED_KEYS) {
|
|
109
|
+
failuresByKey.set(key.file + SEP + key.test, 0);
|
|
110
|
+
}
|
|
111
|
+
const unexpected: Array<{ file: string; test: string }> = [];
|
|
112
|
+
for (const pair of failPairs) {
|
|
113
|
+
const key = pair.file + SEP + pair.test;
|
|
114
|
+
if (failuresByKey.has(key)) {
|
|
115
|
+
failuresByKey.set(key, (failuresByKey.get(key) ?? 0) + 1);
|
|
116
|
+
} else {
|
|
117
|
+
unexpected.push(pair);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (unexpected.length > 0) {
|
|
122
|
+
console.error(
|
|
123
|
+
"release-suite-gate: FAIL - unexpected test failures:\n" +
|
|
124
|
+
unexpected.map((u) => " " + u.file + " :: " + u.test).join("\n"),
|
|
125
|
+
);
|
|
126
|
+
console.error("Full suite log: " + logPath);
|
|
127
|
+
process.exit(1);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// 6. Fail closed on abnormal termination. A non-zero suite exit is only
|
|
131
|
+
// acceptable when it is fully explained by the allowlisted failures: the
|
|
132
|
+
// runner completed (summary line present), every parsed failure is
|
|
133
|
+
// allowlisted, at least one allowlisted failure fired, and the runner's
|
|
134
|
+
// reported failure count equals the parsed count (no unparsed failures).
|
|
135
|
+
// A crash, signal kill, timeout, or runner error produces a non-zero exit
|
|
136
|
+
// without a complete summary and must fail the gate.
|
|
137
|
+
if (suiteStatus !== 0) {
|
|
138
|
+
const summaryMatch = /Ran (\d+) tests across (\d+) files/.exec(output);
|
|
139
|
+
// The tally is "N pass[,/ ] M fail" in the runner's closing summary. The
|
|
140
|
+
// LAST match is taken (bun prints the tally once, but a naive first match
|
|
141
|
+
// can hit "0.13.3 failed to resolve" -> "3 fail" inside a version string).
|
|
142
|
+
const tallyMatches = [...output.matchAll(/(\d+) pass[\s,/]+(\d+) fail/g)];
|
|
143
|
+
const failCountMatch = tallyMatches.length > 0 ? tallyMatches[tallyMatches.length - 1] : null;
|
|
144
|
+
const reportedFails = failCountMatch ? Number(failCountMatch[2]) : null;
|
|
145
|
+
const anyAllowlistedFired = failPairs.length > 0;
|
|
146
|
+
const complete = summaryMatch !== null && reportedFails !== null && reportedFails === failPairs.length;
|
|
147
|
+
if (!anyAllowlistedFired || !complete) {
|
|
148
|
+
console.error(
|
|
149
|
+
"release-suite-gate: FAIL - suite exited " + suiteStatus +
|
|
150
|
+
" with " + failPairs.length + " parsed failure(s) but the run is incomplete or unexplained:" +
|
|
151
|
+
"\n summary line present: " + (summaryMatch !== null) +
|
|
152
|
+
"\n reported failure count: " + String(reportedFails) +
|
|
153
|
+
"\n parsed failure count: " + failPairs.length,
|
|
154
|
+
);
|
|
155
|
+
console.error("Full suite log: " + logPath);
|
|
156
|
+
process.exit(1);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// 7. Self-invalidation: an allowlisted failure that no longer fails must be removed.
|
|
161
|
+
const stale = GATED_KEYS.filter((key) => (failuresByKey.get(key.file + SEP + key.test) ?? 0) === 0);
|
|
162
|
+
if (stale.length > 0) {
|
|
163
|
+
console.error(
|
|
164
|
+
"release-suite-gate: FAIL - allowlisted failure no longer fails; remove the entry from scripts/release-suite-gate.ts:\n" +
|
|
165
|
+
stale.map((s) => " " + s.file + " :: " + s.test + " - " + s.owner).join("\n"),
|
|
166
|
+
);
|
|
167
|
+
process.exit(1);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
console.log(
|
|
171
|
+
"release-suite-gate: PASS - " + failPairs.length + " classified pre-existing failure(s) allowlisted:" +
|
|
172
|
+
failPairs.map((p) => "\n " + p.file + " :: " + p.test).join(""),
|
|
173
|
+
);
|
|
174
|
+
process.exit(0);
|
package/scripts/set-version.ts
CHANGED
|
@@ -16,17 +16,17 @@ export type VersionSite = {
|
|
|
16
16
|
pattern: RegExp;
|
|
17
17
|
};
|
|
18
18
|
|
|
19
|
+
// src/version.ts is deliberately NOT a site: it derives VERSION from
|
|
20
|
+
// package.json at import time (see src/version.ts), so it holds no literal
|
|
21
|
+
// this writer could keep in sync, and a monorepo changesets bump can no
|
|
22
|
+
// longer leave it stale. The remaining sites are the native bundle surfaces,
|
|
23
|
+
// which cannot derive and must be written here.
|
|
19
24
|
export const versionSites: VersionSite[] = [
|
|
20
25
|
{
|
|
21
26
|
file: "package.json",
|
|
22
27
|
label: "package.json version",
|
|
23
28
|
pattern: /("version"\s*:\s*")([^"]+)(")/,
|
|
24
29
|
},
|
|
25
|
-
{
|
|
26
|
-
file: "src/version.ts",
|
|
27
|
-
label: "src/version.ts VERSION",
|
|
28
|
-
pattern: /(export const VERSION = ")([^"]+)(")/,
|
|
29
|
-
},
|
|
30
30
|
{
|
|
31
31
|
file: "src/native/Recordings/RecordingsLib/Info.plist",
|
|
32
32
|
label: "Info.plist CFBundleShortVersionString",
|
|
@@ -9,7 +9,7 @@ if [ "$#" -ge 3 ]; then
|
|
|
9
9
|
SMOKE_VARIANT="${4:-full}"
|
|
10
10
|
;;
|
|
11
11
|
*)
|
|
12
|
-
echo "Usage: $0 <
|
|
12
|
+
echo "Usage: $0 <HasnaRecordings.app> <explicit absolute Bun executable path> [--variant full|bar]" >&2
|
|
13
13
|
exit 2
|
|
14
14
|
;;
|
|
15
15
|
esac
|
|
@@ -51,7 +51,7 @@ HOST_PLATFORM="$("$HOST_UNAME_EXECUTABLE" -s)"
|
|
|
51
51
|
readonly HOST_PLATFORM
|
|
52
52
|
|
|
53
53
|
if [ "$HOST_PLATFORM" != "Darwin" ] && [ "${RECORDINGS_TEST_SMOKE_ALLOW_NON_DARWIN:-0}" != "1" ]; then
|
|
54
|
-
echo "
|
|
54
|
+
echo "HasnaRecordings.app runtime smoke is only supported on macOS." >&2
|
|
55
55
|
exit 1
|
|
56
56
|
fi
|
|
57
57
|
|
|
@@ -123,12 +123,12 @@ HOME_DIRECTORY="${HOME:-}"
|
|
|
123
123
|
case "$HOME_DIRECTORY" in
|
|
124
124
|
/*) ;;
|
|
125
125
|
*)
|
|
126
|
-
echo "
|
|
126
|
+
echo "HasnaRecordings.app runtime smoke requires an absolute HOME path." >&2
|
|
127
127
|
exit 1
|
|
128
128
|
;;
|
|
129
129
|
esac
|
|
130
130
|
if [ ! -d "$HOME_DIRECTORY" ]; then
|
|
131
|
-
echo "
|
|
131
|
+
echo "HasnaRecordings.app runtime smoke HOME is not a directory: $HOME_DIRECTORY" >&2
|
|
132
132
|
exit 1
|
|
133
133
|
fi
|
|
134
134
|
|
|
@@ -136,7 +136,7 @@ APP_PARENT="$(cd -P "$("$DIRNAME_EXECUTABLE" "$1")" && "$PWD_EXECUTABLE" -P)"
|
|
|
136
136
|
APP_PATH="$APP_PARENT/$("$BASENAME_EXECUTABLE" "$1")"
|
|
137
137
|
EXECUTABLE="$APP_PATH/Contents/MacOS/Recordings"
|
|
138
138
|
if [ ! -x "$EXECUTABLE" ]; then
|
|
139
|
-
echo "
|
|
139
|
+
echo "HasnaRecordings.app runtime smoke executable is missing: $EXECUTABLE" >&2
|
|
140
140
|
exit 1
|
|
141
141
|
fi
|
|
142
142
|
|
|
@@ -317,7 +317,7 @@ cleanup() {
|
|
|
317
317
|
fi
|
|
318
318
|
if [ -n "$SMOKE_APP_PID" ] && [ -n "$SMOKE_APP_PID_START_IDENTITY" ]; then
|
|
319
319
|
terminate_verified_process \
|
|
320
|
-
"
|
|
320
|
+
"HasnaRecordings.app" \
|
|
321
321
|
"$SMOKE_APP_PID" \
|
|
322
322
|
"$EXECUTABLE" \
|
|
323
323
|
"$SMOKE_APP_PID_START_IDENTITY" || true
|
|
@@ -377,7 +377,7 @@ run_smoke() {
|
|
|
377
377
|
if ! SMOKE_APP_IDENTITIES_BEFORE_LAUNCH="$(
|
|
378
378
|
process_records_for_exact_executable "$EXECUTABLE"
|
|
379
379
|
)"; then
|
|
380
|
-
echo "
|
|
380
|
+
echo "HasnaRecordings.app runtime smoke ${mode} could not inventory exact app processes before launch." >&2
|
|
381
381
|
return 1
|
|
382
382
|
fi
|
|
383
383
|
|
|
@@ -399,24 +399,24 @@ run_smoke() {
|
|
|
399
399
|
result_pid="$("$ENV_EXECUTABLE" -i HOME="$HOME_DIRECTORY" PATH="$SANITIZED_PATH" TMPDIR="$WORK_DIR" \
|
|
400
400
|
"$BUN_EXECUTABLE" -e 'const value = await Bun.file(process.argv[1]).json(); console.log(value.processIdentifier)' "$output")"
|
|
401
401
|
if ! [[ "$result_pid" =~ ^[1-9][0-9]*$ ]] || ! kill -0 "$result_pid" 2>/dev/null; then
|
|
402
|
-
echo "
|
|
402
|
+
echo "HasnaRecordings.app runtime smoke ${mode} reported a process that is not running." >&2
|
|
403
403
|
return 1
|
|
404
404
|
fi
|
|
405
405
|
if ! result_pid_start_identity="$(
|
|
406
406
|
capture_process_start_identity "$result_pid" "$EXECUTABLE"
|
|
407
407
|
)"; then
|
|
408
|
-
echo "
|
|
408
|
+
echo "HasnaRecordings.app runtime smoke ${mode} reported a process outside the exact app path." >&2
|
|
409
409
|
return 1
|
|
410
410
|
fi
|
|
411
411
|
if process_identity_existed_before_launch "$result_pid" "$result_pid_start_identity"; then
|
|
412
|
-
echo "
|
|
412
|
+
echo "HasnaRecordings.app runtime smoke ${mode} reported an app process that predated this launch." >&2
|
|
413
413
|
return 1
|
|
414
414
|
fi
|
|
415
415
|
if [ -n "$SMOKE_APP_PID" ] && {
|
|
416
416
|
[ "$result_pid" != "$SMOKE_APP_PID" ] ||
|
|
417
417
|
[ "$result_pid_start_identity" != "$SMOKE_APP_PID_START_IDENTITY" ];
|
|
418
418
|
}; then
|
|
419
|
-
echo "
|
|
419
|
+
echo "HasnaRecordings.app runtime smoke ${mode} evidence did not match the exact launched app process." >&2
|
|
420
420
|
return 1
|
|
421
421
|
fi
|
|
422
422
|
SMOKE_APP_PID="$result_pid"
|
|
@@ -429,19 +429,19 @@ run_smoke() {
|
|
|
429
429
|
break
|
|
430
430
|
fi
|
|
431
431
|
if ! kill -0 "$SMOKE_PID" 2>/dev/null; then
|
|
432
|
-
echo "
|
|
432
|
+
echo "HasnaRecordings.app runtime smoke ${mode} exited without evidence." >&2
|
|
433
433
|
"$SED_EXECUTABLE" -n '1,120p' "$log" >&2
|
|
434
434
|
return 1
|
|
435
435
|
fi
|
|
436
436
|
"$SLEEP_EXECUTABLE" 0.1
|
|
437
437
|
done
|
|
438
438
|
if [ ! -f "$output" ]; then
|
|
439
|
-
echo "
|
|
439
|
+
echo "HasnaRecordings.app runtime smoke ${mode} timed out." >&2
|
|
440
440
|
"$SED_EXECUTABLE" -n '1,120p' "$log" >&2
|
|
441
441
|
return 1
|
|
442
442
|
fi
|
|
443
443
|
if [ -z "$SMOKE_APP_PID" ]; then
|
|
444
|
-
echo "
|
|
444
|
+
echo "HasnaRecordings.app runtime smoke ${mode} did not bind evidence to the exact app process." >&2
|
|
445
445
|
return 1
|
|
446
446
|
fi
|
|
447
447
|
|
|
@@ -546,7 +546,7 @@ run_smoke() {
|
|
|
546
546
|
' "$output"
|
|
547
547
|
|
|
548
548
|
if [ -e "$completion" ]; then
|
|
549
|
-
echo "
|
|
549
|
+
echo "HasnaRecordings.app runtime smoke ${mode} produced completion before the challenge." >&2
|
|
550
550
|
return 1
|
|
551
551
|
fi
|
|
552
552
|
challenge="$("$ENV_EXECUTABLE" -i HOME="$HOME_DIRECTORY" PATH="$SANITIZED_PATH" TMPDIR="$WORK_DIR" \
|
|
@@ -554,7 +554,7 @@ run_smoke() {
|
|
|
554
554
|
if ! rechecked_start_identity="$(
|
|
555
555
|
capture_process_start_identity "$result_pid" "$EXECUTABLE"
|
|
556
556
|
)" || [ "$rechecked_start_identity" != "$result_pid_start_identity" ]; then
|
|
557
|
-
echo "
|
|
557
|
+
echo "HasnaRecordings.app runtime smoke ${mode} process identity changed before completion challenge." >&2
|
|
558
558
|
return 1
|
|
559
559
|
fi
|
|
560
560
|
write_atomic_text_file "$acknowledgement" "$challenge"
|
|
@@ -564,7 +564,7 @@ run_smoke() {
|
|
|
564
564
|
"$SLEEP_EXECUTABLE" 0.1
|
|
565
565
|
done
|
|
566
566
|
if kill -0 "$SMOKE_PID" 2>/dev/null; then
|
|
567
|
-
echo "
|
|
567
|
+
echo "HasnaRecordings.app runtime smoke ${mode} completion handshake timed out." >&2
|
|
568
568
|
return 1
|
|
569
569
|
fi
|
|
570
570
|
if wait "$SMOKE_PID" 2>/dev/null; then
|
|
@@ -577,7 +577,7 @@ run_smoke() {
|
|
|
577
577
|
SMOKE_APP_PID=""
|
|
578
578
|
SMOKE_APP_PID_START_IDENTITY=""
|
|
579
579
|
if [ "$wrapper_status" -ne 0 ]; then
|
|
580
|
-
echo "
|
|
580
|
+
echo "HasnaRecordings.app runtime smoke ${mode} open -W wrapper exited unsuccessfully (${wrapper_status})." >&2
|
|
581
581
|
"$SED_EXECUTABLE" -n '1,120p' "$log" >&2
|
|
582
582
|
return 1
|
|
583
583
|
fi
|
|
@@ -602,7 +602,7 @@ run_smoke() {
|
|
|
602
602
|
process.exit(1);
|
|
603
603
|
}
|
|
604
604
|
' "$completion"; then
|
|
605
|
-
echo "
|
|
605
|
+
echo "HasnaRecordings.app runtime smoke ${mode} did not provide a valid completion response." >&2
|
|
606
606
|
return 1
|
|
607
607
|
fi
|
|
608
608
|
|
|
@@ -613,7 +613,7 @@ run_smoke normal
|
|
|
613
613
|
run_smoke permission-helper
|
|
614
614
|
run_smoke resolver
|
|
615
615
|
if [ "$SMOKE_VARIANT" = "bar" ]; then
|
|
616
|
-
echo "
|
|
616
|
+
echo "HasnaRecordings.app runtime smoke passed (bar variant): menu bar, windowless launch, helper isolation, and packaged resolver."
|
|
617
617
|
else
|
|
618
|
-
echo "
|
|
618
|
+
echo "HasnaRecordings.app runtime smoke passed: menu bar, retained window, helper isolation, and packaged resolver."
|
|
619
619
|
fi
|
|
@@ -15,9 +15,13 @@
|
|
|
15
15
|
# manifest is not wired into a package.json aggregate script.
|
|
16
16
|
#
|
|
17
17
|
# --- every version site agrees with package.json. Models 8de7068: one site bumped, the rest not.
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
18
|
+
# I38-00553 (2026-08-21): src/version.ts now DERIVES from package.json and holds no literal, so it
|
|
19
|
+
# is no longer a writer site (see set-version.ts) and cannot be mutated here. The residual drift
|
|
20
|
+
# class is the mirror direction: the package.json authority corrupted while the native sites stay
|
|
21
|
+
# put. Rows below were re-needled to the 0.3.5 tree.
|
|
22
|
+
delete src/__tests__/version-site-guard.test.ts agree in the working tree, on the version package.json declares package.json 0.3.5 1
|
|
23
|
+
duplicate src/__tests__/version-site-guard.test.ts agree in the working tree, on the version package.json declares src/native/Recordings/RecordingsLib/Info.plist 0.3.5 1
|
|
24
|
+
duplicate src/__tests__/version-site-guard.test.ts agree in the working tree, on the version package.json declares src/native/Recordings/RecordingsLib/Info.plist 0.3.5 2
|
|
21
25
|
# --- a stale site is reported, not silently accepted: the mismatch filter removed.
|
|
22
26
|
delete src/__tests__/version-site-guard.test.ts a single stale site is reported rather than silently accepted scripts/set-version.ts .filter((reading) => reading.version !== expected) 1
|
|
23
27
|
# --- applyVersion writes every site in one pass: the per-file grouping removed.
|
|
@@ -58,4 +62,4 @@ delete src/__tests__/version-site-guard.test.ts applyVersion rewrites every site
|
|
|
58
62
|
#
|
|
59
63
|
# The generated SDK's version stamp -- a fifth copy of the release version that `versionSites`
|
|
60
64
|
# deliberately does not write -- goes stale again.
|
|
61
|
-
delete src/__tests__/version-site-guard.test.ts the generated SDK header carries the version package.json declares src/sdk/v1.generated.ts .
|
|
65
|
+
delete src/__tests__/version-site-guard.test.ts the generated SDK header carries the version package.json declares src/sdk/v1.generated.ts 0.3.5 1
|
|
@@ -11,17 +11,17 @@
|
|
|
11
11
|
<key>CFBundleName</key>
|
|
12
12
|
<string>Recordings</string>
|
|
13
13
|
<key>CFBundleDisplayName</key>
|
|
14
|
-
<string>Recordings</string>
|
|
14
|
+
<string>Hasna Recordings</string>
|
|
15
15
|
<key>CFBundleExecutable</key>
|
|
16
16
|
<string>Recordings</string>
|
|
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.9</string>
|
|
21
21
|
<key>CFBundlePackageType</key>
|
|
22
22
|
<string>APPL</string>
|
|
23
23
|
<key>CFBundleShortVersionString</key>
|
|
24
|
-
<string>0.3.
|
|
24
|
+
<string>0.3.9</string>
|
|
25
25
|
<key>LSMinimumSystemVersion</key>
|
|
26
26
|
<string>26.0</string>
|
|
27
27
|
<key>NSMicrophoneUsageDescription</key>
|
|
@@ -8,7 +8,7 @@ struct CLIRunnerTests {
|
|
|
8
8
|
func bundledCLIIsPreferred() throws {
|
|
9
9
|
let root = FileManager.default.temporaryDirectory
|
|
10
10
|
.appendingPathComponent("recordings-bundled-cli-\(UUID().uuidString)")
|
|
11
|
-
let app = root.appendingPathComponent("
|
|
11
|
+
let app = root.appendingPathComponent("HasnaRecordings.app")
|
|
12
12
|
let helper = app.appendingPathComponent("Contents/Helpers/recordings")
|
|
13
13
|
let home = root.appendingPathComponent("home")
|
|
14
14
|
let external = home.appendingPathComponent(".bun/bin/recordings")
|
|
@@ -34,7 +34,7 @@ struct CLIRunnerTests {
|
|
|
34
34
|
func packagedAppDoesNotUseGlobalFallback() throws {
|
|
35
35
|
let root = FileManager.default.temporaryDirectory
|
|
36
36
|
.appendingPathComponent("recordings-missing-companion-\(UUID().uuidString)")
|
|
37
|
-
let app = root.appendingPathComponent("
|
|
37
|
+
let app = root.appendingPathComponent("HasnaRecordings.app")
|
|
38
38
|
let home = root.appendingPathComponent("home")
|
|
39
39
|
let external = home.appendingPathComponent(".bun/bin/recordings")
|
|
40
40
|
try FileManager.default.createDirectory(at: app, withIntermediateDirectories: true)
|
|
@@ -6,7 +6,7 @@ struct RecordingStartGateTests {
|
|
|
6
6
|
@Test("permission-only launch skips global handlers and terminates after handling")
|
|
7
7
|
func permissionHelperLaunchPlan() {
|
|
8
8
|
let plan = PermissionRequestLaunchPlan(arguments: [
|
|
9
|
-
"/Applications/
|
|
9
|
+
"/Applications/HasnaRecordings.app/Contents/MacOS/Recordings",
|
|
10
10
|
"--request-permissions",
|
|
11
11
|
"--open-permission-settings",
|
|
12
12
|
])
|
|
@@ -51,7 +51,7 @@ enum RecordingsBootstrapPreflight {
|
|
|
51
51
|
let manifest = try JSONDecoder().decode(ReleaseManifest.self, from: manifestData)
|
|
52
52
|
try validateManifest(manifest, payload: payload)
|
|
53
53
|
|
|
54
|
-
let app = arguments.payloadRoot + "/Applications/
|
|
54
|
+
let app = arguments.payloadRoot + "/Applications/HasnaRecordings.app"
|
|
55
55
|
let client = app + "/" + RecordingsUpdateConstants.updateClientRelativePath
|
|
56
56
|
let broker = arguments.payloadRoot + "/Library/PrivilegedHelperTools/com.hasna.recordings.updater"
|
|
57
57
|
let artifactVerifier = arguments.payloadRoot
|
|
@@ -7,7 +7,7 @@ import RecordingsUpdateProtocol
|
|
|
7
7
|
/// operation; first installs use exclusive rename semantics and can never replace a
|
|
8
8
|
/// leaf created concurrently in `/Applications`.
|
|
9
9
|
final class ApplicationNamespace {
|
|
10
|
-
private static let liveLeaf = "
|
|
10
|
+
private static let liveLeaf = "HasnaRecordings.app"
|
|
11
11
|
private static let previousLeaf = "previous.app"
|
|
12
12
|
private static let failedCandidateLeaf = "failed-candidate.app"
|
|
13
13
|
|
|
@@ -22,7 +22,7 @@ final class ApplicationNamespace {
|
|
|
22
22
|
.deletingLastPathComponent
|
|
23
23
|
guard journal.applicationPath == RecordingsUpdateConstants.applicationPath,
|
|
24
24
|
journal.candidateApplicationPath == expectedTransactionDirectory
|
|
25
|
-
+ "/candidate/
|
|
25
|
+
+ "/candidate/HasnaRecordings.app",
|
|
26
26
|
journal.previousApplicationPath == nil ||
|
|
27
27
|
journal.previousApplicationPath == expectedTransactionDirectory + "/previous.app"
|
|
28
28
|
else {
|
|
@@ -276,7 +276,7 @@ final class UpdateBrokerSession: NSObject, RecordingsUpdateXPCProtocol {
|
|
|
276
276
|
guard mkdir(candidateRoot, 0o700) == 0 else {
|
|
277
277
|
throw BrokerOperationError.candidateRejected
|
|
278
278
|
}
|
|
279
|
-
let candidateApplication = candidateRoot + "/
|
|
279
|
+
let candidateApplication = candidateRoot + "/HasnaRecordings.app"
|
|
280
280
|
let verifierIDs = try verifierAccountIDs()
|
|
281
281
|
try CanonicalTreeCopier.copyApplication(
|
|
282
282
|
from: verifierOutput,
|
|
@@ -22,7 +22,7 @@ enum CanonicalTreeCopier {
|
|
|
22
22
|
}
|
|
23
23
|
defer { Darwin.close(source) }
|
|
24
24
|
let destinationRoot = URL(fileURLWithPath: rootCandidatePath).deletingLastPathComponent().path
|
|
25
|
-
guard URL(fileURLWithPath: rootCandidatePath).lastPathComponent == "
|
|
25
|
+
guard URL(fileURLWithPath: rootCandidatePath).lastPathComponent == "HasnaRecordings.app" else {
|
|
26
26
|
throw CanonicalCopyError.nonCanonicalRoot
|
|
27
27
|
}
|
|
28
28
|
let destination = Darwin.open(
|
|
@@ -253,7 +253,7 @@ struct DurableInstallJournal {
|
|
|
253
253
|
let leaf = URL(fileURLWithPath: transactionDirectory).lastPathComponent
|
|
254
254
|
guard leaf.hasPrefix("transaction-") else { throw InstallJournalError.invalidJournal }
|
|
255
255
|
let transactionID = String(leaf.dropFirst("transaction-".count))
|
|
256
|
-
let expectedCandidate = transactionDirectory + "/candidate/
|
|
256
|
+
let expectedCandidate = transactionDirectory + "/candidate/HasnaRecordings.app"
|
|
257
257
|
let activationPhases: Set<String> = [
|
|
258
258
|
"prepared", "launch-barrier-pending", "launch-barrier-held",
|
|
259
259
|
"swap-pending", "swapped", "previous-retaining",
|
|
@@ -201,8 +201,8 @@ struct ActivationRecoveryPolicyTests {
|
|
|
201
201
|
"Contents/Helpers/recordings": 493,
|
|
202
202
|
"Contents/Helpers/recordings-update-client": 493
|
|
203
203
|
},
|
|
204
|
-
"candidate_application_path": "/Library/Application Support/Hasna/Recordings/Updates/transaction-00000000-0000-0000-0000-000000000001/candidate/
|
|
205
|
-
"application_path": "/Applications/
|
|
204
|
+
"candidate_application_path": "/Library/Application Support/Hasna/Recordings/Updates/transaction-00000000-0000-0000-0000-000000000001/candidate/HasnaRecordings.app",
|
|
205
|
+
"application_path": "/Applications/HasnaRecordings.app"
|
|
206
206
|
}
|
|
207
207
|
"""#.utf8)
|
|
208
208
|
let journal = try JSONDecoder().decode(BrokerInstallJournal.self, from: legacyJSON)
|
|
@@ -7,7 +7,7 @@ public enum RecordingsUpdateConstants {
|
|
|
7
7
|
public static let rootMaintenanceSupported = false
|
|
8
8
|
public static let keyRotationSupported = false
|
|
9
9
|
public static let machServiceName = "com.hasna.recordings.updater"
|
|
10
|
-
public static let applicationPath = "/Applications/
|
|
10
|
+
public static let applicationPath = "/Applications/HasnaRecordings.app"
|
|
11
11
|
public static let brokerExecutablePath = "/Library/PrivilegedHelperTools/com.hasna.recordings.updater"
|
|
12
12
|
public static let stateRoot = "/Library/Application Support/Hasna/Recordings/Updates"
|
|
13
13
|
public static let trustRoot = "/Library/Application Support/Hasna/Recordings/Trust"
|
|
@@ -729,7 +729,7 @@ int recordings_copy_canonical_application_tree(
|
|
|
729
729
|
if (entry == NULL) break;
|
|
730
730
|
if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) continue;
|
|
731
731
|
visible_entries += 1;
|
|
732
|
-
if (strcmp(entry->d_name, "
|
|
732
|
+
if (strcmp(entry->d_name, "HasnaRecordings.app") != 0) only_application = false;
|
|
733
733
|
}
|
|
734
734
|
int status = errno;
|
|
735
735
|
closedir(directory);
|
|
@@ -739,7 +739,7 @@ int recordings_copy_canonical_application_tree(
|
|
|
739
739
|
status = copy_directory_at(
|
|
740
740
|
verifier_output_directory_descriptor,
|
|
741
741
|
root_candidate_directory_descriptor,
|
|
742
|
-
"
|
|
742
|
+
"HasnaRecordings.app",
|
|
743
743
|
verifier_user_id,
|
|
744
744
|
&budget
|
|
745
745
|
);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/bin/bash
|
|
2
|
-
# Build, sign, notarize, and finalize a
|
|
2
|
+
# Build, sign, notarize, and finalize a HasnaRecordings.app artifact.
|
|
3
3
|
# Usage: ./build.sh [debug|local] | ./build.sh release <initial-bootstrap|app-update>
|
|
4
4
|
|
|
5
5
|
set -euo pipefail
|
|
@@ -119,16 +119,12 @@ if [ "$VARIANT" = "bar" ]; then
|
|
|
119
119
|
VARIANT_SWIFT_FLAGS=(-Xswiftc -DRECORDINGS_BAR_ONLY)
|
|
120
120
|
fi
|
|
121
121
|
# Fleet naming rule (knowledge k_msxd5rz3_jfvl3i): every Hasna macOS app is
|
|
122
|
-
# Hasna<Name>.app with 'Hasna' at the beginning. The
|
|
123
|
-
#
|
|
124
|
-
#
|
|
125
|
-
#
|
|
126
|
-
#
|
|
127
|
-
|
|
128
|
-
APP_BUNDLE_NAME="Recordings.app"
|
|
129
|
-
if [ "$VARIANT" = "bar" ]; then
|
|
130
|
-
APP_BUNDLE_NAME="HasnaRecordings.app"
|
|
131
|
-
fi
|
|
122
|
+
# Hasna<Name>.app with 'Hasna' at the beginning. The bundle is named
|
|
123
|
+
# HasnaRecordings.app for BOTH variants (full and bar); the 'bar' is a variant, never
|
|
124
|
+
# a separate app name. Bundle identifier stays com.hasna.recordings so TCC keeps
|
|
125
|
+
# keying on bundle id + signing identity, and the executable inside the bundle stays
|
|
126
|
+
# "Recordings".
|
|
127
|
+
APP_BUNDLE_NAME="HasnaRecordings.app"
|
|
132
128
|
readonly APP_BUNDLE_NAME
|
|
133
129
|
APP_BASENAME="${APP_BUNDLE_NAME%.app}"
|
|
134
130
|
PLIST_BUDDY="$(select_executable "/usr/libexec/PlistBuddy" "${RECORDINGS_TEST_PLIST_BUDDY_EXECUTABLE:-${PLIST_BUDDY:-}}")"
|
|
@@ -1260,7 +1256,7 @@ fi
|
|
|
1260
1256
|
|
|
1261
1257
|
generate_and_verify_native_fs_guard
|
|
1262
1258
|
|
|
1263
|
-
echo "Building
|
|
1259
|
+
echo "Building HasnaRecordings.app ($MODE)..."
|
|
1264
1260
|
if [ "$MODE" = "release" ]; then
|
|
1265
1261
|
OUTPUT_BUILD_DIR="$BUILD_ROOT/release-output"
|
|
1266
1262
|
else
|
|
@@ -1426,15 +1422,15 @@ if [ "$MODE" = "release" ] && [ "$HOST_PLATFORM" = "Darwin" ]; then
|
|
|
1426
1422
|
fi
|
|
1427
1423
|
fi
|
|
1428
1424
|
"$CP_EXECUTABLE" "$SOURCE_NATIVE_DIR/RecordingsLib/Info.plist" "$CONTENTS/Info.plist"
|
|
1425
|
+
# Bundle display name per the fleet naming rule (knowledge k_msxd5rz3_jfvl3i):
|
|
1426
|
+
# "Hasna Recordings" with 'Hasna' at the beginning, for both variants.
|
|
1427
|
+
"$PLIST_BUDDY" -c 'Set :CFBundleDisplayName Hasna Recordings' "$CONTENTS/Info.plist"
|
|
1429
1428
|
if [ "$VARIANT" = "bar" ]; then
|
|
1430
1429
|
# The bar is an accessory app: LSUIElement keeps it out of the Dock and the app
|
|
1431
1430
|
# switcher, and the app's window-declaring control flow (RECORDINGS_BAR_ONLY)
|
|
1432
1431
|
# keeps the workspace window absent. The bundle identifier stays com.hasna.recordings
|
|
1433
|
-
# so TCC grants continue to key on bundle id + signing identity
|
|
1434
|
-
# named per the fleet rule (knowledge k_msxd5rz3_jfvl3i) — HasnaRecordings.app with
|
|
1435
|
-
# the display name "Hasna Recordings"; 'Hasna' at the beginning of the name.
|
|
1432
|
+
# so TCC grants continue to key on bundle id + signing identity.
|
|
1436
1433
|
"$PLIST_BUDDY" -c 'Add :LSUIElement bool true' "$CONTENTS/Info.plist"
|
|
1437
|
-
"$PLIST_BUDDY" -c 'Set :CFBundleDisplayName Hasna Recordings' "$CONTENTS/Info.plist"
|
|
1438
1434
|
fi
|
|
1439
1435
|
VERSION="$("$PLIST_BUDDY" -c 'Print :CFBundleShortVersionString' "$CONTENTS/Info.plist")"
|
|
1440
1436
|
|
|
@@ -1670,8 +1666,6 @@ run_signed_helper_contract() {
|
|
|
1670
1666
|
HOME="$contract_home"
|
|
1671
1667
|
PATH="/usr/bin:/bin:/usr/sbin:/sbin"
|
|
1672
1668
|
TMPDIR="$contract_home"
|
|
1673
|
-
HASNA_RECORDINGS_CLIENT_STORE="sqlite"
|
|
1674
|
-
RECORDINGS_CLIENT_STORE="sqlite"
|
|
1675
1669
|
HASNA_RECORDINGS_DB_PATH="$contract_home/recordings.db"
|
|
1676
1670
|
RECORDINGS_AUDIO_DIR="$contract_home/audio"
|
|
1677
1671
|
)
|
|
@@ -1998,7 +1992,7 @@ if [ "$MODE" = "local" ]; then
|
|
|
1998
1992
|
fi
|
|
1999
1993
|
|
|
2000
1994
|
verify_signed_code "$HELPERS/recordings" "Companion CLI"
|
|
2001
|
-
verify_signed_code "$APP_DIR" "
|
|
1995
|
+
verify_signed_code "$APP_DIR" "HasnaRecordings.app"
|
|
2002
1996
|
ARTIFACT_BASENAME="${APP_BASENAME}-${VERSION}-macos-${RELEASE_SUBTYPE}"
|
|
2003
1997
|
NOTARY_ARCHIVE="$OUTPUT_BUILD_DIR/${ARTIFACT_BASENAME}-notarization.zip"
|
|
2004
1998
|
FINAL_ARCHIVE="$OUTPUT_BUILD_DIR/${ARTIFACT_BASENAME}.zip"
|
|
@@ -2050,7 +2044,7 @@ run_xcrun stapler validate "$APP_DIR"
|
|
|
2050
2044
|
run_release_sensitive_tool "$SPCTL_EXECUTABLE" --assess --type execute --verbose=2 "$APP_DIR"
|
|
2051
2045
|
run_release_sensitive_tool "$SYSPOLICY_CHECK_EXECUTABLE" distribution "$APP_DIR"
|
|
2052
2046
|
verify_signed_code "$HELPERS/recordings" "Companion CLI"
|
|
2053
|
-
verify_signed_code "$APP_DIR" "
|
|
2047
|
+
verify_signed_code "$APP_DIR" "HasnaRecordings.app"
|
|
2054
2048
|
run_codesign --verify --deep --strict --verbose=2 "$APP_DIR"
|
|
2055
2049
|
|
|
2056
2050
|
run_release_sensitive_tool "$DITTO_EXECUTABLE" -c -k --sequesterRsrc --keepParent "$APP_DIR" "$FINAL_ARCHIVE"
|