@mieweb/pulsevault 0.1.0 → 0.1.2

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 (58) hide show
  1. package/CHANGELOG.md +91 -8
  2. package/OPERATIONS.md +4 -4
  3. package/PROTOCOL.md +66 -27
  4. package/README.md +55 -34
  5. package/dist/app.d.ts +5 -4
  6. package/dist/app.d.ts.map +1 -1
  7. package/dist/app.js +1 -1
  8. package/dist/app.js.map +1 -1
  9. package/dist/core.d.ts +2 -2
  10. package/dist/core.d.ts.map +1 -1
  11. package/dist/core.js +141 -78
  12. package/dist/core.js.map +1 -1
  13. package/dist/lib/authorize.d.ts +1 -1
  14. package/dist/lib/authorize.d.ts.map +1 -1
  15. package/dist/lib/capability-token.d.ts +3 -3
  16. package/dist/lib/capability-token.d.ts.map +1 -1
  17. package/dist/lib/capability-token.js +14 -6
  18. package/dist/lib/capability-token.js.map +1 -1
  19. package/dist/lib/checksum.d.ts.map +1 -1
  20. package/dist/lib/checksum.js +4 -1
  21. package/dist/lib/checksum.js.map +1 -1
  22. package/dist/lib/deeplinks.d.ts +2 -2
  23. package/dist/lib/deeplinks.d.ts.map +1 -1
  24. package/dist/lib/deeplinks.js +2 -2
  25. package/dist/lib/deeplinks.js.map +1 -1
  26. package/dist/lib/errors.d.ts +7 -0
  27. package/dist/lib/errors.d.ts.map +1 -1
  28. package/dist/lib/errors.js +18 -0
  29. package/dist/lib/errors.js.map +1 -1
  30. package/dist/lib/options.d.ts +3 -1
  31. package/dist/lib/options.d.ts.map +1 -1
  32. package/dist/lib/options.js +12 -4
  33. package/dist/lib/options.js.map +1 -1
  34. package/dist/lib/pulsevaultTus.d.ts +1 -0
  35. package/dist/lib/pulsevaultTus.d.ts.map +1 -1
  36. package/dist/lib/pulsevaultTus.js +89 -22
  37. package/dist/lib/pulsevaultTus.js.map +1 -1
  38. package/dist/lib/request.d.ts +2 -0
  39. package/dist/lib/request.d.ts.map +1 -1
  40. package/dist/lib/request.js +6 -0
  41. package/dist/lib/request.js.map +1 -1
  42. package/dist/routes/pulsevault.d.ts +1 -1
  43. package/dist/routes/pulsevault.d.ts.map +1 -1
  44. package/dist/routes/pulsevault.js +4 -3
  45. package/dist/routes/pulsevault.js.map +1 -1
  46. package/dist/storage/local.d.ts +3 -1
  47. package/dist/storage/local.d.ts.map +1 -1
  48. package/dist/storage/local.js +56 -11
  49. package/dist/storage/local.js.map +1 -1
  50. package/dist/storage/s3.d.ts +2 -0
  51. package/dist/storage/s3.d.ts.map +1 -1
  52. package/dist/storage/s3.js +20 -6
  53. package/dist/storage/s3.js.map +1 -1
  54. package/dist/storage/types.d.ts +31 -3
  55. package/dist/storage/types.d.ts.map +1 -1
  56. package/dist/storage/types.js +13 -1
  57. package/dist/storage/types.js.map +1 -1
  58. package/package.json +1 -7
package/CHANGELOG.md CHANGED
@@ -47,6 +47,23 @@ if you've evaluated against an intermediate build.
47
47
  requirement on server implementations generally, not just this package).
48
48
  - Both are covered by new regression tests in `test/plugin.test.mjs` that
49
49
  fail against the pre-fix code and pass against the fix.
50
+ - **Removed `s3rver`** (unmaintained) from the dev/test toolchain, clearing
51
+ its open `npm audit` findings: its dependency chain carried high-severity
52
+ advisories (`busboy`/`dicer`) and forced pinning `fast-xml-parser` via an
53
+ `overrides` entry plus a hand-written API-compat shim. The S3 suite now runs
54
+ against `test/mock-s3.mjs`, a zero-dependency in-memory S3 double
55
+ implementing exactly the eleven operations the code under test uses. Unlike
56
+ s3rver it enforces `If-None-Match: "*"` (412), so the adapter's atomic
57
+ reserve collision guard is now genuinely exercised — a concurrent-reserve
58
+ test that was impossible against s3rver has been added. The `ListParts` shim
59
+ (s3rver never implemented it) is gone too.
60
+ - **5xx responses no longer echo internal error messages.** A `markReady`,
61
+ `onUploadComplete`, or 5xx-class `validatePayload` failure now logs the real
62
+ error server-side and returns a generic body, so a consumer's DB/storage error
63
+ text (schema names, paths, infra detail) can't reach the uploading client. 4xx
64
+ validation rejections (e.g. checksum mismatch) keep their descriptive messages.
65
+ - **Storage subdirectories are created with mode `0o750`** so the upload tree
66
+ isn't world-readable under a permissive umask.
50
67
 
51
68
  ### Breaking
52
69
 
@@ -77,12 +94,24 @@ if you've evaluated against an intermediate build.
77
94
 
78
95
  ### Added
79
96
 
80
- - **`kind: "captions"`** artifact type (default extension `.srt`), running
97
+ - **`name`** — an optional `Upload-Metadata` key carrying a free-form UTF-8
98
+ display title for the artifact (e.g. the draft name typed on the capture
99
+ device). Trimmed and hard-capped server-side (512 chars), persisted to the
100
+ sidecar alongside `relatedTo`/`checksum`, and exposed via the new optional
101
+ `getName` storage method (and `ReserveUploadParams.name`). Display-only
102
+ metadata: it never influences storage paths, routing, or authorization, and
103
+ consumers must escape it for their own output context. Optional everywhere,
104
+ so older clients that don't send it and consumers that don't read it are
105
+ unaffected. Documented in `PROTOCOL.md` §4.1 as a non-normative extension.
106
+ - **`kind: "captions"`** artifact type (default extension `.vtt`), running
81
107
  through the same generic `validatePayload`/`onUploadComplete` hooks as
82
- every other kind.
108
+ every other kind. WebVTT carries word-level inline cue timestamps
109
+ (`<00:00:01.500>word`) for karaoke rendering, and both storage adapters
110
+ serve `.vtt` artifacts with the `text/vtt` content type.
83
111
  - **`relatedTo`** — an optional `Upload-Metadata` key (and matching
84
112
  `ReserveUploadParams`/storage field) linking one artifact to another (e.g.
85
- a video's captions, or a beat belonging to a pulse manifest's session).
113
+ a merged video's captions/beat-manifest/thumbnail, or a clip belonging to a
114
+ segment ordering manifest's session).
86
115
  Storage adapters expose it via the new optional `getRelatedTo` method.
87
116
  - **`checksum`** — an optional `Upload-Metadata` key (`<algorithm>:<hex
88
117
  digest>`) verified post-upload via the new `createChecksumValidator`
