@mieweb/pulsevault 0.0.2 → 0.1.1

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 (76) hide show
  1. package/CHANGELOG.md +258 -0
  2. package/LICENSE +15 -39
  3. package/OPERATIONS.md +223 -0
  4. package/PROTOCOL.md +345 -0
  5. package/README.md +548 -122
  6. package/augment.d.ts +1 -0
  7. package/augment.js +3 -0
  8. package/core.d.ts +1 -0
  9. package/core.js +7 -0
  10. package/dist/app.d.ts +91 -34
  11. package/dist/app.d.ts.map +1 -1
  12. package/dist/app.js +23 -33
  13. package/dist/app.js.map +1 -1
  14. package/dist/augment.d.ts +14 -7
  15. package/dist/augment.d.ts.map +1 -1
  16. package/dist/augment.js.map +1 -1
  17. package/dist/core.d.ts +108 -0
  18. package/dist/core.d.ts.map +1 -0
  19. package/dist/core.js +494 -0
  20. package/dist/core.js.map +1 -0
  21. package/dist/lib/authorize.d.ts +19 -0
  22. package/dist/lib/authorize.d.ts.map +1 -0
  23. package/dist/lib/authorize.js +2 -0
  24. package/dist/lib/authorize.js.map +1 -0
  25. package/dist/lib/capability-token.d.ts +80 -0
  26. package/dist/lib/capability-token.d.ts.map +1 -0
  27. package/dist/lib/capability-token.js +134 -0
  28. package/dist/lib/capability-token.js.map +1 -0
  29. package/dist/lib/checksum.d.ts +45 -0
  30. package/dist/lib/checksum.d.ts.map +1 -0
  31. package/dist/lib/checksum.js +105 -0
  32. package/dist/lib/checksum.js.map +1 -0
  33. package/dist/lib/deeplinks.d.ts +37 -21
  34. package/dist/lib/deeplinks.d.ts.map +1 -1
  35. package/dist/lib/deeplinks.js +54 -13
  36. package/dist/lib/deeplinks.js.map +1 -1
  37. package/dist/lib/errors.d.ts +7 -0
  38. package/dist/lib/errors.d.ts.map +1 -1
  39. package/dist/lib/errors.js +18 -0
  40. package/dist/lib/errors.js.map +1 -1
  41. package/dist/lib/magic.d.ts +30 -5
  42. package/dist/lib/magic.d.ts.map +1 -1
  43. package/dist/lib/magic.js +51 -14
  44. package/dist/lib/magic.js.map +1 -1
  45. package/dist/lib/options.d.ts +41 -0
  46. package/dist/lib/options.d.ts.map +1 -0
  47. package/dist/lib/options.js +111 -0
  48. package/dist/lib/options.js.map +1 -0
  49. package/dist/lib/pulsevaultTus.d.ts +53 -26
  50. package/dist/lib/pulsevaultTus.d.ts.map +1 -1
  51. package/dist/lib/pulsevaultTus.js +174 -55
  52. package/dist/lib/pulsevaultTus.js.map +1 -1
  53. package/dist/lib/request.d.ts +26 -0
  54. package/dist/lib/request.d.ts.map +1 -0
  55. package/dist/lib/request.js +22 -0
  56. package/dist/lib/request.js.map +1 -0
  57. package/dist/lib/uuid.d.ts.map +1 -1
  58. package/dist/lib/uuid.js +1 -1
  59. package/dist/lib/uuid.js.map +1 -1
  60. package/dist/routes/pulsevault.d.ts +18 -16
  61. package/dist/routes/pulsevault.d.ts.map +1 -1
  62. package/dist/routes/pulsevault.js +121 -251
  63. package/dist/routes/pulsevault.js.map +1 -1
  64. package/dist/storage/local.d.ts +31 -10
  65. package/dist/storage/local.d.ts.map +1 -1
  66. package/dist/storage/local.js +205 -84
  67. package/dist/storage/local.js.map +1 -1
  68. package/dist/storage/s3.d.ts +109 -0
  69. package/dist/storage/s3.d.ts.map +1 -0
  70. package/dist/storage/s3.js +438 -0
  71. package/dist/storage/s3.js.map +1 -0
  72. package/dist/storage/types.d.ts +80 -17
  73. package/dist/storage/types.d.ts.map +1 -1
  74. package/dist/storage/types.js +13 -1
  75. package/dist/storage/types.js.map +1 -1
  76. package/package.json +29 -7
