@hasna/recordings 0.3.13 → 0.5.0
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 +279 -43
- package/bun.lock +37 -38
- package/contracts/v1/fixtures.json +1206 -0
- package/dist/cli/index.js +1909 -619
- package/dist/cli/macos-shortcut.d.ts +2 -2
- package/dist/contracts/hosted-v1.d.ts +105 -0
- package/dist/contracts/hosted-v1.d.ts.map +1 -0
- package/dist/contracts/hosted-v1.js +41 -0
- package/dist/contracts/stream-v1.d.ts +117 -0
- package/dist/contracts/stream-v1.d.ts.map +1 -0
- package/dist/contracts/stream-v1.js +35 -0
- package/dist/db/database.d.ts.map +1 -1
- package/dist/db/pg-migrations.d.ts.map +1 -1
- package/dist/db/recordings.d.ts +6 -0
- package/dist/db/recordings.d.ts.map +1 -1
- package/dist/hosted/index.d.ts +58 -0
- package/dist/hosted/index.d.ts.map +1 -0
- package/dist/hosted/index.js +266 -0
- package/dist/hosted/transport.d.ts +36 -0
- package/dist/hosted/transport.d.ts.map +1 -0
- package/dist/hosted-v1-aavn7ktb.js +4114 -0
- package/dist/hosted-v1-gdr9extc.js +84 -0
- package/dist/http/client.d.ts +183 -22
- package/dist/http/client.d.ts.map +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2879 -2288
- package/dist/lib/audio-artifact-storage.d.ts +98 -0
- package/dist/lib/audio-artifact-storage.d.ts.map +1 -0
- 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/local-opt-in.d.ts +132 -0
- package/dist/lib/local-opt-in.d.ts.map +1 -0
- package/dist/lib/macos-bundle.d.ts +1 -1
- package/dist/lib/persistence-probe.d.ts +56 -17
- package/dist/lib/persistence-probe.d.ts.map +1 -1
- package/dist/lib/transcriber.d.ts.map +1 -1
- package/dist/mcp/index.d.ts.map +1 -1
- package/dist/mcp/index.js +1713 -459
- package/dist/sdk/index.d.ts +13 -5
- package/dist/sdk/index.d.ts.map +1 -1
- package/dist/sdk/index.js +904 -1
- package/dist/sdk/resolve.d.ts +53 -0
- package/dist/sdk/resolve.d.ts.map +1 -0
- package/dist/sdk/v1.generated.d.ts +6 -0
- package/dist/sdk/v1.generated.d.ts.map +1 -1
- package/dist/server/client-ip.d.ts +39 -0
- package/dist/server/client-ip.d.ts.map +1 -0
- package/dist/server/cloud-config.d.ts +12 -0
- package/dist/server/cloud-config.d.ts.map +1 -1
- package/dist/server/cloud-readiness.d.ts.map +1 -1
- package/dist/server/cloud.d.ts +20 -2
- package/dist/server/cloud.d.ts.map +1 -1
- package/dist/server/index.js +590 -287
- package/dist/server/migrate-command.d.ts +1 -1
- package/dist/server/migrate-command.d.ts.map +1 -1
- package/dist/server/openapi.d.ts +21 -0
- package/dist/server/openapi.d.ts.map +1 -1
- package/dist/server/repo.d.ts.map +1 -1
- package/dist/server/serve.d.ts.map +1 -1
- package/dist/storage.d.ts +2 -2
- package/dist/storage.d.ts.map +1 -1
- package/dist/storage.js +1358 -874
- package/dist/store.d.ts +12 -3
- package/dist/store.d.ts.map +1 -1
- package/dist/types/index.d.ts +15 -0
- package/dist/types/index.d.ts.map +1 -1
- package/docs/hosted-sdk.md +71 -0
- package/docs/wire-contracts.md +24 -0
- package/package.json +33 -11
- package/packaging/macos/build_release_pkg.sh +3 -3
- package/packaging/macos/managed_bootstrap.sh +4 -4
- package/packaging/macos/scripts/postinstall +2 -2
- package/packaging/macos/scripts/preinstall +2 -2
- package/scripts/build_companion_cli.sh +7 -1
- package/scripts/ci-linux-suite.ts +23 -13
- package/scripts/install_macos_app.sh +27 -24
- package/scripts/macos_artifact.ts +60 -49
- package/scripts/migrate.ts +4 -20
- package/scripts/native/prebuilds/darwin-universal/recordings_fs_guard.node +0 -0
- package/scripts/native/recordings_fs_guard.c +36 -4
- package/scripts/native-core-receipt.py +171 -0
- package/scripts/native_fs_guard.ts +2 -0
- package/scripts/release-suite-gate.ts +227 -150
- package/scripts/resolve_tailscale_cli.sh +24 -3
- package/scripts/smoke_macos_app.sh +21 -21
- package/src/native/Recordings/App/ContentView.swift +53 -68
- package/src/native/Recordings/App/MenuBarStatusView.swift +171 -61
- package/src/native/Recordings/App/RecordWorkspaceView.swift +47 -449
- package/src/native/Recordings/App/RecorderSettingsView.swift +75 -0
- package/src/native/Recordings/App/RecordingDetailView.swift +3 -4
- package/src/native/Recordings/App/RecordingsApp.swift +185 -9
- package/src/native/Recordings/App/RecordingsListView.swift +60 -112
- package/src/native/Recordings/App/RecordingsStore.swift +124 -109
- package/src/native/Recordings/App/Theme.swift +73 -61
- package/src/native/Recordings/Package.swift +1 -0
- package/src/native/Recordings/RecordingsLib/BlockingOperation.swift +29 -0
- package/src/native/Recordings/RecordingsLib/CaptureMonitor.swift +65 -0
- package/src/native/Recordings/RecordingsLib/Info.plist +3 -3
- package/src/native/Recordings/RecordingsLib/OpenAIAPIKeyStore.swift +75 -21
- package/src/native/Recordings/RecordingsLib/ProjectStore.swift +4 -4
- package/src/native/Recordings/RecordingsLib/Recording.swift +14 -2
- package/src/native/Recordings/RecordingsLib/RecordingEngine.swift +539 -89
- package/src/native/Recordings/RecordingsLib/RecordingGlassBackground.swift +34 -0
- package/src/native/Recordings/RecordingsLib/RecordingPasteTarget.swift +114 -0
- package/src/native/Recordings/RecordingsLib/RecordingProvider.swift +112 -0
- package/src/native/Recordings/RecordingsLib/RecordingsCLI.swift +17 -10
- package/src/native/Recordings/RecordingsLib/ServiceAPIConfiguration.swift +107 -0
- package/src/native/Recordings/RecordingsLib/ServiceConnectionView.swift +76 -0
- package/src/native/Recordings/RecordingsLib/SettingsView.swift +45 -149
- package/src/native/Recordings/RecordingsLib/SettingsWindowController.swift +48 -0
- package/src/native/Recordings/RecordingsTests/BlockingOperationTests.swift +85 -0
- package/src/native/Recordings/RecordingsTests/CLIRunnerTests.swift +454 -76
- package/src/native/Recordings/RecordingsTests/OpenAIAPIKeyStoreTests.swift +43 -38
- package/src/native/Recordings/RecordingsTests/PipeClosureFixture.swift +215 -0
- package/src/native/Recordings/RecordingsTests/ProjectStoreTests.swift +10 -10
- package/src/native/Recordings/RecordingsTests/RecorderSurfaceTests.swift +60 -0
- package/src/native/Recordings/RecordingsTests/RecordingDateTests.swift +26 -0
- package/src/native/Recordings/RecordingsTests/RecordingEngineDeliveryTests.swift +1 -1
- package/src/native/Recordings/RecordingsTests/RecordingFrozenPasteTargetTests.swift +50 -0
- package/src/native/Recordings/RecordingsTests/RecordingPasteTargetTrackerTests.swift +48 -0
- package/src/native/Recordings/RecordingsTests/RecordingProviderTests.swift +308 -0
- package/src/native/Recordings/RecordingsTests/RecordingStartGateTests.swift +1 -1
- package/src/native/Recordings/RecordingsTests/RecordingStartTimingTests.swift +123 -21
- package/src/native/Recordings/RecordingsTests/ServiceAPIConfigurationTests.swift +75 -0
- package/src/native/Recordings/RecordingsTests/SettingsWindowControllerTests.swift +28 -0
- package/src/native/Recordings/RecordingsTests/TestHomeDirectory.swift +5 -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 +8 -7
- package/dist/__tests__/helpers/installer-guard-execution.d.ts +0 -22
- package/dist/__tests__/helpers/installer-guard-execution.d.ts.map +0 -1
- package/dist/__tests__/helpers/installer-preflight.d.ts +0 -22
- package/dist/__tests__/helpers/installer-preflight.d.ts.map +0 -1
- package/dist/__tests__/helpers/native-fs-guard.d.ts +0 -2
- package/dist/__tests__/helpers/native-fs-guard.d.ts.map +0 -1
- package/dist/__tests__/helpers/source-assertions.d.ts +0 -171
- package/dist/__tests__/helpers/source-assertions.d.ts.map +0 -1
- package/dist/__tests__/preload.d.ts +0 -2
- package/dist/__tests__/preload.d.ts.map +0 -1
- package/src/native/Recordings/App/SidebarView.swift +0 -212
package/dist/server/index.js
CHANGED
|
@@ -15,14 +15,13 @@ var __export = (target, all) => {
|
|
|
15
15
|
});
|
|
16
16
|
};
|
|
17
17
|
var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
|
|
18
|
-
var __require = import.meta.require;
|
|
19
18
|
|
|
20
19
|
// package.json
|
|
21
20
|
var package_default;
|
|
22
21
|
var init_package = __esm(() => {
|
|
23
22
|
package_default = {
|
|
24
23
|
name: "@hasna/recordings",
|
|
25
|
-
version: "0.
|
|
24
|
+
version: "0.5.0",
|
|
26
25
|
type: "module",
|
|
27
26
|
description: "Speech-to-text recording tool with MCP and CLI \u2014 records, transcribes, and optionally enhances text using AI",
|
|
28
27
|
repository: {
|
|
@@ -48,6 +47,19 @@ var init_package = __esm(() => {
|
|
|
48
47
|
"./sdk": {
|
|
49
48
|
import: "./dist/sdk/index.js",
|
|
50
49
|
types: "./dist/sdk/index.d.ts"
|
|
50
|
+
},
|
|
51
|
+
"./contracts/hosted-v1": {
|
|
52
|
+
import: "./dist/contracts/hosted-v1.js",
|
|
53
|
+
types: "./dist/contracts/hosted-v1.d.ts"
|
|
54
|
+
},
|
|
55
|
+
"./contracts/stream-v1": {
|
|
56
|
+
import: "./dist/contracts/stream-v1.js",
|
|
57
|
+
types: "./dist/contracts/stream-v1.d.ts"
|
|
58
|
+
},
|
|
59
|
+
"./contracts/fixtures/v1": "./contracts/v1/fixtures.json",
|
|
60
|
+
"./hosted": {
|
|
61
|
+
import: "./dist/hosted/index.js",
|
|
62
|
+
types: "./dist/hosted/index.d.ts"
|
|
51
63
|
}
|
|
52
64
|
},
|
|
53
65
|
engines: {
|
|
@@ -56,20 +68,20 @@ var init_package = __esm(() => {
|
|
|
56
68
|
scripts: {
|
|
57
69
|
clean: "rm -rf dist",
|
|
58
70
|
build: "bun run clean && bun run build:cli && bun run build:mcp && bun run build:serve && bun run build:lib && tsc --emitDeclarationOnly --outDir dist",
|
|
59
|
-
"build:cli": "bun build src/cli/index.ts --target=bun --outfile=dist/cli/index.js --external=commander --external=chalk --external=openai",
|
|
60
|
-
"build:mcp": "bun build src/mcp/index.ts --target=bun --outfile=dist/mcp/index.js --external=@modelcontextprotocol/sdk --external=openai",
|
|
61
|
-
"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",
|
|
62
|
-
"build:lib": "bun build src/index.ts src/storage.ts src/sdk/index.ts --target=bun --outdir=dist --external=openai",
|
|
71
|
+
"build:cli": "bun build src/cli/index.ts --target=bun --outfile=dist/cli/index.js --external=commander --external=chalk --external=openai --external=@aws-sdk/client-s3",
|
|
72
|
+
"build:mcp": "bun build src/mcp/index.ts --target=bun --outfile=dist/mcp/index.js --external=@modelcontextprotocol/sdk --external=openai --external=@aws-sdk/client-s3",
|
|
73
|
+
"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 --external=@aws-sdk/client-s3",
|
|
74
|
+
"build:lib": "bun build src/index.ts src/storage.ts src/sdk/index.ts --target=bun --outdir=dist --external=openai --external=@aws-sdk/client-s3 && bun run build:contracts",
|
|
63
75
|
"build:native-fs-guard": "/bin/bash scripts/build_native_fs_guard.sh",
|
|
64
76
|
"generate:sdk": "bun run scripts/generate-sdk.ts",
|
|
65
77
|
migrate: "bun run scripts/migrate.ts",
|
|
66
78
|
typecheck: "tsc --noEmit",
|
|
67
79
|
"typecheck:tcc-contract": "tsc --noEmit -p tsconfig.tcc-contract.json",
|
|
68
|
-
test: "bun
|
|
80
|
+
test: "bun scripts/release-suite-gate.ts --all",
|
|
69
81
|
"verify:ci-suite": "bun scripts/ci-linux-suite.ts --check",
|
|
70
82
|
"verify:ci-suite:run": "bun scripts/ci-linux-suite.ts --verify-run",
|
|
71
83
|
"verify:ci-native": "bun scripts/ci-native-build.ts",
|
|
72
|
-
"test:gated":
|
|
84
|
+
"test:gated": "bun scripts/release-suite-gate.ts",
|
|
73
85
|
"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"',
|
|
74
86
|
"test:vacuity-battery:source": "bun scripts/vacuity-mutation-battery.ts scripts/vacuity-manifests/source-side.tsv",
|
|
75
87
|
"test:vacuity-battery:chain": "bun scripts/vacuity-mutation-battery.ts scripts/vacuity-manifests/install-chain.tsv",
|
|
@@ -87,11 +99,16 @@ var init_package = __esm(() => {
|
|
|
87
99
|
"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'`,
|
|
88
100
|
prepublishOnly: "bun run typecheck && bun run release-suite-gate",
|
|
89
101
|
"release-suite-gate": "bun run scripts/release-suite-gate.ts",
|
|
90
|
-
"typecheck:shortcut-contract": "tsc --noEmit -p tsconfig.test.json"
|
|
102
|
+
"typecheck:shortcut-contract": "tsc --noEmit -p tsconfig.test.json",
|
|
103
|
+
"build:contracts": "bun build src/contracts/hosted-v1.ts src/contracts/stream-v1.ts src/hosted/index.ts --target=bun --root src --outdir=dist --splitting --external=@hasna/contracts"
|
|
91
104
|
},
|
|
92
105
|
files: [
|
|
93
106
|
"dist/",
|
|
107
|
+
"!dist/__tests__/",
|
|
94
108
|
"scripts/",
|
|
109
|
+
"!scripts/test-recorder-fixtures.ts",
|
|
110
|
+
"!scripts/**/__pycache__/",
|
|
111
|
+
"!scripts/**/*.pyc",
|
|
95
112
|
"Dockerfile.package",
|
|
96
113
|
"bun.lock",
|
|
97
114
|
"src/native/Recordings/App/",
|
|
@@ -103,10 +120,14 @@ var init_package = __esm(() => {
|
|
|
103
120
|
"src/native/Recordings/build.sh",
|
|
104
121
|
"packaging/macos/",
|
|
105
122
|
"README.md",
|
|
106
|
-
"LICENSE"
|
|
123
|
+
"LICENSE",
|
|
124
|
+
"contracts/v1/fixtures.json",
|
|
125
|
+
"docs/wire-contracts.md",
|
|
126
|
+
"docs/hosted-sdk.md"
|
|
107
127
|
],
|
|
108
128
|
dependencies: {
|
|
109
|
-
"@
|
|
129
|
+
"@aws-sdk/client-s3": "^3.1007.0",
|
|
130
|
+
"@hasna/contracts": "1.0.2",
|
|
110
131
|
"@hasna/events": "0.1.11",
|
|
111
132
|
"@modelcontextprotocol/sdk": "^1.12.1",
|
|
112
133
|
chalk: "^5.4.1",
|
|
@@ -116,7 +137,7 @@ var init_package = __esm(() => {
|
|
|
116
137
|
zod: "^3.24.2"
|
|
117
138
|
},
|
|
118
139
|
devDependencies: {
|
|
119
|
-
"@types/bun": "
|
|
140
|
+
"@types/bun": "1.3.14",
|
|
120
141
|
"@types/pg": "^8.11.11",
|
|
121
142
|
"node-api-headers": "1.9.0",
|
|
122
143
|
typescript: "^5.8.2"
|
|
@@ -203,6 +224,9 @@ var init_remote_storage = () => {};
|
|
|
203
224
|
function resolveDatabaseUrl(env = process.env) {
|
|
204
225
|
return env.HASNA_RECORDINGS_DATABASE_URL || env.RECORDINGS_DATABASE_URL || env.DATABASE_URL || undefined;
|
|
205
226
|
}
|
|
227
|
+
function resolveMigrationDatabaseUrl(env = process.env) {
|
|
228
|
+
return env.HASNA_RECORDINGS_MIGRATE_DATABASE_URL || env.RECORDINGS_MIGRATE_DATABASE_URL || resolveDatabaseUrl(env);
|
|
229
|
+
}
|
|
206
230
|
function resolveDataBackend(env = process.env) {
|
|
207
231
|
return resolveDatabaseUrl(env) ? "postgresql" : "sqlite";
|
|
208
232
|
}
|
|
@@ -210,7 +234,7 @@ function isPostgresBackendEnabled(env = process.env) {
|
|
|
210
234
|
return resolveDataBackend(env) === "postgresql";
|
|
211
235
|
}
|
|
212
236
|
function resolveSigningSecret(env = process.env) {
|
|
213
|
-
return env.HASNA_RECORDINGS_API_SIGNING_KEY || env.HASNA_API_SIGNING_KEY || env.API_KEY_SIGNING_SECRET || undefined;
|
|
237
|
+
return env.HASNA_RECORDINGS_API_SIGNING_KEY?.trim() || env.HASNA_API_SIGNING_KEY?.trim() || env.API_KEY_SIGNING_SECRET?.trim() || undefined;
|
|
214
238
|
}
|
|
215
239
|
function requireSigningSecret(env = process.env) {
|
|
216
240
|
const signingSecret = resolveSigningSecret(env);
|
|
@@ -224,13 +248,6 @@ function requireSigningSecret(env = process.env) {
|
|
|
224
248
|
}
|
|
225
249
|
|
|
226
250
|
// src/server/cloud-readiness.ts
|
|
227
|
-
var exports_cloud_readiness = {};
|
|
228
|
-
__export(exports_cloud_readiness, {
|
|
229
|
-
pingCloudReadiness: () => pingCloudReadiness,
|
|
230
|
-
pingCloudConnectivity: () => pingCloudConnectivity,
|
|
231
|
-
assertCloudSchemaReady: () => assertCloudSchemaReady,
|
|
232
|
-
assertCloudSchemaContract: () => assertCloudSchemaContract
|
|
233
|
-
});
|
|
234
251
|
function sameColumns(actual, expected) {
|
|
235
252
|
return actual.length === expected.length && actual.every((column, index) => column === expected[index]);
|
|
236
253
|
}
|
|
@@ -365,6 +382,16 @@ async function assertCloudSchemaReady(pg2) {
|
|
|
365
382
|
AND has_schema_privilege(current_user, schema_row.oid, 'CREATE')
|
|
366
383
|
) AS can_create_in_user_schema,
|
|
367
384
|
has_schema_privilege(current_user, 'public', 'USAGE') AS can_use_public,
|
|
385
|
+
COALESCE((
|
|
386
|
+
SELECT has_column_privilege(table_row.relowner, table_row.oid, 'recording_id', 'SELECT') AND
|
|
387
|
+
has_column_privilege(table_row.relowner, table_row.oid, 'recording_id', 'UPDATE')
|
|
388
|
+
FROM pg_class table_row WHERE table_row.oid = 'public.recording_idempotency'::regclass
|
|
389
|
+
), false) AS owner_can_tombstone_recording_idempotency,
|
|
390
|
+
COALESCE((
|
|
391
|
+
SELECT has_column_privilege(table_row.relowner, table_row.oid, 'recording_id', 'SELECT') AND
|
|
392
|
+
has_table_privilege(table_row.relowner, table_row.oid, 'DELETE')
|
|
393
|
+
FROM pg_class table_row WHERE table_row.oid = 'public.recording_tags'::regclass
|
|
394
|
+
), false) AS owner_can_delete_recording_tags,
|
|
368
395
|
EXISTS (
|
|
369
396
|
SELECT 1
|
|
370
397
|
FROM pg_class table_row
|
|
@@ -451,6 +478,12 @@ async function assertCloudSchemaReady(pg2) {
|
|
|
451
478
|
if (excessive.length > 0) {
|
|
452
479
|
throw new Error(`cloud database role has surplus table privileges beyond runtime use: ${excessive.join(", ")}`);
|
|
453
480
|
}
|
|
481
|
+
if (!row?.owner_can_tombstone_recording_idempotency) {
|
|
482
|
+
throw new Error("cloud database recording_idempotency owner lacks SELECT/UPDATE privileges required for recording deletion");
|
|
483
|
+
}
|
|
484
|
+
if (!row?.owner_can_delete_recording_tags) {
|
|
485
|
+
throw new Error("cloud database recording_tags owner lacks SELECT/DELETE privileges required for recording deletion");
|
|
486
|
+
}
|
|
454
487
|
}
|
|
455
488
|
async function pingCloudConnectivity(pg2) {
|
|
456
489
|
const res = await pg2.get("SELECT 1 as ok");
|
|
@@ -488,6 +521,9 @@ var init_cloud_readiness = __esm(() => {
|
|
|
488
521
|
id: ["text", false],
|
|
489
522
|
audio_path: ["text", true],
|
|
490
523
|
raw_text: ["text", false],
|
|
524
|
+
audio_object_key: ["text", true],
|
|
525
|
+
audio_sha256: ["text", true],
|
|
526
|
+
audio_bytes: ["bigint", true],
|
|
491
527
|
processed_text: ["text", true],
|
|
492
528
|
processing_mode: ["text", false],
|
|
493
529
|
model_used: ["text", false],
|
|
@@ -694,16 +730,14 @@ var init_pg_migrations = __esm(() => {
|
|
|
694
730
|
ALTER COLUMN recording_id DROP NOT NULL;
|
|
695
731
|
ALTER TABLE recording_idempotency
|
|
696
732
|
ADD CONSTRAINT recording_idempotency_recording_id_fkey
|
|
697
|
-
FOREIGN KEY (recording_id) REFERENCES recordings(id) ON DELETE SET NULL
|
|
733
|
+
FOREIGN KEY (recording_id) REFERENCES recordings(id) ON DELETE SET NULL`,
|
|
734
|
+
`ALTER TABLE recordings ADD COLUMN IF NOT EXISTS audio_object_key TEXT`,
|
|
735
|
+
`ALTER TABLE recordings ADD COLUMN IF NOT EXISTS audio_sha256 TEXT`,
|
|
736
|
+
`ALTER TABLE recordings ADD COLUMN IF NOT EXISTS audio_bytes BIGINT`
|
|
698
737
|
];
|
|
699
738
|
});
|
|
700
739
|
|
|
701
740
|
// src/server/migrate-command.ts
|
|
702
|
-
var exports_migrate_command = {};
|
|
703
|
-
__export(exports_migrate_command, {
|
|
704
|
-
runCloudMigration: () => runCloudMigration,
|
|
705
|
-
applyRecordedCloudMigrations: () => applyRecordedCloudMigrations
|
|
706
|
-
});
|
|
707
741
|
async function applyRecordedCloudMigrations(pg2, ensureApiKeySchema) {
|
|
708
742
|
await pg2.run(`CREATE TABLE IF NOT EXISTS _pg_migrations (id SERIAL PRIMARY KEY, version INT UNIQUE NOT NULL, applied_at TIMESTAMPTZ DEFAULT NOW())`);
|
|
709
743
|
const applied = await pg2.all("SELECT version FROM _pg_migrations ORDER BY version");
|
|
@@ -720,10 +754,12 @@ async function applyRecordedCloudMigrations(pg2, ensureApiKeySchema) {
|
|
|
720
754
|
}
|
|
721
755
|
await ensureApiKeySchema();
|
|
722
756
|
}
|
|
723
|
-
async function runCloudMigration(steps) {
|
|
757
|
+
async function runCloudMigration(steps, runtimePostureValidate) {
|
|
724
758
|
await steps.pingConnectivity();
|
|
725
759
|
await steps.applyMigrations();
|
|
726
760
|
await steps.validateContract();
|
|
761
|
+
if (runtimePostureValidate)
|
|
762
|
+
await runtimePostureValidate();
|
|
727
763
|
}
|
|
728
764
|
var init_migrate_command = __esm(() => {
|
|
729
765
|
init_pg_migrations();
|
|
@@ -732,7 +768,9 @@ var init_migrate_command = __esm(() => {
|
|
|
732
768
|
// src/server/cloud.ts
|
|
733
769
|
var exports_cloud = {};
|
|
734
770
|
__export(exports_cloud, {
|
|
771
|
+
runRecordedMigration: () => runRecordedMigration,
|
|
735
772
|
resolveSigningSecret: () => resolveSigningSecret,
|
|
773
|
+
resolveMigrationDatabaseUrl: () => resolveMigrationDatabaseUrl,
|
|
736
774
|
resolveDatabaseUrl: () => resolveDatabaseUrl,
|
|
737
775
|
resolveDataBackend: () => resolveDataBackend,
|
|
738
776
|
requireSigningSecret: () => requireSigningSecret,
|
|
@@ -740,6 +778,7 @@ __export(exports_cloud, {
|
|
|
740
778
|
pingCloud: () => pingCloud,
|
|
741
779
|
migrateCloudSchema: () => migrateCloudSchema,
|
|
742
780
|
isPostgresBackendEnabled: () => isPostgresBackendEnabled,
|
|
781
|
+
getMigrationPg: () => getMigrationPg,
|
|
743
782
|
getCloudVerifier: () => getCloudVerifier,
|
|
744
783
|
getCloudPg: () => getCloudPg,
|
|
745
784
|
getApiKeyStore: () => getApiKeyStore,
|
|
@@ -759,8 +798,7 @@ function getCloudPg() {
|
|
|
759
798
|
cachedPg = new PgAdapterAsync(url);
|
|
760
799
|
return cachedPg;
|
|
761
800
|
}
|
|
762
|
-
function authClient() {
|
|
763
|
-
const pg2 = getCloudPg();
|
|
801
|
+
function authClient(pg2 = getCloudPg()) {
|
|
764
802
|
return {
|
|
765
803
|
async many(sql, params = []) {
|
|
766
804
|
return await pg2.all(sql, ...params);
|
|
@@ -796,8 +834,30 @@ function getCloudVerifier() {
|
|
|
796
834
|
});
|
|
797
835
|
return cachedVerifier;
|
|
798
836
|
}
|
|
799
|
-
|
|
800
|
-
|
|
837
|
+
function getMigrationPg() {
|
|
838
|
+
const url = resolveMigrationDatabaseUrl();
|
|
839
|
+
if (!url) {
|
|
840
|
+
throw new Error("migrate requires a database URL (HASNA_RECORDINGS_MIGRATE_DATABASE_URL / RECORDINGS_MIGRATE_DATABASE_URL / HASNA_RECORDINGS_DATABASE_URL / RECORDINGS_DATABASE_URL / DATABASE_URL).");
|
|
841
|
+
}
|
|
842
|
+
return new PgAdapterAsync(url);
|
|
843
|
+
}
|
|
844
|
+
async function migrateCloudSchema(pg2 = getCloudPg()) {
|
|
845
|
+
await applyRecordedCloudMigrations(pg2, () => new ApiKeyStore(authClient(pg2)).ensureSchema());
|
|
846
|
+
}
|
|
847
|
+
async function runRecordedMigration() {
|
|
848
|
+
const migrationPg = getMigrationPg();
|
|
849
|
+
const runtimeUrl = resolveDatabaseUrl();
|
|
850
|
+
const migrationUrl = resolveMigrationDatabaseUrl();
|
|
851
|
+
const separateRoles = runtimeUrl !== undefined && migrationUrl !== undefined && runtimeUrl !== migrationUrl;
|
|
852
|
+
await runCloudMigration({
|
|
853
|
+
pingConnectivity: () => pingCloudConnectivity2(migrationPg),
|
|
854
|
+
applyMigrations: async () => {
|
|
855
|
+
await migrateCloudSchema(migrationPg);
|
|
856
|
+
},
|
|
857
|
+
validateContract: () => assertCloudSchemaContract(migrationPg)
|
|
858
|
+
}, separateRoles ? async () => {
|
|
859
|
+
await assertCloudSchemaReady(getCloudPg());
|
|
860
|
+
} : undefined);
|
|
801
861
|
}
|
|
802
862
|
async function ensureCloudSchema() {
|
|
803
863
|
if (schemaEnsured)
|
|
@@ -942,6 +1002,9 @@ function parseRecording(row) {
|
|
|
942
1002
|
return {
|
|
943
1003
|
id: row["id"],
|
|
944
1004
|
audio_path: row["audio_path"] || null,
|
|
1005
|
+
audio_object_key: row["audio_object_key"] || null,
|
|
1006
|
+
audio_sha256: row["audio_sha256"] || null,
|
|
1007
|
+
audio_bytes: Number(row["audio_bytes"] ?? 0) || null,
|
|
945
1008
|
raw_text: row["raw_text"],
|
|
946
1009
|
processed_text: row["processed_text"] || null,
|
|
947
1010
|
processing_mode: row["processing_mode"] || "raw",
|
|
@@ -1031,9 +1094,9 @@ async function createRecording(pg2, input, idempotencyKey, idempotencyContext) {
|
|
|
1031
1094
|
throw new ProjectNotFoundError(input.project_id);
|
|
1032
1095
|
resolvedProjectId = project.id;
|
|
1033
1096
|
}
|
|
1034
|
-
const insertResult = await transaction.run(`INSERT INTO recordings (id, audio_path, raw_text, processed_text, processing_mode, model_used, enhancement_model, duration_ms, language, tags, agent_id, project_id, session_id, goal, role, task_list_id, machine_id, metadata)
|
|
1035
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1036
|
-
ON CONFLICT(id) DO NOTHING`, id, input.audio_path || null, input.raw_text, input.processed_text || null, input.processing_mode || "raw", input.model_used || "gpt-4o-transcribe", input.enhancement_model || null, input.duration_ms || 0, input.language || null, JSON.stringify(input.tags || []), resolvedAgentId, resolvedProjectId, input.session_id || null, input.goal || null, input.role || null, input.task_list_id || null, input.machine_id || null, JSON.stringify(input.metadata || {}));
|
|
1097
|
+
const insertResult = await transaction.run(`INSERT INTO recordings (id, audio_path, audio_object_key, audio_sha256, audio_bytes, raw_text, processed_text, processing_mode, model_used, enhancement_model, duration_ms, language, tags, agent_id, project_id, session_id, goal, role, task_list_id, machine_id, metadata)
|
|
1098
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1099
|
+
ON CONFLICT(id) DO NOTHING`, id, input.audio_path || null, input.audio_object_key || null, input.audio_sha256 || null, input.audio_bytes || null, input.raw_text, input.processed_text || null, input.processing_mode || "raw", input.model_used || "gpt-4o-transcribe", input.enhancement_model || null, input.duration_ms || 0, input.language || null, JSON.stringify(input.tags || []), resolvedAgentId, resolvedProjectId, input.session_id || null, input.goal || null, input.role || null, input.task_list_id || null, input.machine_id || null, JSON.stringify(input.metadata || {}));
|
|
1037
1100
|
if (insertResult.changes === 0) {
|
|
1038
1101
|
throw new IdempotencyConflictError;
|
|
1039
1102
|
}
|
|
@@ -1454,6 +1517,9 @@ function buildV1OpenApiDocument(version = VERSION) {
|
|
|
1454
1517
|
id: { type: "string" },
|
|
1455
1518
|
raw_text: { type: "string" },
|
|
1456
1519
|
audio_path: { type: "string" },
|
|
1520
|
+
audio_object_key: { type: "string" },
|
|
1521
|
+
audio_sha256: { type: "string" },
|
|
1522
|
+
audio_bytes: { type: "number" },
|
|
1457
1523
|
processed_text: { type: "string" },
|
|
1458
1524
|
processing_mode: { type: "string", enum: ["raw", "enhanced"] },
|
|
1459
1525
|
model_used: { type: "string" },
|
|
@@ -1742,6 +1808,9 @@ var init_openapi = __esm(() => {
|
|
|
1742
1808
|
properties: {
|
|
1743
1809
|
id: { type: "string" },
|
|
1744
1810
|
audio_path: { type: "string", nullable: true },
|
|
1811
|
+
audio_object_key: { type: "string", nullable: true },
|
|
1812
|
+
audio_sha256: { type: "string", nullable: true },
|
|
1813
|
+
audio_bytes: { type: "number", nullable: true },
|
|
1745
1814
|
raw_text: { type: "string" },
|
|
1746
1815
|
processed_text: { type: "string", nullable: true },
|
|
1747
1816
|
processing_mode: { type: "string", enum: ["raw", "enhanced"] },
|
|
@@ -1786,6 +1855,172 @@ var init_openapi = __esm(() => {
|
|
|
1786
1855
|
};
|
|
1787
1856
|
});
|
|
1788
1857
|
|
|
1858
|
+
// src/server/client-ip.ts
|
|
1859
|
+
function normalizeIpLiteral(raw) {
|
|
1860
|
+
let value = (raw ?? "").trim();
|
|
1861
|
+
if (!value)
|
|
1862
|
+
return null;
|
|
1863
|
+
const bracketed = /^\[([^\]]+)\](?::\d+)?$/.exec(value);
|
|
1864
|
+
if (bracketed)
|
|
1865
|
+
value = bracketed[1];
|
|
1866
|
+
else if (/^\d{1,3}(?:\.\d{1,3}){3}:\d+$/.test(value))
|
|
1867
|
+
value = value.slice(0, value.lastIndexOf(":"));
|
|
1868
|
+
value = value.trim().toLowerCase();
|
|
1869
|
+
if (!value || value.length > 45)
|
|
1870
|
+
return null;
|
|
1871
|
+
const mapped = /^::ffff:(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/.exec(value);
|
|
1872
|
+
if (mapped)
|
|
1873
|
+
value = mapped[1];
|
|
1874
|
+
const ipv4 = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/.exec(value);
|
|
1875
|
+
if (ipv4) {
|
|
1876
|
+
return ipv4.slice(1).every((octet) => Number(octet) <= 255 && !/^0\d/.test(octet)) ? value : null;
|
|
1877
|
+
}
|
|
1878
|
+
if (/^[0-9a-f:]+(?:\.\d{1,3}){0,3}$/.test(value) && value.includes(":") && !/:::/.test(value)) {
|
|
1879
|
+
return value;
|
|
1880
|
+
}
|
|
1881
|
+
return null;
|
|
1882
|
+
}
|
|
1883
|
+
function parseForwardedFor(header) {
|
|
1884
|
+
if (!header)
|
|
1885
|
+
return [];
|
|
1886
|
+
return header.split(",").map((entry) => normalizeIpLiteral(entry)).filter((entry) => entry !== null);
|
|
1887
|
+
}
|
|
1888
|
+
function parseCidr(entry) {
|
|
1889
|
+
const slash = entry.indexOf("/");
|
|
1890
|
+
const rawAddress = slash >= 0 ? entry.slice(0, slash) : entry;
|
|
1891
|
+
const rawPrefix = slash >= 0 ? entry.slice(slash + 1) : null;
|
|
1892
|
+
const address = normalizeIpLiteral(rawAddress);
|
|
1893
|
+
if (!address)
|
|
1894
|
+
return null;
|
|
1895
|
+
if (address.includes(":")) {
|
|
1896
|
+
if (rawPrefix !== null && !/^\d{1,3}$/.test(rawPrefix))
|
|
1897
|
+
return null;
|
|
1898
|
+
const prefix2 = rawPrefix === null ? 128 : Number(rawPrefix);
|
|
1899
|
+
if (prefix2 < 0 || prefix2 > 128)
|
|
1900
|
+
return null;
|
|
1901
|
+
const bytes2 = expandV6(address);
|
|
1902
|
+
if (!bytes2)
|
|
1903
|
+
return null;
|
|
1904
|
+
const masked2 = maskNetwork(bytes2, prefix2);
|
|
1905
|
+
return { address, prefix: prefix2, bytes: masked2, family: "ipv6" };
|
|
1906
|
+
}
|
|
1907
|
+
if (rawPrefix !== null && !/^\d{1,3}$/.test(rawPrefix))
|
|
1908
|
+
return null;
|
|
1909
|
+
const prefix = rawPrefix === null ? 32 : Number(rawPrefix);
|
|
1910
|
+
if (prefix < 0 || prefix > 32)
|
|
1911
|
+
return null;
|
|
1912
|
+
const bytes = address.split(".").map((octet) => Number(octet));
|
|
1913
|
+
const masked = maskNetwork(bytes, prefix);
|
|
1914
|
+
return { address, prefix, bytes: masked, family: "ipv4" };
|
|
1915
|
+
}
|
|
1916
|
+
function expandV6(address) {
|
|
1917
|
+
const compressed = address.split("::");
|
|
1918
|
+
if (compressed.length > 2)
|
|
1919
|
+
return null;
|
|
1920
|
+
let groups = [];
|
|
1921
|
+
if (compressed.length === 2) {
|
|
1922
|
+
const left = compressed[0] ? compressed[0].split(":") : [];
|
|
1923
|
+
const right = compressed[1] ? compressed[1].split(":") : [];
|
|
1924
|
+
const missing = 8 - left.length - right.length;
|
|
1925
|
+
if (missing < 1)
|
|
1926
|
+
return null;
|
|
1927
|
+
groups = [...left, ...Array(missing).fill("0"), ...right];
|
|
1928
|
+
} else {
|
|
1929
|
+
groups = address.split(":");
|
|
1930
|
+
}
|
|
1931
|
+
if (groups.length !== 8)
|
|
1932
|
+
return null;
|
|
1933
|
+
const bytes = [];
|
|
1934
|
+
for (const group of groups) {
|
|
1935
|
+
if (!/^[0-9a-f]{1,4}$/.test(group))
|
|
1936
|
+
return null;
|
|
1937
|
+
const value = Number.parseInt(group, 16);
|
|
1938
|
+
bytes.push(value >> 8 & 255, value & 255);
|
|
1939
|
+
}
|
|
1940
|
+
return bytes;
|
|
1941
|
+
}
|
|
1942
|
+
function maskNetwork(bytes, prefix) {
|
|
1943
|
+
const masked = bytes.slice();
|
|
1944
|
+
const fullBytes = Math.floor(prefix / 8);
|
|
1945
|
+
const remainder = prefix % 8;
|
|
1946
|
+
for (let i = 0;i < masked.length; i++) {
|
|
1947
|
+
if (i >= fullBytes) {
|
|
1948
|
+
if (i === fullBytes && remainder > 0) {
|
|
1949
|
+
masked[i] = masked[i] & (255 << 8 - remainder & 255);
|
|
1950
|
+
} else {
|
|
1951
|
+
masked[i] = 0;
|
|
1952
|
+
}
|
|
1953
|
+
}
|
|
1954
|
+
}
|
|
1955
|
+
return masked;
|
|
1956
|
+
}
|
|
1957
|
+
function parseTrustedProxies(raw) {
|
|
1958
|
+
if (!raw)
|
|
1959
|
+
return [];
|
|
1960
|
+
const seen = new Set;
|
|
1961
|
+
const trusted = [];
|
|
1962
|
+
for (const entry of raw.split(",").map((part) => part.trim()).filter(Boolean)) {
|
|
1963
|
+
const cidr = parseCidr(entry);
|
|
1964
|
+
if (cidr && !seen.has(entry)) {
|
|
1965
|
+
seen.add(entry);
|
|
1966
|
+
trusted.push(entry);
|
|
1967
|
+
}
|
|
1968
|
+
}
|
|
1969
|
+
return trusted;
|
|
1970
|
+
}
|
|
1971
|
+
function isMatch(candidateBytes, cidr) {
|
|
1972
|
+
if (cidr.bytes.length !== candidateBytes.length)
|
|
1973
|
+
return false;
|
|
1974
|
+
const masked = maskNetwork(candidateBytes, cidr.prefix);
|
|
1975
|
+
for (let i = 0;i < cidr.bytes.length; i++) {
|
|
1976
|
+
if (cidr.bytes[i] !== masked[i])
|
|
1977
|
+
return false;
|
|
1978
|
+
}
|
|
1979
|
+
return true;
|
|
1980
|
+
}
|
|
1981
|
+
function isTrustedAddress(ip, trusted) {
|
|
1982
|
+
const address = normalizeIpLiteral(ip);
|
|
1983
|
+
if (!address)
|
|
1984
|
+
return false;
|
|
1985
|
+
const cidrs = trusted.map(parseCidr).filter((cidr) => cidr !== null);
|
|
1986
|
+
for (const cidr of cidrs) {
|
|
1987
|
+
if (cidr.address === address)
|
|
1988
|
+
return true;
|
|
1989
|
+
const bytes = cidr.family === "ipv4" ? address.split(".").map((octet) => Number(octet)) : expandV6(address);
|
|
1990
|
+
if (bytes && isMatch(bytes, cidr))
|
|
1991
|
+
return true;
|
|
1992
|
+
}
|
|
1993
|
+
return false;
|
|
1994
|
+
}
|
|
1995
|
+
function resolveTrustProxy(env = process.env) {
|
|
1996
|
+
return TRUST_TRUE_VALUES.has((env[TRUST_PROXY_ENV] ?? "").trim().toLowerCase());
|
|
1997
|
+
}
|
|
1998
|
+
function trustedProxiesFromEnv(env = process.env) {
|
|
1999
|
+
return parseTrustedProxies(env[TRUSTED_PROXIES_ENV]);
|
|
2000
|
+
}
|
|
2001
|
+
function resolveRequestClientIp(input) {
|
|
2002
|
+
const socket = normalizeIpLiteral(input.socketAddress);
|
|
2003
|
+
if (input.trustProxy !== true)
|
|
2004
|
+
return socket;
|
|
2005
|
+
const trusted = input.trustedProxies ?? [];
|
|
2006
|
+
const real = normalizeIpLiteral(input.headers.get("x-real-ip"));
|
|
2007
|
+
if (real)
|
|
2008
|
+
return real;
|
|
2009
|
+
const chain = parseForwardedFor(input.headers.get("x-forwarded-for"));
|
|
2010
|
+
for (let i = chain.length - 1;i >= 0; i--) {
|
|
2011
|
+
const hop = chain[i];
|
|
2012
|
+
if (!isTrustedAddress(hop, trusted))
|
|
2013
|
+
return hop;
|
|
2014
|
+
}
|
|
2015
|
+
if (chain.length > 0)
|
|
2016
|
+
return chain[0];
|
|
2017
|
+
return socket;
|
|
2018
|
+
}
|
|
2019
|
+
var TRUST_PROXY_ENV = "RECORDINGS_TRUST_PROXY", TRUSTED_PROXIES_ENV = "RECORDINGS_TRUSTED_PROXIES", TRUST_TRUE_VALUES;
|
|
2020
|
+
var init_client_ip = __esm(() => {
|
|
2021
|
+
TRUST_TRUE_VALUES = new Set(["1", "true", "yes", "on"]);
|
|
2022
|
+
});
|
|
2023
|
+
|
|
1789
2024
|
// src/mcp/http.ts
|
|
1790
2025
|
var exports_http = {};
|
|
1791
2026
|
__export(exports_http, {
|
|
@@ -5888,32 +6123,6 @@ function assertMaintenanceAbsent(env) {
|
|
|
5888
6123
|
if (pathExists(installMaintenanceMarkerPath(env)))
|
|
5889
6124
|
throw maintenanceUnavailable();
|
|
5890
6125
|
}
|
|
5891
|
-
function canonicalPotentialPath(path) {
|
|
5892
|
-
const absolutePath = resolve(path);
|
|
5893
|
-
let existingPath = absolutePath;
|
|
5894
|
-
const missingEntries = [];
|
|
5895
|
-
while (true) {
|
|
5896
|
-
try {
|
|
5897
|
-
return join(realpathSync(existingPath), ...missingEntries);
|
|
5898
|
-
} catch (error2) {
|
|
5899
|
-
if (error2.code !== "ENOENT")
|
|
5900
|
-
return absolutePath;
|
|
5901
|
-
const parent = dirname(existingPath);
|
|
5902
|
-
if (parent === existingPath)
|
|
5903
|
-
return absolutePath;
|
|
5904
|
-
missingEntries.unshift(basename(existingPath));
|
|
5905
|
-
existingPath = parent;
|
|
5906
|
-
}
|
|
5907
|
-
}
|
|
5908
|
-
}
|
|
5909
|
-
function pathIsWithin(root, candidate) {
|
|
5910
|
-
const relativePath = relative(root, candidate);
|
|
5911
|
-
return relativePath === "" || relativePath !== ".." && !relativePath.startsWith(`..${sep}`) && !isAbsolute(relativePath);
|
|
5912
|
-
}
|
|
5913
|
-
function isGlobalRecordingsStatePath(path, env = process.env) {
|
|
5914
|
-
const globalStateRoot = join(stateParent(env), "recordings");
|
|
5915
|
-
return pathIsWithin(resolve(globalStateRoot), resolve(path)) || pathIsWithin(canonicalPotentialPath(globalStateRoot), canonicalPotentialPath(path));
|
|
5916
|
-
}
|
|
5917
6126
|
function removeActiveLease(activeLease, releasedLease) {
|
|
5918
6127
|
try {
|
|
5919
6128
|
renameSync(activeLease, releasedLease);
|
|
@@ -5999,6 +6208,9 @@ function loadConfig(configPath) {
|
|
|
5999
6208
|
if (process.env.RECORDINGS_API_KEY) {
|
|
6000
6209
|
config.openai_api_key = process.env.RECORDINGS_API_KEY;
|
|
6001
6210
|
}
|
|
6211
|
+
if (process.env.RECORDINGS_OPENAI_API_KEY) {
|
|
6212
|
+
config.openai_api_key = process.env.RECORDINGS_OPENAI_API_KEY;
|
|
6213
|
+
}
|
|
6002
6214
|
if (process.env.RECORDINGS_ENHANCEMENT_KEY) {
|
|
6003
6215
|
config.enhancement_api_key = process.env.RECORDINGS_ENHANCEMENT_KEY;
|
|
6004
6216
|
}
|
|
@@ -6042,14 +6254,26 @@ function loadConfig(configPath) {
|
|
|
6042
6254
|
if (process.env.RECORDINGS_AUDIO_DIR) {
|
|
6043
6255
|
config.audio_dir = process.env.RECORDINGS_AUDIO_DIR;
|
|
6044
6256
|
}
|
|
6257
|
+
if (process.env.HASNA_RECORDINGS_S3_BUCKET) {
|
|
6258
|
+
config.s3_bucket = process.env.HASNA_RECORDINGS_S3_BUCKET;
|
|
6259
|
+
} else if (process.env.RECORDINGS_S3_BUCKET) {
|
|
6260
|
+
config.s3_bucket = process.env.RECORDINGS_S3_BUCKET;
|
|
6261
|
+
}
|
|
6262
|
+
if (process.env.HASNA_RECORDINGS_S3_PREFIX) {
|
|
6263
|
+
config.s3_prefix = process.env.HASNA_RECORDINGS_S3_PREFIX;
|
|
6264
|
+
} else if (process.env.RECORDINGS_S3_PREFIX) {
|
|
6265
|
+
config.s3_prefix = process.env.RECORDINGS_S3_PREFIX;
|
|
6266
|
+
}
|
|
6267
|
+
if (process.env.RECORDINGS_S3_REGION) {
|
|
6268
|
+
config.s3_region = process.env.RECORDINGS_S3_REGION;
|
|
6269
|
+
} else if (process.env.AWS_REGION) {
|
|
6270
|
+
config.s3_region = process.env.AWS_REGION;
|
|
6271
|
+
}
|
|
6045
6272
|
if (process.env.RECORDINGS_MAX_SECONDS) {
|
|
6046
6273
|
config.max_recording_seconds = parseInt(process.env.RECORDINGS_MAX_SECONDS, 10);
|
|
6047
6274
|
}
|
|
6048
|
-
if (!config.openai_api_key) {
|
|
6049
|
-
config.openai_api_key = loadSecretKey("OPENAI_API_KEY");
|
|
6050
|
-
}
|
|
6051
6275
|
if (!config.enhancement_api_key) {
|
|
6052
|
-
config.enhancement_api_key = config.openai_api_key
|
|
6276
|
+
config.enhancement_api_key = config.openai_api_key;
|
|
6053
6277
|
}
|
|
6054
6278
|
if (!explicitTranscriberModel) {
|
|
6055
6279
|
config.transcriber_model = config.enhancement_model;
|
|
@@ -6229,66 +6453,9 @@ function mergeDirectoryContents(sourceDir, targetDir) {
|
|
|
6229
6453
|
}
|
|
6230
6454
|
}
|
|
6231
6455
|
}
|
|
6232
|
-
function loadSecretKey(keyName) {
|
|
6233
|
-
const secretsPath = join2(getHomeDir(), ".secrets");
|
|
6234
|
-
if (!existsSync(secretsPath))
|
|
6235
|
-
return "";
|
|
6236
|
-
for (const candidate of listSecretFiles(secretsPath)) {
|
|
6237
|
-
try {
|
|
6238
|
-
const content = readFileSync(candidate, "utf-8");
|
|
6239
|
-
const match = content.match(new RegExp(`export\\s+${keyName}\\s*=\\s*"([^"]+)"`));
|
|
6240
|
-
const doubleQuotedValue = match?.[1];
|
|
6241
|
-
if (doubleQuotedValue !== undefined)
|
|
6242
|
-
return doubleQuotedValue;
|
|
6243
|
-
const match2 = content.match(new RegExp(`export\\s+${keyName}\\s*=\\s*'([^']+)'`));
|
|
6244
|
-
const singleQuotedValue = match2?.[1];
|
|
6245
|
-
if (singleQuotedValue !== undefined)
|
|
6246
|
-
return singleQuotedValue;
|
|
6247
|
-
const match3 = content.match(new RegExp(`${keyName}\\s*=\\s*(.+)`));
|
|
6248
|
-
const unquotedValue = match3?.[1];
|
|
6249
|
-
if (unquotedValue !== undefined)
|
|
6250
|
-
return unquotedValue.trim().replace(/^["']|["']$/g, "");
|
|
6251
|
-
} catch {}
|
|
6252
|
-
}
|
|
6253
|
-
return "";
|
|
6254
|
-
}
|
|
6255
|
-
function listSecretFiles(path) {
|
|
6256
|
-
try {
|
|
6257
|
-
const stats = statSync(path);
|
|
6258
|
-
if (stats.isFile())
|
|
6259
|
-
return [path];
|
|
6260
|
-
if (!stats.isDirectory())
|
|
6261
|
-
return [];
|
|
6262
|
-
return readdirSync(path).sort().flatMap((entry) => {
|
|
6263
|
-
const child = join2(path, entry);
|
|
6264
|
-
try {
|
|
6265
|
-
const childStats = statSync(child);
|
|
6266
|
-
if (childStats.isDirectory())
|
|
6267
|
-
return listSecretFiles(child);
|
|
6268
|
-
if (childStats.isFile() && child.endsWith(".env"))
|
|
6269
|
-
return [child];
|
|
6270
|
-
} catch {}
|
|
6271
|
-
return [];
|
|
6272
|
-
});
|
|
6273
|
-
} catch {
|
|
6274
|
-
return [];
|
|
6275
|
-
}
|
|
6276
|
-
}
|
|
6277
6456
|
function getHomeDir() {
|
|
6278
6457
|
return process.env["HOME"] || process.env["USERPROFILE"] || homedir2();
|
|
6279
6458
|
}
|
|
6280
|
-
function ensureDataDir(config) {
|
|
6281
|
-
const { mkdirSync: mkdirSync3 } = __require("fs");
|
|
6282
|
-
const dbDir = config.db_path.substring(0, config.db_path.lastIndexOf("/"));
|
|
6283
|
-
const releaseLease = [config.audio_dir, dbDir].some((path) => path.length > 0 && isGlobalRecordingsStatePath(path)) ? acquireLocalStoreReaderLease() : () => {};
|
|
6284
|
-
try {
|
|
6285
|
-
mkdirSync3(config.audio_dir, { recursive: true });
|
|
6286
|
-
if (dbDir)
|
|
6287
|
-
mkdirSync3(dbDir, { recursive: true });
|
|
6288
|
-
} finally {
|
|
6289
|
-
releaseLease();
|
|
6290
|
-
}
|
|
6291
|
-
}
|
|
6292
6459
|
var POST_PROCESSING_MODES, DEFAULT_TRANSCRIPTION_MODEL = "gpt-4o-transcribe", DEFAULT_REALTIME_SESSION_MODEL = "gpt-realtime", DEFAULT_REALTIME_TRANSCRIPTION_MODEL = "gpt-realtime-whisper", DEFAULT_CONFIG;
|
|
6293
6460
|
var init_config = __esm(() => {
|
|
6294
6461
|
init_install_maintenance();
|
|
@@ -6330,6 +6497,9 @@ var init_config = __esm(() => {
|
|
|
6330
6497
|
db_path: "",
|
|
6331
6498
|
audio_dir: "",
|
|
6332
6499
|
max_recording_seconds: 1800,
|
|
6500
|
+
s3_bucket: "",
|
|
6501
|
+
s3_prefix: "",
|
|
6502
|
+
s3_region: "",
|
|
6333
6503
|
config_warnings: []
|
|
6334
6504
|
};
|
|
6335
6505
|
});
|
|
@@ -6421,6 +6591,9 @@ function repairSchemaDrift(db) {
|
|
|
6421
6591
|
ensureColumn(db, "recordings", "task_list_id", "TEXT");
|
|
6422
6592
|
ensureColumn(db, "recordings", "machine_id", "TEXT");
|
|
6423
6593
|
ensureColumn(db, "recordings", "metadata", "TEXT DEFAULT '{}'");
|
|
6594
|
+
ensureColumn(db, "recordings", "audio_object_key", "TEXT");
|
|
6595
|
+
ensureColumn(db, "recordings", "audio_sha256", "TEXT");
|
|
6596
|
+
ensureColumn(db, "recordings", "audio_bytes", "INTEGER");
|
|
6424
6597
|
ensureColumn(db, "agents", "active_project_id", "TEXT REFERENCES projects(id) ON DELETE SET NULL");
|
|
6425
6598
|
}
|
|
6426
6599
|
function ensureColumn(db, table, column, definition) {
|
|
@@ -6514,6 +6687,11 @@ var init_database = __esm(() => {
|
|
|
6514
6687
|
`,
|
|
6515
6688
|
`
|
|
6516
6689
|
ALTER TABLE agents ADD COLUMN active_project_id TEXT REFERENCES projects(id) ON DELETE SET NULL;
|
|
6690
|
+
`,
|
|
6691
|
+
`
|
|
6692
|
+
ALTER TABLE recordings ADD COLUMN audio_object_key TEXT;
|
|
6693
|
+
ALTER TABLE recordings ADD COLUMN audio_sha256 TEXT;
|
|
6694
|
+
ALTER TABLE recordings ADD COLUMN audio_bytes INTEGER;
|
|
6517
6695
|
`
|
|
6518
6696
|
];
|
|
6519
6697
|
CURRENT_MIGRATION_LEVEL = MIGRATIONS.length - 1;
|
|
@@ -6524,6 +6702,9 @@ function parseRow(row) {
|
|
|
6524
6702
|
return {
|
|
6525
6703
|
id: row["id"],
|
|
6526
6704
|
audio_path: row["audio_path"] || null,
|
|
6705
|
+
audio_object_key: row["audio_object_key"] || null,
|
|
6706
|
+
audio_sha256: row["audio_sha256"] || null,
|
|
6707
|
+
audio_bytes: Number(row["audio_bytes"] ?? 0) || null,
|
|
6527
6708
|
raw_text: row["raw_text"],
|
|
6528
6709
|
processed_text: row["processed_text"] || null,
|
|
6529
6710
|
processing_mode: row["processing_mode"] || "raw",
|
|
@@ -6555,9 +6736,9 @@ function createRecording2(input, db, idempotencyKey) {
|
|
|
6555
6736
|
}
|
|
6556
6737
|
const tagsJson = JSON.stringify(input.tags || []);
|
|
6557
6738
|
const metadataJson = JSON.stringify(input.metadata || {});
|
|
6558
|
-
const insertResult = d.query(`INSERT INTO recordings (id, audio_path, raw_text, processed_text, processing_mode, model_used, enhancement_model, duration_ms, language, tags, agent_id, project_id, session_id, goal, role, task_list_id, machine_id, metadata)
|
|
6559
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
6560
|
-
ON CONFLICT(id) DO NOTHING`).run(id, input.audio_path || null, input.raw_text, input.processed_text || null, input.processing_mode || "raw", input.model_used || "gpt-4o-transcribe", input.enhancement_model || null, input.duration_ms || 0, input.language || null, tagsJson, input.agent_id || null, input.project_id || null, input.session_id || null, input.goal || null, input.role || null, input.task_list_id || null, input.machine_id || null, metadataJson);
|
|
6739
|
+
const insertResult = d.query(`INSERT INTO recordings (id, audio_path, audio_object_key, audio_sha256, audio_bytes, raw_text, processed_text, processing_mode, model_used, enhancement_model, duration_ms, language, tags, agent_id, project_id, session_id, goal, role, task_list_id, machine_id, metadata)
|
|
6740
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
6741
|
+
ON CONFLICT(id) DO NOTHING`).run(id, input.audio_path || null, input.audio_object_key || null, input.audio_sha256 || null, input.audio_bytes || null, input.raw_text, input.processed_text || null, input.processing_mode || "raw", input.model_used || "gpt-4o-transcribe", input.enhancement_model || null, input.duration_ms || 0, input.language || null, tagsJson, input.agent_id || null, input.project_id || null, input.session_id || null, input.goal || null, input.role || null, input.task_list_id || null, input.machine_id || null, metadataJson);
|
|
6561
6742
|
if (insertResult.changes === 0) {
|
|
6562
6743
|
const existing = getRecording2(id, d);
|
|
6563
6744
|
if (existing?.id === id)
|
|
@@ -6574,6 +6755,13 @@ function createRecording2(input, db, idempotencyKey) {
|
|
|
6574
6755
|
});
|
|
6575
6756
|
return create();
|
|
6576
6757
|
}
|
|
6758
|
+
function updateRecordingAudio(id, audioObjectKey, audioSha256, audioBytes, db) {
|
|
6759
|
+
const d = db || getDatabase();
|
|
6760
|
+
const result = d.query(`UPDATE recordings SET audio_object_key = ?, audio_sha256 = ?, audio_bytes = ? WHERE id = ?`).run(audioObjectKey, audioSha256, audioBytes, id);
|
|
6761
|
+
if (result.changes === 0)
|
|
6762
|
+
return null;
|
|
6763
|
+
return getRecording2(id, d);
|
|
6764
|
+
}
|
|
6577
6765
|
function getRecording2(id, db) {
|
|
6578
6766
|
const d = db || getDatabase();
|
|
6579
6767
|
let row = d.query("SELECT * FROM recordings WHERE id = ?").get(id);
|
|
@@ -6795,151 +6983,266 @@ var init_feedback = __esm(() => {
|
|
|
6795
6983
|
init_version();
|
|
6796
6984
|
});
|
|
6797
6985
|
|
|
6986
|
+
// src/lib/local-opt-in.ts
|
|
6987
|
+
import {
|
|
6988
|
+
clientTransportEnvKeys,
|
|
6989
|
+
credentialOverrideEnvKey,
|
|
6990
|
+
credentialPointerEnvKey,
|
|
6991
|
+
CREDENTIAL_PROFILE_ENV_KEY
|
|
6992
|
+
} from "@hasna/contracts/client";
|
|
6993
|
+
function isRecordingsLocalOptIn(env = process.env) {
|
|
6994
|
+
return RECORDINGS_LOCAL_OPT_IN_ENV_KEYS.some((key) => (env[key] ?? "").trim() !== "");
|
|
6995
|
+
}
|
|
6996
|
+
function recordingsAuthorityEnvKeys() {
|
|
6997
|
+
const keys = clientTransportEnvKeys("recordings");
|
|
6998
|
+
return [
|
|
6999
|
+
...keys.apiUrlKeys,
|
|
7000
|
+
...keys.apiKeyKeys,
|
|
7001
|
+
credentialOverrideEnvKey("recordings"),
|
|
7002
|
+
credentialPointerEnvKey("recordings"),
|
|
7003
|
+
CREDENTIAL_PROFILE_ENV_KEY
|
|
7004
|
+
].filter((key) => !RECORDINGS_CARVED_LEGACY_ENV_KEYS.includes(key));
|
|
7005
|
+
}
|
|
7006
|
+
function hasRecordingsEnvAuthorityIntent(env = process.env) {
|
|
7007
|
+
return recordingsAuthorityEnvKeys().some((key) => (env[key] ?? "").trim() !== "");
|
|
7008
|
+
}
|
|
7009
|
+
function selectsRecordingsLocalStore(env = process.env) {
|
|
7010
|
+
return !hasRecordingsEnvAuthorityIntent(env) && isRecordingsLocalOptIn(env);
|
|
7011
|
+
}
|
|
7012
|
+
function recordingsResolverEnv(env) {
|
|
7013
|
+
const carved = RECORDINGS_CARVED_LEGACY_ENV_KEYS.filter((key) => (key in env));
|
|
7014
|
+
const blanks = recordingsAuthorityEnvKeys().filter((key) => (key in env) && (env[key] ?? "").trim() === "");
|
|
7015
|
+
const removed = new Set([...carved, ...blanks]);
|
|
7016
|
+
if (removed.size === 0)
|
|
7017
|
+
return env;
|
|
7018
|
+
const next = { ...env };
|
|
7019
|
+
for (const key of removed)
|
|
7020
|
+
delete next[key];
|
|
7021
|
+
return next;
|
|
7022
|
+
}
|
|
7023
|
+
function isAmbientRecordingsEnv(env) {
|
|
7024
|
+
if (typeof process !== "undefined" && env === process.env) {
|
|
7025
|
+
return true;
|
|
7026
|
+
}
|
|
7027
|
+
return env[CONTRACTS_AMBIENT_ENVIRONMENT] === true;
|
|
7028
|
+
}
|
|
7029
|
+
function recordingsResolverInputs(env, credentials = {}) {
|
|
7030
|
+
const normalised = recordingsResolverEnv(env);
|
|
7031
|
+
if (normalised === env)
|
|
7032
|
+
return { env: normalised, credentials };
|
|
7033
|
+
const keychain = { ...credentials.keychain };
|
|
7034
|
+
if (keychain.enabled === undefined && keychain.run === undefined) {
|
|
7035
|
+
keychain.enabled = isAmbientRecordingsEnv(env);
|
|
7036
|
+
}
|
|
7037
|
+
return { env: normalised, credentials: { ...credentials, keychain } };
|
|
7038
|
+
}
|
|
7039
|
+
var RECORDINGS_LOCAL_OPT_IN_ENV_KEYS, RECORDINGS_CARVED_LEGACY_ENV_KEYS, CONTRACTS_AMBIENT_ENVIRONMENT;
|
|
7040
|
+
var init_local_opt_in = __esm(() => {
|
|
7041
|
+
RECORDINGS_LOCAL_OPT_IN_ENV_KEYS = [
|
|
7042
|
+
"HASNA_RECORDINGS_LOCAL",
|
|
7043
|
+
"RECORDINGS_LOCAL"
|
|
7044
|
+
];
|
|
7045
|
+
RECORDINGS_CARVED_LEGACY_ENV_KEYS = [
|
|
7046
|
+
"RECORDINGS_API_URL",
|
|
7047
|
+
"RECORDINGS_API_KEY"
|
|
7048
|
+
];
|
|
7049
|
+
CONTRACTS_AMBIENT_ENVIRONMENT = Symbol.for("hasna:contracts:ambientClientEnvironment");
|
|
7050
|
+
});
|
|
7051
|
+
|
|
6798
7052
|
// src/http/client.ts
|
|
6799
|
-
import { createClientTransport } from "@hasna/contracts/client";
|
|
6800
7053
|
import {
|
|
6801
|
-
|
|
7054
|
+
clientTransportEnvKeys as clientTransportEnvKeys2,
|
|
7055
|
+
resolveClientTransport
|
|
7056
|
+
} from "@hasna/contracts/client";
|
|
7057
|
+
import {
|
|
6802
7058
|
resolveStorageClient
|
|
6803
7059
|
} from "@hasna/contracts/client/storage";
|
|
6804
|
-
function
|
|
6805
|
-
|
|
6806
|
-
|
|
6807
|
-
|
|
6808
|
-
|
|
6809
|
-
return {
|
|
6810
|
-
storeKeys: [`HASNA_${token}_CLIENT_STORE`, `${token}_CLIENT_STORE`],
|
|
6811
|
-
apiUrlKeys: [`HASNA_${token}_API_URL`],
|
|
6812
|
-
apiKeyKeys: [`HASNA_${token}_API_KEY`]
|
|
6813
|
-
};
|
|
6814
|
-
}
|
|
6815
|
-
function normalizeClientStore(value) {
|
|
6816
|
-
const normalized = value.trim().toLowerCase();
|
|
6817
|
-
if (normalized === "sqlite")
|
|
6818
|
-
return "sqlite";
|
|
6819
|
-
if (normalized === "http" || normalized === "https")
|
|
6820
|
-
return "http";
|
|
6821
|
-
throw new Error(`Unknown client store: ${value}. Use sqlite or http.`);
|
|
6822
|
-
}
|
|
6823
|
-
function firstEnv(env, keys) {
|
|
6824
|
-
for (const key of keys) {
|
|
6825
|
-
const value = env[key]?.trim();
|
|
6826
|
-
if (value)
|
|
6827
|
-
return { key, value };
|
|
7060
|
+
function throwRecordingsAuthorityFailure(error2) {
|
|
7061
|
+
const message = error2 instanceof Error ? error2.message : String(error2);
|
|
7062
|
+
const name = error2 instanceof Error ? error2.name : "";
|
|
7063
|
+
if (name === "CredentialResolutionError" || name === "CredentialFileUnsafeError") {
|
|
7064
|
+
throw new Error(`REMOTE_API_CREDENTIAL_INVALID: ${message} There is no local fallback: ` + "local SQLite is opt-in only (HASNA_RECORDINGS_LOCAL=1) and is disabled by default \u2014 failing closed", { cause: error2 });
|
|
6828
7065
|
}
|
|
6829
|
-
|
|
7066
|
+
if (/no API key could be resolved/.test(message)) {
|
|
7067
|
+
if (/is not set and no API key could be resolved/.test(message)) {
|
|
7068
|
+
throw new Error("REMOTE_API_CONFIG_MISSING: no Recordings credential resolved from the Keychain item " + `hasna.credentials.recordings.api-key, ~/.hasna/recordings/config/credentials, or HASNA_RECORDINGS_API_KEY. ${message} ` + "There is no local fallback: local SQLite is opt-in only (HASNA_RECORDINGS_LOCAL=1, alias RECORDINGS_LOCAL=1) " + "and is disabled by default \u2014 failing closed instead of serving the local store", { cause: error2 });
|
|
7069
|
+
}
|
|
7070
|
+
throw new Error("REMOTE_API_KEY_MISSING: remote Recordings storage requires HASNA_RECORDINGS_API_KEY, the Keychain item " + `hasna.credentials.recordings.api-key, or ~/.hasna/recordings/config/credentials. ${message} ` + "There is no local fallback: local SQLite is opt-in only (HASNA_RECORDINGS_LOCAL=1) and is disabled by default \u2014 failing closed", { cause: error2 });
|
|
7071
|
+
}
|
|
7072
|
+
throw new Error(`REMOTE_API_URL_INVALID: ${message} local SQLite fallback is disabled`, { cause: error2 });
|
|
6830
7073
|
}
|
|
6831
|
-
function
|
|
6832
|
-
|
|
6833
|
-
|
|
6834
|
-
|
|
6835
|
-
|
|
6836
|
-
|
|
6837
|
-
|
|
6838
|
-
|
|
6839
|
-
|
|
6840
|
-
|
|
6841
|
-
|
|
6842
|
-
|
|
6843
|
-
}
|
|
6844
|
-
function resolveTransport(name, env = process.env) {
|
|
6845
|
-
const keys = envKeys(name);
|
|
6846
|
-
const storeHit = firstEnv(env, keys.storeKeys);
|
|
6847
|
-
const urlHit = firstEnv(env, keys.apiUrlKeys);
|
|
6848
|
-
const keyHit = firstEnv(env, keys.apiKeyKeys);
|
|
6849
|
-
let requested = "sqlite";
|
|
6850
|
-
let modeSource = "default";
|
|
6851
|
-
if (storeHit) {
|
|
6852
|
-
requested = normalizeClientStore(storeHit.value);
|
|
6853
|
-
modeSource = storeHit.key;
|
|
6854
|
-
} else if (urlHit && keyHit) {
|
|
6855
|
-
requested = "http";
|
|
6856
|
-
modeSource = "auto:api-url+api-key";
|
|
6857
|
-
} else if (urlHit || keyHit) {
|
|
6858
|
-
const missing = urlHit ? keys.apiKeyKeys[0] : keys.apiUrlKeys[0];
|
|
6859
|
-
const present = urlHit ? keys.apiUrlKeys[0] : keys.apiKeyKeys[0];
|
|
6860
|
-
return {
|
|
6861
|
-
transport: "sqlite",
|
|
6862
|
-
requested,
|
|
6863
|
-
modeSource,
|
|
6864
|
-
baseUrl: null,
|
|
6865
|
-
apiKeyPresent: Boolean(keyHit),
|
|
6866
|
-
misconfigured: true,
|
|
6867
|
-
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.`
|
|
6868
|
-
};
|
|
7074
|
+
function resolveRecordingsCloudClient(env = process.env, options = {}) {
|
|
7075
|
+
if (selectsRecordingsLocalStore(env))
|
|
7076
|
+
return null;
|
|
7077
|
+
const resolverInputs = recordingsResolverInputs(env, options.credentials);
|
|
7078
|
+
let resolved;
|
|
7079
|
+
try {
|
|
7080
|
+
resolved = resolveStorageClient("recordings", resolverInputs.env, {
|
|
7081
|
+
fetchImpl: (input, init) => globalThis.fetch(input, { ...init, redirect: "manual" }),
|
|
7082
|
+
credentials: resolverInputs.credentials
|
|
7083
|
+
});
|
|
7084
|
+
} catch (error2) {
|
|
7085
|
+
throwRecordingsAuthorityFailure(error2);
|
|
6869
7086
|
}
|
|
6870
|
-
|
|
6871
|
-
|
|
7087
|
+
const client = resolved.client;
|
|
7088
|
+
return client;
|
|
7089
|
+
}
|
|
7090
|
+
var RETRY_STATUSES, IDEMPOTENT;
|
|
7091
|
+
var init_client = __esm(() => {
|
|
7092
|
+
init_local_opt_in();
|
|
7093
|
+
RETRY_STATUSES = new Set([408, 425, 429, 500, 502, 503, 504]);
|
|
7094
|
+
IDEMPOTENT = new Set(["GET", "HEAD", "PUT", "DELETE", "OPTIONS"]);
|
|
7095
|
+
});
|
|
7096
|
+
|
|
7097
|
+
// src/lib/audio-artifact-storage.ts
|
|
7098
|
+
import { GetObjectCommand, PutObjectCommand, S3Client as AwsS3Client } from "@aws-sdk/client-s3";
|
|
7099
|
+
import { createHash as createHash2 } from "crypto";
|
|
7100
|
+
import { readFile } from "fs/promises";
|
|
7101
|
+
import { extname } from "path";
|
|
7102
|
+
function contentTypeForExtension(extension) {
|
|
7103
|
+
return AUDIO_CONTENT_TYPES[extension.toLowerCase()] ?? "application/octet-stream";
|
|
7104
|
+
}
|
|
7105
|
+
|
|
7106
|
+
class AudioArtifactStorage {
|
|
7107
|
+
bucket;
|
|
7108
|
+
prefix;
|
|
7109
|
+
s3;
|
|
7110
|
+
constructor(options = {}) {
|
|
7111
|
+
this.bucket = options.bucket;
|
|
7112
|
+
this.prefix = (options.prefix || DEFAULT_S3_PREFIX).replace(/^\/+|\/+$/g, "");
|
|
7113
|
+
this.s3 = this.bucket ? options.client ?? new AwsS3Client({ region: options.region || process.env.AWS_REGION || "us-east-1" }) : undefined;
|
|
7114
|
+
}
|
|
7115
|
+
get usesS3() {
|
|
7116
|
+
return Boolean(this.bucket);
|
|
7117
|
+
}
|
|
7118
|
+
objectKeyFor(recordingId, sha256, extension) {
|
|
7119
|
+
assertSha256(sha256);
|
|
7120
|
+
const safeExtension = extension.toLowerCase().match(/^[a-z0-9]+/)?.[0] || "bin";
|
|
7121
|
+
return `${this.prefix}/${recordingId}/${sha256}.${safeExtension}`;
|
|
7122
|
+
}
|
|
7123
|
+
async uploadAudio(recordingId, audioPath) {
|
|
7124
|
+
if (!this.bucket || !this.s3)
|
|
7125
|
+
return null;
|
|
7126
|
+
const bytes = await readFileBytes(audioPath);
|
|
7127
|
+
if (!bytes)
|
|
7128
|
+
return null;
|
|
7129
|
+
const sha256 = sha256Hex(bytes);
|
|
7130
|
+
const extension = extensionOf(audioPath);
|
|
7131
|
+
const key = this.objectKeyFor(recordingId, sha256, extension);
|
|
7132
|
+
await this.s3.send(new PutObjectCommand({
|
|
7133
|
+
Bucket: this.bucket,
|
|
7134
|
+
Key: key,
|
|
7135
|
+
Body: bytes,
|
|
7136
|
+
ContentType: contentTypeForExtension(extension)
|
|
7137
|
+
}));
|
|
7138
|
+
return { objectKey: key, sha256, bytes: bytes.byteLength };
|
|
7139
|
+
}
|
|
7140
|
+
async readAudio(objectKey) {
|
|
7141
|
+
if (!this.bucket || !this.s3)
|
|
7142
|
+
return null;
|
|
7143
|
+
try {
|
|
7144
|
+
const response = await this.s3.send(new GetObjectCommand({ Bucket: this.bucket, Key: objectKey }));
|
|
7145
|
+
return await bodyToBytes(response.Body);
|
|
7146
|
+
} catch {
|
|
7147
|
+
return null;
|
|
7148
|
+
}
|
|
6872
7149
|
}
|
|
6873
|
-
|
|
6874
|
-
|
|
6875
|
-
|
|
6876
|
-
|
|
6877
|
-
|
|
6878
|
-
|
|
6879
|
-
|
|
6880
|
-
|
|
6881
|
-
|
|
6882
|
-
|
|
7150
|
+
}
|
|
7151
|
+
function resolveAudioArtifactStorage(env = process.env, client) {
|
|
7152
|
+
const bucket = env.HASNA_RECORDINGS_S3_BUCKET || env.RECORDINGS_S3_BUCKET || undefined;
|
|
7153
|
+
const prefix = env.HASNA_RECORDINGS_S3_PREFIX || env.RECORDINGS_S3_PREFIX || undefined;
|
|
7154
|
+
const region = env.RECORDINGS_S3_REGION || env.AWS_REGION || undefined;
|
|
7155
|
+
return new AudioArtifactStorage({ bucket, prefix, region, client });
|
|
7156
|
+
}
|
|
7157
|
+
async function uploadAudioAtCreation(recordingId, audioPath, storage = resolveAudioArtifactStorage()) {
|
|
7158
|
+
if (!audioPath)
|
|
7159
|
+
return null;
|
|
7160
|
+
if (!storage.usesS3)
|
|
7161
|
+
return null;
|
|
7162
|
+
try {
|
|
7163
|
+
const uploaded = await storage.uploadAudio(recordingId, audioPath);
|
|
7164
|
+
if (!uploaded)
|
|
7165
|
+
return null;
|
|
7166
|
+
console.warn(`recordings: uploaded audio for ${recordingId} to ${uploaded.objectKey}`);
|
|
7167
|
+
return uploaded;
|
|
7168
|
+
} catch (error2) {
|
|
7169
|
+
console.warn(`recordings: audio upload failed for ${recordingId} (${String(error2 instanceof Error ? error2.message : error2)}); keeping local audio_path only`);
|
|
7170
|
+
return null;
|
|
6883
7171
|
}
|
|
6884
|
-
|
|
6885
|
-
|
|
6886
|
-
|
|
6887
|
-
|
|
6888
|
-
modeSource,
|
|
6889
|
-
baseUrl: null,
|
|
6890
|
-
apiKeyPresent: false,
|
|
6891
|
-
misconfigured: true,
|
|
6892
|
-
warning: `${modeSource}=http but no API key is set (${keys.apiKeyKeys[0]}). Refusing to route to the API.`
|
|
6893
|
-
};
|
|
7172
|
+
}
|
|
7173
|
+
function assertSha256(value) {
|
|
7174
|
+
if (!SHA256_HEX.test(value)) {
|
|
7175
|
+
throw new Error("audio digest must be a lowercase hex sha-256; refusing to build a storage key from it");
|
|
6894
7176
|
}
|
|
6895
|
-
|
|
6896
|
-
|
|
7177
|
+
}
|
|
7178
|
+
function sha256Hex(bytes) {
|
|
7179
|
+
const hash = createHash2("sha256");
|
|
7180
|
+
hash.update(bytes);
|
|
7181
|
+
return hash.digest("hex");
|
|
7182
|
+
}
|
|
7183
|
+
async function readFileBytes(path) {
|
|
6897
7184
|
try {
|
|
6898
|
-
|
|
6899
|
-
} catch
|
|
6900
|
-
|
|
6901
|
-
return { transport: "sqlite", requested, modeSource, baseUrl: null, apiKeyPresent: true, misconfigured: true, warning: `Invalid API URL: ${message}.` };
|
|
7185
|
+
return new Uint8Array(await readFile(path));
|
|
7186
|
+
} catch {
|
|
7187
|
+
return null;
|
|
6902
7188
|
}
|
|
6903
|
-
return { transport: "http", requested, modeSource, baseUrl, apiKeyPresent: true, misconfigured: false, warning: null };
|
|
6904
7189
|
}
|
|
6905
|
-
function
|
|
6906
|
-
const
|
|
6907
|
-
|
|
6908
|
-
|
|
6909
|
-
|
|
6910
|
-
|
|
6911
|
-
|
|
6912
|
-
|
|
6913
|
-
|
|
6914
|
-
|
|
6915
|
-
|
|
6916
|
-
modeSource: resolution.modeSource === "default" ? "auto:api-url+seam-credential" : resolution.modeSource,
|
|
6917
|
-
baseUrl: wired2.resolution.baseUrl,
|
|
6918
|
-
apiKeyPresent: true,
|
|
6919
|
-
misconfigured: false,
|
|
6920
|
-
warning: null
|
|
6921
|
-
}
|
|
6922
|
-
};
|
|
6923
|
-
}
|
|
6924
|
-
throw new Error(resolution.warning ?? `Client for '${name}' is misconfigured for the /v1 API.`);
|
|
7190
|
+
function extensionOf(path) {
|
|
7191
|
+
const extension = extname(path).replace(/^\./, "").toLowerCase();
|
|
7192
|
+
return extension || "bin";
|
|
7193
|
+
}
|
|
7194
|
+
async function bodyToBytes(body) {
|
|
7195
|
+
if (!body)
|
|
7196
|
+
return new Uint8Array(0);
|
|
7197
|
+
if (body instanceof Uint8Array)
|
|
7198
|
+
return body;
|
|
7199
|
+
if (typeof body.transformToByteArray === "function") {
|
|
7200
|
+
return new Uint8Array(await body.transformToByteArray());
|
|
6925
7201
|
}
|
|
6926
|
-
if (
|
|
6927
|
-
|
|
7202
|
+
if (typeof body.getReader === "function") {
|
|
7203
|
+
const reader = body.getReader();
|
|
7204
|
+
const chunks2 = [];
|
|
7205
|
+
for (;; ) {
|
|
7206
|
+
const { value, done } = await reader.read();
|
|
7207
|
+
if (done)
|
|
7208
|
+
break;
|
|
7209
|
+
if (value)
|
|
7210
|
+
chunks2.push(value);
|
|
7211
|
+
}
|
|
7212
|
+
return concat(chunks2);
|
|
6928
7213
|
}
|
|
6929
|
-
const
|
|
6930
|
-
|
|
6931
|
-
|
|
7214
|
+
const chunks = [];
|
|
7215
|
+
for await (const chunk of body) {
|
|
7216
|
+
chunks.push(typeof chunk === "string" ? new TextEncoder().encode(chunk) : chunk);
|
|
6932
7217
|
}
|
|
6933
|
-
return
|
|
7218
|
+
return concat(chunks);
|
|
6934
7219
|
}
|
|
6935
|
-
|
|
6936
|
-
|
|
6937
|
-
|
|
6938
|
-
|
|
7220
|
+
function concat(chunks) {
|
|
7221
|
+
const total = chunks.reduce((sum, chunk) => sum + chunk.byteLength, 0);
|
|
7222
|
+
const merged = new Uint8Array(new ArrayBuffer(total));
|
|
7223
|
+
let offset = 0;
|
|
7224
|
+
for (const chunk of chunks) {
|
|
7225
|
+
merged.set(chunk, offset);
|
|
7226
|
+
offset += chunk.byteLength;
|
|
7227
|
+
}
|
|
7228
|
+
return merged;
|
|
7229
|
+
}
|
|
7230
|
+
var SHA256_HEX, DEFAULT_S3_PREFIX = "recordings", AUDIO_CONTENT_TYPES;
|
|
7231
|
+
var init_audio_artifact_storage = __esm(() => {
|
|
7232
|
+
SHA256_HEX = /^[0-9a-f]{64}$/;
|
|
7233
|
+
AUDIO_CONTENT_TYPES = {
|
|
7234
|
+
wav: "audio/wav",
|
|
7235
|
+
mp3: "audio/mpeg",
|
|
7236
|
+
m4a: "audio/mp4",
|
|
7237
|
+
aac: "audio/aac",
|
|
7238
|
+
webm: "audio/webm",
|
|
7239
|
+
ogg: "audio/ogg",
|
|
7240
|
+
flac: "audio/flac"
|
|
7241
|
+
};
|
|
6939
7242
|
});
|
|
6940
7243
|
|
|
6941
7244
|
// src/store.ts
|
|
6942
|
-
import { createHash as
|
|
7245
|
+
import { createHash as createHash3, randomUUID as randomUUID2 } from "crypto";
|
|
6943
7246
|
function listQuery(filter) {
|
|
6944
7247
|
if (!filter)
|
|
6945
7248
|
return {};
|
|
@@ -7111,7 +7414,7 @@ function apiStore(client) {
|
|
|
7111
7414
|
};
|
|
7112
7415
|
}
|
|
7113
7416
|
function recordingPageFingerprint(items) {
|
|
7114
|
-
const hash =
|
|
7417
|
+
const hash = createHash3("sha256");
|
|
7115
7418
|
const ids = items.map((item) => String(item.id ?? "")).sort();
|
|
7116
7419
|
for (const id of ids) {
|
|
7117
7420
|
hash.update(String(id.length));
|
|
@@ -7146,16 +7449,19 @@ async function countStoreRecordings(store, filter) {
|
|
|
7146
7449
|
}
|
|
7147
7450
|
throw new Error(`Legacy Store exceeded ${maxPageRequests} pages while counting recordings`);
|
|
7148
7451
|
}
|
|
7149
|
-
function
|
|
7150
|
-
|
|
7452
|
+
function localArtifactStorageFor() {
|
|
7453
|
+
return localArtifactStorageOverride ?? resolveAudioArtifactStorage();
|
|
7454
|
+
}
|
|
7455
|
+
function getStore(env = process.env, options = {}) {
|
|
7456
|
+
if (env === process.env && cached && options.credentials === undefined)
|
|
7151
7457
|
return cached;
|
|
7152
|
-
const
|
|
7153
|
-
const store =
|
|
7154
|
-
if (env === process.env)
|
|
7458
|
+
const client = resolveRecordingsCloudClient(env, options);
|
|
7459
|
+
const store = client ? apiStore(client) : localStore;
|
|
7460
|
+
if (env === process.env && options.credentials === undefined)
|
|
7155
7461
|
cached = store;
|
|
7156
7462
|
return store;
|
|
7157
7463
|
}
|
|
7158
|
-
var
|
|
7464
|
+
var localStore, cached = null, localArtifactStorageOverride;
|
|
7159
7465
|
var init_store = __esm(() => {
|
|
7160
7466
|
init_recordings();
|
|
7161
7467
|
init_agents();
|
|
@@ -7164,11 +7470,20 @@ var init_store = __esm(() => {
|
|
|
7164
7470
|
init_client();
|
|
7165
7471
|
init_recording_create_identity();
|
|
7166
7472
|
init_install_maintenance();
|
|
7473
|
+
init_audio_artifact_storage();
|
|
7167
7474
|
localStore = {
|
|
7168
7475
|
mode: "sqlite",
|
|
7169
7476
|
baseUrl: null,
|
|
7170
7477
|
async createRecording(input, idempotencyKey) {
|
|
7171
|
-
return withLocalStoreReaderLease(() =>
|
|
7478
|
+
return withLocalStoreReaderLease(async () => {
|
|
7479
|
+
const recording = createRecording2(input, undefined, idempotencyKey);
|
|
7480
|
+
if (!recording.audio_path)
|
|
7481
|
+
return recording;
|
|
7482
|
+
const uploaded = await uploadAudioAtCreation(recording.id, recording.audio_path, localArtifactStorageFor());
|
|
7483
|
+
if (!uploaded)
|
|
7484
|
+
return recording;
|
|
7485
|
+
return updateRecordingAudio(recording.id, uploaded.objectKey, uploaded.sha256, uploaded.bytes) ?? recording;
|
|
7486
|
+
});
|
|
7172
7487
|
},
|
|
7173
7488
|
async getRecording(id) {
|
|
7174
7489
|
return withLocalStoreReaderLease(() => getRecording2(id));
|
|
@@ -7240,7 +7555,7 @@ import OpenAI from "openai";
|
|
|
7240
7555
|
import { createReadStream } from "fs";
|
|
7241
7556
|
function getClient(config) {
|
|
7242
7557
|
if (!config.openai_api_key) {
|
|
7243
|
-
throw new TranscriptionError("OpenAI API key not configured. Set OPENAI_API_KEY env var
|
|
7558
|
+
throw new TranscriptionError("OpenAI API key not configured. Set OPENAI_API_KEY env var (e.g. " + "'secrets exec <key> --as OPENAI_API_KEY -- recordings \u2026')");
|
|
7244
7559
|
}
|
|
7245
7560
|
if (_client && _clientApiKey === config.openai_api_key)
|
|
7246
7561
|
return _client;
|
|
@@ -8153,8 +8468,9 @@ var init_mcp = __esm(async () => {
|
|
|
8153
8468
|
init_enhancer();
|
|
8154
8469
|
init_version();
|
|
8155
8470
|
init_machine();
|
|
8471
|
+
init_local_opt_in();
|
|
8472
|
+
init_client();
|
|
8156
8473
|
config = loadConfig();
|
|
8157
|
-
ensureDataDir(config);
|
|
8158
8474
|
if (false) {}
|
|
8159
8475
|
});
|
|
8160
8476
|
|
|
@@ -8172,13 +8488,12 @@ function jsonResponse(data, status = 200, headers) {
|
|
|
8172
8488
|
});
|
|
8173
8489
|
}
|
|
8174
8490
|
function resolveClientIp(req, server) {
|
|
8175
|
-
|
|
8176
|
-
|
|
8177
|
-
|
|
8178
|
-
|
|
8179
|
-
|
|
8180
|
-
}
|
|
8181
|
-
return server.requestIP(req)?.address || "unknown";
|
|
8491
|
+
return resolveRequestClientIp({
|
|
8492
|
+
headers: req.headers,
|
|
8493
|
+
socketAddress: server.requestIP(req)?.address,
|
|
8494
|
+
trustProxy: resolveTrustProxy(),
|
|
8495
|
+
trustedProxies: trustedProxiesFromEnv()
|
|
8496
|
+
}) ?? "unknown";
|
|
8182
8497
|
}
|
|
8183
8498
|
function checkRateLimit(ip) {
|
|
8184
8499
|
const now = Date.now();
|
|
@@ -8297,6 +8612,7 @@ var init_serve = __esm(() => {
|
|
|
8297
8612
|
init_version();
|
|
8298
8613
|
init_v1();
|
|
8299
8614
|
init_openapi();
|
|
8615
|
+
init_client_ip();
|
|
8300
8616
|
SECURITY_HEADERS = {
|
|
8301
8617
|
"X-Content-Type-Options": "nosniff",
|
|
8302
8618
|
"X-Frame-Options": "DENY",
|
|
@@ -8345,32 +8661,19 @@ Commands:
|
|
|
8345
8661
|
migrate Apply the PostgreSQL schema, then exit. Idempotent.
|
|
8346
8662
|
|
|
8347
8663
|
Environment:
|
|
8348
|
-
HASNA_RECORDINGS_DATABASE_URL
|
|
8349
|
-
|
|
8664
|
+
HASNA_RECORDINGS_DATABASE_URL PostgreSQL DSN (present -> postgresql backend, else sqlite)
|
|
8665
|
+
HASNA_RECORDINGS_MIGRATE_DATABASE_URL Optional MIGRATION-role DSN for migrate (two-role deploy;
|
|
8666
|
+
falls back to the runtime DSN when unset)
|
|
8667
|
+
HASNA_RECORDINGS_API_SIGNING_KEY HMAC signing secret for API-key auth`);
|
|
8350
8668
|
}
|
|
8351
8669
|
async function runMigrate() {
|
|
8352
|
-
const {
|
|
8353
|
-
|
|
8354
|
-
|
|
8355
|
-
migrateCloudSchema: migrateCloudSchema2,
|
|
8356
|
-
pingCloudConnectivity: pingCloudConnectivity3,
|
|
8357
|
-
resolveDatabaseUrl: resolveDatabaseUrl2
|
|
8358
|
-
} = await Promise.resolve().then(() => (init_cloud(), exports_cloud));
|
|
8359
|
-
const { assertCloudSchemaContract: assertCloudSchemaContract2 } = await Promise.resolve().then(() => (init_cloud_readiness(), exports_cloud_readiness));
|
|
8360
|
-
const { runCloudMigration: runCloudMigration2 } = await Promise.resolve().then(() => (init_migrate_command(), exports_migrate_command));
|
|
8361
|
-
if (!resolveDatabaseUrl2()) {
|
|
8362
|
-
console.error("migrate: no database URL (HASNA_RECORDINGS_DATABASE_URL / RECORDINGS_DATABASE_URL / DATABASE_URL)");
|
|
8670
|
+
const { closeCloud: closeCloud2, resolveMigrationDatabaseUrl: resolveMigrationDatabaseUrl2, runRecordedMigration: runRecordedMigration2 } = await Promise.resolve().then(() => (init_cloud(), exports_cloud));
|
|
8671
|
+
if (!resolveMigrationDatabaseUrl2()) {
|
|
8672
|
+
console.error("migrate: no database URL (HASNA_RECORDINGS_MIGRATE_DATABASE_URL / RECORDINGS_MIGRATE_DATABASE_URL / HASNA_RECORDINGS_DATABASE_URL / RECORDINGS_DATABASE_URL / DATABASE_URL)");
|
|
8363
8673
|
process.exit(2);
|
|
8364
8674
|
}
|
|
8365
8675
|
console.log("migrate: connecting\u2026");
|
|
8366
|
-
await
|
|
8367
|
-
pingConnectivity: pingCloudConnectivity3,
|
|
8368
|
-
applyMigrations: async () => {
|
|
8369
|
-
console.log("migrate: applying schema (recordings tables + api_keys)\u2026");
|
|
8370
|
-
await migrateCloudSchema2();
|
|
8371
|
-
},
|
|
8372
|
-
validateContract: () => assertCloudSchemaContract2(getCloudPg2())
|
|
8373
|
-
});
|
|
8676
|
+
await runRecordedMigration2();
|
|
8374
8677
|
console.log("migrate: done");
|
|
8375
8678
|
await closeCloud2();
|
|
8376
8679
|
process.exit(0);
|