@@ -102,14 +131,21 @@ if you've evaluated against an intermediate build.
102
131
  `uploadUnit`, `kinds`, `allowedExtensions`, `maxUploadSize`, and supported
103
132
  checksum algorithms. Lets independently-versioned deployments and clients
104
133
  detect compatibility before pairing.
105
- - **`uploadUnit` plugin option** (`"beat" | "merged"`, default `"beat"`) —
134
+ - **`uploadUnit` plugin option** (`"segment" | "merged"`, default `"segment"`) —
106
135
  purely advertised via `/capabilities`; the operator declares which upload
107
136
  strategy this deployment expects, the client branches on it. Not enforced
108
- by the plugin.
137
+ by the plugin. `"segment"` uploads each recorded clip plus an ordering
138
+ manifest; `"merged"` uploads one pre-merged video plus its captions, a
139
+ beat-timecode manifest, and a thumbnail. (A "beat" is a timecode range on
140
+ the merged timeline, not an upload unit.)
141
+ - **`kind: "thumbnail"`** artifact type (default extensions `.jpg`/`.jpeg`/
142
+ `.png`) — the pulse poster frame uploaded alongside a merged video, running
143
+ through the same generic `validatePayload`/`onUploadComplete` hooks and a
144
+ `thumbnail/` storage subdir like every other kind.
109
145
  - **`buildUploadLink({ uploadUnit })`** (PROTOCOL.md §3, §8) — optional
110
146
  per-session override of the deployment-wide `uploadUnit`, carried on the
111
147
  pairing link itself instead of only `/capabilities`. Lets an operator run
112
- `"beat"` and `"merged"` sessions concurrently (staged rollout, A/B test,
148
+ `"segment"` and `"merged"` sessions concurrently (staged rollout, A/B test,
113
149
  per-tenant policy) without racing a client's separate `/capabilities` fetch
114
150
  against a single, deployment-wide value. Fully backward compatible: omit it
115
151
  and a client falls back to `/capabilities` exactly as before.
@@ -144,8 +180,55 @@ if you've evaluated against an intermediate build.
144
180
  counterpart to `readAll`, now used internally by
145
181
  `createS3ChecksumValidator` (see "Fixed" below).
146
182
 
183
+ ### Changed
184
+
185
+ - Replaced `examples/rn-demo` with two focused Fastify examples:
186
+ `examples/fastify-demo` (the smallest runnable server — no auth, local
187
+ storage, QR pairing; start here) and `examples/fastify-auth-demo`
188
+ (production-shaped reference — capability tokens always on, failing fast at
189
+ boot without `PULSEVAULT_SECRET`, pulse-session grouping via `relatedTo`,
190
+ WebVTT captions, Swagger UI, artifact-event feed). Both pairing pages
191
+ are React (pinned ESM CDN builds, no bundler). Both examples are
192
+ local-storage only; S3 wiring is documented in the README's S3 section
193
+ rather than demonstrated in an example. `npm run e2e` now drives
194
+ `fastify-auth-demo` and additionally asserts the fail-fast boot contract.
195
+ - `examples/fastify-auth-demo` gained a [Better Auth](https://better-auth.com)
196
+ dashboard layer: the human-facing routes (`/deeplinks`, `/pulses`,
197
+ `/events`, `/reserve`) require an email+password session, while the vault
198
+ routes the Pulse app talks to stay capability-token authorized — two auth
199
+ systems for two audiences, per PROTOCOL.md §5. The demo is Postgres-only,
200
+ run through Docker Compose (`compose.yaml` + `Dockerfile`; `npm run db:up`
201
+ + `npm start` for bare-metal dev against the same db), with one Prisma
202
+ schema (`prisma/schema.prisma`, migrations committed and applied by
203
+ `npm start`) holding both the auth tables and an **artifact index**:
204
+ `onUploadComplete` writes each finished upload once, authorized deletes
205
+ prune it, a boot-time reconcile keeps it honest against the filesystem
206
+ sidecars (still the source of truth), and `/pulses` becomes one indexed
207
+ query instead of a per-request sidecar crawl. `npm run e2e` provisions the
208
+ compose db, applies migrations, and covers the 401 gate plus the
209
+ sign-up/sign-in flow.
210
+
147
211
  ### Fixed
148
212
 
213
+ - **Artifact `GET`/`DELETE` handlers now fail closed.** They run on a hijacked
214
+ socket, so a thrown storage error (S3 unreachable, disk fault) previously left
215
+ the socket hung until timeout; each now emits a `500` (or destroys the socket
216
+ if headers are already sent), mirroring the TUS handler. `GET` streams also
217
+ catch a mid-stream read error instead of letting an unhandled `'error'` crash
218
+ the process, and destroy the source stream on client disconnect so file
219
+ descriptors aren't leaked. Covered by `test/fail-closed.test.mjs`.
220
+ - A consumer error carrying an out-of-range `statusCode` (e.g. `42`) now degrades
221
+ to the handler's fallback status instead of crashing `res.writeHead`; a
222
+ malformed request-target that `URL` can't parse returns `400` instead of an
223
+ uncaught throw.
224
+ - Checksum verification requested against an adapter with no local path now
225
+ reports `500` (server misconfiguration) instead of `422` (which wrongly
226
+ implied the client's file was rejected).
227
+ - `verifyCapabilityToken` no longer throws on a token whose payload is valid
228
+ JSON but not an object (e.g. the literal `null`) — it returns `null` like
229
+ every other failure, per its single-failure-shape contract.
230
+ - Bearer-token extraction accepts the auth scheme case-insensitively per
231
+ RFC 7235 (`bearer`/`BEARER`), not only the canonical `Bearer `.
149
232
  - `maxUploadSize` enforcement and the in-progress-upload 404 behavior now
150
233
  have explicit test coverage proving bytes are rejected/hidden at the right
151
234
  point, not just implied by the implementation.
@@ -175,8 +258,8 @@ if you've evaluated against an intermediate build.
175
258
  - **Silent TOCTOU on S3-compatible backends without `IfNoneMatch` support.**
176
259
  `reserveUpload`'s conditional-write collision guard falls back to a
177
260
  weaker check-then-write on backends that reject `IfNoneMatch` (some
178
- S3-compatible stores, and the `s3rver` mock this repo's own test suite
179
- runs against) — this reopens the race between concurrent/retried creates
261
+ S3-compatible stores) — this reopens the race between concurrent/retried
262
+ creates
180
263
  for the same artifactId. There's no way to close it without an external
181
264
  lock, so this is now at least surfaced: a one-time `console.warn` fires
182
265
  per process the first time a deployment falls into this degraded mode.
package/OPERATIONS.md CHANGED
@@ -66,8 +66,8 @@ you have a specific reason to use local disk.
66
66
  `reserveUpload`'s collision guard normally uses `PutObjectCommand`'s
67
67
  `IfNoneMatch: "*"` to atomically reject a second create for an artifactId
68
68
  that already has an upload. Some S3-compatible backends (older/less-complete
69
- implementations; the `s3rver` mock this package's own test suite runs
70
- against) don't support conditional writes and reject that header outright.
69
+ implementations) don't support conditional writes and reject that header
70
+ outright.
71
71
  On those backends, `reserveUpload` falls back to a weaker check-then-write —
72
72
  functionally the same guard, but with the original race reopened: two
73
73
  truly concurrent (or retried) creates for the same artifactId can both pass
@@ -153,8 +153,8 @@ upload routes.
153
153
  ## Backup and restore (local storage)
154
154
 
155
155
  The entire state of the local adapter lives under `workspaceDir`:
156
- `.pulsevault/` (sidecars), `video/`, `project/`, `captions/` (bytes). Back it
157
- up as a normal filesystem tree — there's no separate database to keep in
156
+ `.pulsevault/` (sidecars), `video/`, `project/`, `captions/`, `thumbnail/`
157
+ (bytes). Back it up as a normal filesystem tree — there's no separate database to keep in
158
158
  sync. To restore, copy the tree back and restart; in-progress uploads at
159
159
  backup time will resume correctly via the normal TUS `HEAD`-then-resume path
160
160
  once the client retries, or will simply sit as abandoned partial uploads
package/PROTOCOL.md CHANGED
@@ -37,9 +37,9 @@ The response body MUST be a JSON object with at least the following fields:
37
37
  "protocolVersion": 1,
38
38
  "minSupportedVersion": 1,
39
39
  "maxSupportedVersion": 1,
40
- "uploadUnit": "beat",
41
- "kinds": ["video", "project", "captions"],
42
- "allowedExtensions": { "video": [".mp4"], "project": [".pulse", ".zip"], "captions": [".srt"] },
40
+ "uploadUnit": "segment",
41
+ "kinds": ["video", "project", "captions", "thumbnail"],
42
+ "allowedExtensions": { "video": [".mp4"], "project": [".pulse", ".zip"], "captions": [".vtt"], "thumbnail": [".jpg", ".jpeg", ".png"] },
43
43
  "maxUploadSize": 5368709120,
44
44
  "checksum": { "algorithms": ["sha256", "sha1", "md5"] }
45
45
  }
