@mieweb/pulsevault 0.5.0 → 0.7.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 (52) hide show
  1. package/CHANGELOG.md +152 -1
  2. package/OPERATIONS.md +123 -46
  3. package/PROTOCOL.md +27 -5
  4. package/README.md +31 -8
  5. package/dist/app.d.ts +8 -7
  6. package/dist/app.d.ts.map +1 -1
  7. package/dist/app.js +4 -3
  8. package/dist/app.js.map +1 -1
  9. package/dist/core.d.ts +23 -9
  10. package/dist/core.d.ts.map +1 -1
  11. package/dist/core.js +106 -21
  12. package/dist/core.js.map +1 -1
  13. package/dist/lib/completion.d.ts +22 -5
  14. package/dist/lib/completion.d.ts.map +1 -1
  15. package/dist/lib/completion.js +247 -35
  16. package/dist/lib/completion.js.map +1 -1
  17. package/dist/lib/deeplinks.d.ts +18 -2
  18. package/dist/lib/deeplinks.d.ts.map +1 -1
  19. package/dist/lib/deeplinks.js +13 -2
  20. package/dist/lib/deeplinks.js.map +1 -1
  21. package/dist/lib/magic.d.ts +41 -1
  22. package/dist/lib/magic.d.ts.map +1 -1
  23. package/dist/lib/magic.js +104 -0
  24. package/dist/lib/magic.js.map +1 -1
  25. package/dist/lib/options.d.ts +2 -0
  26. package/dist/lib/options.d.ts.map +1 -1
  27. package/dist/lib/options.js +8 -4
  28. package/dist/lib/options.js.map +1 -1
  29. package/dist/lib/protocol-version.gen.js +1 -1
  30. package/dist/lib/pulsevaultTus.d.ts.map +1 -1
  31. package/dist/lib/pulsevaultTus.js +35 -1
  32. package/dist/lib/pulsevaultTus.js.map +1 -1
  33. package/dist/lib/web-ready.d.ts +145 -18
  34. package/dist/lib/web-ready.d.ts.map +1 -1
  35. package/dist/lib/web-ready.js +484 -91
  36. package/dist/lib/web-ready.js.map +1 -1
  37. package/dist/storage/local.d.ts +3 -1
  38. package/dist/storage/local.d.ts.map +1 -1
  39. package/dist/storage/local.js +310 -50
  40. package/dist/storage/local.js.map +1 -1
  41. package/dist/storage/s3.d.ts.map +1 -1
  42. package/dist/storage/s3.js +21 -1
  43. package/dist/storage/s3.js.map +1 -1
  44. package/dist/storage/types.d.ts +89 -4
  45. package/dist/storage/types.d.ts.map +1 -1
  46. package/dist/storage/types.js +7 -0
  47. package/dist/storage/types.js.map +1 -1
  48. package/package.json +2 -2
  49. package/protocol/openapi.json +26 -3
  50. package/protocol/schemas/artifact-status.schema.json +13 -1
  51. package/protocol/schemas/deep-link.schema.json +2 -2
  52. package/scripts/web-ready-migrate.mjs +35 -11