package/PROTOCOL.md ADDED
@@ -0,0 +1,345 @@
1
+ # Pulse Upload Protocol
2
+
3
+ **Version 1**
4
+
5
+ This document specifies the wire contract between a Pulse client and any
6
+ server that wants to receive uploads from it. It is independent of
7
+ `@mieweb/pulsevault` — a server that implements everything in this document
8
+ is Pulse-compatible whether or not it uses this package. `@mieweb/pulsevault`
9
+ is the reference implementation.
10
+
11
+ The key words **MUST**, **MUST NOT**, **REQUIRED**, **SHOULD**, **SHOULD
12
+ NOT**, and **MAY** in this document are to be interpreted as described in
13
+ [RFC 2119](https://www.ietf.org/rfc/rfc2119.txt).
14
+
15
+ ## 1. Overview
16
+
17
+ A server (the "operator") mints an `artifactId` and a capability `token`,
18
+ then presents them to a Pulse client via a deep link or QR code (§3). The
19
+ client uploads the artifact's bytes to the server using the [TUS v1
20
+ resumable upload protocol](https://tus.io/protocols/resumable-upload) (§4),
21
+ authenticated with the token on every request (§5). The server validates,
22
+ stores, and serves the finished artifact (§6, §7).
23
+
24
+ No central authority is involved. Any server implementing this document MAY
25
+ be used by any Pulse client. Lifecycle decisions — `artifactId` minting,
26
+ token issuance, expiry policy, secret rotation, revocation — are entirely the
27
+ operator's responsibility (§5.4). This document fixes the wire shape only,
28
+ never the policy behind it.
29
+
30
+ ## 2. Capability discovery
31
+
32
+ A server MUST expose an unauthenticated `GET {prefix}/capabilities` endpoint.
33
+ The response body MUST be a JSON object with at least the following fields:
34
+
35
+ ```json
36
+ {
37
+ "protocolVersion": 1,
38
+ "minSupportedVersion": 1,
39
+ "maxSupportedVersion": 1,
40
+ "uploadUnit": "segment",
41
+ "kinds": ["video", "project", "captions", "thumbnail"],
42
+ "allowedExtensions": { "video": [".mp4"], "project": [".pulse", ".zip"], "captions": [".vtt"], "thumbnail": [".jpg", ".jpeg", ".png"] },
43
+ "maxUploadSize": 5368709120,
44
+ "checksum": { "algorithms": ["sha256", "sha1", "md5"] }
45
+ }
46
+ ```
47
+
48
+ - `protocolVersion` (integer, REQUIRED): the version of this document the
49
+ server implements.
50
+ - `minSupportedVersion`/`maxSupportedVersion` (integer, REQUIRED): the
51
+ inclusive range of client protocol versions this server accepts. A client
52
+ SHOULD refuse to pair if its own version falls outside this range, and
53
+ SHOULD show the user an actionable message ("update the app" /
54
+ "this server needs an update") rather than a generic error.
55
+ - `uploadUnit` (`"segment"` or `"merged"`, REQUIRED): which upload strategy this
56
+ deployment expects (§8). The client MUST read this before doing any
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.)
59
+ - `kinds` (array of string, REQUIRED): artifact kinds this server accepts.
60
+ - `allowedExtensions` (object, REQUIRED): allowed file extensions per kind.
61
+ - `maxUploadSize` (integer, REQUIRED): maximum artifact size in bytes.
62
+ - `checksum.algorithms` (array of string, OPTIONAL): digest algorithms this
63
+ server can verify (§6.3). Absent or empty means the server does not
64
+ support checksum verification. Note for implementers: this field
65
+ describes capability, not whether verification is actually wired in for
66
+ every upload — a server MAY list an algorithm it's capable of checking
67
+ even for a deployment where the operator hasn't enabled that check.
68
+
69
+ A server response to this endpoint MUST NOT include any secret. Every other
70
+ response from the server MUST include a `Protocol-Version` header carrying
71
+ the integer from `protocolVersion`.
72
+
73
+ ## 3. Pairing (deep link)
74
+
75
+ A server presents a pairing link or QR code of the form:
76
+
77
+ ```
78
+ pulsecam://?v=1&artifactId=<uuid>&server=<origin>&token=<opaque>&uploadUnit=<segment|merged>
79
+ ```
80
+
81
+ - `v` (REQUIRED): the deep-link schema version. A client MUST refuse and
82
+ explain (not silently misparse) a `v` it doesn't recognize.
83
+ - `artifactId` (REQUIRED): a UUID minted by the server. The client uses this
84
+ directly as the `artifactId` in `Upload-Metadata` (§4) — it MUST NOT
85
+ perform a separate "reserve" round-trip when an `artifactId` is already
86
+ present in the link.
87
+ - `server` (REQUIRED): the full **base URL** to upload to — origin plus
88
+ whatever path prefix the operator mounted the upload routes under (e.g.
89
+ `https://vault.example.org/pulsevault`), not merely the origin. A client
90
+ MUST treat every route in §2/§4/§6 as `${server}/<path>` and MUST NOT
91
+ assume or invent a separate prefix of its own. A client MUST reject any
92
+ `server` value that is not `https://`, with the sole exception of
93
+ `http://localhost` or a private IP literal for local development — this
94
+ exception MUST NOT be silently extended to any other plaintext origin.
95
+ - `token` (OPTIONAL): an opaque credential forwarded on every subsequent
96
+ request (§5). Servers SHOULD treat this as short-lived and scoped to the
97
+ `artifactId` (or a session it anchors, §5.4), not a standing general
98
+ credential — see §5.4 for the recommended (but not required) capability-
99
+ token shape.
100
+ - `uploadUnit` (OPTIONAL, `"segment"` or `"merged"`): per-session override of the
101
+ deployment-wide value reported by `GET /capabilities` (§2, §8). When
102
+ present, a client MUST use this value for the session anchored to this
103
+ link instead of whatever `/capabilities` currently reports, and MUST NOT
104
+ perform a separate `/capabilities` fetch just to decide merge/upload
105
+ strategy for it. When absent, a client MUST fall back to `/capabilities`
106
+ exactly as before this field existed — an operator that never sets it sees
107
+ no change in client behavior. This lets one deployment run "segment" and
108
+ "merged" sessions concurrently (e.g. a staged rollout) without racing a
109
+ single, deployment-wide `/capabilities` value against whichever moment a
110
+ client happened to fetch it.
111
+
112
+ A client SHOULD display the server's origin (and, where feasible, its TLS
113
+ certificate fingerprint) to the user before uploading anything, rather than
114
+ silently proceeding — this is a trust-on-first-use decision the user should
115
+ be able to see and decline.
116
+
117
+ ## 4. Upload transport (TUS)
118
+
119
+ Upload transport MUST be [TUS v1](https://tus.io/protocols/resumable-upload)
120
+ core protocol (creation + core resumable upload). A server MUST mount:
121
+
122
+ | Method | Path | Purpose |
123
+ |---|---|---|
124
+ | `POST` | `{prefix}/upload` | Create a resumable upload |
125
+ | `PATCH` | `{prefix}/upload/<id>` | Append a chunk at `Upload-Offset` |
126
+ | `HEAD` | `{prefix}/upload/<id>` | Query the current offset |
127
+ | `DELETE` | `{prefix}/upload/<id>` | Cancel an in-flight upload |
128
+
129
+ `PATCH` bodies MUST be the raw bytes for that offset
130
+ (`Content-Type: application/offset+octet-stream`) — never base64-encoded or
131
+ wrapped in another envelope.
132
+
133
+ ### 4.1 `Upload-Metadata`
134
+
135
+ The TUS `Upload-Metadata` header (comma-separated `<key> <base64(value)>`
136
+ pairs) MUST be parsed for at least:
137
+
138
+ | Key | Required | Description |
139
+ |---|---|---|
140
+ | `artifactId` | Yes (or a legacy `videoid`/`projectid` alias) | UUID for this artifact. |
141
+ | `filename` | Yes | Original filename; extension validated against `kind`'s allowed list. |
142
+ | `kind` | No, defaults to `video` | `video`, `project`, `captions`, or `thumbnail`. |
143
+ | `relatedTo` | No | UUID of another artifact this one belongs to (§8). |
144
+ | `checksum` | No | `<algorithm>:<hex digest>` of the finished file (§6.3). |
145
+
146
+ A client MUST always send `artifactId` (not only the legacy aliases) on new
147
+ uploads. A server MUST continue accepting `videoid`/`projectid` as aliases
148
+ for `artifactId` for back-compat with clients built against protocol
149
+ version 1 before this alias was the only spelling — this alias requirement
150
+ holds for the lifetime of protocol version 1.
151
+
152
+ ### 4.2 Resumption
153
+
154
+ A client MUST always issue a `HEAD` request to learn the authoritative
155
+ offset before resuming an interrupted upload — it MUST NOT trust a locally
156
+ cached byte count, which can be stale (server restart, a partial write that
157
+ never committed, clock differences between client and a previous session).
158
+
159
+ ### 4.3 `Location` header validation
160
+
161
+ The TUS spec permits the `POST /upload` response's `Location` header to be
162
+ absolute or relative, and takes no position on where it may point. A client
163
+ MUST resolve `Location` against the request's own origin and MUST reject
164
+ (rather than follow) a result whose origin differs from the server it is
165
+ already talking to. Without this check, a malicious or compromised server
166
+ could return an absolute `Location` on a different host and receive every
167
+ subsequent `HEAD`/`PATCH`/`DELETE` request for that upload — each carrying
168
+ the bearer token per §5.1 — redirecting the credential to that host instead.
169
+ This is the "token redirect" threat named in
170
+ [RFC 6750 §10.4](https://datatracker.ietf.org/doc/html/rfc6750#section-10.4)
171
+ (a bearer token accepted by, or in this case sent to, a party other than the
172
+ one it was issued for) and matches the same-origin validation
173
+ [OWASP recommends](https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html)
174
+ for any server-supplied redirect target before resending credentials.
175
+
176
+ ### 4.4 Consistent request routing (server-side)
177
+
178
+ A server implementation MUST resolve which artifact a `PATCH`/`HEAD`/`DELETE`
179
+ request under `{prefix}/upload/<id>` applies to the same way for every
180
+ purpose within that request — the identifier an authorization check is run
181
+ against MUST be the exact identifier the request is actually applied to. A
182
+ server MUST NOT use one code path (e.g. a hand-rolled URL parser feeding an
183
+ `authorize` hook) to decide "which artifact is this for" and a different code
184
+ path (e.g. an underlying TUS library's own request routing) to decide "which
185
+ artifact do I actually read or write" — any divergence between the two lets a
186
+ party authorized for one artifact write to, or probe, a different artifact
187
+ they are not authorized for, by exploiting a URL shape the two parsers
188
+ disagree on (e.g. extra path segments after the real id, which many routers
189
+ accept on a wildcard route). Where a server is layered over an existing TUS
190
+ implementation, resolve the identifier for authorization by calling that
191
+ library's own identifier-resolution logic directly (or an exact copy of it)
192
+ rather than an independent reimplementation — two parsers that happen to
193
+ agree on well-formed requests can still silently disagree on adversarial
194
+ ones.
195
+
196
+ ## 5. Authentication
197
+
198
+ ### 5.1 Token transport
199
+
200
+ A client MUST send the pairing token as `Authorization: Bearer <token>` on
201
+ every `POST`/`PATCH`/`HEAD`/`DELETE` request to `{prefix}/upload*`, and SHOULD also
202
+ send it as `?token=` on `GET` requests to a watch/playback URL (some servers
203
+ validate playback links without requiring a header, e.g. for browser
204
+ playback). See §4.3 for why the resource URL these requests target must
205
+ itself be validated before the token is attached to a request against it.
206
+
207
+ ### 5.2 Server-side verification
208
+
209
+ A server MAY implement authentication however it chooses — this document
210
+ does not mandate a scheme. A server MUST reject requests it cannot authorize
211
+ with `403` (or `401` if no credential was presented at all), and SHOULD do
212
+ so before any bytes are accepted for a new upload. This applies uniformly to
213
+ every phase of an upload's lifecycle — creation, each `PATCH` chunk, `HEAD`
214
+ offset queries, and `DELETE` (both the in-flight-cancel route and the
215
+ finalized-artifact route) — not only to the initial `POST`. A server
216
+ implementation MUST NOT skip authorization for `PATCH`/`HEAD` merely because
217
+ an internal helper failed to recover the artifactId from the resource URL;
218
+ that failure MUST be treated as an authorization failure (reject), not as
219
+ "no artifactId to check, so allow." See §4.4 for the related requirement
220
+ that the artifactId resolved for this check must be the one the request is
221
+ actually applied to.
222
+
223
+ ### 5.3 Rejection responses
224
+
225
+ Error responses MUST be JSON of the shape `{ "ok": false, "error": string }`.
226
+
227
+ ### 5.4 Recommended capability-token shape (non-normative)
228
+
229
+ Servers that don't already have an auth scheme are encouraged (not required)
230
+ to use a stateless, HMAC-signed token with at least:
231
+
232
+ ```json
233
+ { "artifactId": "<uuid>", "iat": 1234567890, "exp": 1234569690, "kid": "2026-06", "issuer": "https://vault.example.org" }
234
+ ```
235
+
236
+ `kid` lets a secret rotate with an overlap window instead of instantly
237
+ invalidating every outstanding token. `iat` (signed alongside `exp`) closes a
238
+ clock-skew gap where signing only expiry would let a slow clock accept an
239
+ expired token. `issuer` prevents a token minted by one deployment from being
240
+ replayed against a different one that happens to share a secret. A token MAY
241
+ authorize an artifact other than the one it names if that artifact declares
242
+ the token's `artifactId` as its `relatedTo` (§8) — this lets one token cover
243
+ an entire upload session (a merged video plus its captions, beat manifest and
244
+ thumbnail, or every clip plus the ordering manifest under
245
+ `uploadUnit: "segment"`) rather than requiring one token per artifact.
246
+
247
+ This shape is exactly what `@mieweb/pulsevault`'s `issueCapabilityToken`/
248
+ `verifyCapabilityToken`/`createCapabilityAuthorize` implement, but any server
249
+ is free to use its own scheme entirely — only §5.1–§5.3 are normative.
250
+
251
+ ## 6. Storage and validation
252
+
253
+ ### 6.1 Readiness
254
+
255
+ A server MUST NOT serve (via `GET`) an artifact's bytes until the upload is
256
+ fully written and any payload validation has passed. An in-progress or
257
+ rejected upload MUST return `404` from the serving route, not partial or
258
+ corrupt bytes.
259
+
260
+ ### 6.2 Serving
261
+
262
+ A server MUST expose `GET {prefix}/artifacts/<artifactId>` returning either
263
+ the bytes directly or a redirect to a URL serving them (e.g. a presigned
264
+ object-storage URL). The kind is resolved server-side; it is not encoded in
265
+ this URL. A server MUST also expose `DELETE {prefix}/artifacts/<artifactId>`.
266
+
267
+ ### 6.3 Checksum (optional)
268
+
269
+ If a server supports checksum verification (advertised via `/capabilities`),
270
+ it SHOULD verify the client-supplied `checksum` metadata against the
271
+ finished file before marking the upload ready, and MUST reject a mismatch
272
+ with `422` and remove the rejected bytes. This is at-rest integrity
273
+ verification on the finished artifact — it does not substitute for
274
+ in-transit (TLS) integrity, and does not verify individual chunks as they
275
+ arrive.
276
+
277
+ ## 7. Versioning
278
+
279
+ A server MUST report `protocolVersion`, `minSupportedVersion`, and
280
+ `maxSupportedVersion` via `/capabilities` (§2) and `Protocol-Version` on
281
+ every response. A client encountering a server whose supported range
282
+ excludes its own version MUST NOT attempt to pair, and SHOULD surface a
283
+ clear, specific message rather than a generic failure.
284
+
285
+ ## 8. Artifact relationships (`relatedTo`) and `uploadUnit`
286
+
287
+ A pulse (a short composed of one or more recorded **segments**) MAY be uploaded
288
+ as a single pre-merged video (`uploadUnit: "merged"`) or as individual
289
+ per-segment artifacts plus an ordering manifest (`uploadUnit: "segment"`) — the
290
+ operator declares which via `/capabilities` (§2), optionally overridden per
291
+ session via the pairing link's `uploadUnit` param (§3); this document does not
292
+ prefer one over the other.
293
+
294
+ > **Terminology:** a **segment** is a recorded clip (the source unit). A
295
+ > **beat** is a *timecode range on the merged timeline* — one recorded
296
+ > segment's start/end within the merged video — carried only in the merged
297
+ > mode's beat manifest below. (Earlier revisions used "beat" for what is now
298
+ > called the `segment` upload unit; that meaning is retired.)
299
+
300
+ Under `uploadUnit: "segment"`, a client uploads each segment under its own
301
+ `artifactId`, plus exactly one **ordering manifest** artifact (`kind:
302
+ "project"`, e.g. `<draftId>-segments.pulse`, a JSON document listing the
303
+ ordered segment `artifactId`s). Segmented mode carries **no** captions and
304
+ **no** thumbnail.
305
+
306
+ ```json
307
+ { "version": 1, "segments": [ { "artifactId": "<uuid>", "order": 0 } ] }
308
+ ```
309
+
310
+ Under `uploadUnit: "merged"`, a client uploads one pre-merged video as the
311
+ session anchor, plus (all as related artifacts):
312
+
313
+ - **captions** (`kind: "captions"`, `<draftId>.vtt`) — WebVTT for the whole
314
+ merged video, OPTIONAL (absent when the video has no speech or no on-device
315
+ model was available); MAY carry word-level inline cue timestamps like
316
+ `<00:00:01.500>word` for karaoke rendering;
317
+ - a **beat manifest** (`kind: "project"`, e.g. `<draftId>-beats.pulse`) giving
318
+ each recorded segment's precise `startMs`/`endMs` on the merged timeline,
319
+ contiguous and summing to the true merged `durationMs` (groundwork for
320
+ keyframe-aligned deep links / HLS);
321
+ - a **thumbnail** (`kind: "thumbnail"`, `<draftId>.jpg`) — the pulse's poster
322
+ frame.
323
+
324
+ ```json
325
+ { "version": 1, "type": "beat-manifest", "durationMs": 47320,
326
+ "beats": [ { "segmentId": "<local id>", "order": 0, "startMs": 0, "endMs": 4210 } ] }
327
+ ```
328
+
329
+ Every non-anchor artifact in either session SHOULD declare `relatedTo` pointing
330
+ at the session's anchor `artifactId` (the one named in the pairing link) so a
331
+ single capability token can authorize the whole session (§5.4).
332
+
333
+ The relational graph of replies between segments/pulses (who replied to what,
334
+ rendering a thread) is explicitly **out of scope** for this document. That
335
+ graph is a query/relational concern for the operator's own systems, built
336
+ from `relatedTo` and whatever additional metadata the operator chooses to
337
+ record — not something a Pulse-compatible server is required to implement.
338
+
339
+ ## 9. Compatibility notes
340
+
341
+ A client or server MAY support additional, non-normative extensions to this
342
+ contract (additional `Upload-Metadata` keys, additional kinds, additional
343
+ `/capabilities` fields) as long as doing so does not break a client/server
344
+ that only implements what's written here. New REQUIRED fields MUST NOT be
345
+ added to a response without a `protocolVersion` bump.