@hasna/recordings 0.3.1 → 0.3.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +38 -34
- package/bun.lock +96 -36
- package/dist/__tests__/helpers/installer-guard-execution.d.ts.map +1 -1
- package/dist/__tests__/helpers/source-assertions.d.ts +6 -4
- package/dist/__tests__/helpers/source-assertions.d.ts.map +1 -1
- package/dist/cli/index.js +197 -95
- package/dist/cli/macos-shortcut.d.ts +2 -2
- package/dist/db/pg-migrations.d.ts +1 -1
- package/dist/http/client.d.ts +0 -1
- package/dist/http/client.d.ts.map +1 -1
- package/dist/index.js +162 -73
- package/dist/lib/capture-probe.d.ts +3 -3
- package/dist/lib/capture-probe.d.ts.map +1 -1
- package/dist/lib/macos-bundle.d.ts +1 -1
- package/dist/lib/release-install-policy.d.ts +21 -0
- package/dist/lib/release-install-policy.d.ts.map +1 -1
- package/dist/mcp/index.js +144 -59
- package/dist/sdk/index.d.ts +2 -2
- package/dist/sdk/index.js +7 -3
- package/dist/sdk/v1.generated.d.ts.map +1 -1
- package/dist/server/cloud-config.d.ts +4 -19
- package/dist/server/cloud-config.d.ts.map +1 -1
- package/dist/server/cloud.d.ts +1 -1
- package/dist/server/cloud.d.ts.map +1 -1
- package/dist/server/index.js +168 -110
- package/dist/server/serve.d.ts.map +1 -1
- package/dist/storage.d.ts +1 -1
- package/dist/storage.d.ts.map +1 -1
- package/dist/storage.js +144 -60
- package/dist/store.d.ts.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.d.ts.map +1 -1
- package/package.json +4 -3
- package/packaging/macos/build_release_pkg.sh +10 -4
- package/packaging/macos/managed_bootstrap.sh +4 -4
- package/packaging/macos/scripts/postinstall +2 -2
- package/packaging/macos/scripts/preinstall +2 -0
- package/scripts/generate-sdk.ts +17 -16
- package/scripts/install_macos_app.sh +75 -28
- package/scripts/macos_artifact.ts +162 -45
- package/scripts/migrate.ts +2 -2
- package/scripts/release-suite-gate.ts +181 -0
- package/scripts/set-version.ts +5 -5
- package/scripts/smoke_macos_app.sh +80 -34
- package/scripts/vacuity-manifests/version-sites.tsv +8 -4
- package/src/native/Recordings/App/MenuBarStatusView.swift +9 -4
- package/src/native/Recordings/App/RecordingsApp.swift +37 -1
- package/src/native/Recordings/RecordingsLib/Info.plist +3 -3
- package/src/native/Recordings/RecordingsLib/PermissionRequestLaunchPlan.swift +21 -1
- package/src/native/Recordings/RecordingsTests/BarOnlyLaunchPlanTests.swift +99 -0
- package/src/native/Recordings/RecordingsTests/CLIRunnerTests.swift +2 -2
- package/src/native/Recordings/RecordingsTests/RecordingStartGateTests.swift +1 -1
- package/src/native/Recordings/Updater/BootstrapPreflight/BootstrapPreflightMain.swift +1 -1
- package/src/native/Recordings/Updater/Broker/ApplicationNamespace.swift +2 -2
- package/src/native/Recordings/Updater/Broker/BrokerMain.swift +1 -1
- package/src/native/Recordings/Updater/Broker/CanonicalTreeCopy.swift +1 -1
- package/src/native/Recordings/Updater/Broker/InstallJournal.swift +1 -1
- package/src/native/Recordings/Updater/BrokerTests/ActivationRecoveryPolicyTests.swift +2 -2
- package/src/native/Recordings/Updater/Protocol/UpdateProtocol.swift +1 -1
- package/src/native/Recordings/Updater/VerifierLauncher/RecordingsVerifierLauncher.c +2 -2
- package/src/native/Recordings/build.sh +152 -18
- package/dist/lib/retired-deployment-modes.d.ts +0 -24
- package/dist/lib/retired-deployment-modes.d.ts.map +0 -1
package/dist/index.js
CHANGED
|
@@ -30,10 +30,14 @@ class RecordingsV1Client {
|
|
|
30
30
|
const url = new URL(this.baseUrl + path);
|
|
31
31
|
if (opts.query) {
|
|
32
32
|
for (const [key, value] of Object.entries(opts.query)) {
|
|
33
|
+
if (value === undefined || value === null)
|
|
34
|
+
continue;
|
|
33
35
|
if (Array.isArray(value)) {
|
|
34
|
-
for (const item of value)
|
|
35
|
-
|
|
36
|
-
|
|
36
|
+
for (const item of value) {
|
|
37
|
+
if (item !== undefined && item !== null)
|
|
38
|
+
url.searchParams.append(key, String(item));
|
|
39
|
+
}
|
|
40
|
+
} else {
|
|
37
41
|
url.searchParams.set(key, String(value));
|
|
38
42
|
}
|
|
39
43
|
}
|
|
@@ -1184,9 +1188,118 @@ function setAgentFocus(idOrName, projectId, db) {
|
|
|
1184
1188
|
d.query("UPDATE agents SET active_project_id = ?, last_seen_at = ? WHERE id = ?").run(resolvedProjectId, new Date().toISOString(), agent.id);
|
|
1185
1189
|
return getAgent(agent.id, d);
|
|
1186
1190
|
}
|
|
1191
|
+
// package.json
|
|
1192
|
+
var package_default = {
|
|
1193
|
+
name: "@hasna/recordings",
|
|
1194
|
+
version: "0.3.8",
|
|
1195
|
+
type: "module",
|
|
1196
|
+
description: "Speech-to-text recording tool with MCP and CLI \u2014 records, transcribes, and optionally enhances text using AI",
|
|
1197
|
+
repository: {
|
|
1198
|
+
type: "git",
|
|
1199
|
+
url: "git+https://github.com/hasna/apps"
|
|
1200
|
+
},
|
|
1201
|
+
main: "dist/index.js",
|
|
1202
|
+
types: "dist/index.d.ts",
|
|
1203
|
+
bin: {
|
|
1204
|
+
recordings: "dist/cli/index.js",
|
|
1205
|
+
"recordings-mcp": "dist/mcp/index.js",
|
|
1206
|
+
"recordings-serve": "dist/server/index.js"
|
|
1207
|
+
},
|
|
1208
|
+
exports: {
|
|
1209
|
+
".": {
|
|
1210
|
+
import: "./dist/index.js",
|
|
1211
|
+
types: "./dist/index.d.ts"
|
|
1212
|
+
},
|
|
1213
|
+
"./storage": {
|
|
1214
|
+
import: "./dist/storage.js",
|
|
1215
|
+
types: "./dist/storage.d.ts"
|
|
1216
|
+
},
|
|
1217
|
+
"./sdk": {
|
|
1218
|
+
import: "./dist/sdk/index.js",
|
|
1219
|
+
types: "./dist/sdk/index.d.ts"
|
|
1220
|
+
}
|
|
1221
|
+
},
|
|
1222
|
+
engines: {
|
|
1223
|
+
bun: ">=1.0.0"
|
|
1224
|
+
},
|
|
1225
|
+
scripts: {
|
|
1226
|
+
clean: "rm -rf dist",
|
|
1227
|
+
build: "bun run clean && bun run build:cli && bun run build:mcp && bun run build:serve && bun run build:lib && tsc --emitDeclarationOnly --outDir dist",
|
|
1228
|
+
"build:cli": "bun build src/cli/index.ts --target=bun --outfile=dist/cli/index.js --external=commander --external=chalk --external=openai",
|
|
1229
|
+
"build:mcp": "bun build src/mcp/index.ts --target=bun --outfile=dist/mcp/index.js --external=@modelcontextprotocol/sdk --external=openai",
|
|
1230
|
+
"build:serve": "bun build src/server/index.ts --target=bun --outfile=dist/server/index.js --external=@modelcontextprotocol/sdk --external=@hasna/contracts --external=openai --external=pg",
|
|
1231
|
+
"build:lib": "bun build src/index.ts src/storage.ts src/sdk/index.ts --target=bun --outdir=dist --external=openai",
|
|
1232
|
+
"build:native-fs-guard": "/bin/bash scripts/build_native_fs_guard.sh",
|
|
1233
|
+
"generate:sdk": "bun run scripts/generate-sdk.ts",
|
|
1234
|
+
migrate: "bun run scripts/migrate.ts",
|
|
1235
|
+
typecheck: "tsc --noEmit",
|
|
1236
|
+
"typecheck:tcc-contract": "tsc --noEmit -p tsconfig.tcc-contract.json",
|
|
1237
|
+
test: "bun test",
|
|
1238
|
+
"verify:ci-suite": "bun scripts/ci-linux-suite.ts --check",
|
|
1239
|
+
"verify:ci-suite:run": "bun scripts/ci-linux-suite.ts --verify-run",
|
|
1240
|
+
"verify:ci-native": "bun scripts/ci-native-build.ts",
|
|
1241
|
+
"test:gated": 'bun scripts/ci-linux-suite.ts --check && RECORDINGS_TEST_TIMEOUT_MS="${RECORDINGS_TEST_TIMEOUT_MS:-120000}" bun test --timeout "$RECORDINGS_TEST_TIMEOUT_MS" $(bun scripts/ci-linux-suite.ts --gated)',
|
|
1242
|
+
"test:vacuity-battery": 'bun scripts/vacuity-manifest-gen.ts > "${TMPDIR:-/tmp}/vacuity-corrupt-sites.tsv" && bun scripts/vacuity-mutation-battery.ts "${TMPDIR:-/tmp}/vacuity-corrupt-sites.tsv"',
|
|
1243
|
+
"test:vacuity-battery:source": "bun scripts/vacuity-mutation-battery.ts scripts/vacuity-manifests/source-side.tsv",
|
|
1244
|
+
"test:vacuity-battery:chain": "bun scripts/vacuity-mutation-battery.ts scripts/vacuity-manifests/install-chain.tsv",
|
|
1245
|
+
"test:vacuity-battery:vars": "bun scripts/vacuity-mutation-battery.ts scripts/vacuity-manifests/variable-operands.tsv",
|
|
1246
|
+
"test:vacuity-battery:reorder": "bun scripts/vacuity-mutation-battery.ts scripts/vacuity-manifests/reorder.tsv",
|
|
1247
|
+
"test:coverage": "bun test --coverage",
|
|
1248
|
+
"dev:cli": "bun run src/cli/index.ts",
|
|
1249
|
+
"desktop:snapshot": "bun run src/cli/index.ts app snapshot",
|
|
1250
|
+
"dev:mcp": "bun run src/mcp/index.ts",
|
|
1251
|
+
"verify:release": "bun run scripts/release-guard.ts",
|
|
1252
|
+
"scan:artifact": "bun run scripts/scan-artifact.ts",
|
|
1253
|
+
"version:set": "bun run scripts/set-version.ts",
|
|
1254
|
+
"version:check": "bun run scripts/set-version.ts --check",
|
|
1255
|
+
prepack: "bun run prepack:platform-gate && bun run version:check && bun run build && bun run verify:release && bun run scan:artifact",
|
|
1256
|
+
"prepack:platform-gate": `bash -c 'if [ "$(uname -s)" = Darwin ]; then bun run build:native-fs-guard; else echo "WARN: native fs-guard build skipped on $(uname -s) \u2014 dry-run/CI pack; publish recordings from macOS"; fi'`,
|
|
1257
|
+
prepublishOnly: "bun run typecheck && bun run release-suite-gate",
|
|
1258
|
+
"release-suite-gate": "bun run scripts/release-suite-gate.ts",
|
|
1259
|
+
"typecheck:shortcut-contract": "tsc --noEmit -p tsconfig.test.json"
|
|
1260
|
+
},
|
|
1261
|
+
files: [
|
|
1262
|
+
"dist/",
|
|
1263
|
+
"scripts/",
|
|
1264
|
+
"Dockerfile.package",
|
|
1265
|
+
"bun.lock",
|
|
1266
|
+
"src/native/Recordings/App/",
|
|
1267
|
+
"src/native/Recordings/RecordingsLib/",
|
|
1268
|
+
"src/native/Recordings/RecordingsTests/",
|
|
1269
|
+
"src/native/Recordings/Updater/",
|
|
1270
|
+
"src/native/Recordings/Package.swift",
|
|
1271
|
+
"src/native/Recordings/Package.resolved",
|
|
1272
|
+
"src/native/Recordings/build.sh",
|
|
1273
|
+
"packaging/macos/",
|
|
1274
|
+
"README.md",
|
|
1275
|
+
"LICENSE"
|
|
1276
|
+
],
|
|
1277
|
+
dependencies: {
|
|
1278
|
+
"@hasna/contracts": "0.13.3",
|
|
1279
|
+
"@hasna/events": "0.1.11",
|
|
1280
|
+
"@modelcontextprotocol/sdk": "^1.12.1",
|
|
1281
|
+
chalk: "^5.4.1",
|
|
1282
|
+
commander: "^13.1.0",
|
|
1283
|
+
openai: "^5.1.0",
|
|
1284
|
+
pg: "^8.13.3",
|
|
1285
|
+
zod: "^3.24.2"
|
|
1286
|
+
},
|
|
1287
|
+
devDependencies: {
|
|
1288
|
+
"@types/bun": "^1.2.5",
|
|
1289
|
+
"@types/pg": "^8.11.11",
|
|
1290
|
+
"node-api-headers": "1.9.0",
|
|
1291
|
+
typescript: "^5.8.2"
|
|
1292
|
+
},
|
|
1293
|
+
publishConfig: {
|
|
1294
|
+
registry: "https://registry.npmjs.org",
|
|
1295
|
+
access: "public"
|
|
1296
|
+
},
|
|
1297
|
+
license: "Apache-2.0",
|
|
1298
|
+
author: "Hasna <andrei@hasna.com>"
|
|
1299
|
+
};
|
|
1187
1300
|
|
|
1188
1301
|
// src/version.ts
|
|
1189
|
-
var VERSION =
|
|
1302
|
+
var VERSION = package_default.version;
|
|
1190
1303
|
|
|
1191
1304
|
// src/db/feedback.ts
|
|
1192
1305
|
function saveFeedback(input) {
|
|
@@ -1194,58 +1307,26 @@ function saveFeedback(input) {
|
|
|
1194
1307
|
db.query("INSERT INTO feedback (message, email, category, version) VALUES (?, ?, ?, ?)").run(input.message, input.email ?? null, input.category ?? "general", input.version ?? VERSION);
|
|
1195
1308
|
}
|
|
1196
1309
|
|
|
1197
|
-
// src/lib/retired-deployment-modes.ts
|
|
1198
|
-
var RETIRED_DEPLOYMENT_MODES = [
|
|
1199
|
-
"local",
|
|
1200
|
-
"self_hosted",
|
|
1201
|
-
"cloud",
|
|
1202
|
-
"remote",
|
|
1203
|
-
"hybrid"
|
|
1204
|
-
];
|
|
1205
|
-
function normalizeModeToken(value) {
|
|
1206
|
-
return value.trim().toLowerCase().replace(/-/g, "_");
|
|
1207
|
-
}
|
|
1208
|
-
function asRetiredDeploymentMode(value) {
|
|
1209
|
-
const token = normalizeModeToken(value);
|
|
1210
|
-
return RETIRED_DEPLOYMENT_MODES.includes(token) ? token : null;
|
|
1211
|
-
}
|
|
1212
|
-
function retiredDeploymentModeError(rawValue, sourceEnvKey, replacement) {
|
|
1213
|
-
const mode = asRetiredDeploymentMode(rawValue);
|
|
1214
|
-
const value = mode === "local" ? replacement.onBox : replacement.offBox;
|
|
1215
|
-
return new Error(`${sourceEnvKey}=${rawValue} names a deployment mode, and deployment modes are removed: ` + `${RETIRED_DEPLOYMENT_MODES.join(" | ")} no longer select anything. ` + `Set ${replacement.envKey}=${value} instead ` + `(${replacement.envKey} takes ${replacement.onBox} or ${replacement.offBox}).`);
|
|
1216
|
-
}
|
|
1217
|
-
|
|
1218
1310
|
// src/http/client.ts
|
|
1219
|
-
var SERVER_BACKEND_VALUES = ["sqlite", "postgresql", "postgres"];
|
|
1220
1311
|
function envToken(name) {
|
|
1221
1312
|
return name.toUpperCase().replace(/-/g, "_");
|
|
1222
1313
|
}
|
|
1223
|
-
function
|
|
1224
|
-
|
|
1314
|
+
function envKeys(name) {
|
|
1315
|
+
const token = envToken(name);
|
|
1316
|
+
return {
|
|
1317
|
+
storeKeys: [`HASNA_${token}_CLIENT_STORE`, `${token}_CLIENT_STORE`],
|
|
1318
|
+
apiUrlKeys: [`HASNA_${token}_API_URL`],
|
|
1319
|
+
apiKeyKeys: [`HASNA_${token}_API_KEY`]
|
|
1320
|
+
};
|
|
1225
1321
|
}
|
|
1226
|
-
function normalizeClientStore(value
|
|
1227
|
-
const normalized =
|
|
1322
|
+
function normalizeClientStore(value) {
|
|
1323
|
+
const normalized = value.trim().toLowerCase();
|
|
1228
1324
|
if (normalized === "sqlite")
|
|
1229
1325
|
return "sqlite";
|
|
1230
1326
|
if (normalized === "http" || normalized === "https")
|
|
1231
1327
|
return "http";
|
|
1232
|
-
if (asRetiredDeploymentMode(value)) {
|
|
1233
|
-
throw retiredDeploymentModeError(value, sourceEnvKey, clientStoreReplacement(replacementEnvKey));
|
|
1234
|
-
}
|
|
1235
1328
|
throw new Error(`Unknown client store: ${value}. Use sqlite or http.`);
|
|
1236
1329
|
}
|
|
1237
|
-
function defaultApiBaseUrl(name) {
|
|
1238
|
-
return `https://${name}.hasna.xyz`;
|
|
1239
|
-
}
|
|
1240
|
-
function envKeys(name) {
|
|
1241
|
-
const token = envToken(name);
|
|
1242
|
-
return {
|
|
1243
|
-
storeKeys: [`HASNA_${token}_CLIENT_STORE`, `${token}_CLIENT_STORE`],
|
|
1244
|
-
retiredModeKeys: [`HASNA_${token}_STORAGE_MODE`, `HASNA_${token}_MODE`, `${token}_STORAGE_MODE`, `${token}_MODE`],
|
|
1245
|
-
apiUrlKeys: [`HASNA_${token}_API_URL`, `${token}_API_URL`],
|
|
1246
|
-
apiKeyKeys: [`HASNA_${token}_API_KEY`, `${token}_API_KEY`]
|
|
1247
|
-
};
|
|
1248
|
-
}
|
|
1249
1330
|
function firstEnv(env, keys) {
|
|
1250
1331
|
for (const key of keys) {
|
|
1251
1332
|
const value = env[key]?.trim();
|
|
@@ -1254,20 +1335,6 @@ function firstEnv(env, keys) {
|
|
|
1254
1335
|
}
|
|
1255
1336
|
return null;
|
|
1256
1337
|
}
|
|
1257
|
-
function assertNoRetiredMode(env, keys) {
|
|
1258
|
-
for (const key of keys.retiredModeKeys) {
|
|
1259
|
-
const value = env[key]?.trim();
|
|
1260
|
-
if (!value)
|
|
1261
|
-
continue;
|
|
1262
|
-
const normalized = normalizeModeToken(value);
|
|
1263
|
-
if (SERVER_BACKEND_VALUES.includes(normalized))
|
|
1264
|
-
continue;
|
|
1265
|
-
if (asRetiredDeploymentMode(value)) {
|
|
1266
|
-
throw retiredDeploymentModeError(value, key, clientStoreReplacement(keys.storeKeys[0]));
|
|
1267
|
-
}
|
|
1268
|
-
throw new Error(`${key}=${value} is not a client store. ${key} no longer selects one; ` + `set ${keys.storeKeys[0]}=sqlite or ${keys.storeKeys[0]}=http instead.`);
|
|
1269
|
-
}
|
|
1270
|
-
}
|
|
1271
1338
|
function toV1BaseUrl(apiUrl) {
|
|
1272
1339
|
const url = new URL(apiUrl);
|
|
1273
1340
|
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
|
@@ -1283,22 +1350,44 @@ function toV1BaseUrl(apiUrl) {
|
|
|
1283
1350
|
}
|
|
1284
1351
|
function resolveTransport(name, env = process.env) {
|
|
1285
1352
|
const keys = envKeys(name);
|
|
1286
|
-
assertNoRetiredMode(env, keys);
|
|
1287
1353
|
const storeHit = firstEnv(env, keys.storeKeys);
|
|
1288
1354
|
const urlHit = firstEnv(env, keys.apiUrlKeys);
|
|
1289
1355
|
const keyHit = firstEnv(env, keys.apiKeyKeys);
|
|
1290
1356
|
let requested = "sqlite";
|
|
1291
1357
|
let modeSource = "default";
|
|
1292
1358
|
if (storeHit) {
|
|
1293
|
-
requested = normalizeClientStore(storeHit.value
|
|
1359
|
+
requested = normalizeClientStore(storeHit.value);
|
|
1294
1360
|
modeSource = storeHit.key;
|
|
1295
1361
|
} else if (urlHit && keyHit) {
|
|
1296
1362
|
requested = "http";
|
|
1297
1363
|
modeSource = "auto:api-url+api-key";
|
|
1364
|
+
} else if (urlHit || keyHit) {
|
|
1365
|
+
const missing = urlHit ? keys.apiKeyKeys[0] : keys.apiUrlKeys[0];
|
|
1366
|
+
const present = urlHit ? keys.apiUrlKeys[0] : keys.apiKeyKeys[0];
|
|
1367
|
+
return {
|
|
1368
|
+
transport: "sqlite",
|
|
1369
|
+
requested,
|
|
1370
|
+
modeSource,
|
|
1371
|
+
baseUrl: null,
|
|
1372
|
+
apiKeyPresent: Boolean(keyHit),
|
|
1373
|
+
misconfigured: true,
|
|
1374
|
+
warning: `${present} is set but ${missing} is not: the hosted API is only ` + `selected when BOTH are present. Set ${missing}, or unset ${present} to ` + `use the on-box store.`
|
|
1375
|
+
};
|
|
1298
1376
|
}
|
|
1299
1377
|
if (requested === "sqlite") {
|
|
1300
1378
|
return { transport: "sqlite", requested, modeSource, baseUrl: null, apiKeyPresent: Boolean(keyHit), misconfigured: false, warning: null };
|
|
1301
1379
|
}
|
|
1380
|
+
if (!urlHit) {
|
|
1381
|
+
return {
|
|
1382
|
+
transport: "sqlite",
|
|
1383
|
+
requested,
|
|
1384
|
+
modeSource,
|
|
1385
|
+
baseUrl: null,
|
|
1386
|
+
apiKeyPresent: Boolean(keyHit),
|
|
1387
|
+
misconfigured: true,
|
|
1388
|
+
warning: `${modeSource}=http but no API URL is set (${keys.apiUrlKeys[0]}). Refusing to route to the API.`
|
|
1389
|
+
};
|
|
1390
|
+
}
|
|
1302
1391
|
if (!keyHit) {
|
|
1303
1392
|
return {
|
|
1304
1393
|
transport: "sqlite",
|
|
@@ -1310,7 +1399,7 @@ function resolveTransport(name, env = process.env) {
|
|
|
1310
1399
|
warning: `${modeSource}=http but no API key is set (${keys.apiKeyKeys[0]}). Refusing to route to the API.`
|
|
1311
1400
|
};
|
|
1312
1401
|
}
|
|
1313
|
-
const rawUrl = urlHit
|
|
1402
|
+
const rawUrl = urlHit.value;
|
|
1314
1403
|
let baseUrl;
|
|
1315
1404
|
try {
|
|
1316
1405
|
baseUrl = toV1BaseUrl(rawUrl);
|
|
@@ -2360,37 +2449,37 @@ function captureProbeSubject(env = process.env, options = {}) {
|
|
|
2360
2449
|
const termProgram = env.TERM_PROGRAM?.trim();
|
|
2361
2450
|
const hasTty = options.hasTty ?? Boolean(process.stdin.isTTY || process.stdout.isTTY);
|
|
2362
2451
|
if (overSsh) {
|
|
2363
|
-
const subject2 = "the SSH session (sshd), not
|
|
2452
|
+
const subject2 = "the SSH session (sshd), not HasnaRecordings.app";
|
|
2364
2453
|
return {
|
|
2365
2454
|
headless: true,
|
|
2366
2455
|
subject_known: true,
|
|
2367
2456
|
subject: subject2,
|
|
2368
|
-
note: "Running over SSH: macOS cannot display a consent prompt to a session with no GUI, " + "so Microphone stays not_determined and a silent capture here says NOTHING about " + "whether
|
|
2457
|
+
note: "Running over SSH: macOS cannot display a consent prompt to a session with no GUI, " + "so Microphone stays not_determined and a silent capture here says NOTHING about " + "whether HasnaRecordings.app can record. Judge the app by its own TCC entry and its log."
|
|
2369
2458
|
};
|
|
2370
2459
|
}
|
|
2371
2460
|
if (inTmux) {
|
|
2372
|
-
const subject2 = "the tmux server, not
|
|
2461
|
+
const subject2 = "the tmux server, not HasnaRecordings.app and not this pane's shell";
|
|
2373
2462
|
return {
|
|
2374
2463
|
headless: false,
|
|
2375
2464
|
subject_known: true,
|
|
2376
2465
|
subject: subject2,
|
|
2377
|
-
note: "Running inside tmux: TCC attributes this capture to the tmux binary, which holds its " + "own grant. tmux also snapshots the environment at pane creation, so SSH variables may " + "be missing even in a remote session \u2014 treat a silent result as inconclusive about both " + "
|
|
2466
|
+
note: "Running inside tmux: TCC attributes this capture to the tmux binary, which holds its " + "own grant. tmux also snapshots the environment at pane creation, so SSH variables may " + "be missing even in a remote session \u2014 treat a silent result as inconclusive about both " + "HasnaRecordings.app and about whether anyone could have been prompted."
|
|
2378
2467
|
};
|
|
2379
2468
|
}
|
|
2380
2469
|
if (!termProgram && !hasTty) {
|
|
2381
2470
|
return {
|
|
2382
2471
|
headless: true,
|
|
2383
2472
|
subject_known: false,
|
|
2384
|
-
subject: "an unidentified responsible process (not
|
|
2385
|
-
note: "The responsible process could not be identified: no SSH variables, no TERM_PROGRAM and " + "no tty, which is what launchd, cron, CI and sudo look like. Whatever holds the grant, it " + "is not
|
|
2473
|
+
subject: "an unidentified responsible process (not HasnaRecordings.app)",
|
|
2474
|
+
note: "The responsible process could not be identified: no SSH variables, no TERM_PROGRAM and " + "no tty, which is what launchd, cron, CI and sudo look like. Whatever holds the grant, it " + "is not HasnaRecordings.app, and nothing here can be shown a consent prompt. Inconclusive."
|
|
2386
2475
|
};
|
|
2387
2476
|
}
|
|
2388
|
-
const subject = `${termProgram || "the terminal application running this command"}, not
|
|
2477
|
+
const subject = `${termProgram || "the terminal application running this command"}, not HasnaRecordings.app`;
|
|
2389
2478
|
return {
|
|
2390
2479
|
headless: false,
|
|
2391
2480
|
subject_known: Boolean(termProgram),
|
|
2392
2481
|
subject,
|
|
2393
|
-
note: `Grants are per responsible process: this probe exercises ${subject}. ` + "A pass proves the microphone hardware and the input device work; it does not " + "transfer to
|
|
2482
|
+
note: `Grants are per responsible process: this probe exercises ${subject}. ` + "A pass proves the microphone hardware and the input device work; it does not " + "transfer to HasnaRecordings.app, which needs its own grant."
|
|
2394
2483
|
};
|
|
2395
2484
|
}
|
|
2396
2485
|
var TCC_UNREADABLE_STATE = TCC_DATABASE_UNREADABLE_STATE;
|
|
@@ -2420,7 +2509,7 @@ function microphoneGrantInstruction(options) {
|
|
|
2420
2509
|
const bundlePath = candidates[0] ?? null;
|
|
2421
2510
|
const steps = [];
|
|
2422
2511
|
if (!bundlePath) {
|
|
2423
|
-
steps.push("No
|
|
2512
|
+
steps.push("No HasnaRecordings.app bundle was found on disk, so there is nothing to grant Microphone to yet. " + "Install the app first ('recordings app install').");
|
|
2424
2513
|
return {
|
|
2425
2514
|
bundle_path: null,
|
|
2426
2515
|
bundle_identifier: RECORDINGS_BUNDLE_IDENTIFIER,
|
|
@@ -2429,7 +2518,7 @@ function microphoneGrantInstruction(options) {
|
|
|
2429
2518
|
};
|
|
2430
2519
|
}
|
|
2431
2520
|
if (candidates.length > 1) {
|
|
2432
|
-
steps.push(`AMBIGUOUS: ${candidates.length}
|
|
2521
|
+
steps.push(`AMBIGUOUS: ${candidates.length} HasnaRecordings.app bundles exist (${candidates.join(", ")}). ` + "A TCC grant is bound to the bundle's code signature, so granting one does not grant the " + "other, and the toggle in Settings does not say which is which. Remove the bundles you are " + "not running before granting, or the grant may attach to the wrong one.");
|
|
2433
2522
|
}
|
|
2434
2523
|
steps.push(`At the keyboard on the machine itself (not over SSH), launch ${bundlePath} and start a ` + "recording once. macOS shows the consent sheet titled " + `"\u201CRecordings\u201D would like to access the microphone" \u2014 click Allow.`);
|
|
2435
2524
|
steps.push("If no sheet appears, open System Settings \u2192 Privacy & Security \u2192 Microphone " + "and switch ON the row named \u201CRecordings\u201D. " + `That row is bundle ${RECORDINGS_BUNDLE_IDENTIFIER} at ${bundlePath}; the binary that ` + `receives the grant is ${join4(bundlePath, "Contents", "MacOS", "Recordings")}.`);
|
|
@@ -2512,7 +2601,7 @@ function describeActiveStore(config, env = process.env) {
|
|
|
2512
2601
|
warnings.push(`writes go to ${safeBaseUrl(resolution.baseUrl)}, but ${localDbPath} still holds ` + `${localDbRecordings} recordings from an earlier on-box-only period. ` + "That file is NOT the live store \u2014 auditing it undercounts and looks like data loss.");
|
|
2513
2602
|
}
|
|
2514
2603
|
if (resolution.transport === "http" && resolution.modeSource === AUTO_FLIP_MODE_SOURCE) {
|
|
2515
|
-
warnings.push("the API transport was selected by the mere PRESENCE of " + "HASNA_RECORDINGS_API_URL + HASNA_RECORDINGS_API_KEY
|
|
2604
|
+
warnings.push("the API transport was selected by the mere PRESENCE of " + "HASNA_RECORDINGS_API_URL + HASNA_RECORDINGS_API_KEY. " + "Check `launchctl getenv HASNA_RECORDINGS_API_URL` too: a launchd session variable " + "is inherited by the GUI app as well as by shells, and is invisible in a login profile.");
|
|
2516
2605
|
}
|
|
2517
2606
|
return {
|
|
2518
2607
|
transport: resolution.transport,
|
|
@@ -118,7 +118,7 @@ export interface CaptureProbeSubject {
|
|
|
118
118
|
* Name whose Microphone grant this probe actually exercises.
|
|
119
119
|
*
|
|
120
120
|
* macOS attributes microphone access to the RESPONSIBLE process — for a CLI that
|
|
121
|
-
* is the terminal application (or sshd), never
|
|
121
|
+
* is the terminal application (or sshd), never HasnaRecordings.app. So a silent probe
|
|
122
122
|
* means "the thing running this command has no grant", which is a different
|
|
123
123
|
* finding, with a different fix, from "the app has no grant". Reporting the one
|
|
124
124
|
* as the other is how a diagnostic sends someone to the wrong settings pane.
|
|
@@ -141,7 +141,7 @@ export interface MicrophoneGrantInstruction {
|
|
|
141
141
|
/** The bundle that must receive the grant, or null when none was found. */
|
|
142
142
|
bundle_path: string | null;
|
|
143
143
|
bundle_identifier: string;
|
|
144
|
-
/** Every
|
|
144
|
+
/** Every HasnaRecordings.app on disk — more than one makes the grant ambiguous. */
|
|
145
145
|
candidate_bundle_paths: string[];
|
|
146
146
|
/** Ordered steps, already naming the pane, the section and the bundle. */
|
|
147
147
|
steps: string[];
|
|
@@ -197,7 +197,7 @@ export declare function classifyPermissionState(permissionState: string): Permis
|
|
|
197
197
|
export declare function microphoneGrantInstruction(options: {
|
|
198
198
|
/** Path the installer treats as canonical. */
|
|
199
199
|
installedAppPath?: string | null;
|
|
200
|
-
/** Other
|
|
200
|
+
/** Other HasnaRecordings.app bundles found on disk (e.g. legacy install sites). */
|
|
201
201
|
otherAppPaths?: string[];
|
|
202
202
|
/**
|
|
203
203
|
* Whether the app has ever prompted. `unknown` means the TCC state could not
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"capture-probe.d.ts","sourceRoot":"","sources":["../../src/lib/capture-probe.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAE1D,OAAO,EAAE,4BAA4B,EAAiC,MAAM,mBAAmB,CAAC;AAEhG;;;;;;;;;;;;;;;GAeG;AAEH,8DAA8D;AAC9D,MAAM,WAAW,OAAO;IACtB,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;CACd;AAgBD;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,WAAW,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CA2FrD;AAED,MAAM,WAAW,kBAAkB;IACjC,EAAE,EAAE,OAAO,CAAC;IACZ,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,4EAA4E;IAC5E,IAAI,EAAE,MAAM,CAAC;IACb;;;;;;;;;;;OAWG;IACH,MAAM,EAAE,OAAO,GAAG,IAAI,CAAC;IACvB,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,eAAO,MAAM,qBAAqB,IAAI,CAAC;AACvC,eAAO,MAAM,iBAAiB,KAAK,CAAC;AACpC,eAAO,MAAM,yBAAyB,QAAQ,CAAC;AAE/C;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,sBAAsB,QAAQ,CAAC;AAK5C;;;;;;;;;;;GAWG;AACH,wBAAgB,sBAAsB,CACpC,MAAM,EAAE,gBAAgB,EACxB,OAAO,GAAE;IAAE,OAAO,CAAC,EAAE,MAAM,CAAC;IAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAAO,GAC1E,kBAAkB,CAoIpB;AAED,MAAM,WAAW,mBAAmB;IAClC,gFAAgF;IAChF,QAAQ,EAAE,OAAO,CAAC;IAClB;;;OAGG;IACH,aAAa,EAAE,OAAO,CAAC;IACvB,kEAAkE;IAClE,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;CACd;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,mBAAmB,CACjC,GAAG,GAAE,MAAM,CAAC,UAAU,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAe,EACzE,OAAO,GAAE;IAAE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,OAAO,CAAA;CAAO,GACpD,mBAAmB,CAiErB;AAED,MAAM,WAAW,0BAA0B;IACzC,2EAA2E;IAC3E,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,iBAAiB,EAAE,MAAM,CAAC;IAC1B,
|
|
1
|
+
{"version":3,"file":"capture-probe.d.ts","sourceRoot":"","sources":["../../src/lib/capture-probe.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Canonical macOS bundle identity for
|
|
2
|
+
* Canonical macOS bundle identity for HasnaRecordings.app.
|
|
3
3
|
*
|
|
4
4
|
* Why this module exists at all, rather than the constant living next to its first caller:
|
|
5
5
|
* three branches in the #24-#29 queue each introduced their own copy of this literal —
|
|
@@ -24,6 +24,27 @@ export type PreparedReleaseInstall = {
|
|
|
24
24
|
envelopePath: string;
|
|
25
25
|
cleanup: () => void;
|
|
26
26
|
};
|
|
27
|
+
/**
|
|
28
|
+
* Recorded strong reason for these release-path refusals (local-only-capability-
|
|
29
|
+
* removal workflow, 2026-08-18; reviewer-ruled — do not remove or relax this
|
|
30
|
+
* gate by assumption). The local-only artifact policy — ad-hoc or Developer ID
|
|
31
|
+
* signed, NON-NOTARIZED builds bound to one approved target machine
|
|
32
|
+
* (tailscale_node_id_sha256), built on a different authenticated Tailscale
|
|
33
|
+
* node, with signing acknowledgement and identity-migration controls — is a
|
|
34
|
+
* native macOS distribution policy, not a data-backend capability. The
|
|
35
|
+
* two-backend contract (local store vs hosted API) does not reach code
|
|
36
|
+
* signing: the signing identity is keychain-held on the build Mac
|
|
37
|
+
* (`security find-identity`, see src/native/Recordings/build.sh), so no
|
|
38
|
+
* hosted/server path can carry it. The release path IS the notarized
|
|
39
|
+
* counterpart (README.md: "Explicit local-only alternative when Developer ID
|
|
40
|
+
* credentials are unavailable"); it must keep rejecting the local-only fields
|
|
41
|
+
* so that only notarized, Team-ID-pinned, envelope-signed artifacts install
|
|
42
|
+
* fleet-wide through the root-owned update client. Relaxing this gate would
|
|
43
|
+
* let a non-notarized or ad-hoc artifact install on any machine — the
|
|
44
|
+
* supply-chain class this policy blocks. Behavior locks:
|
|
45
|
+
* src/__tests__/local-only-target-policy.test.ts,
|
|
46
|
+
* src/__tests__/cli.test.ts, src/__tests__/macos-artifact.test.ts.
|
|
47
|
+
*/
|
|
27
48
|
export declare function assertReleaseOnlyOptions(options: ReleaseOnlyOptions): void;
|
|
28
49
|
export declare function assertExpectedReleaseHostname(expected: string, actual: string): void;
|
|
29
50
|
export declare function parseLaunchTimeout(value: string | undefined): string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"release-install-policy.d.ts","sourceRoot":"","sources":["../../src/lib/release-install-policy.ts"],"names":[],"mappings":"AAyBA,MAAM,MAAM,kBAAkB,GAAG;IAC/B,cAAc,EAAE,MAAM,CAAC;IACvB,0BAA0B,CAAC,EAAE,MAAM,CAAC;IACpC,4BAA4B,EAAE,MAAM,CAAC;IACrC,qCAAqC,CAAC,EAAE,OAAO,CAAC;IAChD,yBAAyB,CAAC,EAAE,MAAM,CAAC;IACnC,yBAAyB,CAAC,EAAE,MAAM,CAAC;IACnC,6BAA6B,CAAC,EAAE,OAAO,CAAC;IACxC,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB,CAAC;AAEF,MAAM,MAAM,mBAAmB,GAAG;IAChC,YAAY,EAAE,MAAM,CAAC;IACrB,YAAY,EAAE,MAAM,CAAC;IACrB,YAAY,EAAE,MAAM,CAAC;IACrB,cAAc,EAAE,MAAM,CAAC;IACvB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,eAAe,EAAE,MAAM,CAAC;IACxB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB,CAAC;AAEF,MAAM,MAAM,sBAAsB,GAAG;IACnC,YAAY,EAAE,MAAM,CAAC;IACrB,YAAY,EAAE,MAAM,CAAC;IACrB,OAAO,EAAE,MAAM,IAAI,CAAC;CACrB,CAAC;AAEF,wBAAgB,wBAAwB,CAAC,OAAO,EAAE,kBAAkB,GAAG,IAAI,CA4B1E;AAED,wBAAgB,6BAA6B,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI,CASpF;AAED,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,CAMpE;AAED,wBAAgB,2BAA2B,CAAC,KAAK,EAAE,mBAAmB,GAAG,sBAAsB,CAgE9F"}
|
|
1
|
+
{"version":3,"file":"release-install-policy.d.ts","sourceRoot":"","sources":["../../src/lib/release-install-policy.ts"],"names":[],"mappings":"AAyBA,MAAM,MAAM,kBAAkB,GAAG;IAC/B,cAAc,EAAE,MAAM,CAAC;IACvB,0BAA0B,CAAC,EAAE,MAAM,CAAC;IACpC,4BAA4B,EAAE,MAAM,CAAC;IACrC,qCAAqC,CAAC,EAAE,OAAO,CAAC;IAChD,yBAAyB,CAAC,EAAE,MAAM,CAAC;IACnC,yBAAyB,CAAC,EAAE,MAAM,CAAC;IACnC,6BAA6B,CAAC,EAAE,OAAO,CAAC;IACxC,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB,CAAC;AAEF,MAAM,MAAM,mBAAmB,GAAG;IAChC,YAAY,EAAE,MAAM,CAAC;IACrB,YAAY,EAAE,MAAM,CAAC;IACrB,YAAY,EAAE,MAAM,CAAC;IACrB,cAAc,EAAE,MAAM,CAAC;IACvB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,eAAe,EAAE,MAAM,CAAC;IACxB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB,CAAC;AAEF,MAAM,MAAM,sBAAsB,GAAG;IACnC,YAAY,EAAE,MAAM,CAAC;IACrB,YAAY,EAAE,MAAM,CAAC;IACrB,OAAO,EAAE,MAAM,IAAI,CAAC;CACrB,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,wBAAwB,CAAC,OAAO,EAAE,kBAAkB,GAAG,IAAI,CA4B1E;AAED,wBAAgB,6BAA6B,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI,CASpF;AAED,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,CAMpE;AAED,wBAAgB,2BAA2B,CAAC,KAAK,EAAE,mBAAmB,GAAG,sBAAsB,CAgE9F"}
|