package/CHANGELOG.md CHANGED
@@ -7,6 +7,154 @@ breaking changes, called out explicitly below.
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.7.0] - 2026-10-30
11
+
12
+ ### Added
13
+
14
+ - **A poster for every video** (`webReady.poster`, on by default): a video that arrived without
15
+ a thumbnail — a file from a host's own picker — gets one frame as its poster after the
16
+ conversion, stored as a thumbnail `relatedTo` it, so `GET /artifacts/:artifactId/poster`
17
+ serves it like a pulse's. A video whose pulse sent a thumbnail is left alone. A video stored
18
+ before this gets its poster the first time it's asked for, in the background.
19
+ - `extractPosterFrame(path, { ffmpegPath })`: one frame of a video as a JPEG, at most 640 px.
20
+ - `storeArtifact` on the storage contract (and the local adapter): stores a finished artifact
21
+ PulseVault made itself, ready and acknowledged, under `relatedTo`, only while that artifact is
22
+ still the ready one of `relatedGeneration`. Resolves whether it stored it.
23
+ - `generated` on artifact metadata, `true` for one PulseVault made. `getPulse` (and so the poster
24
+ route) prefers a thumbnail the client sent over a generated one, whichever finished last.
25
+
26
+ ### Changed
27
+
28
+ - **Hosts with `webReady` on now get posters without opting in**: each conversion is followed by
29
+ a poster frame (another ffmpeg run, a new thumbnail artifact and file per video), and
30
+ `/artifacts/:artifactId/poster` answers 200 for videos it used to 404 for. Set
31
+ `webReady: { poster: false }` to keep the old behaviour. A thumbnail still uploading holds the
32
+ poster back until it finishes or is given up; one that arrives while the frame is made wins.
33
+
34
+ ## [0.6.0] - 2026-10-12
35
+
36
+ Protocol 2.5 (`PROTOCOL.md` §7.4): 2.4 adds the https pairing link form (#83), 2.5 the conform
37
+ step's `webReady` result. `webReady` becomes a **conform** step: whatever video a
38
+ person uploads, PulseVault serves it in the one format the Pulse app records, and a Pulse upload
39
+ is left untouched (#84).
40
+
41
+ ### Added
42
+
43
+ - **https pairing links** (#83): `buildUploadLink({ …, form: 'https' })`
44
+ returns `https://mieweb.github.io/pulse/open#v=1&artifactId=…&server=…&token=…`,
45
+ the same parameters as the `pulsecam://` form, in the fragment. It is a
46
+ Universal Link / App Link: a phone camera opens it, Pulse opens straight
47
+ from it when installed, and the page sends the person to the store when it
48
+ isn't. Browsers never send the fragment, so the link host never receives the
49
+ token. `PULSE_OPEN_URL` is exported. Without `form` the output is
50
+ unchanged, `pulsecam://`, because Pulse releases from before the link host
51
+ was claimed open the https form only through the page's button.
52
+ - **Conform target** (`CONFORM_TARGET`): faststart MP4, H.264 8-bit `yuv420p` (limited range)
53
+ with even dimensions, SDR (HDR tone-mapped to BT.709; SDR colour tags kept), the longest edge at most `webReady.maxEdge` (default 1920; aspect ratio and
54
+ orientation kept), rotation applied, AAC or no audio. `ensureWebReady` leaves a file already
55
+ in it byte-for-byte (`none`), remuxes losslessly when only the container or `moov` position is
56
+ off, and otherwise runs ffmpeg once, re-encoding only the off-target streams: HEVC and other
57
+ codecs to H.264, 10-bit HDR (PQ/HLG) tone-mapped to SDR (FFmpeg 8's `scale`, or `zscale` on
58
+ older builds), full-range video (iPhone screen recordings) to limited range, oversized video
59
+ scaled down, Opus/PCM and other audio to AAC.
60
+ - **Container change**: a `.mov`, `.m4v`, `.webm`, `.mkv`, `.3gp` or `.avi` becomes a new
61
+ `<id>.mp4` (`WebReadyAction` `conformed`). The artifact's stored file, `ext` and served
62
+ `Content-Type` follow it; the artifact id and its URLs don't change. The sidecar switches
63
+ first, then the original is deleted (`sourceExt` remembers it so `remove` cleans up after a
64
+ crash). New patch fields `ext` and `webReady` (local adapter; the S3 adapter refuses them).
65
+ `describeArtifact` reports the uploaded extension as `sourceExt` once it differs, and
66
+ `uploadIdOf(meta)` names tus's upload by it, so a replayed hook's `ext` and `uploadId` still
67
+ match `filename`.
68
+ - `webReady.timeoutSeconds` (default `60 + 10 ×` the duration in 1080p30 seconds, so 4K or high
69
+ frame rates get proportionally longer, at most an hour since it comes from the upload's own
70
+ metadata; set it explicitly for longer runs): a longer run is killed, the original
71
+ kept, and `skipped` recorded with the reason.
72
+ - The conversion's result is recorded on the artifact: `GET /artifacts/:id/status` and
73
+ `getStatus` report `webReady: { action, reason }` (protocol 2.5), and `describeArtifact`
74
+ returns it.
75
+ - `core.conformAvailable()` (also on `fastify.pulseVaultCore`): `false` when `webReady` is off or
76
+ ffmpeg/ffprobe are missing, for a host's health check.
77
+ - `createVideoValidator({ maxDurationSeconds? })`: a `validatePayload` hook that checks a video
78
+ by its content with ffprobe (a video stream with a duration above zero, including a browser
79
+ WebM without one in its header; a picture — PNG, JPEG, GIF, HEIC, a one-frame clip, counted
80
+ from the first two packets when the container has no frame count — is not a video), refusing with `422 That file isn't a video.` or `That video is
81
+ longer than the limit of …` (the configured limit exactly: "1 minute 1 second"); falls back to `sniffVideo` (`ftyp`, EBML or RIFF AVI) without
82
+ ffprobe. Also exported: `probeVideo`, `webReadyAvailable`, `CONFORM_VIDEO_EXTENSIONS`.
83
+ - An upload over `maxUploadSize` is refused with `413 That file is larger than 500 MB.` instead
84
+ of tus's "Maximum size exceeded".
85
+ - `webReady.probeTimeoutSeconds` and `createVideoValidator({ probeTimeoutSeconds })` (default 60):
86
+ every ffprobe run over an upload is bounded; the validator refuses with `422 That video
87
+ couldn't be checked in time.`
88
+ - Content types for `.webm`, `.mkv`, `.3gp` and `.avi`.
89
+ - While a video is being converted, `GET /artifacts/:id` is served with `max-age=0` instead of
90
+ the configured `cache` (which may be `immutable`), so no client keeps the original bytes once
91
+ the converted file replaces them.
92
+
93
+ ### Changed
94
+
95
+ - The default video `allowedExtensions` are `.mp4`, `.mov`, `.m4v`, `.webm`, `.mkv`, `.3gp` and
96
+ `.avi` (was `.mp4`). A host's own `allowedExtensions` still overrides them.
97
+ - A failed conversion is now recorded (`webReady: { action: "skipped", reason }`) as well as
98
+ logged, and still not retried; a conversion that never recorded a result is resumed by the
99
+ replay, as before.
100
+ - The local adapter's `remove` reads the sidecar itself (not its cache) and deletes the
101
+ converted `<id>.mp4` and the uploaded file as well as the stored one. A converted video's bytes
102
+ are always at `<id>.mp4`, and `resolve`/`getLocalPath` fall back to it when the sidecar's `ext`
103
+ file is gone: across instances sharing a workspace, where the last sidecar write wins (as
104
+ before; there is no cross-instance lock), a write from a copy older than a conversion still
105
+ serves the converted file.
106
+ - A client that re-sends the final `PATCH` of a finished upload whose file a conversion replaced
107
+ (a `.webm` conformed to `<id>.mp4`) gets `204`, as without a conversion: the local adapter's tus
108
+ store reads such an upload as complete and takes its empty tail without a write.
109
+ - A `webReady` conversion needs the adapter's `generation`: an adapter with `getLocalPath` but no
110
+ generation has its videos recorded `skipped` (`the storage adapter keeps no generation for this
111
+ upload`). The local adapter gives one at `markReady` to an upload reserved before this release.
112
+ - Reservations run under the in-process sidecar lock, and `remove` deletes the sidecar last, so a new upload
113
+ of a removed id never has its bytes deleted by the old removal. Each reservation gets a
114
+ `generation` (`describeArtifact`), new every time the id is reserved.
115
+ - A conversion is installed in one patch under the sidecar lock: the runner converts into a temporary
116
+ `.mp4` (`ensureWebReady(…, { install: false })`), then `patchArtifact` with `file` renames it
117
+ onto the stored file, switches `ext`, records the result and deletes the original — only for
118
+ the reservation the conversion began on (`generation`) and only while no other pass has
119
+ recorded one (`unlessConverted`). When a condition doesn't hold, `patchArtifact` resolves
120
+ `false`, changing nothing, and the runner drops its file.
121
+ - Completion work belongs to one reservation: a queued conversion checks, when its turn comes,
122
+ that the id still names the upload it was queued for (and that it's finished); the
123
+ acknowledgement is recorded only on that reservation; and a new upload of a removed id is never
124
+ joined to the old one's work in progress.
125
+ - `GET /artifacts/:id` opens the file before sending headers, checks it's the file `send`
126
+ described (inode, size, mtime), and resolves once more when it's gone or another file was
127
+ renamed onto the path (a conversion just replaced it), instead of a 404, a cut-off response or
128
+ headers that don't match the bytes; bytes this
129
+ instance resolved under an extension storage no longer names are served with `max-age=0`.
130
+ - A failed run's recorded reason is `ffmpeg exited with code N`, never ffmpeg's output (which
131
+ names server paths); an HDR video on an ffmpeg without tone mapping is left as uploaded
132
+ (`skipped`, saying what's missing) rather than converted with wrong colours.
133
+ - The local adapter's `remove` decides from the sidecar alone, never its cache: an artifact
134
+ another reader had cached is reported removed once, not twice.
135
+ - `webReady` needs an adapter with `describeArtifact` too (both built-in adapters have it).
136
+ - The local adapter's `getLocalPath` reads the sidecar, not its cache, so it names the current
137
+ file after another instance removed the id and reserved it again.
138
+ - ffmpeg's capability queries (`-version`, `-h filter=scale`, `-filters`) are bounded (30 s).
139
+ - An `onArtifactEvent` observer that throws on `processed` is logged as its own failure; the
140
+ recorded result still reaches `completeAfter`'s hook as `ctx.webReady`.
141
+ - A replayed `onUploadComplete` (the hook threw, or the process stopped) gets the recorded
142
+ conversion result as `ctx.webReady` too, so a host naming the file from it (an `.mp4` once a
143
+ WebM was conformed) gets it right on the retry.
144
+ - `web-ready-migrate.mjs` rerun after an interruption deletes an original the sidecar no longer
145
+ names.
146
+ - `scripts/web-ready-migrate.mjs` conforms existing artifacts the same way, recording the result
147
+ on each sidecar and switching a changed container to its `.mp4`. Run it with the server
148
+ stopped.
149
+ - `transcode: false` records a file it leaves off-target (HEVC, oversized, non-AAC audio) as
150
+ `skipped`, not `none`: `none` now means only that the file already conforms. (Was `none`.)
151
+ - A browser WebM's missing duration is found by streaming ffprobe's packet list and keeping
152
+ only the latest end, never buffering it.
153
+ - An upload of deferred length that grows past `maxUploadSize` (a chunk with a length, or a
154
+ chunked body cut off mid-stream) gets the same `413 That file is larger than …` message.
155
+ - An MP4-family `.mov`/`.m4v` that already conformed used to be served as is (`video/quicktime`,
156
+ a type not every browser takes); it is now remuxed into an `.mp4`.
157
+
10
158
  ## [0.5.0] - 2026-10-05
11
159
 
12
160
  Protocol 2.3 (`PROTOCOL.md` §7.4). What every host rebuilt around PulseVault
@@ -500,7 +648,10 @@ if you've evaluated against an intermediate build.
500
648
  lock, so this is now at least surfaced: a one-time `console.warn` fires
501
649
  per process the first time a deployment falls into this degraded mode.
502
650
 
503
- [Unreleased]: https://github.com/mieweb/pulsevault/compare/v0.4.2...HEAD
651
+ [Unreleased]: https://github.com/mieweb/pulsevault/compare/v0.7.0...HEAD
652
+ [0.7.0]: https://github.com/mieweb/pulsevault/compare/v0.6.0...v0.7.0
653
+ [0.6.0]: https://github.com/mieweb/pulsevault/compare/v0.5.0...v0.6.0
654
+ [0.5.0]: https://github.com/mieweb/pulsevault/compare/v0.4.2...v0.5.0
504
655
  [0.4.2]: https://github.com/mieweb/pulsevault/compare/v0.4.1...v0.4.2
505
656
  [0.4.1]: https://github.com/mieweb/pulsevault/compare/v0.4.0...v0.4.1
506
657
  [0.4.0]: https://github.com/mieweb/pulsevault/compare/v0.3.0...v0.4.0
package/OPERATIONS.md CHANGED
@@ -54,7 +54,15 @@ Two ways to actually support multiple instances:
54
54
  2. **Shared filesystem**: point every instance's `workspaceDir` at the same
55
55
  NFS/EFS mount. Verify your mount's durability/consistency guarantees
56
56
  under concurrent writes from multiple instances before relying on this in
57
- production.
57
+ production. Each instance changes an artifact's metadata (`.pulsevault/
58
+ <id>.json`) one write at a time, but no lock spans instances: when two
59
+ write the same artifact's metadata at once, the last write wins. The
60
+ completion bookkeeping recovers: the replay redoes a lost acknowledgement
61
+ or conversion record, and a converted video's bytes are always at
62
+ `<id>.mp4`, which is served even when the metadata that won still names
63
+ the uploaded file. A host's `recordOutcome` lost that way is not redone. A
64
+ removal racing another instance's write to the same artifact can leave its
65
+ metadata behind; for strict guarantees across instances, use S3.
58
66
 
59
67
  **The S3/R2 adapter (`createS3Storage`) has no such requirement** — every
60
68
  instance talks to the same bucket, so it scales horizontally with zero
@@ -160,56 +168,124 @@ backup time will resume correctly via the normal TUS `HEAD`-then-resume path
160
168
  once the client retries, or will sit as abandoned partial uploads until
161
169
  `retention` removes them (see "Retention" below) if the client never retries.
162
170
 
163
- ## Web-ready playback (faststart remux + H.264 transcode)
164
-
165
- Browsers streaming an MP4 over progressive HTTP stall (or fail outright) on
166
- two things mobile capture pipelines routinely upload:
167
-
168
- - **moov atom at the end of the file** — the player must fetch the file's tail
169
- before rendering frame one: seconds of startup delay behind Range requests,
170
- or a full download in naive players.
171
- - **HEVC video** — Firefox never decodes it; Chrome usually can't without
172
- hardware support.
173
-
174
- The `ensureWebReady` helper (exported from the package root) fixes both, in
175
- place and atomically (tmp file + rename): a lossless sub-second
176
- `-c copy -movflags +faststart` remux when only the moov position is wrong, and
177
- a one-time `libx264` transcode (audio stream-copied) when the codec is
178
- hostile. It is fail-open by design — without `ffmpeg`/`ffprobe` on `PATH` it
179
- logs one warning and the original bytes keep serving exactly as before.
180
-
181
- **Prerequisite:** install ffmpeg on the serving host — `apt install ffmpeg`
182
- (Debian/Ubuntu), `dnf install ffmpeg` (Fedora/EL + RPM Fusion), or
183
- `brew install ffmpeg` (macOS). Nothing else changes; the hook detects it at
184
- first use.
185
-
186
- **New uploads** — turn on the `webReady` option (the fastify-demo and the
187
- meteor-demo ship with it enabled). The core runs the conversion in the
188
- background after the final `PATCH` is answered, one video at a time by
189
- default, and reports the artifact as `processing` on the status route
190
- meanwhile:
171
+ ## Web-ready playback (conform to one format)
172
+
173
+ Videos reach PulseVault from the Pulse app, which already uploads the format
174
+ below, and from a host's own file picker: a screen recording, an iPhone HEVC
175
+ `.mov` (often 10-bit HDR), a WebM from a browser recorder, a 4K clip. Browsers
176
+ streaming over progressive HTTP stall on a `moov` atom at the end of the file,
177
+ Firefox never decodes HEVC, Chrome usually can't without hardware support,
178
+ 10-bit HDR shown as SDR looks washed out, and a 4K file is far more than a
179
+ phone needs.
180
+
181
+ With the `webReady` option on, every finished video is **conformed** to one
182
+ target (`CONFORM_TARGET`, exported from the package root):
183
+
184
+ | Property | Target |
185
+ |---|---|
186
+ | Container | MP4, `moov` at the front (faststart) |
187
+ | Video | H.264, 8-bit `yuv420p` (limited range), even width and height |
188
+ | Colour | SDR: HDR (PQ or HLG) is tone-mapped to BT.709; an SDR source keeps its own colour tags (BT.709 or BT.601), which browsers honour |
189
+ | Size | longest edge at most 1920 (`maxEdge`); aspect ratio and orientation kept — no crop, pad or stretch |
190
+ | Rotation | applied to the pixels, so every player shows it the way it was held |
191
+ | Audio | AAC; a video with no audio stays silent |
192
+
193
+ `ensureWebReady` does the least work that gets a file there:
194
+
195
+ | The upload | What happens | `action` |
196
+ |---|---|---|
197
+ | Already in the target (a Pulse upload) | nothing; bytes untouched | `none` |
198
+ | MP4 with the `moov` at the end | lossless `-c copy` faststart remux | `remuxed` |
199
+ | MP4 with an off-target stream | one ffmpeg run, in place | `transcoded` |
200
+ | Another container (`.mov`, `.m4v`, `.webm`, `.mkv`, `.3gp`, `.avi`) | one ffmpeg run into a new `<id>.mp4`: a stream copy when the streams already conform, otherwise only the off-target streams re-encoded | `conformed` |
201
+ | No ffmpeg, unreadable, failed or timed out | nothing; the original serves | `skipped` |
202
+
203
+ A re-encode scales by the longest edge, applies the rotation, tone-maps HDR
204
+ (PQ or HLG) to SDR, and converts non-AAC audio (Opus, PCM, …) to AAC. After a
205
+ container change the artifact's stored file, extension and served
206
+ `Content-Type` (`video/mp4`) follow the new file; the artifact id, and so every
207
+ URL a host stored, stay the same. The new file is written beside the original,
208
+ the artifact's sidecar switches to it, and only then is the original deleted:
209
+ a crash at any point leaves either the original serving (the conversion is
210
+ redone by the replay) or an unused original that is deleted with the
211
+ artifact.
212
+
213
+ While a video is being converted, its URL serves the original bytes with
214
+ `max-age=0` instead of the configured `cache`, so a browser that fetched it
215
+ early revalidates and gets the converted file (with a new ETag) afterwards.
216
+
217
+ A failed run is recorded as `conversion failed: ffmpeg exited with code N`;
218
+ ffmpeg's own output, which names server paths, goes only to the server log.
219
+
220
+ The result is recorded on the artifact: the status route reports it as
221
+ `webReady: { action, reason }` (a host can say "Your video is ready", or "We
222
+ couldn't convert this video, so it may not play in every browser"),
223
+ `onArtifactEvent` fires `processed` with it, and `completeAfter` hands it to
224
+ `onUploadComplete` as `ctx.webReady`.
225
+
226
+ **Prerequisite:** install ffmpeg (which includes ffprobe) on the serving host
227
+ — `apt install ffmpeg` (Debian/Ubuntu), `dnf install ffmpeg` (Fedora/EL + RPM
228
+ Fusion), or `brew install ffmpeg` (macOS). It is detected at first use; without
229
+ it every upload is still accepted and served exactly as uploaded, one warning
230
+ is logged, and `core.conformAvailable()` (also `fastify.pulseVaultCore`)
231
+ resolves `false` for a health check. HDR tone mapping uses the `scale` filter
232
+ on FFmpeg 8 and later, or `zscale` (libzimg, included in the Debian and Ubuntu
233
+ packages) on older builds. A build with neither leaves an HDR video as
234
+ uploaded and records `skipped` with what's missing: converting it without tone
235
+ mapping would give wrong colours.
191
236
 
192
237
  ```js
193
238
  await app.register(pulseVault, {
194
239
  storage: createLocalStorage({ workspaceDir: dataDir }),
240
+ validatePayload: createVideoValidator({ maxDurationSeconds: 600 }),
195
241
  webReady: { concurrency: 1, completeAfter: true },
196
242
  });
197
243
  ```
198
244
 
199
- Options: `{ transcode: false }` restricts it to the lossless remux (no CPU
200
- cost beyond a file rewrite); `crf`/`preset` tune the transcode
201
- (defaults `23`/`veryfast`); `ffmpegPath`/`ffprobePath` point at binaries off
202
- `PATH`; `concurrency` runs several at once; `completeAfter: true` holds
203
- `onUploadComplete` until the conversion has finished, for a host that
204
- publishes the video from the hook. `onArtifactEvent` fires `processed` with
205
- what was done. A conversion a restart interrupted is resumed by the
206
- completion replay (`replayCompletions`). Calling `ensureWebReady` yourself
207
- from `onUploadComplete` still works, but holds the client's final `PATCH`
208
- for the whole conversion.
209
-
210
- **Existing artifacts** — uploads that landed before the hook existed are fixed
211
- once with the bundled migration script (idempotent; interrupt and rerun
212
- freely, already-fixed files are skipped for free):
245
+ Settings:
246
+
247
+ - `concurrency` (default `1`): conversions run one at a time per process; the
248
+ rest wait in a queue. A conversion a restart interrupted is resumed by the
249
+ completion replay (`replayCompletions`): an artifact isn't marked converted
250
+ until its result is recorded.
251
+ - `timeoutSeconds` (default `60 + 10 ×` the video's duration counted in
252
+ 1080p30 seconds — a second of 4K at 120 fps counts 16 — at most an hour,
253
+ and an hour when the duration is unknown; the default comes from the
254
+ upload's own metadata, so set this for longer runs): a run past it is
255
+ killed, the original kept, and
256
+ `skipped` recorded with the reason.
257
+ - `probeTimeoutSeconds` (default `60`): the same guard for each ffprobe run
258
+ over the upload, so a malformed file can't hold the queue.
259
+ - `maxEdge` (default `1920`): the longest edge of the served video.
260
+ - `transcode: false`: never re-encode; only lossless remuxes run (a `moov`
261
+ moved to the front, or another container whose streams already conform copied
262
+ into an `.mp4`), so no CPU cost beyond a file rewrite. A file that would need
263
+ a re-encode is left as uploaded and recorded as `skipped` (`none` means only
264
+ "already conforms").
265
+ - `crf`/`preset` (defaults `23`/`veryfast`) tune the H.264 encode;
266
+ `ffmpegPath`/`ffprobePath` point at binaries off `PATH`.
267
+
268
+ **Accepted containers.** The default video `allowedExtensions` are `.mp4`,
269
+ `.mov`, `.m4v`, `.webm`, `.mkv`, `.3gp` and `.avi` (`CONFORM_VIDEO_EXTENSIONS`);
270
+ a host's own `allowedExtensions` still overrides them. The extension only
271
+ decides what's accepted at `create`; `createVideoValidator()` checks the
272
+ received bytes by what they are: ffprobe must find a video stream with a
273
+ duration above zero that isn't a picture (a PNG, JPEG, GIF, HEIC photo or
274
+ one-frame clip is refused), and, with `maxDurationSeconds`, not longer, or the
275
+ upload is refused with `422 That file isn't a video.` / `That video is longer
276
+ than the limit of 10 minutes.` Each ffprobe run is bounded by
277
+ `probeTimeoutSeconds` (default `60`), so a malformed file can't hold the final
278
+ `PATCH` open: past it the upload is refused with `422 That video couldn't be
279
+ checked in time.` Without ffprobe it falls back to sniffing the
280
+ container's first bytes (`ftyp`, EBML or RIFF AVI). An upload over
281
+ `maxUploadSize` is refused at `create` with `413 That file is larger than
282
+ 500 MB.` Without `webReady`, a WebM or MKV is served as uploaded, with its own
283
+ content type.
284
+
285
+ **Existing artifacts** — uploads that landed before `webReady` was on are
286
+ conformed once with the bundled migration script. Stop the server while it
287
+ runs (the server caches each artifact's extension, which a container change
288
+ rewrites). It is idempotent; interrupt and rerun freely:
213
289
 
214
290
  ```sh
215
291
  node node_modules/@mieweb/pulsevault/scripts/web-ready-migrate.mjs /path/to/workspaceDir
@@ -219,11 +295,12 @@ node node_modules/@mieweb/pulsevault/scripts/web-ready-migrate.mjs /path/to/work
219
295
  node node_modules/@mieweb/pulsevault/scripts/web-ready-migrate.mjs /path/to/workspaceDir --no-transcode
220
296
  ```
221
297
 
222
- A transcode re-encodes the video stream (one-time, quality-preserving at the
223
- default CRF but not bit-identical). Take a backup first if that matters to
298
+ A re-encode is one-time, quality-preserving at the default CRF but not
299
+ bit-identical, and a conformed file replaces the upload (the original is not
300
+ kept). Take a backup first if that matters to
224
301
  your deployment (see "Backup and restore" above).
225
302
 
226
- Note on checksums: a remux or transcode changes the artifact's bytes, so an
303
+ Note on checksums: a conversion changes the artifact's bytes, so an
227
304
  upload-time checksum recorded for it (the sidecar `checksum` from
228
305
  `Upload-Metadata`) describes the original upload, not the rewritten file.
229
306
  Treat `getChecksum` as upload-time provenance rather than current-file
package/PROTOCOL.md CHANGED
@@ -3,7 +3,7 @@
3
3
  <!-- BEGIN GENERATED: protocol-version (from package.json pulseProtocol — edit that, then run `npm run protocol`) -->
4
4
  | | |
5
5
  |---|---|
6
- | Spec revision | `2.3` |
6
+ | Spec revision | `2.5` |
7
7
  | Protocol majors accepted | 2 |
8
8
  <!-- END GENERATED: protocol-version -->
9
9
 
@@ -69,12 +69,30 @@ the integer from `protocolVersion`.
69
69
 
70
70
  ## 3. Pairing (deep link)
71
71
 
72
- A server presents a pairing link or QR code of the form:
72
+ A server presents a pairing link or QR code in one of two equivalent forms
73
+ (since 2.4), carrying the same parameters:
73
74
 
74
75
  ```
75
76
  pulsecam://?v=1&artifactId=<uuid>&server=<origin>&token=<opaque>
77
+ https://mieweb.github.io/pulse/open#v=1&artifactId=<uuid>&server=<origin>&token=<opaque>
76
78
  ```
77
79
 
80
+ - **`pulsecam://`**: the parameters are the query string. Every Pulse release
81
+ opens it, but phone cameras and browsers don't open custom schemes reliably.
82
+ - **`https://`**: a Universal Link (iOS) / App Link (Android) on the link host
83
+ the Pulse app claims. When Pulse is installed the OS opens it straight from
84
+ the link; when it isn't, the link opens a static page that sends the person
85
+ to the store and offers a `pulsecam://` fallback. The parameters are the
86
+ **fragment**, which is never sent in an HTTP request, so the link host never
87
+ receives the token. The link host is fixed (an app can only claim domains it
88
+ was built with); `server` still points at the deployment's own server, so
89
+ uploads never touch the link host.
90
+
91
+ A client that accepts the `https://` form MUST treat it exactly like the
92
+ `pulsecam://` form with the same parameters. A server that wants every Pulse
93
+ release to open its links directly keeps the `pulsecam://` form until the
94
+ releases that claim the link host are installed.
95
+
78
96
  - `v` (REQUIRED): the deep-link schema version. A client MUST refuse and
79
97
  explain (not silently misparse) a `v` it doesn't recognize.
80
98
  - `artifactId` (REQUIRED): a UUID minted by the server. The client uses this
@@ -95,7 +113,7 @@ pulsecam://?v=1&artifactId=<uuid>&server=<origin>&token=<opaque>
95
113
  credential — see §5.4 for the recommended (but not required) capability-
96
114
  token shape.
97
115
 
98
- A client SHOULD ignore query params it does not recognize, so links minted by
116
+ A client SHOULD ignore parameters it does not recognize, so links minted by
99
117
  an older or newer server stay valid. The parameters are defined by
100
118
  [`protocol/schemas/deep-link.schema.json`](protocol/schemas/deep-link.schema.json).
101
119
 
@@ -334,7 +352,9 @@ body is defined by
334
352
  `state` is `unknown`, `uploading`, `processing` (the server is converting the
335
353
  bytes for the web) or `ready`, with the bytes received against the declared
336
354
  length while uploading, and whatever outcome the host recorded once finished.
337
- The response MUST NOT be cached.
355
+ A server that converts videos for the web also reports what the conversion did
356
+ (`webReady`, protocol 2.5): nothing, a remux, a transcode, a new container, or
357
+ why it was skipped (the original then serves). The response MUST NOT be cached.
338
358
 
339
359
  A server MAY also expose `GET {prefix}/artifacts/<videoId>/poster`: the finished
340
360
  thumbnail `relatedTo` the video (§8), served exactly as §6.2 serves an artifact
@@ -410,7 +430,7 @@ a server it paired with earlier: a server can be upgraded in between.
410
430
  | [`beat-manifest.schema.json`](protocol/schemas/beat-manifest.schema.json) | Beat manifest |
411
431
  | [`capabilities.schema.json`](protocol/schemas/capabilities.schema.json) | GET /capabilities response |
412
432
  | [`capability-token.schema.json`](protocol/schemas/capability-token.schema.json) | Capability token claims |
413
- | [`deep-link.schema.json`](protocol/schemas/deep-link.schema.json) | Pairing link query parameters |
433
+ | [`deep-link.schema.json`](protocol/schemas/deep-link.schema.json) | Pairing link parameters |
414
434
  | [`pulse-client.schema.json`](protocol/schemas/pulse-client.schema.json) | Pulse-Client request header |
415
435
  | [`upload-metadata.schema.json`](protocol/schemas/upload-metadata.schema.json) | TUS Upload-Metadata keys |
416
436
  | [`view-link.schema.json`](protocol/schemas/view-link.schema.json) | POST /artifacts/<id>/view-link response |
@@ -425,6 +445,8 @@ a server it paired with earlier: a server can be upgraded in between.
425
445
  | 2.1 | Added `protocolRevision` to `/capabilities`, the `Pulse-Client` header with `426 Upgrade Required` (§7.2), and `Upload-Metadata.appVersion` (§4.1). |
426
446
  | 2.2 | Added read-only view links: `viewLinks` in `/capabilities`, `POST {prefix}/artifacts/<id>/view-link` (§6.4), and the view token's `use` claim (§5.5). |
427
447
  | 2.3 | Added `GET {prefix}/artifacts/<id>/status` and `GET {prefix}/artifacts/<id>/poster` (§6.5), the capability token's `ctx` claim (§5.4), and the pulse-shape rule for creates (§5.4). |
448
+ | 2.4 | Added the `https://mieweb.github.io/pulse/open#…` pairing link form (§3), with the parameters in the fragment. |
449
+ | 2.5 | Added `webReady` to the status response (§6.5): what the server's conversion did. |
428
450
 
429
451
  ## 8. Artifact relationships (`relatedTo`)
430
452
 
package/README.md CHANGED
@@ -269,7 +269,7 @@ type PulseVaultPluginOptions = {
269
269
  lockWhenReady?: boolean; // default false: a finished pulse can't be deleted through the routes
270
270
  webReady?: PulseVaultWebReadyOptions | boolean; // default off: background faststart remux / H.264 transcode
271
271
  replayCompletions?: { intervalSeconds?: number } | false; // default every 300 s: re-fire unacknowledged completions
272
- coreDecoratorName?: string; // default "pulseVaultCore": getStatus, getPulse, recordOutcome, replayCompletions
272
+ coreDecoratorName?: string; // default "pulseVaultCore": getStatus, getPulse, recordOutcome, replayCompletions, conformAvailable
273
273
  /** @deprecated use validatePayload + ctx.kind === "project" */
274
274
  validateProjectPayload?: PulseVaultValidatePayload;
275
275
  /** @deprecated use onUploadComplete + ctx.kind === "project" */
@@ -306,8 +306,9 @@ import "@mieweb/pulsevault/augment";
306
306
  File extensions accepted per artifact kind. Three accepted forms:
307
307
 
308
308
  ```ts
309
- // 1. Omit entirely — uses all three defaults:
310
- // video: [".mp4"] project: [".pulse", ".zip"] captions: [".vtt"]
309
+ // 1. Omit entirely — uses the defaults:
310
+ // video: [".mp4", ".mov", ".m4v", ".webm", ".mkv", ".3gp", ".avi"] project: [".pulse", ".zip"]
311
+ // captions: [".vtt"] thumbnail: [".jpg", ".jpeg", ".png"]
311
312
 
312
313
  // 2. Flat array (legacy) — video-only; project/captions keep their defaults:
313
314
  allowedExtensions: [".mp4"]
@@ -316,7 +317,7 @@ allowedExtensions: [".mp4"]
316
317
  allowedExtensions: { video: [".mp4"], project: [".pulse"], captions: [".vtt"] }
317
318
  ```
318
319
 
319
- All extensions must include the leading dot and are matched case-insensitively. The `kind` field in `Upload-Metadata` determines which list is checked.
320
+ All extensions must include the leading dot and are matched case-insensitively. The `kind` field in `Upload-Metadata` determines which list is checked. The default video list is the containers [`webReady`](#webready) conforms to MP4 (`CONFORM_VIDEO_EXTENSIONS`); the extension only decides what's accepted at `create`, so pair it with `createVideoValidator()` to check the bytes themselves.
320
321
 
321
322
  ### `cache`
322
323
 
@@ -416,6 +417,8 @@ await app.register(pulseVault, {
416
417
  });
417
418
  ```
418
419
 
420
+ For videos from anywhere (a WebM from a browser recorder, an MKV, an AVI), check them by what they are with **`createVideoValidator({ maxDurationSeconds?, probeTimeoutSeconds? })`**: ffprobe must find a video stream with a duration above zero that isn't a picture (PNG, JPEG, GIF, HEIC, a one-frame clip), or the upload is refused with `422` and a message a host can show as it is (`That file isn't a video.`, `That video is longer than the limit of 10 minutes.`, or `That video couldn't be checked in time.` when ffprobe runs past `probeTimeoutSeconds`, default 60). It checks `kind: "video"` only and lets other kinds through; without ffprobe it falls back to `sniffVideo(path)` (the container's first bytes: `ftyp`, EBML or RIFF AVI) and logs once. Local storage only.
421
+
419
422
  `createMp4Sniffer` reads the first 12 bytes and verifies the ISOBMFF `ftyp` header (MP4, MOV, M4V, 3GP). Uploads that pass the extension check but contain non-video bytes are rejected with 422 and the disk is cleaned up. The lower-level `sniffMp4(path)` is also exported if you want to drive your own validator.
420
423
 
421
424
  `createChecksumValidator`/`createS3ChecksumValidator` parse `ctx.checksum` with the also-exported `parseChecksumMetadata(raw)`, which returns `{ algorithm, digest }` or `null` for a missing/malformed value — use it directly if you're writing your own checksum check instead of the ship-ready validators.
@@ -542,11 +545,17 @@ Off by default. `true`, or `ensureWebReady`'s options plus:
542
545
  webReady: {
543
546
  concurrency: 1, // conversions at once; a transcode is CPU-bound
544
547
  completeAfter: false, // true: run onUploadComplete only once the conversion has finished
548
+ poster: true, // a video that arrived without a thumbnail gets one frame as its poster
549
+ maxEdge: 1920, // longest edge of the served video
550
+ timeoutSeconds: undefined, // default 60 + 10 × duration in 1080p30 seconds (4K120 counts 16×), at most 1 h: a hang guard
551
+ probeTimeoutSeconds: 60, // the same guard for each ffprobe run over the upload
545
552
  transcode: true, crf: 23, preset: "veryfast", ffmpegPath: "ffmpeg", ffprobePath: "ffprobe",
546
553
  },
547
554
  ```
548
555
 
549
- Every finished video is made web-playable in the background, after the final `PATCH` is answered: a lossless faststart remux when the `moov` atom is at the end, or a one-time H.264 transcode when the codec is one browsers can't play (see `OPERATIONS.md`). While it runs the artifact's status is `processing` (the original bytes serve meanwhile; the rewrite is atomic), and when it's done `onArtifactEvent` fires `processed` with what it did. With `completeAfter: true`, `onUploadComplete` waits for the conversion and receives `ctx.webReady`, so a host that publishes from the hook never publishes a video that's still being rewritten. A conversion a restart interrupted is resumed by the replay below. Needs the local storage adapter and ffmpeg on the host; without ffmpeg it logs once and serves the original bytes.
556
+ Every finished video is conformed in the background, after the final `PATCH` is answered, to the one format the Pulse app records (`CONFORM_TARGET`: faststart MP4, H.264 8-bit SDR, longest edge at most `maxEdge`, rotation applied, AAC or silent; orientation kept). A file already in it — every Pulse upload — keeps its bytes (`none`); a `moov` at the end is a lossless remux (`remuxed`); an off-target stream is re-encoded (`transcoded`: HEVC, 10-bit HDR tone-mapped to SDR, 4K scaled, Opus/PCM to AAC); another container (`.mov`, `.webm`, `.mkv`, …) becomes a new `.mp4` served as `video/mp4` at the same artifact URL (`conformed`). See `OPERATIONS.md` for the table. While it runs the artifact's status is `processing` (the original bytes serve meanwhile, revalidated on every request instead of the configured `cache`; every rewrite is atomic), and when it's done the status reports `webReady: { action, reason }` — `skipped` with the reason when ffmpeg is missing, a run failed or timed out, and the original keeps serving — and `onArtifactEvent` fires `processed` with the same result. With `completeAfter: true`, `onUploadComplete` waits for the conversion and receives `ctx.webReady`, so a host that publishes from the hook never publishes a video that's still being rewritten. A conversion a restart interrupted is resumed by the replay below. Needs the local storage adapter and ffmpeg on the host; without ffmpeg it logs once and serves the original bytes.
557
+
558
+ With `poster` (on by default), a video that arrived without a thumbnail — a file from a host's own picker rather than a pulse — gets one: a frame a second in (the first frame of a shorter clip), its longest edge at most 640 px, turned the way the video plays, stored as a thumbnail `relatedTo` the video exactly as a pulse uploads one. So `getPulse`, the status route and `GET /artifacts/:artifactId/poster` find it like any other, and it goes with the video. It's taken in the conversion's slot, before `onUploadComplete` with `completeAfter`, so the host never publishes the video without it. A video whose pulse sent a thumbnail (or is still sending one) is left alone. A video stored before this (or whose frame couldn't be taken) gets one the first time its poster is asked for: that request answers `404` at once and the poster is made in the background, once per video per process, for the next look. Needs `storeArtifact` on the storage adapter (the local adapter has it).
550
559
 
551
560
  ### `replayCompletions`
552
561
 
@@ -560,12 +569,15 @@ The core behind the routes is `fastify.pulseVaultCore` (rename it with `coreDeco
560
569
  // Where an upload is — what GET /artifacts/:id/status reports — for a host that polls server-side.
561
570
  await app.pulseVaultCore.getStatus(videoId);
562
571
  // → { artifactId, state: "unknown" | "uploading" | "processing" | "ready", kind, relatedTo?, name?,
563
- // bytesReceived?, size?, acknowledged, outcome?, context? }
572
+ // bytesReceived?, size?, acknowledged, webReady?: { action, reason }, outcome?, context? }
564
573
  // `context` (the token's) is for the host — an owner check is a comparison on it; the route leaves it out.
565
574
 
566
575
  // Where it went, or why it didn't: any JSON, reported as `outcome` by the status route. `null` clears it.
567
576
  await app.pulseVaultCore.recordOutcome(videoId, { state: "done", note: "Posted to Huddle" });
568
577
 
578
+ // Whether videos are conformed here (`webReady` on, ffmpeg and ffprobe installed) — for a health check.
579
+ await app.pulseVaultCore.conformAvailable();
580
+
569
581
  // A video and the finished files relatedTo it, by kind — from the adapters' relation index, never a scan.
570
582
  const { video, thumbnail, captions, manifest } = await app.pulseVaultCore.getPulse(videoId);
571
583
  // One per kind; when a file was re-sent, the one that finished last (`readyAt`) wins.
@@ -598,7 +610,7 @@ When the final PATCH lands the plugin runs the following steps in order, for eve
598
610
  "minSupportedVersion": 2,
599
611
  "maxSupportedVersion": 2,
600
612
  "kinds": ["video", "project", "captions", "thumbnail"],
601
- "allowedExtensions": { "video": [".mp4"], "project": [".pulse", ".zip"], "captions": [".vtt"], "thumbnail": [".jpg", ".jpeg", ".png"] },
613
+ "allowedExtensions": { "video": [".mp4", ".mov", ".m4v", ".webm", ".mkv", ".3gp", ".avi"], "project": [".pulse", ".zip"], "captions": [".vtt"], "thumbnail": [".jpg", ".jpeg", ".png"] },
602
614
  "maxUploadSize": 5368709120,
603
615
  "checksum": { "algorithms": ["sha256", "sha1", "md5"] }
604
616
  }
@@ -937,7 +949,7 @@ const storage: PulseVaultStorage = {
937
949
 
938
950
  ## Deep link helper
939
951
 
940
- Use this to generate a `pulsecam://` deep link for pairing the Pulse mobile app with your server. Typically encoded as a QR code on a pairing page.
952
+ Use this to generate a pairing link for the Pulse mobile app and your server. Typically encoded as a QR code on a pairing page.
941
953
 
942
954
  ```ts
943
955
  import { buildUploadLink } from "@mieweb/pulsevault";
@@ -952,8 +964,19 @@ const uploadLink = buildUploadLink({
952
964
  token: "secret", // optional — forwarded to your authorize hook; see Capability tokens
953
965
  });
954
966
  // pulsecam://?v=1&artifactId=...&server=https%3A%2F%2Fexample.com%2Fpulsevault&token=secret
967
+
968
+ // The https form, a Universal Link / App Link (PROTOCOL.md §3):
969
+ const httpsLink = buildUploadLink({
970
+ server: "https://example.com/pulsevault",
971
+ artifactId: randomUUID(),
972
+ token: "secret",
973
+ form: "https",
974
+ });
975
+ // https://mieweb.github.io/pulse/open#v=1&artifactId=...&server=https%3A%2F%2Fexample.com%2Fpulsevault&token=secret
955
976
  ```
956
977
 
978
+ The `https` form is what a phone camera opens: Pulse opens straight from it when installed, and its page sends the person to the store when not. The parameters are after `#`, which browsers never send, so the link host never sees the token. Pulse releases from before the link host was claimed land on the page instead and open Pulse from its button, so `pulsecam://` stays the default.
979
+
957
980
  ## Pulse logo and store badges
958
981
 
959
982
  The package ships the Pulse logo for the button that opens Pulse
package/dist/app.d.ts CHANGED
@@ -157,7 +157,7 @@ export type PulseVaultPluginOptions = {
157
157
  replayCompletions?: PulseVaultReplayOptions | false;
158
158
  /**
159
159
  * Name of the decorator that exposes the core (`fastify.<name>.getStatus`, `.getPulse`,
160
- * `.recordOutcome`, `.replayCompletions`). Defaults to `"pulseVaultCore"`.
160
+ * `.recordOutcome`, `.replayCompletions`, `.conformAvailable`). Defaults to `"pulseVaultCore"`.
161
161
  */
162
162
  coreDecoratorName?: string;
163
163
  /**
@@ -180,17 +180,18 @@ export type PulseVaultPluginOptions = {
180
180
  declare const _default: FastifyPluginAsync<PulseVaultPluginOptions>;
181
181
  export default _default;
182
182
  export { createLocalStorage } from './storage/local.js';
183
+ export { uploadIdOf } from './storage/types.js';
183
184
  export type { LocalStorage, LocalStorageOptions } from './storage/local.js';
184
185
  export { createS3Storage } from './storage/s3.js';
185
186
  export type { S3Storage, S3StorageOptions } from './storage/s3.js';
186
- export type { PulseVaultArtifactRecord, PulseVaultResolution, PulseVaultStorage, ReserveUploadParams, UploadKind, } from './storage/types.js';
187
+ export type { PulseVaultArtifactRecord, PulseVaultResolution, PulseVaultStorage, ReserveUploadParams, StoreArtifactParams, UploadKind, } from './storage/types.js';
187
188
  export type { PulseVaultAuthorize, PulseVaultAuthorizeContext, PulseVaultAuthorizePhase, } from './routes/pulsevault.js';
188
189
  export type { PulseVaultOnUploadComplete, PulseVaultOnArtifactEvent, PulseVaultArtifactEvent, } from './lib/pulsevaultTus.js';
189
- export { sniffMp4, createMp4Sniffer, createS3Mp4Sniffer } from './lib/magic.js';
190
- export type { PulseVaultValidatePayload } from './lib/magic.js';
191
- export { ensureWebReady, scanMoovPosition } from './lib/web-ready.js';
192
- export type { WebReadyAction, WebReadyOptions, WebReadyResult, MoovPosition } from './lib/web-ready.js';
193
- export { buildUploadLink } from './lib/deeplinks.js';
190
+ export { sniffMp4, createMp4Sniffer, createS3Mp4Sniffer, sniffVideo, createVideoValidator } from './lib/magic.js';
191
+ export type { PulseVaultValidatePayload, VideoValidatorOptions } from './lib/magic.js';
192
+ export { ensureWebReady, extractPosterFrame, scanMoovPosition, probeVideo, webReadyAvailable, CONFORM_TARGET, CONFORM_VIDEO_EXTENSIONS, } from './lib/web-ready.js';
193
+ export type { WebReadyAction, WebReadyOptions, WebReadyResult, MoovPosition, VideoProbe } from './lib/web-ready.js';
194
+ export { buildUploadLink, PULSE_OPEN_URL } from './lib/deeplinks.js';
194
195
  export type { UploadLinkOptions } from './lib/deeplinks.js';
195
196
  export { issueCapabilityToken, verifyCapabilityToken, issueViewToken, verifyViewToken, createCapabilityAuthorize, } from './lib/capability-token.js';
196
197
  export type { CapabilityTokenClaims, IssueCapabilityTokenOptions, IssueViewTokenOptions, VerifyCapabilityTokenOptions, LookupSecret, } from './lib/capability-token.js';
package/dist/app.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"app.d.ts","sourceRoot":"","sources":["../src/app.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"app.d.ts","sourceRoot":"","sources":["../src/app.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,SAAS,CAAC;AAElD,OAAyB,EAAE,KAAK,mBAAmB,EAAuB,MAAM,wBAAwB,CAAC;AACzG,OAAO,KAAK,EAAE,0BAA0B,EAAE,yBAAyB,EAAE,MAAM,wBAAwB,CAAC;AACpG,OAAO,KAAK,EAAE,yBAAyB,EAAE,MAAM,gBAAgB,CAAC;AAChE,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,qBAAqB,CAAC;AACnE,OAAO,KAAK,EAAE,uBAAuB,EAAE,yBAAyB,EAAE,MAAM,qBAAqB,CAAC;AAC9F,OAAO,EACL,KAAK,0BAA0B,EAGhC,MAAM,oBAAoB,CAAC;AAC5B,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAC5D,OAAO,EASL,KAAK,gCAAgC,EACtC,MAAM,kBAAkB,CAAC;AAE1B;;;;;GAKG;AACH,MAAM,MAAM,sBAAsB,GAAG;IACnC,sEAAsE;IACtE,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IACzB;;;OAGG;IACH,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB,CAAC;AAEF,MAAM,MAAM,uBAAuB,GAAG;IACpC,wFAAwF;IACxF,OAAO,EAAE,iBAAiB,CAAC;IAC3B;;;;;;OAMG;IACH,MAAM,EAAE,MAAM,CAAC;IACf;;;OAGG;IACH,aAAa,EAAE,MAAM,CAAC;IACtB;;;;;;;;;;;OAWG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB;;;;;;;;;;OAUG;IACH,iBAAiB,CAAC,EAAE,gCAAgC,CAAC;IACrD;;;;OAIG;IACH,KAAK,CAAC,EAAE,sBAAsB,CAAC;IAC/B;;;;;;;;;;;;;;;;;;OAkBG;IACH,SAAS,CAAC,EAAE,mBAAmB,CAAC;IAChC;;;;;;;;;;;OAWG;IACH,eAAe,CAAC,EAAE,yBAAyB,CAAC;IAC5C;;;;;;;;OAQG;IACH,gBAAgB,CAAC,EAAE,0BAA0B,CAAC;IAC9C;;;;;OAKG;IACH,eAAe,CAAC,EAAE,yBAAyB,CAAC;IAC5C;;;;;;;OAOG;IACH,aAAa,CAAC,EAAE,uBAAuB,CAAC;IACxC;;;;;;;;OAQG;IACH,SAAS,CAAC,EAAE,0BAA0B,CAAC;IACvC,6GAA6G;IAC7G,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,gJAAgJ;IAChJ,OAAO,CAAC,EAAE;QAAE,WAAW,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,KAAK,CAAC;IAC3C,uGAAuG;IACvG,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,kGAAkG;IAClG,QAAQ,CAAC,EAAE,yBAAyB,GAAG,OAAO,CAAC;IAC/C,kHAAkH;IAClH,iBAAiB,CAAC,EAAE,uBAAuB,GAAG,KAAK,CAAC;IACpD;;;OAGG;IACH,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B;;;;;;OAMG;IACH,sBAAsB,CAAC,EAAE,yBAAyB,CAAC;IACnD;;;;;;OAMG;IACH,uBAAuB,CAAC,EAAE,0BAA0B,CAAC;CACtD,CAAC;;AAmEF,wBAGG;AAEH,OAAO,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAC;AACxD,OAAO,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAChD,YAAY,EAAE,YAAY,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AAC5E,OAAO,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAClD,YAAY,EAAE,SAAS,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AACnE,YAAY,EACV,wBAAwB,EACxB,oBAAoB,EACpB,iBAAiB,EACjB,mBAAmB,EACnB,mBAAmB,EACnB,UAAU,GACX,MAAM,oBAAoB,CAAC;AAC5B,YAAY,EACV,mBAAmB,EACnB,0BAA0B,EAC1B,wBAAwB,GACzB,MAAM,wBAAwB,CAAC;AAChC,YAAY,EACV,0BAA0B,EAC1B,yBAAyB,EACzB,uBAAuB,GACxB,MAAM,wBAAwB,CAAC;AAChC,OAAO,EAAE,QAAQ,EAAE,gBAAgB,EAAE,kBAAkB,EAAE,UAAU,EAAE,oBAAoB,EAAE,MAAM,gBAAgB,CAAC;AAClH,YAAY,EAAE,yBAAyB,EAAE,qBAAqB,EAAE,MAAM,gBAAgB,CAAC;AACvF,OAAO,EACL,cAAc,EACd,kBAAkB,EAClB,gBAAgB,EAChB,UAAU,EACV,iBAAiB,EACjB,cAAc,EACd,wBAAwB,GACzB,MAAM,oBAAoB,CAAC;AAC5B,YAAY,EAAE,cAAc,EAAE,eAAe,EAAE,cAAc,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AACpH,OAAO,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AACrE,YAAY,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAC5D,OAAO,EACL,oBAAoB,EACpB,qBAAqB,EACrB,cAAc,EACd,eAAe,EACf,yBAAyB,GAC1B,MAAM,2BAA2B,CAAC;AACnC,YAAY,EACV,qBAAqB,EACrB,2BAA2B,EAC3B,qBAAqB,EACrB,4BAA4B,EAC5B,YAAY,GACb,MAAM,2BAA2B,CAAC;AACnC,OAAO,EAAE,oBAAoB,EAAE,MAAM,qBAAqB,CAAC;AAC3D,OAAO,EAAE,qBAAqB,EAAE,MAAM,oBAAoB,CAAC;AAC3D,YAAY,EAAE,0BAA0B,EAAE,MAAM,oBAAoB,CAAC;AACrE,YAAY,EACV,uBAAuB,EACvB,kBAAkB,EAClB,yBAAyB,EACzB,qBAAqB,GACtB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EACL,uBAAuB,EACvB,yBAAyB,EACzB,qBAAqB,GACtB,MAAM,mBAAmB,CAAC;AAC3B,YAAY,EAAE,iBAAiB,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AAC3E,YAAY,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AAC5E,YAAY,EACV,cAAc,EACd,wBAAwB,EACxB,eAAe,GAChB,MAAM,WAAW,CAAC;AACnB,YAAY,EACV,sBAAsB,EACtB,uBAAuB,GACxB,MAAM,oBAAoB,CAAC;AAC5B,YAAY,EAAE,yBAAyB,EAAE,MAAM,oBAAoB,CAAC;AACpE,YAAY,EAAE,+BAA+B,EAAE,MAAM,wBAAwB,CAAC;AAC9E,YAAY,EAAE,yBAAyB,EAAE,uBAAuB,EAAE,MAAM,qBAAqB,CAAC;AAC9F,OAAO,EAAE,iBAAiB,EAAE,MAAM,2BAA2B,CAAC"}