@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/README.md
CHANGED
|
@@ -13,26 +13,58 @@ npm install -g @hasna/recordings
|
|
|
13
13
|
|
|
14
14
|
## macOS App
|
|
15
15
|
|
|
16
|
-
Recordings
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
16
|
+
**Hasna Recordings** is a native macOS 26 app with a companion menu bar control.
|
|
17
|
+
The app opens a compact glass recorder with a microphone/stop/play control and timer.
|
|
18
|
+
The clock opens searchable history and the gear opens Settings inside the same retained app window. A shared title bar and back navigation keep the recorder, transcripts, and settings together. The recorder uses a compact 224 × 244 point layout; normal interface text uses macOS-sized 11–13 point type. Glass backgrounds use one live native blur per surface, with matching translucent controls.
|
|
19
|
+
A floating transcription bar shows live words, the microphone waveform, pause/resume,
|
|
20
|
+
playback, and an Auto-paste switch. Recent pastes show delivery evidence from this session.
|
|
21
|
+
There is no sidebar or project UI. New app recordings are unassigned; existing recordings and their metadata
|
|
22
|
+
are preserved.
|
|
23
|
+
|
|
24
|
+
- Click Record, or hold the global shortcut (F5 by default).
|
|
25
|
+
- Click the clock or press **⌘L** for recordings; select a row to read its transcript.
|
|
26
|
+
Audio playback is available when its file is retained on this Mac.
|
|
27
|
+
- Press **⇧⌘B** for the floating transcription bar, or choose **Keep bar visible** from
|
|
28
|
+
the menu bar’s More menu. Its play control opens Recent pastes.
|
|
29
|
+
- Pause/resume from the bar or the recorder’s context menu. Paused microphone samples
|
|
30
|
+
are excluded from the transcript and recording duration.
|
|
31
|
+
- Turn **Auto-paste** off to keep transcripts available without typing into another app.
|
|
32
|
+
Recordings still save through the configured API. Clearing Recent pastes only clears
|
|
33
|
+
this session’s delivery list; it does not delete recordings.
|
|
34
|
+
- Open **Settings** (⌘,), then **API & Advanced…** to configure the service connection,
|
|
35
|
+
transcription cleanup, shortcuts, and permissions. The input follows the macOS default
|
|
36
|
+
microphone; audio remains uncompressed 24 kHz PCM.
|
|
37
|
+
|
|
38
|
+
New installations default to dictation. Question and edit-command detection is
|
|
39
|
+
optional in Settings; enabling it can add a model request before delivery. For
|
|
40
|
+
verbatim dictation with the shortest delivery path, keep intent detection off and
|
|
41
|
+
set Transcription Cleanup to Raw (Off). Settled realtime text can paste while the API save
|
|
42
|
+
continues in the background. Capture shutdown runs off the UI thread, and the
|
|
43
|
+
recording panel avoids animated glass so rendering does not stall live transcription.
|
|
44
|
+
|
|
45
|
+
In **Settings → API & Advanced… → General → Recordings API**, enter your API URL and service key,
|
|
46
|
+
then choose **Save Connection** and **Test Connection**. There is no compiled-in API
|
|
47
|
+
hostname. Both a service prefix such as `https://api.example.com/recordings` and its
|
|
48
|
+
versioned form `https://api.example.com/recordings/v1/` work; requests append the
|
|
49
|
+
resource to exactly one `/v1`. The service key stays in macOS Keychain, scoped to the
|
|
50
|
+
normalized endpoint, and is separate from the OpenAI transcription key.
|
|
51
|
+
|
|
52
|
+
An explicitly configured launch environment takes precedence over saved connection
|
|
53
|
+
settings: `HASNA_RECORDINGS_API_URL` plus the existing Hasna credential chain, or
|
|
54
|
+
`HASNA_RECORDINGS_CLIENT_STORE=sqlite` for an intentional local store. The native app
|
|
55
|
+
passes the connection to its embedded CLI, so recording persistence, history, and
|
|
56
|
+
deletion use the same API client as the CLI and MCP. No local fallback is selected
|
|
57
|
+
when an API connection is missing or fails.
|
|
58
|
+
|
|
59
|
+
The bundle filename is **Hasna Recordings.app** for both full and menu bar builds.
|
|
60
|
+
Older unspaced bundles remain discoverable as legacy installations. The managed
|
|
61
|
+
updater's canonical path is part of its immutable cohort; an existing cohort bound
|
|
62
|
+
to the old filename requires managed reprovisioning before receiving bundles under
|
|
63
|
+
the new name. This change does not rename an already installed managed app.
|
|
32
64
|
|
|
33
65
|
The app embeds a same-version `recordings` CLI as its data layer, so the CLI, MCP, and app
|
|
34
66
|
share one store without depending on a possibly stale global CLI installation. Production
|
|
35
|
-
release installs use a one-time managed bootstrap at `/Applications/
|
|
67
|
+
release installs use a one-time managed bootstrap at `/Applications/Hasna Recordings.app`; later
|
|
36
68
|
release updates replace only that app through the installed root-owned broker.
|
|
37
69
|
|
|
38
70
|
```bash
|
|
@@ -62,7 +94,7 @@ recordings app status # show install state
|
|
|
62
94
|
recordings app snapshot # write ./desktop-snapshot.png for local UI debugging
|
|
63
95
|
# From this repository, optionally choosing another output path:
|
|
64
96
|
bun run desktop:snapshot -- /tmp/recordings-desktop.png
|
|
65
|
-
/Applications/
|
|
97
|
+
"/Applications/Hasna Recordings.app/Contents/Helpers/recordings-update-client" status
|
|
66
98
|
|
|
67
99
|
# Release builds run only as the isolated _recordingsbuild account. Provision these first:
|
|
68
100
|
# - /private/var/recordings-build owned by _recordingsbuild, mode 0700, beneath a
|
|
@@ -165,13 +197,13 @@ recordings app install \
|
|
|
165
197
|
swift test # run the native test suite
|
|
166
198
|
```
|
|
167
199
|
|
|
168
|
-
The production release location is `/Applications/
|
|
200
|
+
The production release location is `/Applications/Hasna Recordings.app`. The managed bootstrap installs
|
|
169
201
|
one signed/notarized PKG exactly once, including the root broker, no-login verifier, launchd policy,
|
|
170
202
|
release key, and initial app. That root cohort is intentionally immutable:
|
|
171
203
|
`lifecycle=bootstrap-v1-app-updates-only`,
|
|
172
204
|
`root_maintenance_supported=false`, and `key_rotation_supported=false`. Subsequent release
|
|
173
205
|
envelopes must bind the exact installed broker/verifier cohort, protocol version, and pinned key
|
|
174
|
-
epoch, and may replace only `/Applications/
|
|
206
|
+
epoch, and may replace only `/Applications/Hasna Recordings.app`. A second bootstrap PKG, a broker or
|
|
175
207
|
verifier mismatch, a broker-protocol incompatibility, or a key-epoch change fails before app
|
|
176
208
|
activation with `unsupported_lifecycle`. Root updater maintenance and release-key rotation require a
|
|
177
209
|
separate managed reprovisioning lifecycle; the current tooling does not run Installer or overwrite
|
|
@@ -227,7 +259,7 @@ private, fsynced root journal and anti-rollback state recover interrupted app re
|
|
|
227
259
|
next install attempt.
|
|
228
260
|
|
|
229
261
|
The explicit `local-only` development path remains separate and installs
|
|
230
|
-
`~/Applications/
|
|
262
|
+
`~/Applications/Hasna Recordings.app`; it never provisions or imitates the production root cohort.
|
|
231
263
|
For a station-specific deployment, pass `--expected-hostname` so the installer proves the live
|
|
232
264
|
short hostname before taking a lock or mutating state while the release artifact itself remains
|
|
233
265
|
fleet-distributable. Obtain `AUTHENTICATED_MANIFEST_SHA256` from independently authenticated
|
|
@@ -285,7 +317,7 @@ The app's **Transcription Cleanup** setting controls the same post-processing pi
|
|
|
285
317
|
the CLI and MCP server. Use **Raw** to keep verbatim text only, **Auto** to clean up only
|
|
286
318
|
when trigger phrases or instruction patterns are detected, or **Always** to run the
|
|
287
319
|
transcriber cleanup prompt for every recording. Global cleanup instructions can be set in
|
|
288
|
-
Settings
|
|
320
|
+
Settings. The native app applies only those global instructions.
|
|
289
321
|
|
|
290
322
|
The native app uses OpenAI realtime transcription for the stop-and-paste path: settled
|
|
291
323
|
`gpt-realtime-whisper` text is saved and pasted immediately, while full-file
|
|
@@ -440,15 +472,52 @@ directly with API-key auth via
|
|
|
440
472
|
[`@hasna/contracts`](https://www.npmjs.com/package/@hasna/contracts).
|
|
441
473
|
|
|
442
474
|
The CLI and MCP client have exactly two stores and never open Postgres — they
|
|
443
|
-
read the on-box `sqlite` file
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
475
|
+
read the on-box `sqlite` file only when the deliberate unhosted opt-in
|
|
476
|
+
`HASNA_RECORDINGS_LOCAL=1` (alias `RECORDINGS_LOCAL=1`) selects it, or they
|
|
477
|
+
call the server's `/v1` HTTP API. Which one it is is decided by the ONE
|
|
478
|
+
credential resolver in [`@hasna/contracts`](https://www.npmjs.com/package/@hasna/contracts)
|
|
479
|
+
(resolved fresh on every call):
|
|
480
|
+
|
|
481
|
+
| tier | source |
|
|
482
|
+
| ---- | ------ |
|
|
483
|
+
| 1 | explicit `--api-key` / `--profile`, or `HASNA_RECORDINGS_API_KEY_OVERRIDE` / `HASNA_PROFILE` / `HASNA_RECORDINGS_API_KEY_REF` |
|
|
484
|
+
| 2 | the macOS Keychain item `hasna.credentials.recordings.api-key` (account `HASNA_STATION`, else `hostname -s`, else `$USER`) |
|
|
485
|
+
| 3 | `~/.hasna/recordings/config/credentials` (owner-only 0400/0600; `HASNA_HOME` / `HASNA_CONFIG_HOME` move the root) |
|
|
486
|
+
| 4 | `HASNA_RECORDINGS_API_KEY` — a legitimate tier, no deprecation notice |
|
|
487
|
+
|
|
488
|
+
The authority follows the same ladder — `HASNA_RECORDINGS_API_URL`, the
|
|
489
|
+
Keychain `api-url` item, the credentials file — and otherwise DEFAULTS to the
|
|
490
|
+
fleet gateway `https://api.hasna.com/recordings`, so a key alone is a complete
|
|
491
|
+
configuration. Retired inputs are gone: nothing reads the old fleet-env or
|
|
492
|
+
cloud credential directories, `~/.config/hasna` or `$XDG_CONFIG_HOME`, and no
|
|
493
|
+
`*_MODE` / `*_STORAGE_MODE` / `*_CLIENT_STORE` switch exists (the old
|
|
494
|
+
`HASNA_RECORDINGS_CLIENT_STORE` variable selects nothing any more).
|
|
495
|
+
|
|
496
|
+
**Fail closed.** Hosted mode with no credential exits non-zero with one
|
|
497
|
+
`REMOTE_API_*` line naming every tier that was consulted; there is no SQLite
|
|
498
|
+
fallback and no local-fallback event. The on-box file is reachable ONLY
|
|
499
|
+
through the explicit opt-in above, and an opted-in run reads neither the
|
|
500
|
+
Keychain nor any credential file. `RECORDINGS_API_KEY` remains the OpenAI
|
|
501
|
+
transcription-key override only — it is carved out of the resolver
|
|
502
|
+
environment and never selects or fails client transport.
|
|
503
|
+
|
|
504
|
+
The same gate runs before anything else connects: `recordings-mcp` refuses to
|
|
505
|
+
answer `initialize` (exit 1, first stderr line `ERROR: REMOTE_API_…`) when no
|
|
506
|
+
credential resolves and no opt-in is set, and `recordings check` renders
|
|
507
|
+
`✗ Active store: none — fail-closed (REMOTE_API_…); <db path> is present but
|
|
508
|
+
NOT opened` (or `absent`) and exits non-zero instead of displaying the on-box
|
|
509
|
+
file as the live store. With the opt-in set, `recordings-mcp` prints one
|
|
510
|
+
`recordings: LOCAL mode` line on stderr before serving the on-box store.
|
|
511
|
+
|
|
512
|
+
Native Settings saves new OpenAI transcription keys in macOS Keychain under
|
|
513
|
+
service `hasna.credentials.openai.api-key`, account `openai/api_key`. Finder
|
|
514
|
+
launches read that entry and pass it to the embedded helper in memory. Use
|
|
515
|
+
`RECORDINGS_OPENAI_API_KEY` for an explicit provider-key override, separate
|
|
516
|
+
from the Hasna service credential. The CLI no longer walks
|
|
517
|
+
`~/.secrets/**/*.env` for the OpenAI key (retired in the #1720 validation
|
|
518
|
+
wave): the declared stores are the env vars above, the config file, and the
|
|
519
|
+
macOS Keychain entry.
|
|
520
|
+
|
|
452
521
|
|
|
453
522
|
```bash
|
|
454
523
|
recordings-serve --port 8874 # start the API
|
|
@@ -473,21 +542,70 @@ Env: `HASNA_RECORDINGS_DATABASE_URL` (PostgreSQL DSN — selects the
|
|
|
473
542
|
`postgresql` backend) and `HASNA_RECORDINGS_API_SIGNING_KEY` (HMAC signing
|
|
474
543
|
secret for API-key auth).
|
|
475
544
|
|
|
545
|
+
### Production two-role deploy contract
|
|
546
|
+
|
|
547
|
+
The ECS deploy uses TWO database roles and TWO DSNs (the `migrate` one-shot
|
|
548
|
+
resolves `HASNA_RECORDINGS_MIGRATE_DATABASE_URL` /
|
|
549
|
+
`RECORDINGS_MIGRATE_DATABASE_URL`, falling back to the runtime DSN):
|
|
550
|
+
|
|
551
|
+
- **Migration / owner role** (e.g. `recordings_owner`) — runs the one-shot
|
|
552
|
+
`migrate` task and OWNS the schema. The task definition's
|
|
553
|
+
`HASNA_RECORDINGS_MIGRATE_DATABASE_URL` secret must point at this role's
|
|
554
|
+
DSN; the ECS execution role needs `secretsmanager:GetSecretValue` on that
|
|
555
|
+
secret.
|
|
556
|
+
- **Runtime role** (e.g. `recordings_app`) — the `DATABASE_URL` the serve
|
|
557
|
+
process reads. It must be STRICTLY DML-only; `recordings-serve` `/ready`
|
|
558
|
+
(and the migrate verb, when a dedicated migration DSN is configured)
|
|
559
|
+
enforce the least-privilege posture contract: no table/sequence ownership,
|
|
560
|
+
no `CREATE` on any schema, no `TEMPORARY` on the database, and exactly
|
|
561
|
+
these grants on the `public` schema:
|
|
562
|
+
|
|
563
|
+
| table | grants |
|
|
564
|
+
| --- | --- |
|
|
565
|
+
| `recordings` | SELECT, INSERT, DELETE |
|
|
566
|
+
| `recording_tags` | SELECT, INSERT |
|
|
567
|
+
| `agents` | SELECT, INSERT, UPDATE |
|
|
568
|
+
| `projects` | SELECT, INSERT, UPDATE |
|
|
569
|
+
| `feedback` | INSERT |
|
|
570
|
+
| `api_keys` | SELECT |
|
|
571
|
+
| `recording_idempotency` | SELECT, INSERT |
|
|
572
|
+
|
|
573
|
+
plus `USAGE` on schema `public`. A runtime role that owns tables (for
|
|
574
|
+
example because the migrate task ran DDL with the runtime DSN) makes
|
|
575
|
+
`/ready` return `503 {"error":"dependency unavailable"}` — the database is
|
|
576
|
+
healthy; the role posture is not. Remediate by re-owning the tables and
|
|
577
|
+
sequences to the owner role and granting the DML set above.
|
|
578
|
+
|
|
579
|
+
The table owner must also retain the privileges used by PostgreSQL's foreign
|
|
580
|
+
key cleanup: SELECT/UPDATE on `recording_idempotency.recording_id` and
|
|
581
|
+
SELECT/DELETE on `recording_tags`. Revoking the owner's DML privileges can
|
|
582
|
+
make recording deletion fail even when the runtime grants are correct.
|
|
583
|
+
Readiness checks both owner cleanup permissions. Restore these grants to the
|
|
584
|
+
affected table's owner; the runtime role remains SELECT/INSERT on both tables.
|
|
585
|
+
|
|
476
586
|
## SDK
|
|
477
587
|
|
|
478
588
|
The typed `/v1` client is generated from the serve OpenAPI document
|
|
479
|
-
(`bun run generate:sdk`)
|
|
589
|
+
(`bun run generate:sdk`). The resolver-backed factory resolves the credential
|
|
590
|
+
and authority through the same `@hasna/contracts` chain as the CLI, fresh on
|
|
591
|
+
every request, so a key rotation heals a client held open for hours:
|
|
480
592
|
|
|
481
593
|
```ts
|
|
482
|
-
import {
|
|
594
|
+
import { createRecordingsV1Client } from "@hasna/recordings/sdk";
|
|
483
595
|
|
|
484
|
-
const client =
|
|
485
|
-
baseUrl: process.env.HASNA_RECORDINGS_API_URL!,
|
|
486
|
-
apiKey: process.env.HASNA_RECORDINGS_API_KEY!,
|
|
487
|
-
});
|
|
596
|
+
const client = createRecordingsV1Client(); // resolves through the chain
|
|
488
597
|
const { recordings } = await client.listRecordings({ limit: 20 });
|
|
489
598
|
```
|
|
490
599
|
|
|
600
|
+
An explicit `baseUrl` pins the authority: with no `apiKey` beside it the
|
|
601
|
+
client sends NO credential at all (the ambient chain is never consulted), and
|
|
602
|
+
with one it sends exactly that key, every request. The unhosted local
|
|
603
|
+
`recordings-serve` (`http://localhost:8874`) is reachable only under
|
|
604
|
+
`HASNA_RECORDINGS_LOCAL=1` and prints one "LOCAL mode" line on stderr; every
|
|
605
|
+
other refusal throws `RECORDINGS_CREDENTIAL_MISSING`. The raw generated
|
|
606
|
+
constructor `new RecordingsV1Client({ baseUrl, apiKey })` still works for
|
|
607
|
+
explicit configurations.
|
|
608
|
+
|
|
491
609
|
Useful agent tools include `recordings_status` for safe service/config diagnostics,
|
|
492
610
|
`transcribe_audio`, `save_recording`, `list_recordings`, `search_recordings`,
|
|
493
611
|
`register_agent`, `heartbeat`, and `set_focus`.
|
|
@@ -504,17 +622,18 @@ only when post-processing actually produced enhanced output.
|
|
|
504
622
|
|
|
505
623
|
## Releasing
|
|
506
624
|
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
`src/native/Recordings/RecordingsLib/Info.plist
|
|
625
|
+
Apply the applicable Changesets in a release worktree first. `package.json` is
|
|
626
|
+
the version authority; `src/version.ts` derives its value at runtime. Synchronize
|
|
627
|
+
both native bundle version fields in `src/native/Recordings/RecordingsLib/Info.plist`
|
|
628
|
+
and regenerate the SDK after the Changesets bump:
|
|
510
629
|
|
|
511
630
|
```bash
|
|
512
|
-
bun run version:set 0.
|
|
631
|
+
bun run version:set 0.4.0 # use the version selected by Changesets
|
|
513
632
|
bun run generate:sdk # restamps the generated SDK's header
|
|
514
633
|
bun run version:check # exits 1 if any site disagrees with package.json
|
|
515
634
|
```
|
|
516
635
|
|
|
517
|
-
|
|
636
|
+
The SDK version stamp is **generated**, not written: `src/server/openapi.ts` stamps `VERSION` into the
|
|
518
637
|
OpenAPI document and `bun run generate:sdk` bakes it into the `// Source: …` header of
|
|
519
638
|
`src/sdk/v1.generated.ts`. `version:set` leaves that file alone — the generator owns it, and
|
|
520
639
|
patching the stamp by hand would hide real regeneration drift — so regenerate after every bump.
|
|
@@ -532,7 +651,7 @@ suite on every push through the turbo `build-test` job (this app's `test` script
|
|
|
532
651
|
what makes the two guards below actually block a branch rather than wait for someone to run
|
|
533
652
|
them locally. `prepack` runs `build:native-fs-guard` first (the
|
|
534
653
|
fail-closed macOS gate), then `version:check`, so a partial bump stops before the build
|
|
535
|
-
rather than deep inside it. And `prepublishOnly` runs
|
|
654
|
+
rather than deep inside it. And `prepublishOnly` runs the complete gated release suite, which covers the sites
|
|
536
655
|
through `src/__tests__/native-bundle-version.test.ts` and
|
|
537
656
|
`src/__tests__/version-site-guard.test.ts`.
|
|
538
657
|
|
|
@@ -546,6 +665,123 @@ bundle is only as verified as the last macOS build.
|
|
|
546
665
|
|
|
547
666
|
Data is stored in `~/.hasna/recordings/`.
|
|
548
667
|
|
|
668
|
+
## Audio artifact upload (S3 via the artifact kit)
|
|
669
|
+
|
|
670
|
+
On `recordings record` / `recordings transcribe`, the audio is uploaded at
|
|
671
|
+
creation as a content-addressed object and the row records
|
|
672
|
+
`audio_object_key`, `audio_sha256` and `audio_bytes` (the local `audio_path`
|
|
673
|
+
stays as provenance). Without configuration, nothing changes: audio remains
|
|
674
|
+
local-only.
|
|
675
|
+
|
|
676
|
+
Environment:
|
|
677
|
+
|
|
678
|
+
- `HASNA_RECORDINGS_S3_BUCKET` (fallback `RECORDINGS_S3_BUCKET`) — the bucket
|
|
679
|
+
to upload into; unset/empty keeps the historical local-only behaviour.
|
|
680
|
+
- `HASNA_RECORDINGS_S3_PREFIX` (fallback `RECORDINGS_S3_PREFIX`) — object-key
|
|
681
|
+
prefix inside the bucket; defaults to `recordings`, so keys look like
|
|
682
|
+
`recordings/<recording_id>/<sha256>.<ext>`.
|
|
683
|
+
- `RECORDINGS_S3_REGION` (fallback `AWS_REGION`, then `us-east-1`) — the
|
|
684
|
+
region for the S3 client. Credentials are never read by the app: the AWS SDK
|
|
685
|
+
resolves them from the ambient environment or instance role at send time.
|
|
686
|
+
|
|
687
|
+
Upload failures never lose a recording: the row is still created and the
|
|
688
|
+
warning is logged, matching the fail-soft contract of the app-side fix.
|
|
689
|
+
|
|
549
690
|
## License
|
|
550
691
|
|
|
551
692
|
Apache-2.0 -- see [LICENSE](LICENSE)
|
|
693
|
+
|
|
694
|
+
## Reusing the native recorder
|
|
695
|
+
|
|
696
|
+
The Swift package at `src/native/Recordings` exports the `RecordingsLib` library.
|
|
697
|
+
A separate macOS application can depend on that package and reuse `RecordingEngine`
|
|
698
|
+
for real PCM capture, pause, meters, recording state, clipboard and verified paste:
|
|
699
|
+
|
|
700
|
+
```swift
|
|
701
|
+
import RecordingsLib
|
|
702
|
+
|
|
703
|
+
let configuration = try RecordingEngineConfiguration(
|
|
704
|
+
isolatedHomePath: candidateStateHome.path,
|
|
705
|
+
preferencesSuiteName: "com.example.recorder.preferences"
|
|
706
|
+
)
|
|
707
|
+
let engine = RecordingEngine(configuration: configuration, transcriptionProvider: provider)
|
|
708
|
+
```
|
|
709
|
+
|
|
710
|
+
The isolated initializer never installs global shortcuts or fn monitors and does
|
|
711
|
+
not read the legacy provider credentials, service URL, launch environment or CLI.
|
|
712
|
+
It rejects the installed recorder's home and preferences suite. Its audio and logs
|
|
713
|
+
live below `<isolatedHomePath>/.hasna/recordings`; preferences use only the named
|
|
714
|
+
suite. The original `RecordingEngine(homePath:installsGlobalHandlers:)` initializer
|
|
715
|
+
retains the legacy application's behavior.
|
|
716
|
+
|
|
717
|
+
Implement `RecordingTranscriptionProvider.makeSession(configuration:onPartialTranscript:)`
|
|
718
|
+
to create one session per recording. Construction must return promptly. Its
|
|
719
|
+
`appendPCM` method receives ordered 24 kHz mono PCM16LE chunks during recording;
|
|
720
|
+
paused samples are excluded. Publish partial callbacks as the complete current
|
|
721
|
+
transcript. `finish` receives the finalized WAV URL, duration, language and capture
|
|
722
|
+
ID, and returns `RecordingProviderResult`. `cancel` must safely interrupt a pending
|
|
723
|
+
connection or finish; any in-flight append must be ignored after cancellation.
|
|
724
|
+
Providers own their bounded networking queues and credential namespace. A local
|
|
725
|
+
file recognizer may ignore streaming chunks and recognize the WAV at finish.
|
|
726
|
+
|
|
727
|
+
Completed results appear in `recentTranscriptions` with `captureID` and `audioURL`
|
|
728
|
+
before automatic paste starts, allowing the consumer to persist them asynchronously.
|
|
729
|
+
This publication is not a storage acknowledgement; `persistedRecordingRevision`
|
|
730
|
+
continues to describe only legacy CLI persistence. Call `cancelRecording()` to
|
|
731
|
+
discard capture or cancel an isolated provider that is still finalizing.
|
|
732
|
+
|
|
733
|
+
Completed paste transactions publish `RecentPaste.captureID` and the typed
|
|
734
|
+
`deliveryStatus` (`confirmed`, `unconfirmed`, or `notDelivered`). A posted
|
|
735
|
+
keystroke is never a confirmed delivery. Pass a `captureID` to
|
|
736
|
+
`pasteIntoFrontApp` when manually pasting a saved recording. Early permission
|
|
737
|
+
or target failures remain visible in the engine status without a completed
|
|
738
|
+
transaction receipt.
|
|
739
|
+
|
|
740
|
+
|
|
741
|
+
### Native dependency receipts
|
|
742
|
+
|
|
743
|
+
An npm archive includes the complete Swift package at `src/native/Recordings`,
|
|
744
|
+
including `RecordingsLib`, its resources, updater targets, and resolved dependency
|
|
745
|
+
pins. An application can extract the verified archive into its dependency cache
|
|
746
|
+
and use that Swift package as a local SwiftPM dependency. This is consumption of
|
|
747
|
+
published package bytes; a development checkout alone is not release evidence.
|
|
748
|
+
|
|
749
|
+
Create a sidecar receipt for the exact reviewed npm archive after committing the
|
|
750
|
+
release metadata. The helper checks the shipped native bytes and package version
|
|
751
|
+
against the specified public Git revision. It does not extract into the working
|
|
752
|
+
app, access credentials, install software, or publish anything:
|
|
753
|
+
|
|
754
|
+
```sh
|
|
755
|
+
python3 scripts/native-core-receipt.py /path/to/recordings-0.4.0.tgz \
|
|
756
|
+
--source-revision <full-public-release-commit> \
|
|
757
|
+
--repository /path/to/hasna-apps \
|
|
758
|
+
--output /path/to/recordings-0.4.0.prepared.json
|
|
759
|
+
```
|
|
760
|
+
|
|
761
|
+
After the separately authorized npm publication, run the same command with
|
|
762
|
+
`--verify-registry` and a new output filename. That explicit read-only step checks
|
|
763
|
+
the version and integrity in public registry metadata, downloads the registry
|
|
764
|
+
archive, and requires byte equality with the reviewed archive. It never upgrades
|
|
765
|
+
a prepared receipt merely because a version exists. Existing receipt files are
|
|
766
|
+
never overwritten.
|
|
767
|
+
|
|
768
|
+
The external JSON format has `schemaVersion: 1` and
|
|
769
|
+
`kind: "hasna.recordings.native-core"`. It records:
|
|
770
|
+
|
|
771
|
+
- `package`: exact npm name and version.
|
|
772
|
+
- `source`: public repository URL and full verified Git revision.
|
|
773
|
+
- `distribution`: `status: "prepared"`, or `status: "published"` with registry
|
|
774
|
+
authority and verification time after the actual registry archive matches.
|
|
775
|
+
- `archive`: fixed npm URL, byte count, SHA-256, and npm SHA-512 integrity.
|
|
776
|
+
- `native`: package path, `RecordingsLib` product, Swift 6.2/macOS 26 requirements,
|
|
777
|
+
and each shipped native file's relative path, mode, byte count, and SHA-256.
|
|
778
|
+
`treeSHA256` hashes UTF-8 lines sorted by path, each formatted as
|
|
779
|
+
`path + NUL + octalMode + NUL + decimalBytes + NUL + sha256 + LF`.
|
|
780
|
+
|
|
781
|
+
A receipt is integrity evidence, not a signature or permission to release. A
|
|
782
|
+
consumer must pin the reviewed receipt digest through its own trusted release
|
|
783
|
+
configuration, require `distribution.status == "published"`, and compare the
|
|
784
|
+
archive and extracted source bytes before building. Co-delivered metadata must
|
|
785
|
+
not supply its own trusted expected digest. The receipt stays outside the npm
|
|
786
|
+
archive so its archive hash has no circular dependency. Notarization, signing,
|
|
787
|
+
and end-user acceptance of the consuming application remain separate checks.
|