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