@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.
Files changed (148) hide show
  1. package/README.md +279 -43
  2. package/bun.lock +37 -38
  3. package/contracts/v1/fixtures.json +1206 -0
  4. package/dist/cli/index.js +1909 -619
  5. package/dist/cli/macos-shortcut.d.ts +2 -2
  6. package/dist/contracts/hosted-v1.d.ts +105 -0
  7. package/dist/contracts/hosted-v1.d.ts.map +1 -0
  8. package/dist/contracts/hosted-v1.js +41 -0
  9. package/dist/contracts/stream-v1.d.ts +117 -0
  10. package/dist/contracts/stream-v1.d.ts.map +1 -0
  11. package/dist/contracts/stream-v1.js +35 -0
  12. package/dist/db/database.d.ts.map +1 -1
  13. package/dist/db/pg-migrations.d.ts.map +1 -1
  14. package/dist/db/recordings.d.ts +6 -0
  15. package/dist/db/recordings.d.ts.map +1 -1
  16. package/dist/hosted/index.d.ts +58 -0
  17. package/dist/hosted/index.d.ts.map +1 -0
  18. package/dist/hosted/index.js +266 -0
  19. package/dist/hosted/transport.d.ts +36 -0
  20. package/dist/hosted/transport.d.ts.map +1 -0
  21. package/dist/hosted-v1-aavn7ktb.js +4114 -0
  22. package/dist/hosted-v1-gdr9extc.js +84 -0
  23. package/dist/http/client.d.ts +183 -22
  24. package/dist/http/client.d.ts.map +1 -1
  25. package/dist/index.d.ts +3 -3
  26. package/dist/index.d.ts.map +1 -1
  27. package/dist/index.js +2879 -2288
  28. package/dist/lib/audio-artifact-storage.d.ts +98 -0
  29. package/dist/lib/audio-artifact-storage.d.ts.map +1 -0
  30. package/dist/lib/capture-probe.d.ts +3 -3
  31. package/dist/lib/capture-probe.d.ts.map +1 -1
  32. package/dist/lib/config.d.ts.map +1 -1
  33. package/dist/lib/local-opt-in.d.ts +132 -0
  34. package/dist/lib/local-opt-in.d.ts.map +1 -0
  35. package/dist/lib/macos-bundle.d.ts +1 -1
  36. package/dist/lib/persistence-probe.d.ts +56 -17
  37. package/dist/lib/persistence-probe.d.ts.map +1 -1
  38. package/dist/lib/transcriber.d.ts.map +1 -1
  39. package/dist/mcp/index.d.ts.map +1 -1
  40. package/dist/mcp/index.js +1713 -459
  41. package/dist/sdk/index.d.ts +13 -5
  42. package/dist/sdk/index.d.ts.map +1 -1
  43. package/dist/sdk/index.js +904 -1
  44. package/dist/sdk/resolve.d.ts +53 -0
  45. package/dist/sdk/resolve.d.ts.map +1 -0
  46. package/dist/sdk/v1.generated.d.ts +6 -0
  47. package/dist/sdk/v1.generated.d.ts.map +1 -1
  48. package/dist/server/client-ip.d.ts +39 -0
  49. package/dist/server/client-ip.d.ts.map +1 -0
  50. package/dist/server/cloud-config.d.ts +12 -0
  51. package/dist/server/cloud-config.d.ts.map +1 -1
  52. package/dist/server/cloud-readiness.d.ts.map +1 -1
  53. package/dist/server/cloud.d.ts +20 -2
  54. package/dist/server/cloud.d.ts.map +1 -1
  55. package/dist/server/index.js +590 -287
  56. package/dist/server/migrate-command.d.ts +1 -1
  57. package/dist/server/migrate-command.d.ts.map +1 -1
  58. package/dist/server/openapi.d.ts +21 -0
  59. package/dist/server/openapi.d.ts.map +1 -1
  60. package/dist/server/repo.d.ts.map +1 -1
  61. package/dist/server/serve.d.ts.map +1 -1
  62. package/dist/storage.d.ts +2 -2
  63. package/dist/storage.d.ts.map +1 -1
  64. package/dist/storage.js +1358 -874
  65. package/dist/store.d.ts +12 -3
  66. package/dist/store.d.ts.map +1 -1
  67. package/dist/types/index.d.ts +15 -0
  68. package/dist/types/index.d.ts.map +1 -1
  69. package/docs/hosted-sdk.md +71 -0
  70. package/docs/wire-contracts.md +24 -0
  71. package/package.json +33 -11
  72. package/packaging/macos/build_release_pkg.sh +3 -3
  73. package/packaging/macos/managed_bootstrap.sh +4 -4
  74. package/packaging/macos/scripts/postinstall +2 -2
  75. package/packaging/macos/scripts/preinstall +2 -2
  76. package/scripts/build_companion_cli.sh +7 -1
  77. package/scripts/ci-linux-suite.ts +23 -13
  78. package/scripts/install_macos_app.sh +27 -24
  79. package/scripts/macos_artifact.ts +60 -49
  80. package/scripts/migrate.ts +4 -20
  81. package/scripts/native/prebuilds/darwin-universal/recordings_fs_guard.node +0 -0
  82. package/scripts/native/recordings_fs_guard.c +36 -4
  83. package/scripts/native-core-receipt.py +171 -0
  84. package/scripts/native_fs_guard.ts +2 -0
  85. package/scripts/release-suite-gate.ts +227 -150
  86. package/scripts/resolve_tailscale_cli.sh +24 -3
  87. package/scripts/smoke_macos_app.sh +21 -21
  88. package/src/native/Recordings/App/ContentView.swift +53 -68
  89. package/src/native/Recordings/App/MenuBarStatusView.swift +171 -61
  90. package/src/native/Recordings/App/RecordWorkspaceView.swift +47 -449
  91. package/src/native/Recordings/App/RecorderSettingsView.swift +75 -0
  92. package/src/native/Recordings/App/RecordingDetailView.swift +3 -4
  93. package/src/native/Recordings/App/RecordingsApp.swift +185 -9
  94. package/src/native/Recordings/App/RecordingsListView.swift +60 -112
  95. package/src/native/Recordings/App/RecordingsStore.swift +124 -109
  96. package/src/native/Recordings/App/Theme.swift +73 -61
  97. package/src/native/Recordings/Package.swift +1 -0
  98. package/src/native/Recordings/RecordingsLib/BlockingOperation.swift +29 -0
  99. package/src/native/Recordings/RecordingsLib/CaptureMonitor.swift +65 -0
  100. package/src/native/Recordings/RecordingsLib/Info.plist +3 -3
  101. package/src/native/Recordings/RecordingsLib/OpenAIAPIKeyStore.swift +75 -21
  102. package/src/native/Recordings/RecordingsLib/ProjectStore.swift +4 -4
  103. package/src/native/Recordings/RecordingsLib/Recording.swift +14 -2
  104. package/src/native/Recordings/RecordingsLib/RecordingEngine.swift +539 -89
  105. package/src/native/Recordings/RecordingsLib/RecordingGlassBackground.swift +34 -0
  106. package/src/native/Recordings/RecordingsLib/RecordingPasteTarget.swift +114 -0
  107. package/src/native/Recordings/RecordingsLib/RecordingProvider.swift +112 -0
  108. package/src/native/Recordings/RecordingsLib/RecordingsCLI.swift +17 -10
  109. package/src/native/Recordings/RecordingsLib/ServiceAPIConfiguration.swift +107 -0
  110. package/src/native/Recordings/RecordingsLib/ServiceConnectionView.swift +76 -0
  111. package/src/native/Recordings/RecordingsLib/SettingsView.swift +45 -149
  112. package/src/native/Recordings/RecordingsLib/SettingsWindowController.swift +48 -0
  113. package/src/native/Recordings/RecordingsTests/BlockingOperationTests.swift +85 -0
  114. package/src/native/Recordings/RecordingsTests/CLIRunnerTests.swift +454 -76
  115. package/src/native/Recordings/RecordingsTests/OpenAIAPIKeyStoreTests.swift +43 -38
  116. package/src/native/Recordings/RecordingsTests/PipeClosureFixture.swift +215 -0
  117. package/src/native/Recordings/RecordingsTests/ProjectStoreTests.swift +10 -10
  118. package/src/native/Recordings/RecordingsTests/RecorderSurfaceTests.swift +60 -0
  119. package/src/native/Recordings/RecordingsTests/RecordingDateTests.swift +26 -0
  120. package/src/native/Recordings/RecordingsTests/RecordingEngineDeliveryTests.swift +1 -1
  121. package/src/native/Recordings/RecordingsTests/RecordingFrozenPasteTargetTests.swift +50 -0
  122. package/src/native/Recordings/RecordingsTests/RecordingPasteTargetTrackerTests.swift +48 -0
  123. package/src/native/Recordings/RecordingsTests/RecordingProviderTests.swift +308 -0
  124. package/src/native/Recordings/RecordingsTests/RecordingStartGateTests.swift +1 -1
  125. package/src/native/Recordings/RecordingsTests/RecordingStartTimingTests.swift +123 -21
  126. package/src/native/Recordings/RecordingsTests/ServiceAPIConfigurationTests.swift +75 -0
  127. package/src/native/Recordings/RecordingsTests/SettingsWindowControllerTests.swift +28 -0
  128. package/src/native/Recordings/RecordingsTests/TestHomeDirectory.swift +5 -1
  129. package/src/native/Recordings/Updater/BootstrapPreflight/BootstrapPreflightMain.swift +1 -1
  130. package/src/native/Recordings/Updater/Broker/ApplicationNamespace.swift +2 -2
  131. package/src/native/Recordings/Updater/Broker/BrokerMain.swift +1 -1
  132. package/src/native/Recordings/Updater/Broker/CanonicalTreeCopy.swift +1 -1
  133. package/src/native/Recordings/Updater/Broker/InstallJournal.swift +1 -1
  134. package/src/native/Recordings/Updater/BrokerTests/ActivationRecoveryPolicyTests.swift +2 -2
  135. package/src/native/Recordings/Updater/Protocol/UpdateProtocol.swift +1 -1
  136. package/src/native/Recordings/Updater/VerifierLauncher/RecordingsVerifierLauncher.c +2 -2
  137. package/src/native/Recordings/build.sh +8 -7
  138. package/dist/__tests__/helpers/installer-guard-execution.d.ts +0 -22
  139. package/dist/__tests__/helpers/installer-guard-execution.d.ts.map +0 -1
  140. package/dist/__tests__/helpers/installer-preflight.d.ts +0 -22
  141. package/dist/__tests__/helpers/installer-preflight.d.ts.map +0 -1
  142. package/dist/__tests__/helpers/native-fs-guard.d.ts +0 -2
  143. package/dist/__tests__/helpers/native-fs-guard.d.ts.map +0 -1
  144. package/dist/__tests__/helpers/source-assertions.d.ts +0 -171
  145. package/dist/__tests__/helpers/source-assertions.d.ts.map +0 -1
  146. package/dist/__tests__/preload.d.ts +0 -2
  147. package/dist/__tests__/preload.d.ts.map +0 -1
  148. 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 ships a **full native macOS app** (SwiftUI, macOS 26 / Liquid Glass) with a
