@mieweb/pulsevault 0.0.1 → 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.
- package/CHANGELOG.md +184 -0
- package/LICENSE +15 -39
- package/OPERATIONS.md +223 -0
- package/PROTOCOL.md +312 -0
- package/README.md +532 -122
- package/augment.d.ts +1 -0
- package/augment.js +3 -0
- package/core.d.ts +1 -0
- package/core.js +7 -0
- package/dist/app.d.ts +90 -34
- package/dist/app.d.ts.map +1 -1
- package/dist/app.js +23 -33
- package/dist/app.js.map +1 -1
- package/dist/augment.d.ts +14 -7
- package/dist/augment.d.ts.map +1 -1
- package/dist/augment.js.map +1 -1
- package/dist/core.d.ts +108 -0
- package/dist/core.d.ts.map +1 -0
- package/dist/core.js +431 -0
- package/dist/core.js.map +1 -0
- package/dist/lib/authorize.d.ts +19 -0
- package/dist/lib/authorize.d.ts.map +1 -0
- package/dist/lib/authorize.js +2 -0
- package/dist/lib/authorize.js.map +1 -0
- package/dist/lib/capability-token.d.ts +80 -0
- package/dist/lib/capability-token.d.ts.map +1 -0
- package/dist/lib/capability-token.js +126 -0
- package/dist/lib/capability-token.js.map +1 -0
- package/dist/lib/checksum.d.ts +45 -0
- package/dist/lib/checksum.d.ts.map +1 -0
- package/dist/lib/checksum.js +102 -0
- package/dist/lib/checksum.js.map +1 -0
- package/dist/lib/deeplinks.d.ts +37 -21
- package/dist/lib/deeplinks.d.ts.map +1 -1
- package/dist/lib/deeplinks.js +54 -13
- package/dist/lib/deeplinks.js.map +1 -1
- package/dist/lib/magic.d.ts +30 -5
- package/dist/lib/magic.d.ts.map +1 -1
- package/dist/lib/magic.js +51 -14
- package/dist/lib/magic.js.map +1 -1
- package/dist/lib/options.d.ts +39 -0
- package/dist/lib/options.d.ts.map +1 -0
- package/dist/lib/options.js +103 -0
- package/dist/lib/options.js.map +1 -0
- package/dist/lib/pulsevaultTus.d.ts +52 -26
- package/dist/lib/pulsevaultTus.d.ts.map +1 -1
- package/dist/lib/pulsevaultTus.js +111 -43
- package/dist/lib/pulsevaultTus.js.map +1 -1
- package/dist/lib/request.d.ts +24 -0
- package/dist/lib/request.d.ts.map +1 -0
- package/dist/lib/request.js +16 -0
- package/dist/lib/request.js.map +1 -0
- package/dist/lib/uuid.d.ts.map +1 -1
- package/dist/lib/uuid.js +1 -1
- package/dist/lib/uuid.js.map +1 -1
- package/dist/routes/pulsevault.d.ts +18 -14
- package/dist/routes/pulsevault.d.ts.map +1 -1
- package/dist/routes/pulsevault.js +127 -247
- package/dist/routes/pulsevault.js.map +1 -1
- package/dist/storage/local.d.ts +31 -10
- package/dist/storage/local.d.ts.map +1 -1
- package/dist/storage/local.js +166 -82
- package/dist/storage/local.js.map +1 -1
- package/dist/storage/s3.d.ts +109 -0
- package/dist/storage/s3.d.ts.map +1 -0
- package/dist/storage/s3.js +432 -0
- package/dist/storage/s3.js.map +1 -0
- package/dist/storage/types.d.ts +69 -17
- package/dist/storage/types.d.ts.map +1 -1
- 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.
|