@@ -52,9 +52,10 @@ The response body MUST be a JSON object with at least the following fields:
52
52
  SHOULD refuse to pair if its own version falls outside this range, and
53
53
  SHOULD show the user an actionable message ("update the app" /
54
54
  "this server needs an update") rather than a generic error.
55
- - `uploadUnit` (`"beat"` or `"merged"`, REQUIRED): which upload strategy this
55
+ - `uploadUnit` (`"segment"` or `"merged"`, REQUIRED): which upload strategy this
56
56
  deployment expects (§8). The client MUST read this before doing any
57
- merge/upload work and branch accordingly.
57
+ merge/upload work and branch accordingly. (Note: "beat" no longer refers to
58
+ an upload unit — it now names a timecode range on the merged timeline; see §8.)
58
59
  - `kinds` (array of string, REQUIRED): artifact kinds this server accepts.
59
60
  - `allowedExtensions` (object, REQUIRED): allowed file extensions per kind.
60
61
  - `maxUploadSize` (integer, REQUIRED): maximum artifact size in bytes.
@@ -74,7 +75,7 @@ the integer from `protocolVersion`.
74
75
  A server presents a pairing link or QR code of the form:
75
76
 
76
77
  ```
77
- pulsecam://?v=1&artifactId=<uuid>&server=<origin>&token=<opaque>&uploadUnit=<beat|merged>
78
+ pulsecam://?v=1&artifactId=<uuid>&server=<origin>&token=<opaque>&uploadUnit=<segment|merged>
78
79
  ```
79
80
 
80
81
  - `v` (REQUIRED): the deep-link schema version. A client MUST refuse and
@@ -96,14 +97,14 @@ pulsecam://?v=1&artifactId=<uuid>&server=<origin>&token=<opaque>&uploadUnit=<bea
96
97
  `artifactId` (or a session it anchors, §5.4), not a standing general
97
98
  credential — see §5.4 for the recommended (but not required) capability-
98
99
  token shape.
99
- - `uploadUnit` (OPTIONAL, `"beat"` or `"merged"`): per-session override of the
100
+ - `uploadUnit` (OPTIONAL, `"segment"` or `"merged"`): per-session override of the
100
101
  deployment-wide value reported by `GET /capabilities` (§2, §8). When
101
102
  present, a client MUST use this value for the session anchored to this
102
103
  link instead of whatever `/capabilities` currently reports, and MUST NOT
103
104
  perform a separate `/capabilities` fetch just to decide merge/upload
104
105
  strategy for it. When absent, a client MUST fall back to `/capabilities`
105
106
  exactly as before this field existed — an operator that never sets it sees
106
- no change in client behavior. This lets one deployment run "beat" and
107
+ no change in client behavior. This lets one deployment run "segment" and
107
108
  "merged" sessions concurrently (e.g. a staged rollout) without racing a
108
109
  single, deployment-wide `/capabilities` value against whichever moment a
109
110
  client happened to fetch it.
@@ -138,9 +139,15 @@ pairs) MUST be parsed for at least:
138
139
  |---|---|---|
139
140
  | `artifactId` | Yes (or a legacy `videoid`/`projectid` alias) | UUID for this artifact. |
140
141
  | `filename` | Yes | Original filename; extension validated against `kind`'s allowed list. |
141
- | `kind` | No, defaults to `video` | `video`, `project`, or `captions`. |
142
+ | `kind` | No, defaults to `video` | `video`, `project`, `captions`, or `thumbnail`. |
142
143
  | `relatedTo` | No | UUID of another artifact this one belongs to (§8). |
143
144
  | `checksum` | No | `<algorithm>:<hex digest>` of the finished file (§6.3). |
145
+ | `name` | No | Free-form UTF-8 display title for the artifact (e.g. the draft name typed on the capture device). Trimmed and length-capped by the server; persisted for consumers to label the artifact. Display metadata only — it MUST NOT influence storage paths, routing, or authorization, and consumers MUST escape it for their output context. |
146
+
147
+ Metadata values are base64 per the TUS spec; a free-form value such as `name`
148
+ MUST be base64 of its **UTF-8** bytes. A client that base64-encodes a raw
149
+ Latin-1 string (e.g. a browser `btoa()`) corrupts any non-ASCII title (accents,
150
+ emoji), so encode the UTF-8 bytes explicitly.
144
151
 
145
152
  A client MUST always send `artifactId` (not only the legacy aliases) on new
146
153
  uploads. A server MUST continue accepting `videoid`/`projectid` as aliases
@@ -239,8 +246,9 @@ expired token. `issuer` prevents a token minted by one deployment from being
239
246
  replayed against a different one that happens to share a secret. A token MAY
240
247
  authorize an artifact other than the one it names if that artifact declares
241
248
  the token's `artifactId` as its `relatedTo` (§8) — this lets one token cover
242
- an entire upload session (a video, its captions, and every beat + manifest
243
- under `uploadUnit: "beat"`) rather than requiring one token per artifact.
249
+ an entire upload session (a merged video plus its captions, beat manifest and
250
+ thumbnail, or every clip plus the ordering manifest under
251
+ `uploadUnit: "segment"`) rather than requiring one token per artifact.
244
252
 