17
- companion menu-bar control. It opens to a **Recordings workspace**: a narrow violet
18
- Liquid-Glass sidebar (Workspace · Library · Projects · Modes · Machines) beside one
19
- continuous canvas with the record hero, transcript library, and detail view. The menu bar
20
- provides recording controls and access to the main window while it is in the background.
21
-
22
- - **Record** — large push-to-talk / dictation / command hero with live transcription,
23
- duration, the active project, and a "just now" strip. Global shortcut (default F5, or
24
- hold fn) works while the window is in the background.
25
- - **Library** every past transcript (read straight from the active local or HTTP Store
26
- the CLI and MCP write), searchable and filterable by project, mode, and machine, with a
27
- detail pane (copy, paste-into-front-app, audio playback, metadata).
28
- - **Projects** app projects are registered through the same canonical Store before a
29
- recording can reference them, preserving referential integrity on either store.
30
- - **Settings** (⌘,) OpenAI key, language, recording shortcut, permissions, projects,
31
- and voice shortcuts.
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/HasnaRecordings.app`; later
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/HasnaRecordings.app/Contents/Helpers/recordings-update-client status
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/HasnaRecordings.app`. The managed bootstrap installs
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/HasnaRecordings.app`. A second bootstrap PKG, a broker or
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/HasnaRecordings.app`; it never provisions or imitates the production root cohort.
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, and project-specific instructions are appended when a project is active.
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, or call the server's `/v1` HTTP API. The
444
- presence of BOTH `HASNA_RECORDINGS_API_URL` and `HASNA_RECORDINGS_API_KEY`
445
- selects the API; any other environment reads the on-box file. A partial setup
446
- (one of the two variables set) fails closed rather than silently reading the
447
- wrong dataset. The explicit `HASNA_RECORDINGS_CLIENT_STORE` switch (`sqlite` |
448
- `http`) wins over the auto-selection, so a configuration that sets it to
449
- `sqlite` keeps reading the local file even when the hosted pair is present.
450
- `RECORDINGS_API_KEY` is the OpenAI transcription-key override only and never
451
- selects client transport.
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 { RecordingsV1Client } from "@hasna/recordings/sdk";
594
+ import { createRecordingsV1Client } from "@hasna/recordings/sdk";
483
595
 
484
- const client = new RecordingsV1Client({
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
- The release version is hand-maintained in four places: `package.json`, `src/version.ts`, and both
508
- `CFBundleShortVersionString` and `CFBundleVersion` in
509
- `src/native/Recordings/RecordingsLib/Info.plist`. Bump them together, never by hand:
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.3.0 # rewrites every hand-maintained site
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
- A fifth copy is **generated**, not written: `src/server/openapi.ts` stamps `VERSION` into the
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 `bun test`, which covers the sites
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.