@mieweb/pulsevault 0.3.0 → 0.4.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.
- package/CHANGELOG.md +113 -1
- package/OPERATIONS.md +24 -8
- package/PROTOCOL.md +187 -105
- package/README.md +102 -41
- package/dist/app.d.ts +28 -12
- package/dist/app.d.ts.map +1 -1
- package/dist/app.js +17 -4
- package/dist/app.js.map +1 -1
- package/dist/core.d.ts +18 -8
- package/dist/core.d.ts.map +1 -1
- package/dist/core.js +91 -23
- package/dist/core.js.map +1 -1
- package/dist/lib/authorize.d.ts +7 -1
- package/dist/lib/authorize.d.ts.map +1 -1
- package/dist/lib/capabilities.d.ts +30 -0
- package/dist/lib/capabilities.d.ts.map +1 -0
- package/dist/lib/capabilities.js +23 -0
- package/dist/lib/capabilities.js.map +1 -0
- package/dist/lib/capability-token.d.ts +41 -7
- package/dist/lib/capability-token.d.ts.map +1 -1
- package/dist/lib/capability-token.js +68 -13
- package/dist/lib/capability-token.js.map +1 -1
- package/dist/lib/deeplinks.d.ts +0 -13
- package/dist/lib/deeplinks.d.ts.map +1 -1
- package/dist/lib/deeplinks.js +0 -5
- package/dist/lib/deeplinks.js.map +1 -1
- package/dist/lib/options.d.ts +6 -1
- package/dist/lib/options.d.ts.map +1 -1
- package/dist/lib/options.js +9 -4
- package/dist/lib/options.js.map +1 -1
- package/dist/lib/protocol-version.gen.d.ts +6 -0
- package/dist/lib/protocol-version.gen.d.ts.map +1 -0
- package/dist/lib/protocol-version.gen.js +8 -0
- package/dist/lib/protocol-version.gen.js.map +1 -0
- package/dist/lib/protocol.d.ts +31 -0
- package/dist/lib/protocol.d.ts.map +1 -0
- package/dist/lib/protocol.js +64 -0
- package/dist/lib/protocol.js.map +1 -0
- package/dist/lib/pulsevaultTus.d.ts +7 -2
- package/dist/lib/pulsevaultTus.d.ts.map +1 -1
- package/dist/lib/pulsevaultTus.js +80 -6
- package/dist/lib/pulsevaultTus.js.map +1 -1
- package/dist/lib/retention.d.ts +54 -0
- package/dist/lib/retention.d.ts.map +1 -0
- package/dist/lib/retention.js +119 -0
- package/dist/lib/retention.js.map +1 -0
- package/dist/lib/view-links.d.ts +49 -0
- package/dist/lib/view-links.d.ts.map +1 -0
- package/dist/lib/view-links.js +35 -0
- package/dist/lib/view-links.js.map +1 -0
- package/dist/routes/pulsevault.d.ts +2 -2
- package/dist/routes/pulsevault.d.ts.map +1 -1
- package/dist/routes/pulsevault.js +51 -30
- package/dist/routes/pulsevault.js.map +1 -1
- package/dist/storage/local.d.ts +5 -1
- package/dist/storage/local.d.ts.map +1 -1
- package/dist/storage/local.js +55 -1
- package/dist/storage/local.js.map +1 -1
- package/dist/storage/s3.d.ts +5 -1
- package/dist/storage/s3.d.ts.map +1 -1
- package/dist/storage/s3.js +52 -5
- package/dist/storage/s3.js.map +1 -1
- package/dist/storage/types.d.ts +34 -6
- package/dist/storage/types.d.ts.map +1 -1
- package/package.json +16 -4
- package/protocol/openapi.json +1058 -0
- package/protocol/schemas/beat-manifest.schema.json +54 -0
- package/protocol/schemas/capabilities.schema.json +65 -0
- package/protocol/schemas/capability-token.schema.json +36 -0
- package/protocol/schemas/deep-link.schema.json +29 -0
- package/protocol/schemas/pulse-client.schema.json +8 -0
- package/protocol/schemas/upload-metadata.schema.json +45 -0
- package/protocol/schemas/view-link.schema.json +23 -0
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,116 @@ breaking changes, called out explicitly below.
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.4.1] - 2026-09-30
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- **`@mieweb/pulsevault/core` loads under Meteor again.** 0.4.0 read the
|
|
15
|
+
protocol version from `package.json` at runtime with
|
|
16
|
+
`createRequire(import.meta.url)`. That's valid ESM for Node, but Meteor
|
|
17
|
+
compiles npm packages into its own module wrapper, which already declares
|
|
18
|
+
`require` and can't compile `import.meta`, so a Meteor app crashed at boot
|
|
19
|
+
(`SyntaxError: Identifier 'require' has already been declared`). `npm run
|
|
20
|
+
build` now writes the version into `src/lib/protocol-version.gen.ts`
|
|
21
|
+
(`scripts/gen-protocol-version.mjs`); `package.json` `pulseProtocol` is still
|
|
22
|
+
the one place it's set, and a missing or malformed value now fails the build.
|
|
23
|
+
|
|
24
|
+
## [0.4.0] - 2026-09-30
|
|
25
|
+
|
|
26
|
+
Protocol 2.2 (`PROTOCOL.md` §7.4). Protocol 2 records the `uploadUnit`
|
|
27
|
+
removal below as the breaking change it is; clients built for protocol 1
|
|
28
|
+
can't pair with this release.
|
|
29
|
+
|
|
30
|
+
### Added
|
|
31
|
+
|
|
32
|
+
- **Read-only view links** (protocol 2.2). The pairing token is a full
|
|
33
|
+
capability, so a watch link carrying it let anyone holding the link
|
|
34
|
+
upload to the artifact or delete it. The new `issueViewLink` option turns
|
|
35
|
+
on `POST {prefix}/artifacts/:id/view-link` (authorized as the new
|
|
36
|
+
`"share"` phase): for a finished artifact it returns `{ token, expiresAt }`,
|
|
37
|
+
a token that only opens the artifact and the artifacts `relatedTo` it
|
|
38
|
+
(`GET ?token=`). The host decides each link's lifetime, or refuses one;
|
|
39
|
+
PulseVault sets none. `createViewLinkIssuer` / `issueViewToken` /
|
|
40
|
+
`verifyViewToken` implement it for capability tokens: view tokens carry
|
|
41
|
+
`use: "view"`, are signed with a key derived from the same secret (so a
|
|
42
|
+
verifier that predates them rejects one), and `createCapabilityAuthorize`
|
|
43
|
+
accepts them only for `resolve`. `/capabilities` reports `viewLinks`, and
|
|
44
|
+
`protocol/schemas/view-link.schema.json` defines the response.
|
|
45
|
+
- **Opt-in cleanup of abandoned uploads.** The `retention` option
|
|
46
|
+
(`{ abandonedAfterSeconds, sweepIntervalSeconds? }`, on the plugin and
|
|
47
|
+
`createPulseVaultCore`) sweeps on a timer, removing uploads left
|
|
48
|
+
unfinished past the cutoff and the related artifacts (never videos) of a
|
|
49
|
+
video that never finished; finished videos are never touched.
|
|
50
|
+
`sweepAbandonedUploads` runs one sweep for your own scheduler. Storage
|
|
51
|
+
adapters gain an optional `listArtifacts`, implemented by both built-in
|
|
52
|
+
adapters.
|
|
53
|
+
- **`onArtifactEvent` reports removals:** `phase: "remove"`, with `reason:
|
|
54
|
+
"deleted"` (`DELETE /artifacts/:id` or a TUS `DELETE`) or `"abandoned"` (the
|
|
55
|
+
`retention` sweep), so a host keeping its own index of artifacts can drop
|
|
56
|
+
one. A consumer that switches exhaustively on `phase` sees a new value.
|
|
57
|
+
|
|
58
|
+
- **The protocol is written down as files, and CI keeps it honest.**
|
|
59
|
+
`protocol/schemas/*.schema.json` define `/capabilities`, pairing links,
|
|
60
|
+
`Upload-Metadata`, the beat manifest, capability-token claims and the
|
|
61
|
+
`Pulse-Client` header; `protocol/openapi.json` is generated from the
|
|
62
|
+
route schemas (`npm run protocol`), and so are the field tables in
|
|
63
|
+
`PROTOCOL.md`. `scripts/check-protocol.mjs` fails a change that alters the
|
|
64
|
+
protocol without the right version bump (oasdiff decides what's breaking
|
|
65
|
+
for the HTTP routes). The `protocol/` folder is published with the package.
|
|
66
|
+
- **`Pulse-Client` request header and `426 Upgrade Required`** (protocol
|
|
67
|
+
2.1). A client that says its newest protocol is older than this server's
|
|
68
|
+
oldest gets 426 with the supported range on uploads and artifact
|
|
69
|
+
requests; `/capabilities` always answers. No header, no change.
|
|
70
|
+
- **`Upload-Metadata.appVersion`** (protocol 2.1): stored with the artifact
|
|
71
|
+
by both storage adapters and reported on the `complete`/`reject` events
|
|
72
|
+
(`PulseVaultArtifactEvent.appVersion`).
|
|
73
|
+
- **`protocolRevision`** in `/capabilities`: the spec revision, `major.minor`.
|
|
74
|
+
- **CI** (`.github/workflows/ci.yml`): tests on Node 22 and 24, the protocol
|
|
75
|
+
checks, the e2e against Postgres, and the Pulse app's contract suite
|
|
76
|
+
against each PR's build.
|
|
77
|
+
|
|
78
|
+
### Changed
|
|
79
|
+
|
|
80
|
+
- **A TUS `DELETE` is authorized as `"delete"`, not `"patch"`.** It now
|
|
81
|
+
removes the artifact (below), the same as `DELETE /artifacts/:id`, so an
|
|
82
|
+
`authorize` hook sees one phase for every removal — and a rejected one is
|
|
83
|
+
reported on `onArtifactEvent` like any other rejected delete. If your hook
|
|
84
|
+
allows `"patch"` but refuses `"delete"`, clients can no longer cancel an
|
|
85
|
+
upload through TUS; allow `"delete"` for the artifact's own token.
|
|
86
|
+
- **Breaking: protocol 2.** `/capabilities` reports `protocolVersion: 2` and
|
|
87
|
+
accepts protocol majors 2–2, read from the new `package.json`
|
|
88
|
+
`pulseProtocol` field (`{ "version": "2.1", "min": 2, "max": 2 }`).
|
|
89
|
+
|
|
90
|
+
### Removed
|
|
91
|
+
|
|
92
|
+
- **Breaking: `uploadUnit` is gone — a pulse uploads as one video; beats are
|
|
93
|
+
timestamps within it** (#64). A pulse is one video (the artifact named by
|
|
94
|
+
the pairing link) plus its related captions, beat manifest and thumbnail;
|
|
95
|
+
a beat is a `startMs`/`endMs` range inside that video, not an upload
|
|
96
|
+
strategy (`PROTOCOL.md` §8). Concretely:
|
|
97
|
+
- The `uploadUnit` option is removed from the Fastify plugin and from
|
|
98
|
+
`createPulseVaultCore`. Passing it now throws a `TypeError` at boot
|
|
99
|
+
("`uploadUnit` was removed — delete the option") instead of being
|
|
100
|
+
silently ignored — delete the option from your config.
|
|
101
|
+
- `buildUploadLink` no longer accepts or emits an `uploadUnit` param.
|
|
102
|
+
Links minted by older releases stay valid: clients ignore unknown params
|
|
103
|
+
(`PROTOCOL.md` §3).
|
|
104
|
+
- `GET /capabilities` no longer returns an `uploadUnit` field.
|
|
105
|
+
- Needs Pulse 2.1.0 or later (mieweb/pulse#213) —
|
|
106
|
+
older Pulse builds require `uploadUnit` in `/capabilities`.
|
|
107
|
+
|
|
108
|
+
### Fixed
|
|
109
|
+
|
|
110
|
+
- **A TUS `DELETE` removes the whole artifact.** It used to reach only the
|
|
111
|
+
tus datastore, so PulseVault's sidecar stayed behind: the artifactId
|
|
112
|
+
answered every later create with `409`, a finished upload deleted on the
|
|
113
|
+
local adapter kept a `ready` sidecar pointing at nothing, and on S3 a
|
|
114
|
+
finished upload wasn't deleted at all (aborting its completed multipart
|
|
115
|
+
upload fails with `NoSuchUpload` before anything is removed). Termination
|
|
116
|
+
now runs `storage.remove` under tus's per-upload lock — bytes, tus record
|
|
117
|
+
and sidecar, in flight or finished. The S3 adapter's `remove` also deletes
|
|
118
|
+
the incomplete `.part` object @tus/s3-store parks between PATCHes.
|
|
119
|
+
|
|
10
120
|
## [0.3.0] - 2026-09-16
|
|
11
121
|
|
|
12
122
|
### Changed
|
|
@@ -290,6 +400,8 @@ if you've evaluated against an intermediate build.
|
|
|
290
400
|
lock, so this is now at least surfaced: a one-time `console.warn` fires
|
|
291
401
|
per process the first time a deployment falls into this degraded mode.
|
|
292
402
|
|
|
293
|
-
[Unreleased]: https://github.com/mieweb/pulsevault/compare/v0.
|
|
403
|
+
[Unreleased]: https://github.com/mieweb/pulsevault/compare/v0.4.1...HEAD
|
|
404
|
+
[0.4.1]: https://github.com/mieweb/pulsevault/compare/v0.4.0...v0.4.1
|
|
405
|
+
[0.4.0]: https://github.com/mieweb/pulsevault/compare/v0.3.0...v0.4.0
|
|
294
406
|
[0.3.0]: https://github.com/mieweb/pulsevault/compare/v0.2.0...v0.3.0
|
|
295
407
|
[0.2.0]: https://github.com/mieweb/pulsevault/compare/v0.0.1...v0.2.0
|
package/OPERATIONS.md
CHANGED
|
@@ -157,8 +157,8 @@ The entire state of the local adapter lives under `workspaceDir`:
|
|
|
157
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
|
-
once the client retries, or will
|
|
161
|
-
(see "Retention" below) if the client never retries.
|
|
160
|
+
once the client retries, or will sit as abandoned partial uploads until
|
|
161
|
+
`retention` removes them (see "Retention" below) if the client never retries.
|
|
162
162
|
|
|
163
163
|
## Web-ready playback (faststart remux + H.264 transcode)
|
|
164
164
|
|
|
@@ -227,11 +227,16 @@ integrity for artifacts this feature has touched.
|
|
|
227
227
|
|
|
228
228
|
## Retention
|
|
229
229
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
230
|
+
**Abandoned uploads** — unfinished uploads a client gave up on, and the
|
|
231
|
+
captions, manifest or thumbnail of a video that never finished — are cleaned
|
|
232
|
+
up by the opt-in `retention` option (or `sweepAbandonedUploads` from your own
|
|
233
|
+
scheduler); see the README's `retention` option. Both adapters support it.
|
|
234
|
+
|
|
235
|
+
How long to keep **finished** content is a policy decision `pulsevault`
|
|
236
|
+
leaves to the operator (see `PROTOCOL.md` §1 on lifecycle ownership). A
|
|
237
|
+
sample cron script for the local adapter, deleting artifacts older than a
|
|
238
|
+
retention window (compliance-driven deletion), alongside the abandoned-upload
|
|
239
|
+
cutoff it predates:
|
|
235
240
|
|
|
236
241
|
```ts
|
|
237
242
|
import { readdir, readFile, stat } from "node:fs/promises";
|
|
@@ -263,7 +268,9 @@ for (const file of await readdir(sidecarDir)) {
|
|
|
263
268
|
|
|
264
269
|
Run this on whatever schedule your retention policy requires. For S3/R2
|
|
265
270
|
storage, prefer your bucket provider's native lifecycle-policy feature (S3
|
|
266
|
-
Lifecycle Rules, R2 Object Lifecycle) over a custom script where available
|
|
271
|
+
Lifecycle Rules, R2 Object Lifecycle) over a custom script where available —
|
|
272
|
+
and let `retention` handle abandoned uploads, which a lifecycle rule can't tell
|
|
273
|
+
apart from finished ones (it doesn't read the sidecar's status).
|
|
267
274
|
|
|
268
275
|
## Secrets management
|
|
269
276
|
|
|
@@ -286,3 +293,12 @@ async function loadKey(kid: string) {
|
|
|
286
293
|
Rotate by adding the new `kid` to your lookup table alongside the old one,
|
|
287
294
|
switching issuance to the new `kid`, and removing the old entry only after
|
|
288
295
|
its longest-lived outstanding token has expired.
|
|
296
|
+
|
|
297
|
+
View links (`issueViewLink` / `createViewLinkIssuer`) are signed with a key
|
|
298
|
+
derived from the same secrets, so they rotate with them — and since they can
|
|
299
|
+
be much longer-lived than an upload token, keep a retired `kid` in the table
|
|
300
|
+
as long as the view links you still want working, or remove it to revoke
|
|
301
|
+
every view link signed under it at once. Deleting a video stops its view
|
|
302
|
+
links opening the video (the `GET` then 404s), but the links keep opening its
|
|
303
|
+
related captions, manifest and thumbnail until those are deleted too — by the
|
|
304
|
+
client, or by `retention` once the video has been gone past its cutoff.
|
package/PROTOCOL.md
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
# Pulse Upload Protocol
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
<!-- BEGIN GENERATED: protocol-version (from package.json pulseProtocol — edit that, then run `npm run protocol`) -->
|
|
4
|
+
| | |
|
|
5
|
+
|---|---|
|
|
6
|
+
| Spec revision | `2.2` |
|
|
7
|
+
| Protocol majors accepted | 2 |
|
|
8
|
+
<!-- END GENERATED: protocol-version -->
|
|
4
9
|
|
|
5
10
|
This document specifies the wire contract between a Pulse client and any
|
|
6
11
|
server that wants to receive uploads from it. It is independent of
|
|
@@ -30,41 +35,33 @@ never the policy behind it.
|
|
|
30
35
|
## 2. Capability discovery
|
|
31
36
|
|
|
32
37
|
A server MUST expose an unauthenticated `GET {prefix}/capabilities` endpoint.
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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.
|
|
38
|
+
Its JSON body is defined by
|
|
39
|
+
[`protocol/schemas/capabilities.schema.json`](protocol/schemas/capabilities.schema.json):
|
|
40
|
+
|
|
41
|
+
<!-- BEGIN GENERATED: capabilities (from protocol/schemas/capabilities.schema.json — edit that, then run `npm run protocol`) -->
|
|
42
|
+
| Field | Type | Required | Description |
|
|
43
|
+
|---|---|---|---|
|
|
44
|
+
| `protocolVersion` | integer | Yes | Protocol major this server implements. Also sent as the `Protocol-Version` header on every response. |
|
|
45
|
+
| `protocolRevision` | string | Yes | Spec revision this server implements, `major.minor`. The minor counts additions older clients can ignore. |
|
|
46
|
+
| `minSupportedVersion` | integer | Yes | Oldest protocol major this server accepts. Clients whose newest protocol is older get 426 Upgrade Required. |
|
|
47
|
+
| `maxSupportedVersion` | integer | Yes | Newest protocol major this server accepts. |
|
|
48
|
+
| `kinds` | string[] | Yes | Artifact kinds this server accepts. |
|
|
49
|
+
| `allowedExtensions` | object | Yes | Allowed file extensions per kind, lowercase with the leading dot. |
|
|
50
|
+
| `maxUploadSize` | number | Yes | Largest artifact the server accepts, in bytes. |
|
|
51
|
+
| `checksum` | object | Yes | Checksum algorithms accepted in `Upload-Metadata.checksum`. |
|
|
52
|
+
| `viewLinks` | boolean | No | Whether `POST {prefix}/artifacts/<id>/view-link` mints read-only view links (§6.4). Absent before protocol 2.2, which a client treats as false. |
|
|
53
|
+
<!-- END GENERATED: capabilities -->
|
|
54
|
+
|
|
55
|
+
A client pairs only if its protocol range overlaps
|
|
56
|
+
`[minSupportedVersion, maxSupportedVersion]` (§7). If it doesn't, the client
|
|
57
|
+
MUST NOT pair, and SHOULD show an actionable message ("update the app" /
|
|
58
|
+
"this server needs an update") rather than a generic error. A client MUST
|
|
59
|
+
ignore fields it doesn't recognize.
|
|
60
|
+
|
|
61
|
+
`checksum.algorithms` describes capability, not whether verification is
|
|
62
|
+
actually wired in for every upload — a server MAY list an algorithm it's
|
|
63
|
+
capable of checking even where the operator hasn't enabled that check. A
|
|
64
|
+
client MUST treat an absent or empty list as "no checksum verification".
|
|
68
65
|
|
|
69
66
|
A server response to this endpoint MUST NOT include any secret. Every other
|
|
70
67
|
response from the server MUST include a `Protocol-Version` header carrying
|
|
@@ -75,7 +72,7 @@ the integer from `protocolVersion`.
|
|
|
75
72
|
A server presents a pairing link or QR code of the form:
|
|
76
73
|
|
|
77
74
|
```
|
|
78
|
-
pulsecam://?v=1&artifactId=<uuid>&server=<origin>&token=<opaque
|
|
75
|
+
pulsecam://?v=1&artifactId=<uuid>&server=<origin>&token=<opaque>
|
|
79
76
|
```
|
|
80
77
|
|
|
81
78
|
- `v` (REQUIRED): the deep-link schema version. A client MUST refuse and
|
|
@@ -97,17 +94,10 @@ pulsecam://?v=1&artifactId=<uuid>&server=<origin>&token=<opaque>&uploadUnit=<seg
|
|
|
97
94
|
`artifactId` (or a session it anchors, §5.4), not a standing general
|
|
98
95
|
credential — see §5.4 for the recommended (but not required) capability-
|
|
99
96
|
token shape.
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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.
|
|
97
|
+
|
|
98
|
+
A client SHOULD ignore query params it does not recognize, so links minted by
|
|
99
|
+
an older or newer server stay valid. The parameters are defined by
|
|
100
|
+
[`protocol/schemas/deep-link.schema.json`](protocol/schemas/deep-link.schema.json).
|
|
111
101
|
|
|
112
102
|
A client SHOULD display the server's origin (and, where feasible, its TLS
|
|
113
103
|
certificate fingerprint) to the user before uploading anything, rather than
|
|
@@ -124,7 +114,7 @@ core protocol (creation + core resumable upload). A server MUST mount:
|
|
|
124
114
|
| `POST` | `{prefix}/upload` | Create a resumable upload |
|
|
125
115
|
| `PATCH` | `{prefix}/upload/<id>` | Append a chunk at `Upload-Offset` |
|
|
126
116
|
| `HEAD` | `{prefix}/upload/<id>` | Query the current offset |
|
|
127
|
-
| `DELETE` | `{prefix}/upload/<id>` |
|
|
117
|
+
| `DELETE` | `{prefix}/upload/<id>` | Delete the upload and its artifact, in flight or finished |
|
|
128
118
|
|
|
129
119
|
`PATCH` bodies MUST be the raw bytes for that offset
|
|
130
120
|
(`Content-Type: application/offset+octet-stream`) — never base64-encoded or
|
|
@@ -135,14 +125,21 @@ wrapped in another envelope.
|
|
|
135
125
|
The TUS `Upload-Metadata` header (comma-separated `<key> <base64(value)>`
|
|
136
126
|
pairs) MUST be parsed for at least:
|
|
137
127
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
| `
|
|
142
|
-
| `
|
|
143
|
-
| `
|
|
144
|
-
| `
|
|
145
|
-
| `
|
|
128
|
+
<!-- BEGIN GENERATED: upload-metadata (from protocol/schemas/upload-metadata.schema.json — edit that, then run `npm run protocol`) -->
|
|
129
|
+
| Key | Type | Required | Description |
|
|
130
|
+
|---|---|---|---|
|
|
131
|
+
| `artifactId` | string (uuid) | Yes | The artifact's id. For the video it's the pairing link's `artifactId`; related artifacts use a fresh UUID. Legacy aliases `videoid` and `projectid` are still accepted. |
|
|
132
|
+
| `filename` | string | Yes | Original file name. Its extension must be allowed for `kind` (see `/capabilities`). |
|
|
133
|
+
| `kind` | `video`, `project`, `captions`, `thumbnail` | No | Artifact kind. Defaults to `video`. |
|
|
134
|
+
| `relatedTo` | string (uuid) | No | The artifact this one belongs to: the video, for its captions, beat manifest and thumbnail. A capability token for the video also authorizes artifacts related to it. |
|
|
135
|
+
| `checksum` | string | No | `<algorithm>:<hex digest>` of the finished file, verified by servers that check checksums. |
|
|
136
|
+
| `name` | string | No | Display title (the draft name), sent on the video. Display-only; servers trim and cap it. |
|
|
137
|
+
| `appVersion` | string | No | Since 2.1. Version of the app that uploaded it, e.g. `2.1.0 (45)`. Stored with the artifact. Display-only; servers trim and cap it. |
|
|
138
|
+
<!-- END GENERATED: upload-metadata -->
|
|
139
|
+
|
|
140
|
+
`name` and `appVersion` are display metadata only: they MUST NOT influence
|
|
141
|
+
storage paths, routing, or authorization, and consumers MUST escape them for
|
|
142
|
+
their output context.
|
|
146
143
|
|
|
147
144
|
Metadata values are base64 per the TUS spec; a free-form value such as `name`
|
|
148
145
|
MUST be base64 of its **UTF-8** bytes. A client that base64-encodes a raw
|
|
@@ -151,9 +148,9 @@ emoji), so encode the UTF-8 bytes explicitly.
|
|
|
151
148
|
|
|
152
149
|
A client MUST always send `artifactId` (not only the legacy aliases) on new
|
|
153
150
|
uploads. A server MUST continue accepting `videoid`/`projectid` as aliases
|
|
154
|
-
for `artifactId` for back-compat with clients built
|
|
155
|
-
|
|
156
|
-
|
|
151
|
+
for `artifactId` for back-compat with clients built before `artifactId`
|
|
152
|
+
was the only spelling — this alias requirement holds until a protocol major
|
|
153
|
+
removes it (§7).
|
|
157
154
|
|
|
158
155
|
### 4.2 Resumption
|
|
159
156
|
|
|
@@ -246,14 +243,27 @@ expired token. `issuer` prevents a token minted by one deployment from being
|
|
|
246
243
|
replayed against a different one that happens to share a secret. A token MAY
|
|
247
244
|
authorize an artifact other than the one it names if that artifact declares
|
|
248
245
|
the token's `artifactId` as its `relatedTo` (§8) — this lets one token cover
|
|
249
|
-
an entire upload session (a
|
|
250
|
-
thumbnail
|
|
251
|
-
`uploadUnit: "segment"`) rather than requiring one token per artifact.
|
|
246
|
+
an entire upload session (a pulse's video plus its captions, beat manifest and
|
|
247
|
+
thumbnail) rather than requiring one token per artifact.
|
|
252
248
|
|
|
253
249
|
This shape is exactly what `@mieweb/pulsevault`'s `issueCapabilityToken`/
|
|
254
250
|
`verifyCapabilityToken`/`createCapabilityAuthorize` implement, but any server
|
|
255
251
|
is free to use its own scheme entirely — only §5.1–§5.3 are normative.
|
|
256
252
|
|
|
253
|
+
### 5.5 View tokens (non-normative)
|
|
254
|
+
|
|
255
|
+
The pairing token is a full capability: whoever holds it can upload to the
|
|
256
|
+
artifact, delete it, and create artifacts related to it. A watch link that
|
|
257
|
+
carries it is unsafe to share. A server that offers view links (§6.4) mints a
|
|
258
|
+
separate **view token** that only opens an artifact and the artifacts
|
|
259
|
+
`relatedTo` it. `@mieweb/pulsevault`'s view token has the §5.4 claims plus
|
|
260
|
+
`"use": "view"`, and is signed with a key derived from the deployment's
|
|
261
|
+
secret, so a verifier that predates view tokens rejects one outright instead of
|
|
262
|
+
treating it as an upload capability. `createCapabilityAuthorize` accepts it
|
|
263
|
+
only for `GET {prefix}/artifacts/<id>` (the `resolve` phase); minting a view
|
|
264
|
+
link (`share`) needs the pairing token itself, so a view link can't extend
|
|
265
|
+
itself.
|
|
266
|
+
|
|
257
267
|
## 6. Storage and validation
|
|
258
268
|
|
|
259
269
|
### 6.1 Readiness
|
|
@@ -269,6 +279,8 @@ A server MUST expose `GET {prefix}/artifacts/<artifactId>` returning either
|
|
|
269
279
|
the bytes directly or a redirect to a URL serving them (e.g. a presigned
|
|
270
280
|
object-storage URL). The kind is resolved server-side; it is not encoded in
|
|
271
281
|
this URL. A server MUST also expose `DELETE {prefix}/artifacts/<artifactId>`.
|
|
282
|
+
A TUS `DELETE {prefix}/upload/<id>` removes the artifact the same way,
|
|
283
|
+
whether its upload is in flight or finished.
|
|
272
284
|
|
|
273
285
|
### 6.3 Checksum (optional)
|
|
274
286
|
|
|
@@ -280,63 +292,133 @@ verification on the finished artifact — it does not substitute for
|
|
|
280
292
|
in-transit (TLS) integrity, and does not verify individual chunks as they
|
|
281
293
|
arrive.
|
|
282
294
|
|
|
283
|
-
|
|
295
|
+
### 6.4 View links (optional, protocol 2.2)
|
|
284
296
|
|
|
285
|
-
A server
|
|
286
|
-
`
|
|
287
|
-
|
|
288
|
-
excludes its own version MUST NOT attempt to pair, and SHOULD surface a
|
|
289
|
-
clear, specific message rather than a generic failure.
|
|
290
|
-
|
|
291
|
-
## 8. Artifact relationships (`relatedTo`) and `uploadUnit`
|
|
292
|
-
|
|
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.
|
|
297
|
+
A server that reports `viewLinks: true` in `/capabilities` (§2) MUST expose
|
|
298
|
+
`POST {prefix}/artifacts/<artifactId>/view-link`. Authorized with the pairing
|
|
299
|
+
token (§5.1), it returns a read-only link to a **finished** artifact:
|
|
311
300
|
|
|
312
301
|
```json
|
|
313
|
-
{ "
|
|
302
|
+
{ "token": "<view token>", "expiresAt": 1790000000 }
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
The token opens the artifact, and the artifacts `relatedTo` it, as
|
|
306
|
+
`GET {prefix}/artifacts/<artifactId>?token=<token>` until `expiresAt` (seconds
|
|
307
|
+
since the Unix epoch) — and does nothing else, so it is safe to share (§5.5).
|
|
308
|
+
The server decides how long each link works. It returns `404` for an artifact
|
|
309
|
+
that isn't finished (or when view links aren't enabled), and `403` when it
|
|
310
|
+
won't link to that artifact. A client that wants a shareable link asks for one
|
|
311
|
+
right after the upload finishes, while its pairing token is still valid; a
|
|
312
|
+
server without `viewLinks` offers no shareable link, and the client MUST NOT
|
|
313
|
+
share the pairing token in its place.
|
|
314
|
+
|
|
315
|
+
## 7. Versioning
|
|
316
|
+
|
|
317
|
+
The protocol has a version `major.minor`, written once in `@mieweb/pulsevault`'s
|
|
318
|
+
`package.json` (`pulseProtocol`) — the reference implementation reads it from
|
|
319
|
+
there, and npm keeps it for every published release.
|
|
320
|
+
|
|
321
|
+
- The **major** changes only for breaking changes. It's `protocolVersion` in
|
|
322
|
+
`/capabilities` and the `Protocol-Version` header, and it's what pairing
|
|
323
|
+
compares.
|
|
324
|
+
- The **minor** counts additions an older client or server can safely ignore
|
|
325
|
+
(a new optional field, key or header). It's `protocolRevision`.
|
|
326
|
+
|
|
327
|
+
A server MUST report `protocolVersion`, `protocolRevision`,
|
|
328
|
+
`minSupportedVersion` and `maxSupportedVersion` via `/capabilities` (§2), and
|
|
329
|
+
`Protocol-Version` on every response. A client supports a range of majors;
|
|
330
|
+
it pairs only if that range overlaps the server's, and speaks the highest
|
|
331
|
+
major both support. If they don't overlap, it MUST NOT pair, and SHOULD
|
|
332
|
+
surface a clear, specific message rather than a generic failure.
|
|
333
|
+
|
|
334
|
+
### 7.1 Changing the protocol
|
|
335
|
+
|
|
336
|
+
Every fact about the protocol lives in `protocol/`: the JSON Schemas in
|
|
337
|
+
`protocol/schemas/`, and `protocol/openapi.json`, generated from the plugin's
|
|
338
|
+
route schemas. The field tables in this document are generated from the same
|
|
339
|
+
files (`npm run protocol`). CI enforces the rules:
|
|
340
|
+
|
|
341
|
+
- Anything under `protocol/` changes → `pulseProtocol.version` MUST change.
|
|
342
|
+
- A **breaking** change — removing a field, key or parameter; making one
|
|
343
|
+
required; removing an allowed value; changing a type — MUST bump the major.
|
|
344
|
+
Anything else bumps the minor.
|
|
345
|
+
- Every version MUST have a row in the history below saying what changed.
|
|
346
|
+
|
|
347
|
+
### 7.2 `Pulse-Client` (since 2.1)
|
|
348
|
+
|
|
349
|
+
A client SHOULD send `Pulse-Client` on every request, saying what it is and
|
|
350
|
+
which protocol majors it speaks:
|
|
351
|
+
|
|
352
|
+
```
|
|
353
|
+
Pulse-Client: Pulse/2.1.0 (45; ios); protocol=1-2
|
|
314
354
|
```
|
|
315
355
|
|
|
316
|
-
|
|
317
|
-
|
|
356
|
+
On uploads and artifact requests, a server MUST answer
|
|
357
|
+
`426 Upgrade Required` when the client's newest major is older than the
|
|
358
|
+
server's `minSupportedVersion`, with a JSON body carrying `error`,
|
|
359
|
+
`minSupportedVersion` and `maxSupportedVersion`. `/capabilities` MUST always
|
|
360
|
+
answer, so the client can learn why. A request without the header is handled
|
|
361
|
+
as before this header existed. A client that gets `426` SHOULD tell the user
|
|
362
|
+
to update the app. See
|
|
363
|
+
[`protocol/schemas/pulse-client.schema.json`](protocol/schemas/pulse-client.schema.json).
|
|
364
|
+
|
|
365
|
+
A client SHOULD also check `/capabilities` again before starting an upload to
|
|
366
|
+
a server it paired with earlier: a server can be upgraded in between.
|
|
367
|
+
|
|
368
|
+
### 7.3 Where it's defined
|
|
369
|
+
|
|
370
|
+
<!-- BEGIN GENERATED: schemas (from protocol/schemas/ — edit that, then run `npm run protocol`) -->
|
|
371
|
+
| File | Defines |
|
|
372
|
+
|---|---|
|
|
373
|
+
| [`openapi.json`](protocol/openapi.json) | Every HTTP route, generated from the plugin |
|
|
374
|
+
| [`beat-manifest.schema.json`](protocol/schemas/beat-manifest.schema.json) | Beat manifest |
|
|
375
|
+
| [`capabilities.schema.json`](protocol/schemas/capabilities.schema.json) | GET /capabilities response |
|
|
376
|
+
| [`capability-token.schema.json`](protocol/schemas/capability-token.schema.json) | Capability token claims |
|
|
377
|
+
| [`deep-link.schema.json`](protocol/schemas/deep-link.schema.json) | Pairing link query parameters |
|
|
378
|
+
| [`pulse-client.schema.json`](protocol/schemas/pulse-client.schema.json) | Pulse-Client request header |
|
|
379
|
+
| [`upload-metadata.schema.json`](protocol/schemas/upload-metadata.schema.json) | TUS Upload-Metadata keys |
|
|
380
|
+
| [`view-link.schema.json`](protocol/schemas/view-link.schema.json) | POST /artifacts/<id>/view-link response |
|
|
381
|
+
<!-- END GENERATED: schemas -->
|
|
382
|
+
|
|
383
|
+
### 7.4 History
|
|
384
|
+
|
|
385
|
+
| Version | Change |
|
|
386
|
+
|---|---|
|
|
387
|
+
| 1.0 | First version. `/capabilities` and pairing links carried `uploadUnit` (`segment` or `merged`). |
|
|
388
|
+
| 2.0 | **Breaking:** `uploadUnit` removed from `/capabilities` and pairing links — a pulse always uploads as one video (§8). Clients built for 1.0 required it. |
|
|
389
|
+
| 2.1 | Added `protocolRevision` to `/capabilities`, the `Pulse-Client` header with `426 Upgrade Required` (§7.2), and `Upload-Metadata.appVersion` (§4.1). |
|
|
390
|
+
| 2.2 | Added read-only view links: `viewLinks` in `/capabilities`, `POST {prefix}/artifacts/<id>/view-link` (§6.4), and the view token's `use` claim (§5.5). |
|
|
391
|
+
|
|
392
|
+
## 8. Artifact relationships (`relatedTo`)
|
|
393
|
+
|
|
394
|
+
A pulse (a short composed of one or more recorded clips) uploads as **one
|
|
395
|
+
video** — the session anchor, named by the pairing link's `artifactId` (§3) —
|
|
396
|
+
plus these related artifacts:
|
|
318
397
|
|
|
319
398
|
- **captions** (`kind: "captions"`, `<draftId>.vtt`) — WebVTT for the whole
|
|
320
|
-
|
|
321
|
-
|
|
399
|
+
video, OPTIONAL (absent when the video has no speech or no on-device model
|
|
400
|
+
was available); MAY carry word-level inline cue timestamps like
|
|
322
401
|
`<00:00:01.500>word` for karaoke rendering;
|
|
323
|
-
- a **beat manifest** (`kind: "project"`, e.g. `<draftId>-beats.pulse`)
|
|
324
|
-
|
|
325
|
-
contiguous and summing to the true merged `durationMs` (groundwork for
|
|
326
|
-
keyframe-aligned deep links / HLS);
|
|
402
|
+
- a **beat manifest** (`kind: "project"`, e.g. `<draftId>-beats.pulse`)
|
|
403
|
+
listing the pulse's beats (below);
|
|
327
404
|
- a **thumbnail** (`kind: "thumbnail"`, `<draftId>.jpg`) — the pulse's poster
|
|
328
405
|
frame.
|
|
329
406
|
|
|
407
|
+
A **beat** is a timestamp range inside the video: where one recorded clip
|
|
408
|
+
starts and ends (`startMs`/`endMs`). A pulse's beats are contiguous and sum to
|
|
409
|
+
the video's `durationMs`. The beat manifest is groundwork for keyframe-aligned
|
|
410
|
+
deep links / HLS; it does not change how the video is uploaded.
|
|
411
|
+
|
|
330
412
|
```json
|
|
331
413
|
{ "version": 1, "type": "beat-manifest", "durationMs": 47320,
|
|
332
414
|
"beats": [ { "segmentId": "<local id>", "order": 0, "startMs": 0, "endMs": 4210 } ] }
|
|
333
415
|
```
|
|
334
416
|
|
|
335
|
-
Every non-anchor artifact
|
|
336
|
-
|
|
337
|
-
|
|
417
|
+
Every non-anchor artifact SHOULD declare `relatedTo` pointing at the anchor
|
|
418
|
+
`artifactId` so a single capability token can authorize the whole session
|
|
419
|
+
(§5.4).
|
|
338
420
|
|
|
339
|
-
The relational graph of replies between
|
|
421
|
+
The relational graph of replies between pulses (who replied to what,
|
|
340
422
|
rendering a thread) is explicitly **out of scope** for this document. That
|
|
341
423
|
graph is a query/relational concern for the operator's own systems, built
|
|
342
424
|
from `relatedTo` and whatever additional metadata the operator chooses to
|
|
@@ -347,5 +429,5 @@ record — not something a Pulse-compatible server is required to implement.
|
|
|
347
429
|
A client or server MAY support additional, non-normative extensions to this
|
|
348
430
|
contract (additional `Upload-Metadata` keys, additional kinds, additional
|
|
349
431
|
`/capabilities` fields) as long as doing so does not break a client/server
|
|
350
|
-
that only implements what's written here.
|
|
351
|
-
|
|
432
|
+
that only implements what's written here. Changes to the protocol itself
|
|
433
|
+
follow §7.1.
|