@hasna/recordings 0.3.13 → 0.4.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 +223 -37
- package/bun.lock +37 -38
- package/dist/cli/index.js +1884 -615
- package/dist/cli/macos-shortcut.d.ts +2 -2
- 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/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 +2858 -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 +1689 -456
- 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 +566 -284
- 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 +1334 -871
- 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/package.json +9 -8
- 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/install_macos_app.sh +27 -24
- package/scripts/macos_artifact.ts +20 -16
- package/scripts/migrate.ts +4 -20
- 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/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/Recording.swift +14 -2
- package/src/native/Recordings/RecordingsLib/RecordingEngine.swift +316 -34
- package/src/native/Recordings/RecordingsLib/RecordingGlassBackground.swift +34 -0
- package/src/native/Recordings/RecordingsLib/RecordingProvider.swift +102 -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/CLIRunnerTests.swift +13 -4
- package/src/native/Recordings/RecordingsTests/OpenAIAPIKeyStoreTests.swift +43 -38
- package/src/native/Recordings/RecordingsTests/RecorderSurfaceTests.swift +60 -0
- package/src/native/Recordings/RecordingsTests/RecordingDateTests.swift +26 -0
- package/src/native/Recordings/RecordingsTests/RecordingProviderTests.swift +195 -0
- package/src/native/Recordings/RecordingsTests/RecordingStartGateTests.swift +1 -1
- package/src/native/Recordings/RecordingsTests/RecordingStartTimingTests.swift +30 -0
- package/src/native/Recordings/RecordingsTests/ServiceAPIConfigurationTests.swift +75 -0
- package/src/native/Recordings/RecordingsTests/SettingsWindowControllerTests.swift +28 -0
- 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 +6 -6
- 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.4.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: {
|
|
@@ -56,10 +55,10 @@ var init_package = __esm(() => {
|
|
|
56
55
|
scripts: {
|
|
57
56
|
clean: "rm -rf dist",
|
|
58
57
|
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",
|
|
58
|
+
"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",
|
|
59
|
+
"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",
|
|
60
|
+
"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",
|
|
61
|
+
"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",
|
|
63
62
|
"build:native-fs-guard": "/bin/bash scripts/build_native_fs_guard.sh",
|
|
64
63
|
"generate:sdk": "bun run scripts/generate-sdk.ts",
|
|
65
64
|
migrate: "bun run scripts/migrate.ts",
|
|
@@ -69,7 +68,7 @@ var init_package = __esm(() => {
|
|
|
69
68
|
"verify:ci-suite": "bun scripts/ci-linux-suite.ts --check",
|
|
70
69
|
"verify:ci-suite:run": "bun scripts/ci-linux-suite.ts --verify-run",
|
|
71
70
|
"verify:ci-native": "bun scripts/ci-native-build.ts",
|
|
72
|
-
"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)',
|
|
71
|
+
"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:-120000}" $(bun scripts/ci-linux-suite.ts --gated)',
|
|
73
72
|
"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
73
|
"test:vacuity-battery:source": "bun scripts/vacuity-mutation-battery.ts scripts/vacuity-manifests/source-side.tsv",
|
|
75
74
|
"test:vacuity-battery:chain": "bun scripts/vacuity-mutation-battery.ts scripts/vacuity-manifests/install-chain.tsv",
|
|
@@ -106,7 +105,8 @@ var init_package = __esm(() => {
|
|
|
106
105
|
"LICENSE"
|
|
107
106
|
],
|
|
108
107
|
dependencies: {
|
|
109
|
-
"@
|
|
108
|
+
"@aws-sdk/client-s3": "^3.1007.0",
|
|
109
|
+
"@hasna/contracts": "1.0.2",
|
|
110
110
|
"@hasna/events": "0.1.11",
|
|
111
111
|
"@modelcontextprotocol/sdk": "^1.12.1",
|
|
112
112
|
chalk: "^5.4.1",
|
|
@@ -116,7 +116,7 @@ var init_package = __esm(() => {
|
|
|
116
116
|
zod: "^3.24.2"
|
|
117
117
|
},
|
|
118
118
|
devDependencies: {
|
|
119
|
-
"@types/bun": "
|
|
119
|
+
"@types/bun": "1.3.14",
|
|
120
120
|
"@types/pg": "^8.11.11",
|
|
121
121
|
"node-api-headers": "1.9.0",
|
|
122
122
|
typescript: "^5.8.2"
|
|
@@ -203,6 +203,9 @@ var init_remote_storage = () => {};
|
|
|
203
203
|
function resolveDatabaseUrl(env = process.env) {
|
|
204
204
|
return env.HASNA_RECORDINGS_DATABASE_URL || env.RECORDINGS_DATABASE_URL || env.DATABASE_URL || undefined;
|
|
205
205
|
}
|
|
206
|
+
function resolveMigrationDatabaseUrl(env = process.env) {
|
|
207
|
+
return env.HASNA_RECORDINGS_MIGRATE_DATABASE_URL || env.RECORDINGS_MIGRATE_DATABASE_URL || resolveDatabaseUrl(env);
|
|
208
|
+
}
|
|
206
209
|
function resolveDataBackend(env = process.env) {
|
|
207
210
|
return resolveDatabaseUrl(env) ? "postgresql" : "sqlite";
|
|
208
211
|
}
|
|
@@ -210,7 +213,7 @@ function isPostgresBackendEnabled(env = process.env) {
|
|
|
210
213
|
return resolveDataBackend(env) === "postgresql";
|
|
211
214
|
}
|
|
212
215
|
function resolveSigningSecret(env = process.env) {
|
|
213
|
-
return env.HASNA_RECORDINGS_API_SIGNING_KEY || env.HASNA_API_SIGNING_KEY || env.API_KEY_SIGNING_SECRET || undefined;
|
|
216
|
+
return env.HASNA_RECORDINGS_API_SIGNING_KEY?.trim() || env.HASNA_API_SIGNING_KEY?.trim() || env.API_KEY_SIGNING_SECRET?.trim() || undefined;
|
|
214
217
|
}
|
|
215
218
|
function requireSigningSecret(env = process.env) {
|
|
216
219
|
const signingSecret = resolveSigningSecret(env);
|
|
@@ -224,13 +227,6 @@ function requireSigningSecret(env = process.env) {
|
|
|
224
227
|
}
|
|
225
228
|
|
|
226
229
|
// 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
230
|
function sameColumns(actual, expected) {
|
|
235
231
|
return actual.length === expected.length && actual.every((column, index) => column === expected[index]);
|
|
236
232
|
}
|
|
@@ -365,6 +361,16 @@ async function assertCloudSchemaReady(pg2) {
|
|
|
365
361
|
AND has_schema_privilege(current_user, schema_row.oid, 'CREATE')
|
|
366
362
|
) AS can_create_in_user_schema,
|
|
367
363
|
has_schema_privilege(current_user, 'public', 'USAGE') AS can_use_public,
|
|
364
|
+
COALESCE((
|
|
365
|
+
SELECT has_column_privilege(table_row.relowner, table_row.oid, 'recording_id', 'SELECT') AND
|
|
366
|
+
has_column_privilege(table_row.relowner, table_row.oid, 'recording_id', 'UPDATE')
|
|
367
|
+
FROM pg_class table_row WHERE table_row.oid = 'public.recording_idempotency'::regclass
|
|
368
|
+
), false) AS owner_can_tombstone_recording_idempotency,
|
|
369
|
+
COALESCE((
|
|
370
|
+
SELECT has_column_privilege(table_row.relowner, table_row.oid, 'recording_id', 'SELECT') AND
|
|
371
|
+
has_table_privilege(table_row.relowner, table_row.oid, 'DELETE')
|
|
372
|
+
FROM pg_class table_row WHERE table_row.oid = 'public.recording_tags'::regclass
|
|
373
|
+
), false) AS owner_can_delete_recording_tags,
|
|
368
374
|
EXISTS (
|
|
369
375
|
SELECT 1
|
|
370
376
|
FROM pg_class table_row
|
|
@@ -451,6 +457,12 @@ async function assertCloudSchemaReady(pg2) {
|
|
|
451
457
|
if (excessive.length > 0) {
|
|
452
458
|
throw new Error(`cloud database role has surplus table privileges beyond runtime use: ${excessive.join(", ")}`);
|
|
453
459
|
}
|
|
460
|
+
if (!row?.owner_can_tombstone_recording_idempotency) {
|
|
461
|
+
throw new Error("cloud database recording_idempotency owner lacks SELECT/UPDATE privileges required for recording deletion");
|
|
462
|
+
}
|
|
463
|
+
if (!row?.owner_can_delete_recording_tags) {
|
|
464
|
+
throw new Error("cloud database recording_tags owner lacks SELECT/DELETE privileges required for recording deletion");
|
|
465
|
+
}
|
|
454
466
|
}
|
|
455
467
|
async function pingCloudConnectivity(pg2) {
|
|
456
468
|
const res = await pg2.get("SELECT 1 as ok");
|
|
@@ -488,6 +500,9 @@ var init_cloud_readiness = __esm(() => {
|
|
|
488
500
|
id: ["text", false],
|
|
489
501
|
audio_path: ["text", true],
|
|
490
502
|
raw_text: ["text", false],
|
|
503
|
+
audio_object_key: ["text", true],
|
|
504
|
+
audio_sha256: ["text", true],
|
|
505
|
+
audio_bytes: ["bigint", true],
|
|
491
506
|
processed_text: ["text", true],
|
|
492
507
|
processing_mode: ["text", false],
|
|
493
508
|
model_used: ["text", false],
|
|
@@ -694,16 +709,14 @@ var init_pg_migrations = __esm(() => {
|
|
|
694
709
|
ALTER COLUMN recording_id DROP NOT NULL;
|
|
695
710
|
ALTER TABLE recording_idempotency
|
|
696
711
|
ADD CONSTRAINT recording_idempotency_recording_id_fkey
|
|
697
|
-
FOREIGN KEY (recording_id) REFERENCES recordings(id) ON DELETE SET NULL
|
|
712
|
+
FOREIGN KEY (recording_id) REFERENCES recordings(id) ON DELETE SET NULL`,
|
|
713
|
+
`ALTER TABLE recordings ADD COLUMN IF NOT EXISTS audio_object_key TEXT`,
|
|
714
|
+
`ALTER TABLE recordings ADD COLUMN IF NOT EXISTS audio_sha256 TEXT`,
|
|
715
|
+
`ALTER TABLE recordings ADD COLUMN IF NOT EXISTS audio_bytes BIGINT`
|
|
698
716
|
];
|
|
699
717
|
});
|
|
700
718
|
|
|
701
719
|
// src/server/migrate-command.ts
|
|
702
|
-
var exports_migrate_command = {};
|
|
703
|
-
__export(exports_migrate_command, {
|
|
704
|
-
runCloudMigration: () => runCloudMigration,
|
|
705
|
-
applyRecordedCloudMigrations: () => applyRecordedCloudMigrations
|
|
706
|
-
});
|
|
707
720
|
async function applyRecordedCloudMigrations(pg2, ensureApiKeySchema) {
|
|
708
721
|
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
722
|
const applied = await pg2.all("SELECT version FROM _pg_migrations ORDER BY version");
|
|
@@ -720,10 +733,12 @@ async function applyRecordedCloudMigrations(pg2, ensureApiKeySchema) {
|
|
|
720
733
|
}
|
|
721
734
|
await ensureApiKeySchema();
|
|
722
735
|
}
|
|
723
|
-
async function runCloudMigration(steps) {
|
|
736
|
+
async function runCloudMigration(steps, runtimePostureValidate) {
|
|
724
737
|
await steps.pingConnectivity();
|
|
725
738
|
await steps.applyMigrations();
|
|
726
739
|
await steps.validateContract();
|
|
740
|
+
if (runtimePostureValidate)
|
|
741
|
+
await runtimePostureValidate();
|
|
727
742
|
}
|
|
728
743
|
var init_migrate_command = __esm(() => {
|
|
729
744
|
init_pg_migrations();
|
|
@@ -732,7 +747,9 @@ var init_migrate_command = __esm(() => {
|
|
|
732
747
|
// src/server/cloud.ts
|
|
733
748
|
var exports_cloud = {};
|
|
734
749
|
__export(exports_cloud, {
|
|
750
|
+
runRecordedMigration: () => runRecordedMigration,
|
|
735
751
|
resolveSigningSecret: () => resolveSigningSecret,
|
|
752
|
+
resolveMigrationDatabaseUrl: () => resolveMigrationDatabaseUrl,
|
|
736
753
|
resolveDatabaseUrl: () => resolveDatabaseUrl,
|
|
737
754
|
resolveDataBackend: () => resolveDataBackend,
|
|
738
755
|
requireSigningSecret: () => requireSigningSecret,
|
|
@@ -740,6 +757,7 @@ __export(exports_cloud, {
|
|
|
740
757
|
pingCloud: () => pingCloud,
|
|
741
758
|
migrateCloudSchema: () => migrateCloudSchema,
|
|
742
759
|
isPostgresBackendEnabled: () => isPostgresBackendEnabled,
|
|
760
|
+
getMigrationPg: () => getMigrationPg,
|
|
743
761
|
getCloudVerifier: () => getCloudVerifier,
|
|
744
762
|
getCloudPg: () => getCloudPg,
|
|
745
763
|
getApiKeyStore: () => getApiKeyStore,
|
|
@@ -759,8 +777,7 @@ function getCloudPg() {
|
|
|
759
777
|
cachedPg = new PgAdapterAsync(url);
|
|
760
778
|
return cachedPg;
|
|
761
779
|
}
|
|
762
|
-
function authClient() {
|
|
763
|
-
const pg2 = getCloudPg();
|
|
780
|
+
function authClient(pg2 = getCloudPg()) {
|
|
764
781
|
return {
|
|
765
782
|
async many(sql, params = []) {
|
|
766
783
|
return await pg2.all(sql, ...params);
|
|
@@ -796,8 +813,30 @@ function getCloudVerifier() {
|
|
|
796
813
|
});
|
|
797
814
|
return cachedVerifier;
|
|
798
815
|
}
|
|
799
|
-
|
|
800
|
-
|
|
816
|
+
function getMigrationPg() {
|
|
817
|
+
const url = resolveMigrationDatabaseUrl();
|
|
818
|
+
if (!url) {
|
|
819
|
+
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).");
|
|
820
|
+
}
|
|
821
|
+
return new PgAdapterAsync(url);
|
|
822
|
+
}
|
|
823
|
+
async function migrateCloudSchema(pg2 = getCloudPg()) {
|
|
824
|
+
await applyRecordedCloudMigrations(pg2, () => new ApiKeyStore(authClient(pg2)).ensureSchema());
|
|
825
|
+
}
|
|
826
|
+
async function runRecordedMigration() {
|
|
827
|
+
const migrationPg = getMigrationPg();
|
|
828
|
+
const runtimeUrl = resolveDatabaseUrl();
|
|
829
|
+
const migrationUrl = resolveMigrationDatabaseUrl();
|
|
830
|
+
const separateRoles = runtimeUrl !== undefined && migrationUrl !== undefined && runtimeUrl !== migrationUrl;
|
|
831
|
+
await runCloudMigration({
|
|
832
|
+
pingConnectivity: () => pingCloudConnectivity2(migrationPg),
|
|
833
|
+
applyMigrations: async () => {
|
|
834
|
+
await migrateCloudSchema(migrationPg);
|
|
835
|
+
},
|
|
836
|
+
validateContract: () => assertCloudSchemaContract(migrationPg)
|
|
837
|
+
}, separateRoles ? async () => {
|
|
838
|
+
await assertCloudSchemaReady(getCloudPg());
|
|
839
|
+
} : undefined);
|
|
801
840
|
}
|
|
802
841
|
async function ensureCloudSchema() {
|
|
803
842
|
if (schemaEnsured)
|
|
@@ -942,6 +981,9 @@ function parseRecording(row) {
|
|
|
942
981
|
return {
|
|
943
982
|
id: row["id"],
|
|
944
983
|
audio_path: row["audio_path"] || null,
|
|
984
|
+
audio_object_key: row["audio_object_key"] || null,
|
|
985
|
+
audio_sha256: row["audio_sha256"] || null,
|
|
986
|
+
audio_bytes: Number(row["audio_bytes"] ?? 0) || null,
|
|
945
987
|
raw_text: row["raw_text"],
|
|
946
988
|
processed_text: row["processed_text"] || null,
|
|
947
989
|
processing_mode: row["processing_mode"] || "raw",
|
|
@@ -1031,9 +1073,9 @@ async function createRecording(pg2, input, idempotencyKey, idempotencyContext) {
|
|
|
1031
1073
|
throw new ProjectNotFoundError(input.project_id);
|
|
1032
1074
|
resolvedProjectId = project.id;
|
|
1033
1075
|
}
|
|
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 || {}));
|
|
1076
|
+
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)
|
|
1077
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1078
|
+
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
1079
|
if (insertResult.changes === 0) {
|
|
1038
1080
|
throw new IdempotencyConflictError;
|
|
1039
1081
|
}
|
|
@@ -1454,6 +1496,9 @@ function buildV1OpenApiDocument(version = VERSION) {
|
|
|
1454
1496
|
id: { type: "string" },
|
|
1455
1497
|
raw_text: { type: "string" },
|
|
1456
1498
|
audio_path: { type: "string" },
|
|
1499
|
+
audio_object_key: { type: "string" },
|
|
1500
|
+
audio_sha256: { type: "string" },
|
|
1501
|
+
audio_bytes: { type: "number" },
|
|
1457
1502
|
processed_text: { type: "string" },
|
|
1458
1503
|
processing_mode: { type: "string", enum: ["raw", "enhanced"] },
|
|
1459
1504
|
model_used: { type: "string" },
|
|
@@ -1742,6 +1787,9 @@ var init_openapi = __esm(() => {
|
|
|
1742
1787
|
properties: {
|
|
1743
1788
|
id: { type: "string" },
|
|
1744
1789
|
audio_path: { type: "string", nullable: true },
|
|
1790
|
+
audio_object_key: { type: "string", nullable: true },
|
|
1791
|
+
audio_sha256: { type: "string", nullable: true },
|
|
1792
|
+
audio_bytes: { type: "number", nullable: true },
|
|
1745
1793
|
raw_text: { type: "string" },
|
|
1746
1794
|
processed_text: { type: "string", nullable: true },
|
|
1747
1795
|
processing_mode: { type: "string", enum: ["raw", "enhanced"] },
|
|
@@ -1786,6 +1834,172 @@ var init_openapi = __esm(() => {
|
|
|
1786
1834
|
};
|
|
1787
1835
|
});
|
|
1788
1836
|
|
|
1837
|
+
// src/server/client-ip.ts
|
|
1838
|
+
function normalizeIpLiteral(raw) {
|
|
1839
|
+
let value = (raw ?? "").trim();
|
|
1840
|
+
if (!value)
|
|
1841
|
+
return null;
|
|
1842
|
+
const bracketed = /^\[([^\]]+)\](?::\d+)?$/.exec(value);
|
|
1843
|
+
if (bracketed)
|
|
1844
|
+
value = bracketed[1];
|
|
1845
|
+
else if (/^\d{1,3}(?:\.\d{1,3}){3}:\d+$/.test(value))
|
|
1846
|
+
value = value.slice(0, value.lastIndexOf(":"));
|
|
1847
|
+
value = value.trim().toLowerCase();
|
|
1848
|
+
if (!value || value.length > 45)
|
|
1849
|
+
return null;
|
|
1850
|
+
const mapped = /^::ffff:(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/.exec(value);
|
|
1851
|
+
if (mapped)
|
|
1852
|
+
value = mapped[1];
|
|
1853
|
+
const ipv4 = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/.exec(value);
|
|
1854
|
+
if (ipv4) {
|
|
1855
|
+
return ipv4.slice(1).every((octet) => Number(octet) <= 255 && !/^0\d/.test(octet)) ? value : null;
|
|
1856
|
+
}
|
|
1857
|
+
if (/^[0-9a-f:]+(?:\.\d{1,3}){0,3}$/.test(value) && value.includes(":") && !/:::/.test(value)) {
|
|
1858
|
+
return value;
|
|
1859
|
+
}
|
|
1860
|
+
return null;
|
|
1861
|
+
}
|
|
1862
|
+
function parseForwardedFor(header) {
|
|
1863
|
+
if (!header)
|
|
1864
|
+
return [];
|
|
1865
|
+
return header.split(",").map((entry) => normalizeIpLiteral(entry)).filter((entry) => entry !== null);
|
|
1866
|
+
}
|
|
1867
|
+
function parseCidr(entry) {
|
|
1868
|
+
const slash = entry.indexOf("/");
|
|
1869
|
+
const rawAddress = slash >= 0 ? entry.slice(0, slash) : entry;
|
|
1870
|
+
const rawPrefix = slash >= 0 ? entry.slice(slash + 1) : null;
|
|
1871
|
+
const address = normalizeIpLiteral(rawAddress);
|
|
1872
|
+
if (!address)
|
|
1873
|
+
return null;
|
|
1874
|
+
if (address.includes(":")) {
|
|
1875
|
+
if (rawPrefix !== null && !/^\d{1,3}$/.test(rawPrefix))
|
|
1876
|
+
return null;
|
|
1877
|
+
const prefix2 = rawPrefix === null ? 128 : Number(rawPrefix);
|
|
1878
|
+
if (prefix2 < 0 || prefix2 > 128)
|
|
1879
|
+
return null;
|
|
1880
|
+
const bytes2 = expandV6(address);
|
|
1881
|
+
if (!bytes2)
|
|
1882
|
+
return null;
|
|
1883
|
+
const masked2 = maskNetwork(bytes2, prefix2);
|
|
1884
|
+
return { address, prefix: prefix2, bytes: masked2, family: "ipv6" };
|
|
1885
|
+
}
|
|
1886
|
+
if (rawPrefix !== null && !/^\d{1,3}$/.test(rawPrefix))
|
|
1887
|
+
return null;
|
|
1888
|
+
const prefix = rawPrefix === null ? 32 : Number(rawPrefix);
|
|
1889
|
+
if (prefix < 0 || prefix > 32)
|
|
1890
|
+
return null;
|
|
1891
|
+
const bytes = address.split(".").map((octet) => Number(octet));
|
|
1892
|
+
const masked = maskNetwork(bytes, prefix);
|
|
1893
|
+
return { address, prefix, bytes: masked, family: "ipv4" };
|
|
1894
|
+
}
|
|
1895
|
+
function expandV6(address) {
|
|
1896
|
+
const compressed = address.split("::");
|
|
1897
|
+
if (compressed.length > 2)
|
|
1898
|
+
return null;
|
|
1899
|
+
let groups = [];
|
|
1900
|
+
if (compressed.length === 2) {
|
|
1901
|
+
const left = compressed[0] ? compressed[0].split(":") : [];
|
|
1902
|
+
const right = compressed[1] ? compressed[1].split(":") : [];
|
|
1903
|
+
const missing = 8 - left.length - right.length;
|
|
1904
|
+
if (missing < 1)
|
|
1905
|
+
return null;
|
|
1906
|
+
groups = [...left, ...Array(missing).fill("0"), ...right];
|
|
1907
|
+
} else {
|
|
1908
|
+
groups = address.split(":");
|
|
1909
|
+
}
|
|
1910
|
+
if (groups.length !== 8)
|
|
1911
|
+
return null;
|
|
1912
|
+
const bytes = [];
|
|
1913
|
+
for (const group of groups) {
|
|
1914
|
+
if (!/^[0-9a-f]{1,4}$/.test(group))
|
|
1915
|
+
return null;
|
|
1916
|
+
const value = Number.parseInt(group, 16);
|
|
1917
|
+
bytes.push(value >> 8 & 255, value & 255);
|
|
1918
|
+
}
|
|
1919
|
+
return bytes;
|
|
1920
|
+
}
|
|
1921
|
+
function maskNetwork(bytes, prefix) {
|
|
1922
|
+
const masked = bytes.slice();
|
|
1923
|
+
const fullBytes = Math.floor(prefix / 8);
|
|
1924
|
+
const remainder = prefix % 8;
|
|
1925
|
+
for (let i = 0;i < masked.length; i++) {
|
|
1926
|
+
if (i >= fullBytes) {
|
|
1927
|
+
if (i === fullBytes && remainder > 0) {
|
|
1928
|
+
masked[i] = masked[i] & (255 << 8 - remainder & 255);
|
|
1929
|
+
} else {
|
|
1930
|
+
masked[i] = 0;
|
|
1931
|
+
}
|
|
1932
|
+
}
|
|
1933
|
+
}
|
|
1934
|
+
return masked;
|
|
1935
|
+
}
|
|
1936
|
+
function parseTrustedProxies(raw) {
|
|
1937
|
+
if (!raw)
|
|
1938
|
+
return [];
|
|
1939
|
+
const seen = new Set;
|
|
1940
|
+
const trusted = [];
|
|
1941
|
+
for (const entry of raw.split(",").map((part) => part.trim()).filter(Boolean)) {
|
|
1942
|
+
const cidr = parseCidr(entry);
|
|
1943
|
+
if (cidr && !seen.has(entry)) {
|
|
1944
|
+
seen.add(entry);
|
|
1945
|
+
trusted.push(entry);
|
|
1946
|
+
}
|
|
1947
|
+
}
|
|
1948
|
+
return trusted;
|
|
1949
|
+
}
|
|
1950
|
+
function isMatch(candidateBytes, cidr) {
|
|
1951
|
+
if (cidr.bytes.length !== candidateBytes.length)
|
|
1952
|
+
return false;
|
|
1953
|
+
const masked = maskNetwork(candidateBytes, cidr.prefix);
|
|
1954
|
+
for (let i = 0;i < cidr.bytes.length; i++) {
|
|
1955
|
+
if (cidr.bytes[i] !== masked[i])
|
|
1956
|
+
return false;
|
|
1957
|
+
}
|
|
1958
|
+
return true;
|
|
1959
|
+
}
|
|
1960
|
+
function isTrustedAddress(ip, trusted) {
|
|
1961
|
+
const address = normalizeIpLiteral(ip);
|
|
1962
|
+
if (!address)
|
|
1963
|
+
return false;
|
|
1964
|
+
const cidrs = trusted.map(parseCidr).filter((cidr) => cidr !== null);
|
|
1965
|
+
for (const cidr of cidrs) {
|
|
1966
|
+
if (cidr.address === address)
|
|
1967
|
+
return true;
|
|
1968
|
+
const bytes = cidr.family === "ipv4" ? address.split(".").map((octet) => Number(octet)) : expandV6(address);
|
|
1969
|
+
if (bytes && isMatch(bytes, cidr))
|
|
1970
|
+
return true;
|
|
1971
|
+
}
|
|
1972
|
+
return false;
|
|
1973
|
+
}
|
|
1974
|
+
function resolveTrustProxy(env = process.env) {
|
|
1975
|
+
return TRUST_TRUE_VALUES.has((env[TRUST_PROXY_ENV] ?? "").trim().toLowerCase());
|
|
1976
|
+
}
|
|
1977
|
+
function trustedProxiesFromEnv(env = process.env) {
|
|
1978
|
+
return parseTrustedProxies(env[TRUSTED_PROXIES_ENV]);
|
|
1979
|
+
}
|
|
1980
|
+
function resolveRequestClientIp(input) {
|
|
1981
|
+
const socket = normalizeIpLiteral(input.socketAddress);
|
|
1982
|
+
if (input.trustProxy !== true)
|
|
1983
|
+
return socket;
|
|
1984
|
+
const trusted = input.trustedProxies ?? [];
|
|
1985
|
+
const real = normalizeIpLiteral(input.headers.get("x-real-ip"));
|
|
1986
|
+
if (real)
|
|
1987
|
+
return real;
|
|
1988
|
+
const chain = parseForwardedFor(input.headers.get("x-forwarded-for"));
|
|
1989
|
+
for (let i = chain.length - 1;i >= 0; i--) {
|
|
1990
|
+
const hop = chain[i];
|
|
1991
|
+
if (!isTrustedAddress(hop, trusted))
|
|
1992
|
+
return hop;
|
|
1993
|
+
}
|
|
1994
|
+
if (chain.length > 0)
|
|
1995
|
+
return chain[0];
|
|
1996
|
+
return socket;
|
|
1997
|
+
}
|
|
1998
|
+
var TRUST_PROXY_ENV = "RECORDINGS_TRUST_PROXY", TRUSTED_PROXIES_ENV = "RECORDINGS_TRUSTED_PROXIES", TRUST_TRUE_VALUES;
|
|
1999
|
+
var init_client_ip = __esm(() => {
|
|
2000
|
+
TRUST_TRUE_VALUES = new Set(["1", "true", "yes", "on"]);
|
|
2001
|
+
});
|
|
2002
|
+
|
|
1789
2003
|
// src/mcp/http.ts
|
|
1790
2004
|
var exports_http = {};
|
|
1791
2005
|
__export(exports_http, {
|
|
@@ -5888,32 +6102,6 @@ function assertMaintenanceAbsent(env) {
|
|
|
5888
6102
|
if (pathExists(installMaintenanceMarkerPath(env)))
|
|
5889
6103
|
throw maintenanceUnavailable();
|
|
5890
6104
|
}
|
|
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
6105
|
function removeActiveLease(activeLease, releasedLease) {
|
|
5918
6106
|
try {
|
|
5919
6107
|
renameSync(activeLease, releasedLease);
|
|
@@ -5999,6 +6187,9 @@ function loadConfig(configPath) {
|
|
|
5999
6187
|
if (process.env.RECORDINGS_API_KEY) {
|
|
6000
6188
|
config.openai_api_key = process.env.RECORDINGS_API_KEY;
|
|
6001
6189
|
}
|
|
6190
|
+
if (process.env.RECORDINGS_OPENAI_API_KEY) {
|
|
6191
|
+
config.openai_api_key = process.env.RECORDINGS_OPENAI_API_KEY;
|
|
6192
|
+
}
|
|
6002
6193
|
if (process.env.RECORDINGS_ENHANCEMENT_KEY) {
|
|
6003
6194
|
config.enhancement_api_key = process.env.RECORDINGS_ENHANCEMENT_KEY;
|
|
6004
6195
|
}
|
|
@@ -6042,14 +6233,26 @@ function loadConfig(configPath) {
|
|
|
6042
6233
|
if (process.env.RECORDINGS_AUDIO_DIR) {
|
|
6043
6234
|
config.audio_dir = process.env.RECORDINGS_AUDIO_DIR;
|
|
6044
6235
|
}
|
|
6236
|
+
if (process.env.HASNA_RECORDINGS_S3_BUCKET) {
|
|
6237
|
+
config.s3_bucket = process.env.HASNA_RECORDINGS_S3_BUCKET;
|
|
6238
|
+
} else if (process.env.RECORDINGS_S3_BUCKET) {
|
|
6239
|
+
config.s3_bucket = process.env.RECORDINGS_S3_BUCKET;
|
|
6240
|
+
}
|
|
6241
|
+
if (process.env.HASNA_RECORDINGS_S3_PREFIX) {
|
|
6242
|
+
config.s3_prefix = process.env.HASNA_RECORDINGS_S3_PREFIX;
|
|
6243
|
+
} else if (process.env.RECORDINGS_S3_PREFIX) {
|
|
6244
|
+
config.s3_prefix = process.env.RECORDINGS_S3_PREFIX;
|
|
6245
|
+
}
|
|
6246
|
+
if (process.env.RECORDINGS_S3_REGION) {
|
|
6247
|
+
config.s3_region = process.env.RECORDINGS_S3_REGION;
|
|
6248
|
+
} else if (process.env.AWS_REGION) {
|
|
6249
|
+
config.s3_region = process.env.AWS_REGION;
|
|
6250
|
+
}
|
|
6045
6251
|
if (process.env.RECORDINGS_MAX_SECONDS) {
|
|
6046
6252
|
config.max_recording_seconds = parseInt(process.env.RECORDINGS_MAX_SECONDS, 10);
|
|
6047
6253
|
}
|
|
6048
|
-
if (!config.openai_api_key) {
|
|
6049
|
-
config.openai_api_key = loadSecretKey("OPENAI_API_KEY");
|
|
6050
|
-
}
|
|
6051
6254
|
if (!config.enhancement_api_key) {
|
|
6052
|
-
config.enhancement_api_key = config.openai_api_key
|
|
6255
|
+
config.enhancement_api_key = config.openai_api_key;
|
|
6053
6256
|
}
|
|
6054
6257
|
if (!explicitTranscriberModel) {
|
|
6055
6258
|
config.transcriber_model = config.enhancement_model;
|
|
@@ -6229,66 +6432,9 @@ function mergeDirectoryContents(sourceDir, targetDir) {
|
|
|
6229
6432
|
}
|
|
6230
6433
|
}
|
|
6231
6434
|
}
|
|
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
6435
|
function getHomeDir() {
|
|
6278
6436
|
return process.env["HOME"] || process.env["USERPROFILE"] || homedir2();
|
|
6279
6437
|
}
|
|
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
6438
|
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
6439
|
var init_config = __esm(() => {
|
|
6294
6440
|
init_install_maintenance();
|
|
@@ -6330,6 +6476,9 @@ var init_config = __esm(() => {
|
|
|
6330
6476
|
db_path: "",
|
|
6331
6477
|
audio_dir: "",
|
|
6332
6478
|
max_recording_seconds: 1800,
|
|
6479
|
+
s3_bucket: "",
|
|
6480
|
+
s3_prefix: "",
|
|
6481
|
+
s3_region: "",
|
|
6333
6482
|
config_warnings: []
|
|
6334
6483
|
};
|
|
6335
6484
|
});
|
|
@@ -6421,6 +6570,9 @@ function repairSchemaDrift(db) {
|
|
|
6421
6570
|
ensureColumn(db, "recordings", "task_list_id", "TEXT");
|
|
6422
6571
|
ensureColumn(db, "recordings", "machine_id", "TEXT");
|
|
6423
6572
|
ensureColumn(db, "recordings", "metadata", "TEXT DEFAULT '{}'");
|
|
6573
|
+
ensureColumn(db, "recordings", "audio_object_key", "TEXT");
|
|
6574
|
+
ensureColumn(db, "recordings", "audio_sha256", "TEXT");
|
|
6575
|
+
ensureColumn(db, "recordings", "audio_bytes", "INTEGER");
|
|
6424
6576
|
ensureColumn(db, "agents", "active_project_id", "TEXT REFERENCES projects(id) ON DELETE SET NULL");
|
|
6425
6577
|
}
|
|
6426
6578
|
function ensureColumn(db, table, column, definition) {
|
|
@@ -6514,6 +6666,11 @@ var init_database = __esm(() => {
|
|
|
6514
6666
|
`,
|
|
6515
6667
|
`
|
|
6516
6668
|
ALTER TABLE agents ADD COLUMN active_project_id TEXT REFERENCES projects(id) ON DELETE SET NULL;
|
|
6669
|
+
`,
|
|
6670
|
+
`
|
|
6671
|
+
ALTER TABLE recordings ADD COLUMN audio_object_key TEXT;
|
|
6672
|
+
ALTER TABLE recordings ADD COLUMN audio_sha256 TEXT;
|
|
6673
|
+
ALTER TABLE recordings ADD COLUMN audio_bytes INTEGER;
|
|
6517
6674
|
`
|
|
6518
6675
|
];
|
|
6519
6676
|
CURRENT_MIGRATION_LEVEL = MIGRATIONS.length - 1;
|
|
@@ -6524,6 +6681,9 @@ function parseRow(row) {
|
|
|
6524
6681
|
return {
|
|
6525
6682
|
id: row["id"],
|
|
6526
6683
|
audio_path: row["audio_path"] || null,
|
|
6684
|
+
audio_object_key: row["audio_object_key"] || null,
|
|
6685
|
+
audio_sha256: row["audio_sha256"] || null,
|
|
6686
|
+
audio_bytes: Number(row["audio_bytes"] ?? 0) || null,
|
|
6527
6687
|
raw_text: row["raw_text"],
|
|
6528
6688
|
processed_text: row["processed_text"] || null,
|
|
6529
6689
|
processing_mode: row["processing_mode"] || "raw",
|
|
@@ -6555,9 +6715,9 @@ function createRecording2(input, db, idempotencyKey) {
|
|
|
6555
6715
|
}
|
|
6556
6716
|
const tagsJson = JSON.stringify(input.tags || []);
|
|
6557
6717
|
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);
|
|
6718
|
+
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)
|
|
6719
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
6720
|
+
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
6721
|
if (insertResult.changes === 0) {
|
|
6562
6722
|
const existing = getRecording2(id, d);
|
|
6563
6723
|
if (existing?.id === id)
|
|
@@ -6574,6 +6734,13 @@ function createRecording2(input, db, idempotencyKey) {
|
|
|
6574
6734
|
});
|
|
6575
6735
|
return create();
|
|
6576
6736
|
}
|
|
6737
|
+
function updateRecordingAudio(id, audioObjectKey, audioSha256, audioBytes, db) {
|
|
6738
|
+
const d = db || getDatabase();
|
|
6739
|
+
const result = d.query(`UPDATE recordings SET audio_object_key = ?, audio_sha256 = ?, audio_bytes = ? WHERE id = ?`).run(audioObjectKey, audioSha256, audioBytes, id);
|
|
6740
|
+
if (result.changes === 0)
|
|
6741
|
+
return null;
|
|
6742
|
+
return getRecording2(id, d);
|
|
6743
|
+
}
|
|
6577
6744
|
function getRecording2(id, db) {
|
|
6578
6745
|
const d = db || getDatabase();
|
|
6579
6746
|
let row = d.query("SELECT * FROM recordings WHERE id = ?").get(id);
|
|
@@ -6795,151 +6962,266 @@ var init_feedback = __esm(() => {
|
|
|
6795
6962
|
init_version();
|
|
6796
6963
|
});
|
|
6797
6964
|
|
|
6965
|
+
// src/lib/local-opt-in.ts
|
|
6966
|
+
import {
|
|
6967
|
+
clientTransportEnvKeys,
|
|
6968
|
+
credentialOverrideEnvKey,
|
|
6969
|
+
credentialPointerEnvKey,
|
|
6970
|
+
CREDENTIAL_PROFILE_ENV_KEY
|
|
6971
|
+
} from "@hasna/contracts/client";
|
|
6972
|
+
function isRecordingsLocalOptIn(env = process.env) {
|
|
6973
|
+
return RECORDINGS_LOCAL_OPT_IN_ENV_KEYS.some((key) => (env[key] ?? "").trim() !== "");
|
|
6974
|
+
}
|
|
6975
|
+
function recordingsAuthorityEnvKeys() {
|
|
6976
|
+
const keys = clientTransportEnvKeys("recordings");
|
|
6977
|
+
return [
|
|
6978
|
+
...keys.apiUrlKeys,
|
|
6979
|
+
...keys.apiKeyKeys,
|
|
6980
|
+
credentialOverrideEnvKey("recordings"),
|
|
6981
|
+
credentialPointerEnvKey("recordings"),
|
|
6982
|
+
CREDENTIAL_PROFILE_ENV_KEY
|
|
6983
|
+
].filter((key) => !RECORDINGS_CARVED_LEGACY_ENV_KEYS.includes(key));
|
|
6984
|
+
}
|
|
6985
|
+
function hasRecordingsEnvAuthorityIntent(env = process.env) {
|
|
6986
|
+
return recordingsAuthorityEnvKeys().some((key) => (env[key] ?? "").trim() !== "");
|
|
6987
|
+
}
|
|
6988
|
+
function selectsRecordingsLocalStore(env = process.env) {
|
|
6989
|
+
return !hasRecordingsEnvAuthorityIntent(env) && isRecordingsLocalOptIn(env);
|
|
6990
|
+
}
|
|
6991
|
+
function recordingsResolverEnv(env) {
|
|
6992
|
+
const carved = RECORDINGS_CARVED_LEGACY_ENV_KEYS.filter((key) => (key in env));
|
|
6993
|
+
const blanks = recordingsAuthorityEnvKeys().filter((key) => (key in env) && (env[key] ?? "").trim() === "");
|
|
6994
|
+
const removed = new Set([...carved, ...blanks]);
|
|
6995
|
+
if (removed.size === 0)
|
|
6996
|
+
return env;
|
|
6997
|
+
const next = { ...env };
|
|
6998
|
+
for (const key of removed)
|
|
6999
|
+
delete next[key];
|
|
7000
|
+
return next;
|
|
7001
|
+
}
|
|
7002
|
+
function isAmbientRecordingsEnv(env) {
|
|
7003
|
+
if (typeof process !== "undefined" && env === process.env) {
|
|
7004
|
+
return true;
|
|
7005
|
+
}
|
|
7006
|
+
return env[CONTRACTS_AMBIENT_ENVIRONMENT] === true;
|
|
7007
|
+
}
|
|
7008
|
+
function recordingsResolverInputs(env, credentials = {}) {
|
|
7009
|
+
const normalised = recordingsResolverEnv(env);
|
|
7010
|
+
if (normalised === env)
|
|
7011
|
+
return { env: normalised, credentials };
|
|
7012
|
+
const keychain = { ...credentials.keychain };
|
|
7013
|
+
if (keychain.enabled === undefined && keychain.run === undefined) {
|
|
7014
|
+
keychain.enabled = isAmbientRecordingsEnv(env);
|
|
7015
|
+
}
|
|
7016
|
+
return { env: normalised, credentials: { ...credentials, keychain } };
|
|
7017
|
+
}
|
|
7018
|
+
var RECORDINGS_LOCAL_OPT_IN_ENV_KEYS, RECORDINGS_CARVED_LEGACY_ENV_KEYS, CONTRACTS_AMBIENT_ENVIRONMENT;
|
|
7019
|
+
var init_local_opt_in = __esm(() => {
|
|
7020
|
+
RECORDINGS_LOCAL_OPT_IN_ENV_KEYS = [
|
|
7021
|
+
"HASNA_RECORDINGS_LOCAL",
|
|
7022
|
+
"RECORDINGS_LOCAL"
|
|
7023
|
+
];
|
|
7024
|
+
RECORDINGS_CARVED_LEGACY_ENV_KEYS = [
|
|
7025
|
+
"RECORDINGS_API_URL",
|
|
7026
|
+
"RECORDINGS_API_KEY"
|
|
7027
|
+
];
|
|
7028
|
+
CONTRACTS_AMBIENT_ENVIRONMENT = Symbol.for("hasna:contracts:ambientClientEnvironment");
|
|
7029
|
+
});
|
|
7030
|
+
|
|
6798
7031
|
// src/http/client.ts
|
|
6799
|
-
import { createClientTransport } from "@hasna/contracts/client";
|
|
6800
7032
|
import {
|
|
6801
|
-
|
|
7033
|
+
clientTransportEnvKeys as clientTransportEnvKeys2,
|
|
7034
|
+
resolveClientTransport
|
|
7035
|
+
} from "@hasna/contracts/client";
|
|
7036
|
+
import {
|
|
6802
7037
|
resolveStorageClient
|
|
6803
7038
|
} 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 };
|
|
7039
|
+
function throwRecordingsAuthorityFailure(error2) {
|
|
7040
|
+
const message = error2 instanceof Error ? error2.message : String(error2);
|
|
7041
|
+
const name = error2 instanceof Error ? error2.name : "";
|
|
7042
|
+
if (name === "CredentialResolutionError" || name === "CredentialFileUnsafeError") {
|
|
7043
|
+
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
7044
|
}
|
|
6829
|
-
|
|
7045
|
+
if (/no API key could be resolved/.test(message)) {
|
|
7046
|
+
if (/is not set and no API key could be resolved/.test(message)) {
|
|
7047
|
+
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 });
|
|
7048
|
+
}
|
|
7049
|
+
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 });
|
|
7050
|
+
}
|
|
7051
|
+
throw new Error(`REMOTE_API_URL_INVALID: ${message} local SQLite fallback is disabled`, { cause: error2 });
|
|
6830
7052
|
}
|
|
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
|
-
};
|
|
7053
|
+
function resolveRecordingsCloudClient(env = process.env, options = {}) {
|
|
7054
|
+
if (selectsRecordingsLocalStore(env))
|
|
7055
|
+
return null;
|
|
7056
|
+
const resolverInputs = recordingsResolverInputs(env, options.credentials);
|
|
7057
|
+
let resolved;
|
|
7058
|
+
try {
|
|
7059
|
+
resolved = resolveStorageClient("recordings", resolverInputs.env, {
|
|
7060
|
+
fetchImpl: (input, init) => globalThis.fetch(input, { ...init, redirect: "manual" }),
|
|
7061
|
+
credentials: resolverInputs.credentials
|
|
7062
|
+
});
|
|
7063
|
+
} catch (error2) {
|
|
7064
|
+
throwRecordingsAuthorityFailure(error2);
|
|
6869
7065
|
}
|
|
6870
|
-
|
|
6871
|
-
|
|
7066
|
+
const client = resolved.client;
|
|
7067
|
+
return client;
|
|
7068
|
+
}
|
|
7069
|
+
var RETRY_STATUSES, IDEMPOTENT;
|
|
7070
|
+
var init_client = __esm(() => {
|
|
7071
|
+
init_local_opt_in();
|
|
7072
|
+
RETRY_STATUSES = new Set([408, 425, 429, 500, 502, 503, 504]);
|
|
7073
|
+
IDEMPOTENT = new Set(["GET", "HEAD", "PUT", "DELETE", "OPTIONS"]);
|
|
7074
|
+
});
|
|
7075
|
+
|
|
7076
|
+
// src/lib/audio-artifact-storage.ts
|
|
7077
|
+
import { GetObjectCommand, PutObjectCommand, S3Client as AwsS3Client } from "@aws-sdk/client-s3";
|
|
7078
|
+
import { createHash as createHash2 } from "crypto";
|
|
7079
|
+
import { readFile } from "fs/promises";
|
|
7080
|
+
import { extname } from "path";
|
|
7081
|
+
function contentTypeForExtension(extension) {
|
|
7082
|
+
return AUDIO_CONTENT_TYPES[extension.toLowerCase()] ?? "application/octet-stream";
|
|
7083
|
+
}
|
|
7084
|
+
|
|
7085
|
+
class AudioArtifactStorage {
|
|
7086
|
+
bucket;
|
|
7087
|
+
prefix;
|
|
7088
|
+
s3;
|
|
7089
|
+
constructor(options = {}) {
|
|
7090
|
+
this.bucket = options.bucket;
|
|
7091
|
+
this.prefix = (options.prefix || DEFAULT_S3_PREFIX).replace(/^\/+|\/+$/g, "");
|
|
7092
|
+
this.s3 = this.bucket ? options.client ?? new AwsS3Client({ region: options.region || process.env.AWS_REGION || "us-east-1" }) : undefined;
|
|
7093
|
+
}
|
|
7094
|
+
get usesS3() {
|
|
7095
|
+
return Boolean(this.bucket);
|
|
7096
|
+
}
|
|
7097
|
+
objectKeyFor(recordingId, sha256, extension) {
|
|
7098
|
+
assertSha256(sha256);
|
|
7099
|
+
const safeExtension = extension.toLowerCase().match(/^[a-z0-9]+/)?.[0] || "bin";
|
|
7100
|
+
return `${this.prefix}/${recordingId}/${sha256}.${safeExtension}`;
|
|
7101
|
+
}
|
|
7102
|
+
async uploadAudio(recordingId, audioPath) {
|
|
7103
|
+
if (!this.bucket || !this.s3)
|
|
7104
|
+
return null;
|
|
7105
|
+
const bytes = await readFileBytes(audioPath);
|
|
7106
|
+
if (!bytes)
|
|
7107
|
+
return null;
|
|
7108
|
+
const sha256 = sha256Hex(bytes);
|
|
7109
|
+
const extension = extensionOf(audioPath);
|
|
7110
|
+
const key = this.objectKeyFor(recordingId, sha256, extension);
|
|
7111
|
+
await this.s3.send(new PutObjectCommand({
|
|
7112
|
+
Bucket: this.bucket,
|
|
7113
|
+
Key: key,
|
|
7114
|
+
Body: bytes,
|
|
7115
|
+
ContentType: contentTypeForExtension(extension)
|
|
7116
|
+
}));
|
|
7117
|
+
return { objectKey: key, sha256, bytes: bytes.byteLength };
|
|
7118
|
+
}
|
|
7119
|
+
async readAudio(objectKey) {
|
|
7120
|
+
if (!this.bucket || !this.s3)
|
|
7121
|
+
return null;
|
|
7122
|
+
try {
|
|
7123
|
+
const response = await this.s3.send(new GetObjectCommand({ Bucket: this.bucket, Key: objectKey }));
|
|
7124
|
+
return await bodyToBytes(response.Body);
|
|
7125
|
+
} catch {
|
|
7126
|
+
return null;
|
|
7127
|
+
}
|
|
6872
7128
|
}
|
|
6873
|
-
|
|
6874
|
-
|
|
6875
|
-
|
|
6876
|
-
|
|
6877
|
-
|
|
6878
|
-
|
|
6879
|
-
|
|
6880
|
-
|
|
6881
|
-
|
|
6882
|
-
|
|
7129
|
+
}
|
|
7130
|
+
function resolveAudioArtifactStorage(env = process.env, client) {
|
|
7131
|
+
const bucket = env.HASNA_RECORDINGS_S3_BUCKET || env.RECORDINGS_S3_BUCKET || undefined;
|
|
7132
|
+
const prefix = env.HASNA_RECORDINGS_S3_PREFIX || env.RECORDINGS_S3_PREFIX || undefined;
|
|
7133
|
+
const region = env.RECORDINGS_S3_REGION || env.AWS_REGION || undefined;
|
|
7134
|
+
return new AudioArtifactStorage({ bucket, prefix, region, client });
|
|
7135
|
+
}
|
|
7136
|
+
async function uploadAudioAtCreation(recordingId, audioPath, storage = resolveAudioArtifactStorage()) {
|
|
7137
|
+
if (!audioPath)
|
|
7138
|
+
return null;
|
|
7139
|
+
if (!storage.usesS3)
|
|
7140
|
+
return null;
|
|
7141
|
+
try {
|
|
7142
|
+
const uploaded = await storage.uploadAudio(recordingId, audioPath);
|
|
7143
|
+
if (!uploaded)
|
|
7144
|
+
return null;
|
|
7145
|
+
console.warn(`recordings: uploaded audio for ${recordingId} to ${uploaded.objectKey}`);
|
|
7146
|
+
return uploaded;
|
|
7147
|
+
} catch (error2) {
|
|
7148
|
+
console.warn(`recordings: audio upload failed for ${recordingId} (${String(error2 instanceof Error ? error2.message : error2)}); keeping local audio_path only`);
|
|
7149
|
+
return null;
|
|
6883
7150
|
}
|
|
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
|
-
};
|
|
7151
|
+
}
|
|
7152
|
+
function assertSha256(value) {
|
|
7153
|
+
if (!SHA256_HEX.test(value)) {
|
|
7154
|
+
throw new Error("audio digest must be a lowercase hex sha-256; refusing to build a storage key from it");
|
|
6894
7155
|
}
|
|
6895
|
-
|
|
6896
|
-
|
|
7156
|
+
}
|
|
7157
|
+
function sha256Hex(bytes) {
|
|
7158
|
+
const hash = createHash2("sha256");
|
|
7159
|
+
hash.update(bytes);
|
|
7160
|
+
return hash.digest("hex");
|
|
7161
|
+
}
|
|
7162
|
+
async function readFileBytes(path) {
|
|
6897
7163
|
try {
|
|
6898
|
-
|
|
6899
|
-
} catch
|
|
6900
|
-
|
|
6901
|
-
return { transport: "sqlite", requested, modeSource, baseUrl: null, apiKeyPresent: true, misconfigured: true, warning: `Invalid API URL: ${message}.` };
|
|
7164
|
+
return new Uint8Array(await readFile(path));
|
|
7165
|
+
} catch {
|
|
7166
|
+
return null;
|
|
6902
7167
|
}
|
|
6903
|
-
return { transport: "http", requested, modeSource, baseUrl, apiKeyPresent: true, misconfigured: false, warning: null };
|
|
6904
7168
|
}
|
|
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.`);
|
|
7169
|
+
function extensionOf(path) {
|
|
7170
|
+
const extension = extname(path).replace(/^\./, "").toLowerCase();
|
|
7171
|
+
return extension || "bin";
|
|
7172
|
+
}
|
|
7173
|
+
async function bodyToBytes(body) {
|
|
7174
|
+
if (!body)
|
|
7175
|
+
return new Uint8Array(0);
|
|
7176
|
+
if (body instanceof Uint8Array)
|
|
7177
|
+
return body;
|
|
7178
|
+
if (typeof body.transformToByteArray === "function") {
|
|
7179
|
+
return new Uint8Array(await body.transformToByteArray());
|
|
6925
7180
|
}
|
|
6926
|
-
if (
|
|
6927
|
-
|
|
7181
|
+
if (typeof body.getReader === "function") {
|
|
7182
|
+
const reader = body.getReader();
|
|
7183
|
+
const chunks2 = [];
|
|
7184
|
+
for (;; ) {
|
|
7185
|
+
const { value, done } = await reader.read();
|
|
7186
|
+
if (done)
|
|
7187
|
+
break;
|
|
7188
|
+
if (value)
|
|
7189
|
+
chunks2.push(value);
|
|
7190
|
+
}
|
|
7191
|
+
return concat(chunks2);
|
|
6928
7192
|
}
|
|
6929
|
-
const
|
|
6930
|
-
|
|
6931
|
-
|
|
7193
|
+
const chunks = [];
|
|
7194
|
+
for await (const chunk of body) {
|
|
7195
|
+
chunks.push(typeof chunk === "string" ? new TextEncoder().encode(chunk) : chunk);
|
|
6932
7196
|
}
|
|
6933
|
-
return
|
|
7197
|
+
return concat(chunks);
|
|
6934
7198
|
}
|
|
6935
|
-
|
|
6936
|
-
|
|
6937
|
-
|
|
6938
|
-
|
|
7199
|
+
function concat(chunks) {
|
|
7200
|
+
const total = chunks.reduce((sum, chunk) => sum + chunk.byteLength, 0);
|
|
7201
|
+
const merged = new Uint8Array(new ArrayBuffer(total));
|
|
7202
|
+
let offset = 0;
|
|
7203
|
+
for (const chunk of chunks) {
|
|
7204
|
+
merged.set(chunk, offset);
|
|
7205
|
+
offset += chunk.byteLength;
|
|
7206
|
+
}
|
|
7207
|
+
return merged;
|
|
7208
|
+
}
|
|
7209
|
+
var SHA256_HEX, DEFAULT_S3_PREFIX = "recordings", AUDIO_CONTENT_TYPES;
|
|
7210
|
+
var init_audio_artifact_storage = __esm(() => {
|
|
7211
|
+
SHA256_HEX = /^[0-9a-f]{64}$/;
|
|
7212
|
+
AUDIO_CONTENT_TYPES = {
|
|
7213
|
+
wav: "audio/wav",
|
|
7214
|
+
mp3: "audio/mpeg",
|
|
7215
|
+
m4a: "audio/mp4",
|
|
7216
|
+
aac: "audio/aac",
|
|
7217
|
+
webm: "audio/webm",
|
|
7218
|
+
ogg: "audio/ogg",
|
|
7219
|
+
flac: "audio/flac"
|
|
7220
|
+
};
|
|
6939
7221
|
});
|
|
6940
7222
|
|
|
6941
7223
|
// src/store.ts
|
|
6942
|
-
import { createHash as
|
|
7224
|
+
import { createHash as createHash3, randomUUID as randomUUID2 } from "crypto";
|
|
6943
7225
|
function listQuery(filter) {
|
|
6944
7226
|
if (!filter)
|
|
6945
7227
|
return {};
|
|
@@ -7111,7 +7393,7 @@ function apiStore(client) {
|
|
|
7111
7393
|
};
|
|
7112
7394
|
}
|
|
7113
7395
|
function recordingPageFingerprint(items) {
|
|
7114
|
-
const hash =
|
|
7396
|
+
const hash = createHash3("sha256");
|
|
7115
7397
|
const ids = items.map((item) => String(item.id ?? "")).sort();
|
|
7116
7398
|
for (const id of ids) {
|
|
7117
7399
|
hash.update(String(id.length));
|
|
@@ -7146,16 +7428,19 @@ async function countStoreRecordings(store, filter) {
|
|
|
7146
7428
|
}
|
|
7147
7429
|
throw new Error(`Legacy Store exceeded ${maxPageRequests} pages while counting recordings`);
|
|
7148
7430
|
}
|
|
7149
|
-
function
|
|
7150
|
-
|
|
7431
|
+
function localArtifactStorageFor() {
|
|
7432
|
+
return localArtifactStorageOverride ?? resolveAudioArtifactStorage();
|
|
7433
|
+
}
|
|
7434
|
+
function getStore(env = process.env, options = {}) {
|
|
7435
|
+
if (env === process.env && cached && options.credentials === undefined)
|
|
7151
7436
|
return cached;
|
|
7152
|
-
const
|
|
7153
|
-
const store =
|
|
7154
|
-
if (env === process.env)
|
|
7437
|
+
const client = resolveRecordingsCloudClient(env, options);
|
|
7438
|
+
const store = client ? apiStore(client) : localStore;
|
|
7439
|
+
if (env === process.env && options.credentials === undefined)
|
|
7155
7440
|
cached = store;
|
|
7156
7441
|
return store;
|
|
7157
7442
|
}
|
|
7158
|
-
var
|
|
7443
|
+
var localStore, cached = null, localArtifactStorageOverride;
|
|
7159
7444
|
var init_store = __esm(() => {
|
|
7160
7445
|
init_recordings();
|
|
7161
7446
|
init_agents();
|
|
@@ -7164,11 +7449,20 @@ var init_store = __esm(() => {
|
|
|
7164
7449
|
init_client();
|
|
7165
7450
|
init_recording_create_identity();
|
|
7166
7451
|
init_install_maintenance();
|
|
7452
|
+
init_audio_artifact_storage();
|
|
7167
7453
|
localStore = {
|
|
7168
7454
|
mode: "sqlite",
|
|
7169
7455
|
baseUrl: null,
|
|
7170
7456
|
async createRecording(input, idempotencyKey) {
|
|
7171
|
-
return withLocalStoreReaderLease(() =>
|
|
7457
|
+
return withLocalStoreReaderLease(async () => {
|
|
7458
|
+
const recording = createRecording2(input, undefined, idempotencyKey);
|
|
7459
|
+
if (!recording.audio_path)
|
|
7460
|
+
return recording;
|
|
7461
|
+
const uploaded = await uploadAudioAtCreation(recording.id, recording.audio_path, localArtifactStorageFor());
|
|
7462
|
+
if (!uploaded)
|
|
7463
|
+
return recording;
|
|
7464
|
+
return updateRecordingAudio(recording.id, uploaded.objectKey, uploaded.sha256, uploaded.bytes) ?? recording;
|
|
7465
|
+
});
|
|
7172
7466
|
},
|
|
7173
7467
|
async getRecording(id) {
|
|
7174
7468
|
return withLocalStoreReaderLease(() => getRecording2(id));
|
|
@@ -7240,7 +7534,7 @@ import OpenAI from "openai";
|
|
|
7240
7534
|
import { createReadStream } from "fs";
|
|
7241
7535
|
function getClient(config) {
|
|
7242
7536
|
if (!config.openai_api_key) {
|
|
7243
|
-
throw new TranscriptionError("OpenAI API key not configured. Set OPENAI_API_KEY env var
|
|
7537
|
+
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
7538
|
}
|
|
7245
7539
|
if (_client && _clientApiKey === config.openai_api_key)
|
|
7246
7540
|
return _client;
|
|
@@ -8153,8 +8447,9 @@ var init_mcp = __esm(async () => {
|
|
|
8153
8447
|
init_enhancer();
|
|
8154
8448
|
init_version();
|
|
8155
8449
|
init_machine();
|
|
8450
|
+
init_local_opt_in();
|
|
8451
|
+
init_client();
|
|
8156
8452
|
config = loadConfig();
|
|
8157
|
-
ensureDataDir(config);
|
|
8158
8453
|
if (false) {}
|
|
8159
8454
|
});
|
|
8160
8455
|
|
|
@@ -8172,13 +8467,12 @@ function jsonResponse(data, status = 200, headers) {
|
|
|
8172
8467
|
});
|
|
8173
8468
|
}
|
|
8174
8469
|
function resolveClientIp(req, server) {
|
|
8175
|
-
|
|
8176
|
-
|
|
8177
|
-
|
|
8178
|
-
|
|
8179
|
-
|
|
8180
|
-
}
|
|
8181
|
-
return server.requestIP(req)?.address || "unknown";
|
|
8470
|
+
return resolveRequestClientIp({
|
|
8471
|
+
headers: req.headers,
|
|
8472
|
+
socketAddress: server.requestIP(req)?.address,
|
|
8473
|
+
trustProxy: resolveTrustProxy(),
|
|
8474
|
+
trustedProxies: trustedProxiesFromEnv()
|
|
8475
|
+
}) ?? "unknown";
|
|
8182
8476
|
}
|
|
8183
8477
|
function checkRateLimit(ip) {
|
|
8184
8478
|
const now = Date.now();
|
|
@@ -8297,6 +8591,7 @@ var init_serve = __esm(() => {
|
|
|
8297
8591
|
init_version();
|
|
8298
8592
|
init_v1();
|
|
8299
8593
|
init_openapi();
|
|
8594
|
+
init_client_ip();
|
|
8300
8595
|
SECURITY_HEADERS = {
|
|
8301
8596
|
"X-Content-Type-Options": "nosniff",
|
|
8302
8597
|
"X-Frame-Options": "DENY",
|
|
@@ -8345,32 +8640,19 @@ Commands:
|
|
|
8345
8640
|
migrate Apply the PostgreSQL schema, then exit. Idempotent.
|
|
8346
8641
|
|
|
8347
8642
|
Environment:
|
|
8348
|
-
HASNA_RECORDINGS_DATABASE_URL
|
|
8349
|
-
|
|
8643
|
+
HASNA_RECORDINGS_DATABASE_URL PostgreSQL DSN (present -> postgresql backend, else sqlite)
|
|
8644
|
+
HASNA_RECORDINGS_MIGRATE_DATABASE_URL Optional MIGRATION-role DSN for migrate (two-role deploy;
|
|
8645
|
+
falls back to the runtime DSN when unset)
|
|
8646
|
+
HASNA_RECORDINGS_API_SIGNING_KEY HMAC signing secret for API-key auth`);
|
|
8350
8647
|
}
|
|
8351
8648
|
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)");
|
|
8649
|
+
const { closeCloud: closeCloud2, resolveMigrationDatabaseUrl: resolveMigrationDatabaseUrl2, runRecordedMigration: runRecordedMigration2 } = await Promise.resolve().then(() => (init_cloud(), exports_cloud));
|
|
8650
|
+
if (!resolveMigrationDatabaseUrl2()) {
|
|
8651
|
+
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
8652
|
process.exit(2);
|
|
8364
8653
|
}
|
|
8365
8654
|
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
|
-
});
|
|
8655
|
+
await runRecordedMigration2();
|
|
8374
8656
|
console.log("migrate: done");
|
|
8375
8657
|
await closeCloud2();
|
|
8376
8658
|
process.exit(0);
|