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