245
253
  This shape is exactly what `@mieweb/pulsevault`'s `issueCapabilityToken`/
246
254
  `verifyCapabilityToken`/`createCapabilityAuthorize` implement, but any server
@@ -282,22 +290,53 @@ clear, specific message rather than a generic failure.
282
290
 
283
291
  ## 8. Artifact relationships (`relatedTo`) and `uploadUnit`
284
292
 
285
- A pulse (a short composed of one or more beats) MAY be uploaded as a single
286
- pre-merged video (`uploadUnit: "merged"`) or as individual per-beat artifacts
287
- plus a manifest (`uploadUnit: "beat"`) — the operator declares which via
288
- `/capabilities` (§2), optionally overridden per session via the pairing
289
- link's `uploadUnit` param (§3); this document does not prefer one over the
290
- other.
291
-
292
- Under `uploadUnit: "beat"`, a client uploads each beat under its own
293
- `artifactId`, plus one manifest artifact (`kind: "project"`, a JSON document
294
- listing the ordered beat `artifactId`s) and, optionally, per-beat captions
295
- (`kind: "captions"`). Every non-primary artifact in the session SHOULD
296
- declare `relatedTo` pointing at the session's anchor `artifactId` (the one
297
- named in the pairing link) so a single capability token can authorize the
298
- whole session (§5.4).
299
-
300
- The relational graph of replies between beats/pulses (who replied to what,
293
+ A pulse (a short composed of one or more recorded **segments**) MAY be uploaded
294
+ as a single pre-merged video (`uploadUnit: "merged"`) or as individual
295
+ per-segment artifacts plus an ordering manifest (`uploadUnit: "segment"`) — the
296
+ operator declares which via `/capabilities` (§2), optionally overridden per
297
+ session via the pairing link's `uploadUnit` param (§3); this document does not
298
+ prefer one over the other.
299
+
300
+ > **Terminology:** a **segment** is a recorded clip (the source unit). A
301
+ > **beat** is a *timecode range on the merged timeline* — one recorded
302
+ > segment's start/end within the merged video — carried only in the merged
303
+ > mode's beat manifest below. (Earlier revisions used "beat" for what is now
304
+ > called the `segment` upload unit; that meaning is retired.)
305
+
306
+ Under `uploadUnit: "segment"`, a client uploads each segment under its own
307
+ `artifactId`, plus exactly one **ordering manifest** artifact (`kind:
308
+ "project"`, e.g. `<draftId>-segments.pulse`, a JSON document listing the
309
+ ordered segment `artifactId`s). Segmented mode carries **no** captions and
310
+ **no** thumbnail.
311
+
312
+ ```json
313
+ { "version": 1, "segments": [ { "artifactId": "<uuid>", "order": 0 } ] }
314
+ ```
315
+
316
+ Under `uploadUnit: "merged"`, a client uploads one pre-merged video as the
317
+ session anchor, plus (all as related artifacts):
318
+
319
+ - **captions** (`kind: "captions"`, `<draftId>.vtt`) — WebVTT for the whole
320
+ merged video, OPTIONAL (absent when the video has no speech or no on-device
321
+ model was available); MAY carry word-level inline cue timestamps like
322
+ `<00:00:01.500>word` for karaoke rendering;
323
+ - a **beat manifest** (`kind: "project"`, e.g. `<draftId>-beats.pulse`) giving
324
+ each recorded segment's precise `startMs`/`endMs` on the merged timeline,
325
+ contiguous and summing to the true merged `durationMs` (groundwork for
326
+ keyframe-aligned deep links / HLS);
327
+ - a **thumbnail** (`kind: "thumbnail"`, `<draftId>.jpg`) — the pulse's poster
328
+ frame.
329
+
330
+ ```json
331
+ { "version": 1, "type": "beat-manifest", "durationMs": 47320,
332
+ "beats": [ { "segmentId": "<local id>", "order": 0, "startMs": 0, "endMs": 4210 } ] }
333
+ ```
334
+
335
+ Every non-anchor artifact in either session SHOULD declare `relatedTo` pointing
336
+ at the session's anchor `artifactId` (the one named in the pairing link) so a
337
+ single capability token can authorize the whole session (§5.4).
338
+
339
+ The relational graph of replies between segments/pulses (who replied to what,
301
340
  rendering a thread) is explicitly **out of scope** for this document. That
302
341
  graph is a query/relational concern for the operator's own systems, built
303
342
  from `relatedTo` and whatever additional metadata the operator chooses to
package/README.md CHANGED
@@ -173,14 +173,28 @@ diagnostics — the Fastify plugin always uses `request.log` for these
173
173
  automatically, but a non-Fastify host has no equivalent to infer one from,
174
174
  so it falls back to `console` when omitted.
175
175
 
176
- See [`examples/express-demo`](examples/express-demo) and
177
- [`examples/meteor-demo`](examples/meteor-demo) for full runnable servers
178
- (pairing page, QR codes, auth demo, local/S3 storage) — the Express and
179
- Meteor counterparts to [`examples/rn-demo`](examples/rn-demo)'s Fastify
180
- demo, both built on `@mieweb/pulsevault/core` instead of the plugin. Both
181
- are verified against the real frameworks, not just the test suite —
182
- `meteor-demo` in particular against a real `meteor create` app, since
183
- Meteor's bundler needed the compatibility fixes described above.
176
+ Four runnable example servers live under [`examples/`](examples):
177
+
178
+ - [`examples/fastify-demo`](examples/fastify-demo) — the smallest runnable
179
+ server: Fastify plugin mount, QR pairing, flat upload listing, no auth,
180
+ local storage. Start here.
181
+ - [`examples/fastify-auth-demo`](examples/fastify-auth-demo) —
182
+ production-shaped Fastify reference, run through Docker Compose (server +
183
+ Postgres, one `docker compose up --build`): capability tokens always on
184
+ (fails fast at boot without `PULSEVAULT_SECRET`), a [Better
185
+ Auth](https://better-auth.com)-protected React dashboard (email+password),
186
+ a Prisma schema holding both the auth tables and an artifact index
187
+ (`onUploadComplete` writes each finished upload once; `/pulses` is one
188
+ query instead of a sidecar crawl), uploads grouped by recording session
189
+ via `relatedTo`, WebVTT captions, Swagger UI, and an `onArtifactEvent`
190
+ live feed.
191
+ - [`examples/express-demo`](examples/express-demo) and
192
+ [`examples/meteor-demo`](examples/meteor-demo) — the same demo on
193
+ `@mieweb/pulsevault/core` instead of the plugin, proving the core needs
194
+ about the same amount of glue code under a different framework. Both are
195
+ verified against the real frameworks, not just the test suite —
196
+ `meteor-demo` in particular against a real `meteor create` app, since
197
+ Meteor's bundler needed the compatibility fixes described above.
184
198
 
185
199
  ## How a pairing + upload session flows
186
200
 
@@ -229,12 +243,12 @@ type PulseVaultPluginOptions = {
229
243
  storage: PulseVaultStorage;
230
244
  prefix: string;
231
245
  maxUploadSize: number;
232
- uploadUnit?: "beat" | "merged"; // default: "beat" — see "Upload unit"
246
+ uploadUnit?: "segment" | "merged"; // default: "segment" — see "Upload unit"
233
247
  decoratorName?: string; // default: "pulseVault"
234
248
  allowedExtensions?:
235
- | string[] // legacy — treated as video-only
236
- | { video?: string[]; project?: string[]; captions?: string[] }; // per-kind (recommended)
237
- // defaults: { video: [".mp4"], project: [".pulse", ".zip"], captions: [".srt"] }
249
+ | string[] // legacy — treated as video-only
250
+ | { video?: string[]; project?: string[]; captions?: string[]; thumbnail?: string[] }; // per-kind (recommended)
251
+ // defaults: { video: [".mp4"], project: [".pulse", ".zip"], captions: [".vtt"], thumbnail: [".jpg", ".jpeg", ".png"] }
238
252
  cache?: PulseVaultCacheOptions;
239
253
  authorize?: PulseVaultAuthorize;
240
254
  validatePayload?: PulseVaultValidatePayload; // runs for every kind; branch on ctx.kind
@@ -263,12 +277,14 @@ Maximum upload size in bytes. Use `Infinity` for no cap.
263
277
 
264
278
  ### Upload unit
265
279
 
266
- `uploadUnit: "beat" | "merged"` — purely advertised via `GET /pulsevault/capabilities`; **the plugin doesn't enforce either.** It tells the client which upload strategy this deployment expects:
280
+ `uploadUnit: "segment" | "merged"` — purely advertised via `GET /pulsevault/capabilities`; **the plugin doesn't enforce either.** It tells the client which upload strategy this deployment expects:
267
281
 
268
- - `"beat"` (default): the client uploads each clip individually (no merge/re-encode pass), plus a `kind=project` manifest artifact for ordering. Lower client-side cost, finer-grained resumability.
269
- - `"merged"`: the client pre-merges a pulse's clips into one video before uploading. Simpler server-side mental model (one artifact per pulse), more client-side work.
282
+ - `"segment"` (default): the client uploads each recorded clip individually (no merge/re-encode pass), plus one `kind=project` ordering manifest (`<draftId>-segments.pulse`). No captions or thumbnail. Lower client-side cost, finer-grained resumability.
283
+ - `"merged"`: the client pre-merges a pulse's clips into one video before uploading, alongside its captions (`kind=captions`), a beat-timecode manifest (`kind=project`, `<draftId>-beats.pulse` — per-segment `startMs`/`endMs` on the merged timeline), and a poster `kind=thumbnail`. Simpler server-side mental model (one primary artifact per pulse), more client-side work.
270
284
 
271
- A client reads this at pairing time, before doing any merge or upload work, and branches accordingly. See `PROTOCOL.md` §8 for the full contract.
285
+ (A "beat" is a timecode range on the merged timeline — one recorded segment's placement within the merged video — not an upload unit. The old `"beat"` upload unit was renamed to `"segment"`.)
286
+
287
+ A client reads `uploadUnit` at pairing time, before doing any merge or upload work, and branches accordingly. See `PROTOCOL.md` §8 for the full contract.
272
288
 
273
289
  Need both strategies live at once instead of one fixed value for the whole deployment? Pass `uploadUnit` to [`buildUploadLink`](#deep-link-helper) per session — it overrides `/capabilities` for that pairing only, with no server-side option to change.
274
290
 
@@ -288,13 +304,13 @@ File extensions accepted per artifact kind. Three accepted forms:
288
304
 
289
305
  ```ts
290
306
  // 1. Omit entirely — uses all three defaults:
291
- // video: [".mp4"] project: [".pulse", ".zip"] captions: [".srt"]
307
+ // video: [".mp4"] project: [".pulse", ".zip"] captions: [".vtt"]
292
308
 
293
309
  // 2. Flat array (legacy) — video-only; project/captions keep their defaults:
294
310
  allowedExtensions: [".mp4"]
295
311
 
296
312
  // 3. Per-kind object — unset keys fall back to their defaults:
297
- allowedExtensions: { video: [".mp4"], project: [".pulse"], captions: [".srt"] }
313
+ allowedExtensions: { video: [".mp4"], project: [".pulse"], captions: [".vtt"] }
298
314
  ```
299
315
 
300
316
  All extensions must include the leading dot and are matched case-insensitively. The `kind` field in `Upload-Metadata` determines which list is checked.
@@ -457,9 +473,9 @@ When the final PATCH lands the plugin runs the following steps in order, for eve
457
473
  "protocolVersion": 1,
458
474
  "minSupportedVersion": 1,
459
475
  "maxSupportedVersion": 1,
460
- "uploadUnit": "beat",
461
- "kinds": ["video", "project", "captions"],
462
- "allowedExtensions": { "video": [".mp4"], "project": [".pulse", ".zip"], "captions": [".srt"] },
476
+ "uploadUnit": "segment",
477
+ "kinds": ["video", "project", "captions", "thumbnail"],
478
+ "allowedExtensions": { "video": [".mp4"], "project": [".pulse", ".zip"], "captions": [".vtt"], "thumbnail": [".jpg", ".jpeg", ".png"] },
463
479
  "maxUploadSize": 5368709120,
464
480
  "checksum": { "algorithms": ["sha256", "sha1", "md5"] }
465
481
  }
@@ -506,7 +522,7 @@ app.post("/pair", async (_req, reply) => {
506
522
  });
507
523
  ```
508
524
 
509
- A token authorizes either the artifact it names, or any artifact that declares that one as its `relatedTo` — so one token issued for a video also covers its captions and (under `uploadUnit: "beat"`) every beat and the manifest in the same session, without minting a token per artifact. See `PROTOCOL.md` §5.4 for the full claim shape (`kid`/`iat`/`exp`/`issuer`/`artifactId`) and rationale.
525
+ A token authorizes either the artifact it names, or any artifact that declares that one as its `relatedTo` — so one token issued for a merged video also covers its captions, beat manifest and thumbnail (or, under `uploadUnit: "segment"`, every clip and the ordering manifest) in the same session, without minting a token per artifact. See `PROTOCOL.md` §5.4 for the full claim shape (`kid`/`iat`/`exp`/`issuer`/`artifactId`) and rationale.
510
526
 
511
527
  ## Upload-Metadata protocol
512
528
 
@@ -520,11 +536,14 @@ The TUS `Upload-Metadata` header is a comma-separated list of `<key> <base64>` p
520
536
  | `kind` | No | `video` (default), `project`, or `captions`. Determines the storage subdir and which hooks fire. |
521
537
  | `relatedTo` | No | UUID of another artifact this one belongs to (e.g. a video's captions). Lets one capability token authorize a whole session. |
522
538
  | `checksum` | No | `<algorithm>:<hex digest>` of the finished file, verified by `createChecksumValidator`/`createS3ChecksumValidator` if configured. |
539
+ | `name` | No | Free-form UTF-8 display title (e.g. the draft name typed on the capture device). Trimmed + length-capped, persisted to the sidecar, and readable via `storage.getName(artifactId)`. Display-only — never used for storage paths or authorization; escape it for your output context. |
540
+
541
+ > Base64 the **UTF-8 bytes** of each value. `filename`/`kind`/`artifactId` are ASCII, but a free-form `name` can carry accents or emoji — a browser `btoa()` (Latin-1) corrupts those, so encode via `Buffer`/`TextEncoder` (or your platform's UTF-8-safe base64) for the `name` value.
523
542
 
524
543
  Example (`kind=captions`, linked to a video):
525
544
 
526
545
  ```
527
- Upload-Metadata: artifactId <base64(uuid)>, filename <base64("clip.srt")>, kind <base64("captions")>, relatedTo <base64(videoArtifactId)>
546
+ Upload-Metadata: artifactId <base64(uuid)>, filename <base64("clip.vtt")>, kind <base64("captions")>, relatedTo <base64(videoArtifactId)>
528
547
  ```
529
548
 
530
549
  ## Local storage
@@ -544,7 +563,7 @@ The local adapter writes uploads into flat kind-scoped subdirectories. Downstrea
544
563
 
545
564
  ```text
546
565
  <workspaceRoot>/
547
- .pulsevault/<id>.json # sidecar: { version, ext, filename, status, kind, relatedTo, checksum }
566
+ .pulsevault/<id>.json # sidecar: { version, ext, filename, status, kind, relatedTo, checksum, name }
548
567
  video/<id><ext> # video upload bytes (kind="video")
549
568
  video/<id><ext>.json # @tus/file-store offset/metadata sidecar
550
569
  project/<id><ext> # project bundle bytes (kind="project")
@@ -555,7 +574,7 @@ The local adapter writes uploads into flat kind-scoped subdirectories. Downstrea
555
574
 
556
575
  `status` is `"uploading"` between `reserveUpload` and the successful final PATCH; `"ready"` thereafter. `GET /artifacts/:id` only serves `"ready"` uploads. `kind` defaults to `"video"` when absent (back-compat with pre-kind sidecars).
557
576
 
558
- The adapter exposes `storage.workspaceRoot` (absolute, resolved from `workspaceDir`) so consumers can compute per-resource paths without re-implementing the layout. `storage.getKind(id)` returns `"video" | "project" | "captions" | null`; `storage.getRelatedTo(id)` and `storage.getChecksum(id)` return the corresponding sidecar fields, each `null` if absent/unknown.
577
+ The adapter exposes `storage.workspaceRoot` (absolute, resolved from `workspaceDir`) so consumers can compute per-resource paths without re-implementing the layout. `storage.getKind(id)` returns `"video" | "project" | "captions" | null`; `storage.getRelatedTo(id)`, `storage.getChecksum(id)`, and `storage.getName(id)` return the corresponding sidecar fields, each `null` if absent/unknown.
559
578
 
560
579
  **Horizontal scaling**: this adapter requires sticky-session routing or a shared filesystem across instances — see `OPERATIONS.md`.
561
580
 
@@ -640,14 +659,14 @@ Credentials are optional — omit `accessKeyId`/`secretAccessKey` to use the AWS
640
659
 
641
660
  ```text
642
661
  <bucket>/
643
- .pulsevault/<id>.json # metadata sidecar: { version, ext, filename, status, kind, relatedTo, checksum }
662
+ .pulsevault/<id>.json # metadata sidecar: { version, ext, filename, status, kind, relatedTo, checksum, name }
644
663
  video/<id><ext> # finalized video object (kind="video")
645
664
  project/<id><ext> # finalized project object (kind="project")
646
665
  captions/<id><ext> # finalized captions object (kind="captions")
647
666
  <key>.info # @tus/s3-store multipart bookkeeping (transient)
648
667
  ```
649
668
 
650
- `resolve()` only returns a presigned URL once the sidecar `status` is `"ready"` (after the final byte lands and `validatePayload` passes), so in-progress or rejected uploads are never served. `getKind(id)` returns the kind without a full resolve; `getRelatedTo(id)`/`getChecksum(id)` mirror the local adapter.
669
+ `resolve()` only returns a presigned URL once the sidecar `status` is `"ready"` (after the final byte lands and `validatePayload` passes), so in-progress or rejected uploads are never served. `getKind(id)` returns the kind without a full resolve; `getRelatedTo(id)`/`getChecksum(id)`/`getName(id)` mirror the local adapter.
651
670
 
652
671
  ### Options
653
672
 
@@ -664,7 +683,7 @@ Credentials are optional — omit `accessKeyId`/`secretAccessKey` to use the AWS
664
683
  | `metaCacheLimit` | `10000` | Caps the in-memory metadata cache before evicting the oldest entry. |
665
684
  | `clientConfig` | — | Advanced: extra `S3ClientConfig` merged into the client. |
666
685
 
667
- > The runnable demo wires these to environment variables — see [`examples/rn-demo/.env.example`](examples/rn-demo/.env.example) for the full list (`STORAGE`, `S3_BUCKET`, `S3_ENDPOINT`, `AWS_REGION`, `S3_ACCESS_KEY_ID`, `S3_SECRET_ACCESS_KEY`, …).
686
+ > A typical deployment wires these to environment variables (e.g. `S3_BUCKET`, `S3_ENDPOINT`, `AWS_REGION`, `S3_ACCESS_KEY_ID`, `S3_SECRET_ACCESS_KEY`) and picks the storage adapter with a `STORAGE=local|s3` switch — the options table above maps one-to-one onto `createS3Storage(...)`.
668
687
 
669
688
  ### Payload validation on remote storage
670
689
 
@@ -692,9 +711,9 @@ const storage: PulseVaultStorage = {
692
711
  async shutdown() {
693
712
  /* optional teardown */
694
713
  },
695
- async reserveUpload({ artifactId, filename, ext, kind, relatedTo, checksum }) {
714
+ async reserveUpload({ artifactId, filename, ext, kind, relatedTo, checksum, name }) {
696
715
  // Called by the TUS naming function. Return the file id for the datastore.
697
- await db.createArtifact({ artifactId, filename, kind, relatedTo, checksum, status: "uploading" });
716
+ await db.createArtifact({ artifactId, filename, kind, relatedTo, checksum, name, status: "uploading" });
698
717
  return `${kind}/${artifactId}${ext}`;
699
718
  },
700
719
  async resolve(artifactId): Promise<PulseVaultResolution | null> {
@@ -718,10 +737,12 @@ const storage: PulseVaultStorage = {
718
737
  const result = await db.deleteArtifact(artifactId);
719
738
  return result.deleted;
720
739
  },
721
- // Optional — only needed if you use relatedTo/checksum-aware hooks/validators:
740
+ // Optional — relatedTo/checksum-aware hooks/validators, plus getName so a
741
+ // display title is available via storage.getName:
722
742
  async getKind(artifactId) { return (await db.findArtifact(artifactId))?.kind ?? null; },
723
743
  async getRelatedTo(artifactId) { return (await db.findArtifact(artifactId))?.relatedTo ?? null; },
724
744
  async getChecksum(artifactId) { return (await db.findArtifact(artifactId))?.checksum ?? null; },
745
+ async getName(artifactId) { return (await db.findArtifact(artifactId))?.name ?? null; },
725
746
  };
726
747
  ```
727
748
 
@@ -745,7 +766,7 @@ const uploadLink = buildUploadLink({
745
766
  // pulsecam://?v=1&artifactId=...&server=https%3A%2F%2Fexample.com%2Fpulsevault&token=secret&uploadUnit=merged
746
767
  ```
747
768
 
748
- `uploadUnit` on the link is a **per-session override** of whatever `GET /capabilities` currently reports (PROTOCOL.md §3, §8). Omit it and nothing changes — the client falls back to `/capabilities` exactly as before. Set it when you want "beat" and "merged" sessions live at the same time (staged rollout, A/B test, per-tenant policy) instead of one fixed value for the whole deployment — `/capabilities` can only ever report one current value, and a client reading it separately from opening the link is racing whatever the server happened to be serving at that moment, not the value this specific session was paired under.
769
+ `uploadUnit` on the link is a **per-session override** of whatever `GET /capabilities` currently reports (PROTOCOL.md §3, §8). Omit it and nothing changes — the client falls back to `/capabilities` exactly as before. Set it when you want "segment" and "merged" sessions live at the same time (staged rollout, A/B test, per-tenant policy) instead of one fixed value for the whole deployment — `/capabilities` can only ever report one current value, and a client reading it separately from opening the link is racing whatever the server happened to be serving at that moment, not the value this specific session was paired under.
749
770
 
750
771
  ## Tests
751
772
 
@@ -763,14 +784,14 @@ Runs a Node `--test` suite against the built plugin. Coverage includes:
763
784
  - `kind=project` and `kind=captions` happy paths — correct subdir, `Content-Type`, sidecar, `relatedTo` linking
764
785
  - Extension mismatch rejections in both directions
765
786
  - `artifactId`/`videoid`/`projectid` metadata aliases
766
- - `getKind()`/`getRelatedTo()`/`getChecksum()` storage methods
787
+ - `getKind()`/`getRelatedTo()`/`getChecksum()`/`getName()` storage methods
767
788
  - Legacy sidecars (no `kind` field) default to `"video"`, readable through the new generic route with no data migration
768
789
  - Sidecar corruption recovery
769
790
  - `allowedExtensions` object form
770
791
  - `createChecksumValidator`/`createS3ChecksumValidator`: matching digest accepted, mismatch rejected with cleanup
771
792
  - `issueCapabilityToken`/`verifyCapabilityToken`/`createCapabilityAuthorize`: round-trip, tampered signature/payload, expiry + clock tolerance, unknown `kid`, issuer mismatch, key-rotation overlap, `relatedTo`-based session authorization — both as fast unit tests (no server) and wired into real HTTP requests
772
793
  - `GET /capabilities`
773
- - S3/R2 backend (`createS3Storage`): full resumable upload → presigned-redirect playback, `createS3Mp4Sniffer`, `createS3ChecksumValidator`, `DELETE`, `kind=project`/`captions`, run against an in-process [s3rver](https://github.com/jamhall/s3rver) mock (no cloud credentials needed)
794
+ - S3/R2 backend (`createS3Storage`): full resumable upload → presigned-redirect playback, `createS3Mp4Sniffer`, `createS3ChecksumValidator`, `DELETE`, `kind=project`/`captions`, run against an in-process zero-dependency S3 mock (`test/mock-s3.mjs`, no cloud credentials needed)
774
795
  - `@mieweb/pulsevault/core` (the framework-agnostic entry point): the same protocol suite re-run against a bare `http.createServer` wrapping `core.handler`, plus an Express-specific smoke test proving `app.use(prefix, handler)` composition
775
796
 
776
797
  ## Accessing storage outside the plugin routes
package/dist/app.d.ts CHANGED
@@ -40,13 +40,14 @@ export type PulseVaultPluginOptions = {
40
40
  */
41
41
  maxUploadSize: number;
42
42
  /**
43
- * Which upload strategy this deployment expects: `"beat"` uploads each
43
+ * Which upload strategy this deployment expects: `"segment"` uploads each
44
44
  * clip individually (no client-side merge/re-encode pass) plus a manifest
45
45
  * artifact for ordering, while `"merged"` expects one pre-merged video per
46
- * pulse. Purely advertised via `GET /capabilities` for the client to branch
47
- * on — `pulsevault` doesn't enforce either. Defaults to `"beat"`.
46
+ * pulse (plus its captions, beat manifest and thumbnail). Purely advertised
47
+ * via `GET /capabilities` for the client to branch on — `pulsevault` doesn't
48
+ * enforce either. Defaults to `"segment"`.
48
49
  */
49
- uploadUnit?: 'beat' | 'merged';
50
+ uploadUnit?: 'segment' | 'merged';
50
51
  /**
51
52
  * Fastify instance decorator name under which the storage adapter is
52
53
  * exposed. Defaults to `"pulseVault"`. Override when registering this
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":"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"}
package/dist/app.js CHANGED
@@ -22,7 +22,7 @@ const app = async (fastify, opts) => {
22
22
  prefix: opts.prefix,
23
23
  storage: opts.storage,
24
24
  maxUploadSize: opts.maxUploadSize,
25
- uploadUnit: opts.uploadUnit ?? 'beat',
25
+ uploadUnit: opts.uploadUnit ?? 'segment',
26
26
  allowedExtensions,
27
27
  cache: opts.cache,
28
28
  authorize: opts.authorize,
package/dist/app.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"app.js","sourceRoot":"","sources":["../src/app.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,MAAM,gBAAgB,CAAC;AAChC,OAAO,gBAAgB,EAAE,EAA4B,MAAM,wBAAwB,CAAC;AAIpF,OAAO,EACL,0BAA0B,EAC1B,gBAAgB,EAChB,qBAAqB,EACrB,kBAAkB,EAClB,yBAAyB,EACzB,iCAAiC,EACjC,sBAAsB,EACtB,uBAAuB,GACxB,MAAM,kBAAkB,CAAC;AAoJ1B,MAAM,sBAAsB,GAAG,YAAY,CAAC;AAE5C,MAAM,GAAG,GAAgD,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,EAAE;IAC/E,gBAAgB,CAAC,IAAI,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC;IACxC,qBAAqB,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC;IAC1C,kBAAkB,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;IACpC,yBAAyB,CAAC,IAAI,CAAC,iBAAiB,CAAC,CAAC;IAClD,iCAAiC,CAAC,IAAI,CAAC,CAAC;IAExC,2EAA2E;IAC3E,wEAAwE;IACxE,+BAA+B;IAC/B,OAAO,CAAC,OAAO,CAAC,SAAS,EAAE,KAAK,IAAI,EAAE;QACpC,MAAM,IAAI,CAAC,OAAO,CAAC,QAAQ,EAAE,EAAE,CAAC;IAClC,CAAC,CAAC,CAAC;IACH,MAAM,IAAI,CAAC,OAAO,CAAC,UAAU,EAAE,EAAE,CAAC;IAElC,MAAM,aAAa,GAAG,IAAI,CAAC,aAAa,IAAI,sBAAsB,CAAC;IACnE,MAAM,iBAAiB,GAAG,0BAA0B,CAAC,IAAI,CAAC,iBAAiB,CAAC,CAAC;IAE7E,OAAO,CAAC,QAAQ,CAAC,aAAa,EAAE,IAAI,CAAC,OAAO,CAAC,CAAC;IAE9C,MAAM,OAAO,CAAC,QAAQ,CAAC,gBAAgB,EAAE;QACvC,MAAM,EAAE,IAAI,CAAC,MAAM;QACnB,OAAO,EAAE,IAAI,CAAC,OAAO;QACrB,aAAa,EAAE,IAAI,CAAC,aAAa;QACjC,UAAU,EAAE,IAAI,CAAC,UAAU,IAAI,MAAM;QACrC,iBAAiB;QACjB,KAAK,EAAE,IAAI,CAAC,KAAK;QACjB,SAAS,EAAE,IAAI,CAAC,SAAS;QACzB,eAAe,EAAE,sBAAsB,CAAC,IAAI,CAAC,eAAe,EAAE,IAAI,CAAC,sBAAsB,CAAC;QAC1F,gBAAgB,EAAE,uBAAuB,CAAC,IAAI,CAAC,gBAAgB,EAAE,IAAI,CAAC,uBAAuB,CAAC;QAC9F,eAAe,EAAE,IAAI,CAAC,eAAe;KACtC,CAAC,CAAC;AACL,CAAC,CAAC;AAEF,eAAe,EAAE,CAAC,GAAG,EAAE;IACrB,IAAI,EAAE,YAAY;IAClB,OAAO,EAAE,KAAK;CACf,CAAC,CAAC;AAEH,OAAO,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAC;AAExD,OAAO,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAkBlD,OAAO,EAAE,QAAQ,EAAE,gBAAgB,EAAE,kBAAkB,EAAE,MAAM,gBAAgB,CAAC;AAEhF,OAAO,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AAErD,OAAO,EACL,oBAAoB,EACpB,qBAAqB,EACrB,yBAAyB,GAC1B,MAAM,2BAA2B,CAAC;AAOnC,OAAO,EACL,uBAAuB,EACvB,yBAAyB,EACzB,qBAAqB,GACtB,MAAM,mBAAmB,CAAC"}
1
+ {"version":3,"file":"app.js","sourceRoot":"","sources":["../src/app.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,MAAM,gBAAgB,CAAC;AAChC,OAAO,gBAAgB,EAAE,EAA4B,MAAM,wBAAwB,CAAC;AAIpF,OAAO,EACL,0BAA0B,EAC1B,gBAAgB,EAChB,qBAAqB,EACrB,kBAAkB,EAClB,yBAAyB,EACzB,iCAAiC,EACjC,sBAAsB,EACtB,uBAAuB,GACxB,MAAM,kBAAkB,CAAC;AAqJ1B,MAAM,sBAAsB,GAAG,YAAY,CAAC;AAE5C,MAAM,GAAG,GAAgD,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,EAAE;IAC/E,gBAAgB,CAAC,IAAI,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC;IACxC,qBAAqB,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC;IAC1C,kBAAkB,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;IACpC,yBAAyB,CAAC,IAAI,CAAC,iBAAiB,CAAC,CAAC;IAClD,iCAAiC,CAAC,IAAI,CAAC,CAAC;IAExC,2EAA2E;IAC3E,wEAAwE;IACxE,+BAA+B;IAC/B,OAAO,CAAC,OAAO,CAAC,SAAS,EAAE,KAAK,IAAI,EAAE;QACpC,MAAM,IAAI,CAAC,OAAO,CAAC,QAAQ,EAAE,EAAE,CAAC;IAClC,CAAC,CAAC,CAAC;IACH,MAAM,IAAI,CAAC,OAAO,CAAC,UAAU,EAAE,EAAE,CAAC;IAElC,MAAM,aAAa,GAAG,IAAI,CAAC,aAAa,IAAI,sBAAsB,CAAC;IACnE,MAAM,iBAAiB,GAAG,0BAA0B,CAAC,IAAI,CAAC,iBAAiB,CAAC,CAAC;IAE7E,OAAO,CAAC,QAAQ,CAAC,aAAa,EAAE,IAAI,CAAC,OAAO,CAAC,CAAC;IAE9C,MAAM,OAAO,CAAC,QAAQ,CAAC,gBAAgB,EAAE;QACvC,MAAM,EAAE,IAAI,CAAC,MAAM;QACnB,OAAO,EAAE,IAAI,CAAC,OAAO;QACrB,aAAa,EAAE,IAAI,CAAC,aAAa;QACjC,UAAU,EAAE,IAAI,CAAC,UAAU,IAAI,SAAS;QACxC,iBAAiB;QACjB,KAAK,EAAE,IAAI,CAAC,KAAK;QACjB,SAAS,EAAE,IAAI,CAAC,SAAS;QACzB,eAAe,EAAE,sBAAsB,CAAC,IAAI,CAAC,eAAe,EAAE,IAAI,CAAC,sBAAsB,CAAC;QAC1F,gBAAgB,EAAE,uBAAuB,CAAC,IAAI,CAAC,gBAAgB,EAAE,IAAI,CAAC,uBAAuB,CAAC;QAC9F,eAAe,EAAE,IAAI,CAAC,eAAe;KACtC,CAAC,CAAC;AACL,CAAC,CAAC;AAEF,eAAe,EAAE,CAAC,GAAG,EAAE;IACrB,IAAI,EAAE,YAAY;IAClB,OAAO,EAAE,KAAK;CACf,CAAC,CAAC;AAEH,OAAO,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAC;AAExD,OAAO,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAkBlD,OAAO,EAAE,QAAQ,EAAE,gBAAgB,EAAE,kBAAkB,EAAE,MAAM,gBAAgB,CAAC;AAEhF,OAAO,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AAErD,OAAO,EACL,oBAAoB,EACpB,qBAAqB,EACrB,yBAAyB,GAC1B,MAAM,2BAA2B,CAAC;AAOnC,OAAO,EACL,uBAAuB,EACvB,yBAAyB,EACzB,qBAAqB,GACtB,MAAM,mBAAmB,CAAC"}
package/dist/core.d.ts CHANGED
@@ -38,8 +38,8 @@ export type PulseVaultCoreOptions = {
38
38
  stripBasePath?: boolean;
39
39
  /** Max TUS upload size in bytes. Required — consumers must choose an explicit cap. Use `Infinity` for no cap. */
40
40
  maxUploadSize: number;
41
- /** Which upload strategy this deployment expects. Purely advertised via `GET /capabilities`. Defaults to `"beat"`. */
42
- uploadUnit?: 'beat' | 'merged';
41
+ /** Which upload strategy this deployment expects. Purely advertised via `GET /capabilities`. Defaults to `"segment"`. */
42
+ uploadUnit?: 'segment' | 'merged';
43
43
  /** File extensions allowed per artifact kind. See the Fastify plugin's `allowedExtensions` for the full shape. */
44
44
  allowedExtensions?: PulseVaultAllowedExtensionsInput;
45
45
  /** Cache-control options forwarded to `@fastify/send` for the GET route. */