@mieweb/pulsevault 0.4.2 → 0.5.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 +89 -0
- package/OPERATIONS.md +24 -10
- package/PROTOCOL.md +38 -1
- package/README.md +161 -29
- package/dist/app.d.ts +24 -0
- package/dist/app.d.ts.map +1 -1
- package/dist/app.js +20 -2
- package/dist/app.js.map +1 -1
- package/dist/augment.d.ts +3 -0
- package/dist/augment.d.ts.map +1 -1
- package/dist/augment.js.map +1 -1
- package/dist/core.d.ts +89 -4
- package/dist/core.d.ts.map +1 -1
- package/dist/core.js +301 -40
- package/dist/core.js.map +1 -1
- package/dist/lib/authorize.d.ts +28 -6
- package/dist/lib/authorize.d.ts.map +1 -1
- package/dist/lib/capability-token.d.ts +26 -0
- package/dist/lib/capability-token.d.ts.map +1 -1
- package/dist/lib/capability-token.js +73 -5
- package/dist/lib/capability-token.js.map +1 -1
- package/dist/lib/completion.d.ts +135 -0
- package/dist/lib/completion.d.ts.map +1 -0
- package/dist/lib/completion.js +288 -0
- package/dist/lib/completion.js.map +1 -0
- package/dist/lib/options.d.ts.map +1 -1
- package/dist/lib/options.js +2 -0
- package/dist/lib/options.js.map +1 -1
- package/dist/lib/protocol-version.gen.js +1 -1
- package/dist/lib/pulsevaultTus.d.ts +28 -26
- package/dist/lib/pulsevaultTus.d.ts.map +1 -1
- package/dist/lib/pulsevaultTus.js +114 -31
- package/dist/lib/pulsevaultTus.js.map +1 -1
- package/dist/routes/pulsevault.d.ts +12 -1
- package/dist/routes/pulsevault.d.ts.map +1 -1
- package/dist/routes/pulsevault.js +92 -1
- package/dist/routes/pulsevault.js.map +1 -1
- package/dist/storage/local.d.ts +7 -1
- package/dist/storage/local.d.ts.map +1 -1
- package/dist/storage/local.js +173 -8
- package/dist/storage/local.js.map +1 -1
- package/dist/storage/s3.d.ts +7 -1
- package/dist/storage/s3.d.ts.map +1 -1
- package/dist/storage/s3.js +236 -14
- package/dist/storage/s3.js.map +1 -1
- package/dist/storage/types.d.ts +73 -0
- package/dist/storage/types.d.ts.map +1 -1
- package/package.json +2 -2
- package/protocol/openapi.json +247 -1
- package/protocol/schemas/artifact-status.schema.json +49 -0
- package/protocol/schemas/capability-token.schema.json +3 -0
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,95 @@ breaking changes, called out explicitly below.
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.5.0] - 2026-10-05
|
|
11
|
+
|
|
12
|
+
Protocol 2.3 (`PROTOCOL.md` §7.4). What every host rebuilt around PulseVault
|
|
13
|
+
(#80), in the core: the shape of a pulse, a completion that survives a throw
|
|
14
|
+
or a restart, a lock on finished pulses, the token's own context, a status
|
|
15
|
+
route, background web-ready, a poster route and `getPulse`, and the content
|
|
16
|
+
types phones send. Two defaults change; both have an escape hatch.
|
|
17
|
+
|
|
18
|
+
### Changed (breaking)
|
|
19
|
+
|
|
20
|
+
- **A create must have the shape of a pulse** (`pulseShape`, on by default):
|
|
21
|
+
a video with no `relatedTo`; a thumbnail, beat manifest or captions under
|
|
22
|
+
its own id, `relatedTo` the video. With `createCapabilityAuthorize` the ids
|
|
23
|
+
are also tied to the token: the video only under the token's own
|
|
24
|
+
`artifactId`, the rest only `relatedTo` it. Before, one token could create
|
|
25
|
+
any number of artifacts under ids the client picked, or take the video's
|
|
26
|
+
own id with a thumbnail so the real video answered `409` for good. The
|
|
27
|
+
Pulse app has always uploaded in this shape. For uploads that aren't
|
|
28
|
+
pulses, pass `pulseShape: false` to the plugin or core *and* to
|
|
29
|
+
`createCapabilityAuthorize`.
|
|
30
|
+
- **A create reclaims an idle unfinished upload of the same artifactId**
|
|
31
|
+
(`reclaim`, `{ idleSeconds: 300 }` by default): an app killed mid-upload
|
|
32
|
+
sends no TUS `DELETE`, so re-scanning the same link answered `409` until
|
|
33
|
+
`retention` ran. Only an upload of the same kind and `relatedTo` is taken
|
|
34
|
+
over (the same token authorized both), and only once the local adapter has
|
|
35
|
+
seen no bytes for `idleSeconds`. `reclaim: false` keeps the old `409`.
|
|
36
|
+
- **`onUploadComplete` is at-least-once.** PulseVault records that the hook
|
|
37
|
+
returned (`acknowledged` in the sidecar) and fires it again, with
|
|
38
|
+
`ctx.replay: true`, for a finished artifact it never recorded: the hook
|
|
39
|
+
threw, or the process stopped first. Every 300 seconds, with the first pass
|
|
40
|
+
shortly after start (`replayCompletions`; `false` turns it off, and
|
|
41
|
+
`core.replayCompletions()` / `fastify.pulseVaultCore.replayCompletions()`
|
|
42
|
+
runs a pass on demand). Hosts must check their own record before writing
|
|
43
|
+
it again. Finished sidecars written before this release read as acknowledged, so an
|
|
44
|
+
upgrade replays nothing; one still uploading at the upgrade completes
|
|
45
|
+
normally. A retried final `PATCH` no longer fires the hook a
|
|
46
|
+
second time.
|
|
47
|
+
- **`authorize` gains the `status` phase** (`GET /artifacts/:id/status`).
|
|
48
|
+
An exhaustive `switch` on the phase needs a case for it.
|
|
49
|
+
- **Content types**: the `.pulse` beat manifest is served as
|
|
50
|
+
`application/json`, and `.mov`, `.m4v` and `.srt` — when a host allows them
|
|
51
|
+
in `allowedExtensions`; the defaults stay what the Pulse app sends — as
|
|
52
|
+
`video/quicktime`, `video/x-m4v` and `application/x-subrip`, instead of
|
|
53
|
+
`application/octet-stream`, which `<video>` downloads instead of playing.
|
|
54
|
+
|
|
55
|
+
### Added
|
|
56
|
+
|
|
57
|
+
- **Hook context**: `authorize` and `onUploadComplete` receive `relatedTo`,
|
|
58
|
+
`name`, `appVersion`, `filename`, `ext` and the token's `context`, so a
|
|
59
|
+
host maps a finished file to its own record without reading storage back.
|
|
60
|
+
- **`context` in the capability token** (`issueCapabilityToken(id, secret,
|
|
61
|
+
{ context })`, the `ctx` claim, at most 1 KiB of JSON): an owner, a
|
|
62
|
+
destination — signed, stored with the artifact at create (`authorize` may
|
|
63
|
+
return `{ context }`), and handed to every hook, `getStatus` and `getPulse`.
|
|
64
|
+
Most hosts need no table keyed by artifactId any more.
|
|
65
|
+
- **`GET /artifacts/:id/status`** and `core.getStatus(id)`: `unknown`,
|
|
66
|
+
`uploading` (with `bytesReceived` against `size`), `processing` or
|
|
67
|
+
`ready`, plus `acknowledged` and whatever the host recorded with
|
|
68
|
+
`recordOutcome(id, outcome)`. Authorized as `status`;
|
|
69
|
+
`createCapabilityAuthorize` grants it to the pairing token and a view token.
|
|
70
|
+
- **`webReady`**: the conversion `ensureWebReady` does, run by the core in
|
|
71
|
+
the background after the final `PATCH` is answered, `concurrency` at a
|
|
72
|
+
time, with a `processing` status meanwhile, a `processed` event when done,
|
|
73
|
+
and `completeAfter: true` to hold `onUploadComplete` until the conversion
|
|
74
|
+
has finished (`ctx.webReady` says what it did). Whether it finished is
|
|
75
|
+
recorded on the sidecar (`converted`), so a conversion a restart
|
|
76
|
+
interrupted is redone by the replay. Local storage only.
|
|
77
|
+
- **`lockWhenReady`**: a finished artifact, and anything `relatedTo` a
|
|
78
|
+
finished video, can't be deleted through the routes (`403`); the host
|
|
79
|
+
removes through `storage.remove` on its own terms.
|
|
80
|
+
- **`core.getPulse(videoId)`** → `{ video, thumbnail, captions, manifest }`
|
|
81
|
+
and **`GET /artifacts/:videoId/poster`**, both from a relation index the
|
|
82
|
+
adapters keep at reserve time (`storage.listRelated`), so neither scans
|
|
83
|
+
storage and nothing is backfilled.
|
|
84
|
+
- **Storage adapter contract**: optional `describeArtifact`, `patchArtifact`
|
|
85
|
+
and `listRelated`, and `context` in `reserveUpload` (both built-in
|
|
86
|
+
adapters implement them). The local adapter's relation index lives at
|
|
87
|
+
`.pulsevault/related/<videoId>/<artifactId>`; the S3 adapter's under the
|
|
88
|
+
same key prefix.
|
|
89
|
+
- **`onArtifactEvent`**: `phase: "processed"` (with `webReady`), and
|
|
90
|
+
`reason: "reclaimed"` on `remove`.
|
|
91
|
+
- **Fastify**: `fastify.pulseVaultCore` (`coreDecoratorName`) exposes
|
|
92
|
+
`getStatus`, `getPulse`, `recordOutcome` and `replayCompletions`.
|
|
93
|
+
- **`examples/meteor-demo`** is now the reference host integration, shaped
|
|
94
|
+
like a team app: people, destinations, a reserve that signs the owner and
|
|
95
|
+
destination into the token, delivery from `onUploadComplete`, status
|
|
96
|
+
polling, and video cards with posters — with no table of uploads, no shape
|
|
97
|
+
check, no stale-upload cleanup and no content-type fix-up of its own.
|
|
98
|
+
|
|
10
99
|
## [0.4.2] - 2026-10-02
|
|
11
100
|
|
|
12
101
|
### Added
|
package/OPERATIONS.md
CHANGED
|
@@ -183,25 +183,29 @@ logs one warning and the original bytes keep serving exactly as before.
|
|
|
183
183
|
`brew install ffmpeg` (macOS). Nothing else changes; the hook detects it at
|
|
184
184
|
first use.
|
|
185
185
|
|
|
186
|
-
**New uploads** —
|
|
187
|
-
|
|
186
|
+
**New uploads** — turn on the `webReady` option (the fastify-demo and the
|
|
187
|
+
meteor-demo ship with it enabled). The core runs the conversion in the
|
|
188
|
+
background after the final `PATCH` is answered, one video at a time by
|
|
189
|
+
default, and reports the artifact as `processing` on the status route
|
|
190
|
+
meanwhile:
|
|
188
191
|
|
|
189
192
|
```js
|
|
190
|
-
const storage = createLocalStorage({ workspaceDir: dataDir });
|
|
191
193
|
await app.register(pulseVault, {
|
|
192
|
-
storage,
|
|
193
|
-
|
|
194
|
-
if (kind !== "video") return;
|
|
195
|
-
const localPath = await storage.getLocalPath(artifactId);
|
|
196
|
-
if (localPath) await ensureWebReady(localPath, { logger: app.log });
|
|
197
|
-
},
|
|
194
|
+
storage: createLocalStorage({ workspaceDir: dataDir }),
|
|
195
|
+
webReady: { concurrency: 1, completeAfter: true },
|
|
198
196
|
});
|
|
199
197
|
```
|
|
200
198
|
|
|
201
199
|
Options: `{ transcode: false }` restricts it to the lossless remux (no CPU
|
|
202
200
|
cost beyond a file rewrite); `crf`/`preset` tune the transcode
|
|
203
201
|
(defaults `23`/`veryfast`); `ffmpegPath`/`ffprobePath` point at binaries off
|
|
204
|
-
`PATH
|
|
202
|
+
`PATH`; `concurrency` runs several at once; `completeAfter: true` holds
|
|
203
|
+
`onUploadComplete` until the conversion has finished, for a host that
|
|
204
|
+
publishes the video from the hook. `onArtifactEvent` fires `processed` with
|
|
205
|
+
what was done. A conversion a restart interrupted is resumed by the
|
|
206
|
+
completion replay (`replayCompletions`). Calling `ensureWebReady` yourself
|
|
207
|
+
from `onUploadComplete` still works, but holds the client's final `PATCH`
|
|
208
|
+
for the whole conversion.
|
|
205
209
|
|
|
206
210
|
**Existing artifacts** — uploads that landed before the hook existed are fixed
|
|
207
211
|
once with the bundled migration script (idempotent; interrupt and rerun
|
|
@@ -231,6 +235,16 @@ integrity for artifacts this feature has touched.
|
|
|
231
235
|
captions, manifest or thumbnail of a video that never finished — are cleaned
|
|
232
236
|
up by the opt-in `retention` option (or `sweepAbandonedUploads` from your own
|
|
233
237
|
scheduler); see the README's `retention` option. Both adapters support it.
|
|
238
|
+
Separately, a create that arrives for an unfinished upload of the same id
|
|
239
|
+
(the person scanned the same link again after the app died) takes it over
|
|
240
|
+
once it has been idle for `reclaim.idleSeconds` (300 by default), so nobody
|
|
241
|
+
waits for the sweep.
|
|
242
|
+
|
|
243
|
+
**Completions the host never recorded** — an `onUploadComplete` that threw,
|
|
244
|
+
or a restart between the final byte and the hook — are fired again by the
|
|
245
|
+
completion replay (`replayCompletions`, every 300 seconds by default, and
|
|
246
|
+
`pulseVaultCore.replayCompletions()` on demand). Nothing to schedule; make the
|
|
247
|
+
hook idempotent.
|
|
234
248
|
|
|
235
249
|
How long to keep **finished** content is a policy decision `pulsevault`
|
|
236
250
|
leaves to the operator (see `PROTOCOL.md` §1 on lifecycle ownership). A
|
package/PROTOCOL.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
<!-- BEGIN GENERATED: protocol-version (from package.json pulseProtocol — edit that, then run `npm run protocol`) -->
|
|
4
4
|
| | |
|
|
5
5
|
|---|---|
|
|
6
|
-
| Spec revision | `2.
|
|
6
|
+
| Spec revision | `2.3` |
|
|
7
7
|
| Protocol majors accepted | 2 |
|
|
8
8
|
<!-- END GENERATED: protocol-version -->
|
|
9
9
|
|
|
@@ -246,6 +246,18 @@ the token's `artifactId` as its `relatedTo` (§8) — this lets one token cover
|
|
|
246
246
|
an entire upload session (a pulse's video plus its captions, beat manifest and
|
|
247
247
|
thumbnail) rather than requiring one token per artifact.
|
|
248
248
|
|
|
249
|
+
A token MAY also carry an opaque `ctx` claim (protocol 2.3): host data signed
|
|
250
|
+
with the rest — who the upload is for, where it goes — that the server stores
|
|
251
|
+
with the artifact the token creates and hands back to its own hooks, so the
|
|
252
|
+
host needs no table keyed by artifactId. A client never reads it.
|
|
253
|
+
|
|
254
|
+
When a token names a pulse's video, a server SHOULD hold the token to the
|
|
255
|
+
shape of a pulse (§8): the video only under the token's own `artifactId`, with
|
|
256
|
+
no `relatedTo`, and a thumbnail, beat manifest or captions only under another
|
|
257
|
+
id, `relatedTo` the token's. Otherwise one token can create any number of
|
|
258
|
+
artifacts under ids of the client's choosing, or take the video's own id with a
|
|
259
|
+
thumbnail so the video itself can never land.
|
|
260
|
+
|
|
249
261
|
This shape is exactly what `@mieweb/pulsevault`'s `issueCapabilityToken`/
|
|
250
262
|
`verifyCapabilityToken`/`createCapabilityAuthorize` implement, but any server
|
|
251
263
|
is free to use its own scheme entirely — only §5.1–§5.3 are normative.
|
|
@@ -312,6 +324,29 @@ right after the upload finishes, while its pairing token is still valid; a
|
|
|
312
324
|
server without `viewLinks` offers no shareable link, and the client MUST NOT
|
|
313
325
|
share the pairing token in its place.
|
|
314
326
|
|
|
327
|
+
### 6.5 Status and poster (optional, protocol 2.3)
|
|
328
|
+
|
|
329
|
+
A server MAY expose `GET {prefix}/artifacts/<artifactId>/status`, authorized
|
|
330
|
+
like opening the artifact (§5.1; a view token, §5.5, is enough), reporting
|
|
331
|
+
where an upload is so a page waiting on it needs nothing from the host. Its
|
|
332
|
+
body is defined by
|
|
333
|
+
[`protocol/schemas/artifact-status.schema.json`](protocol/schemas/artifact-status.schema.json):
|
|
334
|
+
`state` is `unknown`, `uploading`, `processing` (the server is converting the
|
|
335
|
+
bytes for the web) or `ready`, with the bytes received against the declared
|
|
336
|
+
length while uploading, and whatever outcome the host recorded once finished.
|
|
337
|
+
The response MUST NOT be cached.
|
|
338
|
+
|
|
339
|
+
A server MAY also expose `GET {prefix}/artifacts/<videoId>/poster`: the finished
|
|
340
|
+
thumbnail `relatedTo` the video (§8), served exactly as §6.2 serves an artifact
|
|
341
|
+
and authorized as opening the video, so whoever may watch it may see its poster.
|
|
342
|
+
It returns `404` until the thumbnail has landed.
|
|
343
|
+
|
|
344
|
+
An unfinished upload whose client died (an app killed mid-upload sends no
|
|
345
|
+
TUS `DELETE`) would otherwise answer `409` to every later create of the same
|
|
346
|
+
`artifactId`. A server SHOULD let a create authorized by the same token take
|
|
347
|
+
over such an upload once it has been idle for a while, instead of refusing it
|
|
348
|
+
until cleanup runs.
|
|
349
|
+
|
|
315
350
|
## 7. Versioning
|
|
316
351
|
|
|
317
352
|
The protocol has a version `major.minor`, written once in `@mieweb/pulsevault`'s
|
|
@@ -371,6 +406,7 @@ a server it paired with earlier: a server can be upgraded in between.
|
|
|
371
406
|
| File | Defines |
|
|
372
407
|
|---|---|
|
|
373
408
|
| [`openapi.json`](protocol/openapi.json) | Every HTTP route, generated from the plugin |
|
|
409
|
+
| [`artifact-status.schema.json`](protocol/schemas/artifact-status.schema.json) | GET /artifacts/<id>/status response |
|
|
374
410
|
| [`beat-manifest.schema.json`](protocol/schemas/beat-manifest.schema.json) | Beat manifest |
|
|
375
411
|
| [`capabilities.schema.json`](protocol/schemas/capabilities.schema.json) | GET /capabilities response |
|
|
376
412
|
| [`capability-token.schema.json`](protocol/schemas/capability-token.schema.json) | Capability token claims |
|
|
@@ -388,6 +424,7 @@ a server it paired with earlier: a server can be upgraded in between.
|
|
|
388
424
|
| 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
425
|
| 2.1 | Added `protocolRevision` to `/capabilities`, the `Pulse-Client` header with `426 Upgrade Required` (§7.2), and `Upload-Metadata.appVersion` (§4.1). |
|
|
390
426
|
| 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). |
|
|
427
|
+
| 2.3 | Added `GET {prefix}/artifacts/<id>/status` and `GET {prefix}/artifacts/<id>/poster` (§6.5), the capability token's `ctx` claim (§5.4), and the pulse-shape rule for creates (§5.4). |
|
|
391
428
|
|
|
392
429
|
## 8. Artifact relationships (`relatedTo`)
|
|
393
430
|
|
package/README.md
CHANGED
|
@@ -188,13 +188,17 @@ Four runnable example servers live under [`examples/`](examples):
|
|
|
188
188
|
query instead of a sidecar crawl), uploads grouped by recording session
|
|
189
189
|
via `relatedTo`, WebVTT captions, Swagger UI, and an `onArtifactEvent`
|
|
190
190
|
live feed.
|
|
191
|
-
- [`examples/
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
191
|
+
- [`examples/meteor-demo`](examples/meteor-demo) — the **reference host
|
|
192
|
+
integration**, on `@mieweb/pulsevault/core` under Meteor: shaped like a
|
|
193
|
+
team app (people, destinations), it reserves an upload by signing the owner
|
|
194
|
+
and destination into the capability token, delivers the video from
|
|
195
|
+
`onUploadComplete` to where it was started, polls the status route from
|
|
196
|
+
the pairing popup, and shows every video as a card with its poster — with
|
|
197
|
+
no table of uploads, no shape check, no stale-upload cleanup and no
|
|
198
|
+
content-type fix-up of its own. Verified against a real `meteor create`
|
|
199
|
+
app, since Meteor's bundler needed the compatibility fixes described above.
|
|
200
|
+
- [`examples/express-demo`](examples/express-demo) — the fastify-demo on
|
|
201
|
+
`@mieweb/pulsevault/core` under Express.
|
|
198
202
|
|
|
199
203
|
## How a pairing + upload session flows
|
|
200
204
|
|
|
@@ -231,7 +235,9 @@ The plugin mounts the following routes under `prefix` (`@mieweb/pulsevault/core`
|
|
|
231
235
|
| `PATCH` / `HEAD` / `DELETE` \* | `/pulsevault/upload/:id` | Upload chunks, probe offset, delete the upload (TUS) |
|
|
232
236
|
| `GET` | `/pulsevault/artifacts/:artifactId` | Stream or redirect to the uploaded artifact (any kind) |
|
|
233
237
|
| `POST` | `/pulsevault/artifacts/:artifactId/view-link` | Mint a read-only view link to a finished artifact (only with [`issueViewLink`](#issueviewlink)) |
|
|
234
|
-
| `
|
|
238
|
+
| `GET` | `/pulsevault/artifacts/:artifactId/status` | Where an upload is: `uploading` (with progress), `processing`, `ready`, plus the host's recorded outcome (see [Status, outcome and pulses](#status-outcome-and-pulses)) |
|
|
239
|
+
| `GET` | `/pulsevault/artifacts/:artifactId/poster` | A video's poster frame: the finished thumbnail `relatedTo` it, served like the artifact route |
|
|
240
|
+
| `DELETE` | `/pulsevault/artifacts/:artifactId` | Delete a finalized upload (bytes + sidecar); refused once finished with [`lockWhenReady`](#lockwhenready) |
|
|
235
241
|
|
|
236
242
|
\* `DELETE /pulsevault/upload/:id` is TUS termination, addressed by the upload URL: it removes the artifact whether the upload is still in flight (a cancel) or finished, exactly as `DELETE /pulsevault/artifacts/:artifactId` does by artifactId.
|
|
237
243
|
|
|
@@ -250,12 +256,20 @@ type PulseVaultPluginOptions = {
|
|
|
250
256
|
allowedExtensions?:
|
|
251
257
|
| string[] // legacy — treated as video-only
|
|
252
258
|
| { video?: string[]; project?: string[]; captions?: string[]; thumbnail?: string[] }; // per-kind (recommended)
|
|
253
|
-
// defaults: { video: [".mp4"], project: [".pulse", ".zip"], captions: [".vtt"], thumbnail: [".jpg", ".jpeg", ".png"] }
|
|
259
|
+
// defaults: { video: [".mp4"], project: [".pulse", ".zip"], captions: [".vtt"], thumbnail: [".jpg", ".jpeg", ".png"] } — what Pulse sends
|
|
254
260
|
cache?: PulseVaultCacheOptions;
|
|
255
261
|
authorize?: PulseVaultAuthorize;
|
|
256
262
|
validatePayload?: PulseVaultValidatePayload; // runs for every kind; branch on ctx.kind
|
|
257
|
-
onUploadComplete?: PulseVaultOnUploadComplete; // runs for every kind; branch on ctx.kind
|
|
263
|
+
onUploadComplete?: PulseVaultOnUploadComplete; // runs for every kind; branch on ctx.kind; at-least-once
|
|
258
264
|
onArtifactEvent?: PulseVaultOnArtifactEvent; // low-frequency hook for metrics + audit logging
|
|
265
|
+
issueViewLink?: PulseVaultIssueViewLink; // read-only view links
|
|
266
|
+
retention?: { abandonedAfterSeconds: number; sweepIntervalSeconds?: number };
|
|
267
|
+
pulseShape?: boolean; // default true: creates must have the shape of a pulse
|
|
268
|
+
reclaim?: { idleSeconds?: number } | false; // default { idleSeconds: 300 }: a create takes over an idle unfinished upload
|
|
269
|
+
lockWhenReady?: boolean; // default false: a finished pulse can't be deleted through the routes
|
|
270
|
+
webReady?: PulseVaultWebReadyOptions | boolean; // default off: background faststart remux / H.264 transcode
|
|
271
|
+
replayCompletions?: { intervalSeconds?: number } | false; // default every 300 s: re-fire unacknowledged completions
|
|
272
|
+
coreDecoratorName?: string; // default "pulseVaultCore": getStatus, getPulse, recordOutcome, replayCompletions
|
|
259
273
|
/** @deprecated use validatePayload + ctx.kind === "project" */
|
|
260
274
|
validateProjectPayload?: PulseVaultValidatePayload;
|
|
261
275
|
/** @deprecated use onUploadComplete + ctx.kind === "project" */
|
|
@@ -322,7 +336,7 @@ Upload filenames are keyed by UUID, so `immutable: true` is safe when `maxAge` i
|
|
|
322
336
|
|
|
323
337
|
Optional async hook called before TUS create/patch, before GET resolve, before DELETE, and before minting a view link. Throw to reject — a `statusCode` or `status_code` number on the thrown error is used as the HTTP status (default `403`).
|
|
324
338
|
|
|
325
|
-
Phase mapping: `"create"` is the initial TUS `POST`; `"patch"` covers `PATCH` chunks and `HEAD` offset queries on the upload routes; `"resolve"` is `GET {prefix}/artifacts/<id
|
|
339
|
+
Phase mapping: `"create"` is the initial TUS `POST`; `"patch"` covers `PATCH` chunks and `HEAD` offset queries on the upload routes; `"resolve"` is `GET {prefix}/artifacts/<id>` and the poster route (authorized on the video); `"status"` is `GET {prefix}/artifacts/<id>/status`; `"delete"` is **both** ways of removing an artifact — `DELETE {prefix}/artifacts/<id>` and the TUS `DELETE {prefix}/upload/<id>` (a cancel of an in-flight upload, or removal of a finished one). Gating `phase === "delete"` gates every removal. `"share"` is `POST {prefix}/artifacts/<id>/view-link` — minting a read-only link — and only happens when [`issueViewLink`](#issueviewlink) is set.
|
|
326
340
|
|
|
327
341
|
```ts
|
|
328
342
|
type PulseVaultAuthorize = (
|
|
@@ -333,15 +347,22 @@ type PulseVaultAuthorize = (
|
|
|
333
347
|
// `http.IncomingMessage`, etc.) — the same hook works under either.
|
|
334
348
|
request: PulseVaultRequest,
|
|
335
349
|
ctx: {
|
|
336
|
-
phase: "create" | "patch" | "resolve" | "delete" | "share";
|
|
350
|
+
phase: "create" | "patch" | "resolve" | "status" | "delete" | "share";
|
|
337
351
|
artifactId: string;
|
|
338
|
-
kind: "video" | "project" | "captions"; // artifact kind; always present
|
|
339
|
-
token?: string; // only on "resolve"
|
|
340
|
-
relatedTo?: string; // the
|
|
352
|
+
kind: "video" | "project" | "captions" | "thumbnail"; // artifact kind; always present
|
|
353
|
+
token?: string; // only on the "resolve" and "status" phases
|
|
354
|
+
relatedTo?: string; // the video this artifact belongs to, if it declared one
|
|
355
|
+
name?: string; // Upload-Metadata.name, when sent
|
|
356
|
+
appVersion?: string; // Upload-Metadata.appVersion, when sent
|
|
357
|
+
filename?: string; // Upload-Metadata.filename
|
|
358
|
+
ext?: string; // its lowercase extension, with the dot
|
|
359
|
+
context?: unknown; // the token's context, read back from storage (absent on "create")
|
|
341
360
|
},
|
|
342
|
-
) => void | Promise<void>;
|
|
361
|
+
) => void | { context?: unknown } | Promise<void | { context?: unknown }>;
|
|
343
362
|
```
|
|
344
363
|
|
|
364
|
+
On `create` the hook may return `{ context }`: opaque host data (an owner, a destination) stored with the artifact and handed to every later `authorize`, to `onUploadComplete`, to `getStatus` and to `getPulse`. `createCapabilityAuthorize` returns the token's own `context` claim (see [Capability tokens](#capability-tokens)), so most hosts never touch this.
|
|
365
|
+
|
|
345
366
|
```ts
|
|
346
367
|
await app.register(pulseVault, {
|
|
347
368
|
// ...
|
|
@@ -414,25 +435,42 @@ Optional async hook fired once the final byte is written, `validatePayload` has
|
|
|
414
435
|
|
|
415
436
|
```ts
|
|
416
437
|
type PulseVaultOnUploadComplete = (
|
|
417
|
-
request: PulseVaultRequest, // see the note under `authorize` above
|
|
418
|
-
ctx: {
|
|
438
|
+
request: PulseVaultRequest, // see the note under `authorize` above; `{ headers: {} }` on a replay
|
|
439
|
+
ctx: {
|
|
440
|
+
artifactId: string;
|
|
441
|
+
kind: "video" | "project" | "captions" | "thumbnail";
|
|
442
|
+
size: number;
|
|
443
|
+
uploadId: string;
|
|
444
|
+
filename: string; // Upload-Metadata.filename
|
|
445
|
+
ext: string; // its lowercase extension, with the dot
|
|
446
|
+
relatedTo?: string; // the video a thumbnail, beat manifest or captions belongs to
|
|
447
|
+
name?: string; // Upload-Metadata.name (the draft's title), when sent
|
|
448
|
+
appVersion?: string; // Upload-Metadata.appVersion, when sent
|
|
449
|
+
context?: unknown; // the capability token's context (see Capability tokens)
|
|
450
|
+
replay: boolean; // true when PulseVault is firing a completion it never recorded
|
|
451
|
+
webReady?: WebReadyResult; // with `webReady.completeAfter`: what the conversion did
|
|
452
|
+
},
|
|
419
453
|
) => void | Promise<void>;
|
|
420
454
|
```
|
|
421
455
|
|
|
456
|
+
**Completion is at-least-once.** PulseVault records that the hook returned (`acknowledged` in the artifact's sidecar). A finished artifact whose hook never returned — it threw, or the process stopped between the final byte and the hook — is fired again later with `ctx.replay: true` (see [`replayCompletions`](#replaycompletions)). So the hook must check its own record before writing it again: an upsert, or a claim that only one caller wins. A retried final `PATCH` for an acknowledged upload does not fire the hook a second time.
|
|
457
|
+
|
|
422
458
|
### `onArtifactEvent`
|
|
423
459
|
|
|
424
460
|
Optional low-frequency hook — fired on authorize rejection (every phase but per-chunk `patch`), upload completion, payload-validation rejection, and removal. One hook covers both ops metrics and a compliance audit trail instead of hand-wiring both from the lower-level hooks above, and `remove` is where a host keeping its own index of artifacts drops one:
|
|
425
461
|
|
|
426
462
|
```ts
|
|
427
463
|
type PulseVaultArtifactEvent = {
|
|
428
|
-
phase: "authorize" | "complete" | "reject" | "remove";
|
|
464
|
+
phase: "authorize" | "complete" | "reject" | "remove" | "processed";
|
|
429
465
|
artifactId: string;
|
|
430
466
|
kind: "video" | "project" | "captions" | "thumbnail";
|
|
431
467
|
size?: number;
|
|
432
468
|
// present for "authorize" and "reject"; on "remove", "deleted" (DELETE /artifacts/:id or a
|
|
433
|
-
// TUS DELETE)
|
|
469
|
+
// TUS DELETE), "abandoned" (the `retention` sweep) or "reclaimed" (a create took over an
|
|
470
|
+
// idle unfinished upload); on "processed", what the web-ready conversion did
|
|
434
471
|
reason?: string;
|
|
435
472
|
appVersion?: string; // the uploading app's version, on "complete"/"reject" (protocol 2.1)
|
|
473
|
+
webReady?: WebReadyResult; // on "processed": { action, reason }
|
|
436
474
|
};
|
|
437
475
|
|
|
438
476
|
onArtifactEvent: (event) => {
|
|
@@ -482,6 +520,59 @@ Finished videos, and anything whose video finished, are never touched; this is n
|
|
|
482
520
|
|
|
483
521
|
A sweep reads the metadata of every artifact older than the cutoff — one `GetObject` each on S3 — so on a large bucket sweep daily (or off-peak from a cron job) rather than hourly. Several instances may sweep the same storage (a removal that finds nothing is a no-op), but each instance caches metadata: on S3, an id another instance removed can still answer `409` on this one until the entry is evicted — the same as for `DELETE /artifacts/:id`. To schedule it yourself (a cron job, a queue), call `sweepAbandonedUploads(storage, { abandonedAfterSeconds, onRemoved? })` instead; it resolves the removed artifactIds.
|
|
484
522
|
|
|
523
|
+
### `pulseShape`
|
|
524
|
+
|
|
525
|
+
On by default. Every create must have the shape of a pulse (`PROTOCOL.md` §8): a video is created with no `relatedTo`, and a thumbnail, beat manifest or captions under its own id, `relatedTo` the video it belongs to. Anything else is refused at create with `403`, before any bytes move. With [`createCapabilityAuthorize`](#capability-tokens) the ids are also tied to the token: the video only under the token's own `artifactId`, the rest only `relatedTo` it. Without this, one token could create any number of videos under ids the client picks (never attached to anything, still converted, never removed), or take the video's own id with a thumbnail so the real video answers `409` for good.
|
|
526
|
+
|
|
527
|
+
The Pulse app has always uploaded in this shape. For uploads that aren't pulses, pass `pulseShape: false` here **and** to `createCapabilityAuthorize`.
|
|
528
|
+
|
|
529
|
+
### `reclaim`
|
|
530
|
+
|
|
531
|
+
`{ idleSeconds: 300 }` by default; `false` turns it off. An app killed mid-upload sends no TUS `DELETE`, so its unfinished upload keeps its artifactId and every later create of that id — the person scanning the same link again — answers `409` until `retention` removes it. With `reclaim`, a create takes over an unfinished upload of the same artifactId once it has been idle for `idleSeconds` — no bytes received: the bytes file's mtime on the local adapter, the newest multipart part (or the incomplete part parked between `PATCH`es) on S3: the old upload is removed (`onArtifactEvent` `remove` with `reason: "reclaimed"`) and the new one starts from byte 0. Only an upload of the same kind and `relatedTo` is taken over, so the token that authorized the new create authorized the old one; only one whose last activity the adapter knows; and creates for one artifactId run one at a time per instance, so two rescans can't both take it over.
|
|
532
|
+
|
|
533
|
+
### `lockWhenReady`
|
|
534
|
+
|
|
535
|
+
Off by default. Once an artifact is finished, `DELETE {prefix}/artifacts/<id>` and the TUS `DELETE` answer `403` for it, and for anything `relatedTo` a finished video: a pulse that landed stays, whatever token is presented. Removal then goes through `storage.remove` on the host's own terms (an admin action, a retention policy). The check reads storage (not the per-instance cache) just before the removal. For the TUS `DELETE` it runs inside tus's per-upload lock, so a cancel racing the final chunk of the same upload waits for it and then finds the artifact finished. `DELETE {prefix}/artifacts/<id>` takes no such lock — the Pulse app never uses it while uploading — so a delete and a final chunk arriving in the same instant can both succeed there.
|
|
536
|
+
|
|
537
|
+
### `webReady`
|
|
538
|
+
|
|
539
|
+
Off by default. `true`, or `ensureWebReady`'s options plus:
|
|
540
|
+
|
|
541
|
+
```ts
|
|
542
|
+
webReady: {
|
|
543
|
+
concurrency: 1, // conversions at once; a transcode is CPU-bound
|
|
544
|
+
completeAfter: false, // true: run onUploadComplete only once the conversion has finished
|
|
545
|
+
transcode: true, crf: 23, preset: "veryfast", ffmpegPath: "ffmpeg", ffprobePath: "ffprobe",
|
|
546
|
+
},
|
|
547
|
+
```
|
|
548
|
+
|
|
549
|
+
Every finished video is made web-playable in the background, after the final `PATCH` is answered: a lossless faststart remux when the `moov` atom is at the end, or a one-time H.264 transcode when the codec is one browsers can't play (see `OPERATIONS.md`). While it runs the artifact's status is `processing` (the original bytes serve meanwhile; the rewrite is atomic), and when it's done `onArtifactEvent` fires `processed` with what it did. With `completeAfter: true`, `onUploadComplete` waits for the conversion and receives `ctx.webReady`, so a host that publishes from the hook never publishes a video that's still being rewritten. A conversion a restart interrupted is resumed by the replay below. Needs the local storage adapter and ffmpeg on the host; without ffmpeg it logs once and serves the original bytes.
|
|
550
|
+
|
|
551
|
+
### `replayCompletions`
|
|
552
|
+
|
|
553
|
+
On by default, every 300 seconds, with the first pass shortly after start; `{ intervalSeconds }` changes the interval and `false` turns it off. Each pass applies the one completion rule to every finished artifact that isn't settled: convert it if `webReady` applies and it isn't converted yet, then fire `onUploadComplete` again (`ctx.replay: true`) if the host hasn't acknowledged it. Both facts live on the artifact's sidecar (`converted`, `acknowledged`), written `false` at reserve, so nothing depends on what a process remembered. Turning `webReady` on later converts, on the first pass, the videos uploaded since this release that weren't. `fastify.pulseVaultCore.replayCompletions()` (or `core.replayCompletions()`) runs a pass on demand, for example at boot once the host's own services are up. Finished sidecars written before PulseVault recorded acknowledgements read as acknowledged, so an upgrade replays nothing (one still uploading at the upgrade completes normally).
|
|
554
|
+
|
|
555
|
+
### Status, outcome and pulses
|
|
556
|
+
|
|
557
|
+
The core behind the routes is `fastify.pulseVaultCore` (rename it with `coreDecoratorName`; non-Fastify hosts have it as the object `createPulseVaultCore` returns):
|
|
558
|
+
|
|
559
|
+
```ts
|
|
560
|
+
// Where an upload is — what GET /artifacts/:id/status reports — for a host that polls server-side.
|
|
561
|
+
await app.pulseVaultCore.getStatus(videoId);
|
|
562
|
+
// → { artifactId, state: "unknown" | "uploading" | "processing" | "ready", kind, relatedTo?, name?,
|
|
563
|
+
// bytesReceived?, size?, acknowledged, outcome?, context? }
|
|
564
|
+
// `context` (the token's) is for the host — an owner check is a comparison on it; the route leaves it out.
|
|
565
|
+
|
|
566
|
+
// Where it went, or why it didn't: any JSON, reported as `outcome` by the status route. `null` clears it.
|
|
567
|
+
await app.pulseVaultCore.recordOutcome(videoId, { state: "done", note: "Posted to Huddle" });
|
|
568
|
+
|
|
569
|
+
// A video and the finished files relatedTo it, by kind — from the adapters' relation index, never a scan.
|
|
570
|
+
const { video, thumbnail, captions, manifest } = await app.pulseVaultCore.getPulse(videoId);
|
|
571
|
+
// One per kind; when a file was re-sent, the one that finished last (`readyAt`) wins.
|
|
572
|
+
```
|
|
573
|
+
|
|
574
|
+
A page waiting on an upload polls `GET {prefix}/artifacts/<id>/status` (never cached) with the pairing token or a view token, and reads `outcome` once the host recorded one — no table, SSE route or polling method of the host's own. `GET {prefix}/artifacts/<videoId>/poster` serves the video's thumbnail the way the artifact route serves it, authorized as `resolve` on the video, and `404`s until the poster has landed, so a card asks by the video's id and nothing is copied onto the host's records.
|
|
575
|
+
|
|
485
576
|
### `validateProjectPayload` / `onProjectUploadComplete` (deprecated)
|
|
486
577
|
|
|
487
578
|
Same lifecycle as `validatePayload`/`onUploadComplete`, but only fired for `kind=project` uploads. **Deprecated** — use the generic `validatePayload`/`onUploadComplete` with a `ctx.kind === "project"` branch instead. Still honored this release (passing either emits a one-time `DeprecationWarning` at registration); will be removed in a future major version.
|
|
@@ -492,7 +583,7 @@ When the final PATCH lands the plugin runs the following steps in order, for eve
|
|
|
492
583
|
|
|
493
584
|
1. **`validatePayload`** (optional, runs for every kind) — throws → `storage.remove(artifactId)`, HTTP 4xx (default 422).
|
|
494
585
|
2. **`storage.markReady(artifactId)`** — flips the sidecar so `resolve()` will serve the bytes.
|
|
495
|
-
3. **`onUploadComplete`** (optional, runs for every kind) — throws → HTTP 500; bytes remain ready unless the consumer removes them.
|
|
586
|
+
3. **`onUploadComplete`** (optional, runs for every kind) — throws → HTTP 500; bytes remain ready unless the consumer removes them, and the completion is replayed later (`ctx.replay: true`). Once it returns, the artifact is acknowledged. With `webReady`, the conversion is queued here; with `webReady.completeAfter`, the hook runs after it instead.
|
|
496
587
|
|
|
497
588
|
(`validateProjectPayload`/`onProjectUploadComplete`, if passed, run instead of the generic hooks specifically for `kind=project` — see the deprecation note above.)
|
|
498
589
|
|
|
@@ -568,7 +659,19 @@ app.post("/pair", async (_req, reply) => {
|
|
|
568
659
|
});
|
|
569
660
|
```
|
|
570
661
|
|
|
571
|
-
A token authorizes either the artifact it names, or any artifact that declares that one as its `relatedTo` — so one token issued for a pulse's video also covers its captions, beat manifest and thumbnail 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.
|
|
662
|
+
A token authorizes either the artifact it names, or any artifact that declares that one as its `relatedTo` — so one token issued for a pulse's video also covers its captions, beat manifest and thumbnail in the same session, without minting a token per artifact. With [`pulseShape`](#pulseshape) (the default, on both the plugin and `createCapabilityAuthorize`), a create is held to that shape: the video only under the token's own `artifactId`, with no `relatedTo`; the rest only under other ids, `relatedTo` it. See `PROTOCOL.md` §5.4 for the full claim shape (`kid`/`iat`/`exp`/`issuer`/`artifactId`/`ctx`) and rationale.
|
|
663
|
+
|
|
664
|
+
**Context.** The token can carry the host's own data about the upload — who it's for, where it goes — signed with the rest, so whoever holds the token can't change it:
|
|
665
|
+
|
|
666
|
+
```ts
|
|
667
|
+
const token = issueCapabilityToken(artifactId, keys["2026-06"], {
|
|
668
|
+
keyId: "2026-06",
|
|
669
|
+
issuer,
|
|
670
|
+
context: { userId: user.id, destination: { kind: "huddle", teamId } }, // any JSON, at most 1 KiB encoded
|
|
671
|
+
});
|
|
672
|
+
```
|
|
673
|
+
|
|
674
|
+
`createCapabilityAuthorize` returns it from the `create` phase, PulseVault stores it with the artifact (and with every file uploaded `relatedTo` it under the same token), and every later `authorize`, `onUploadComplete` (`ctx.context`, on replays too), `getStatus` and `getPulse` get it back. Most hosts then keep no table keyed by artifactId, and "is this the person who uploaded it?" is a comparison, not a lookup.
|
|
572
675
|
|
|
573
676
|
## Upload-Metadata protocol
|
|
574
677
|
|
|
@@ -612,7 +715,9 @@ The local adapter writes uploads into flat kind-scoped subdirectories. Downstrea
|
|
|
612
715
|
|
|
613
716
|
```text
|
|
614
717
|
<workspaceRoot>/
|
|
615
|
-
.pulsevault/<id>.json # sidecar: { version, ext, filename, status, kind, relatedTo, checksum, name
|
|
718
|
+
.pulsevault/<id>.json # sidecar: { version, ext, filename, status, kind, relatedTo, checksum, name,
|
|
719
|
+
# appVersion, context, acknowledged, converted, readyAt, outcome }
|
|
720
|
+
.pulsevault/related/<videoId>/<id> # relation index: an empty marker per artifact relatedTo the video
|
|
616
721
|
video/<id><ext> # video upload bytes (kind="video")
|
|
617
722
|
video/<id><ext>.json # @tus/file-store offset/metadata sidecar
|
|
618
723
|
project/<id><ext> # project bundle bytes (kind="project")
|
|
@@ -623,7 +728,7 @@ The local adapter writes uploads into flat kind-scoped subdirectories. Downstrea
|
|
|
623
728
|
|
|
624
729
|
`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).
|
|
625
730
|
|
|
626
|
-
The adapter exposes `storage.workspaceRoot` (absolute, resolved from `workspaceDir`) so consumers can compute per-resource paths without re-implementing the layout. `storage.
|
|
731
|
+
The adapter exposes `storage.workspaceRoot` (absolute, resolved from `workspaceDir`) so consumers can compute per-resource paths without re-implementing the layout. `storage.describeArtifact(id)` returns everything the sidecar holds plus `ready`, `acknowledged`, `processing` and `updatedAt` (`null` if unknown); `storage.listRelated(videoId)` walks the relation index; `storage.getKind(id)`, `storage.getRelatedTo(id)`, `storage.getChecksum(id)` and `storage.getName(id)` return single fields, each `null` if absent/unknown.
|
|
627
732
|
|
|
628
733
|
**Horizontal scaling**: this adapter requires sticky-session routing or a shared filesystem across instances — see `OPERATIONS.md`.
|
|
629
734
|
|
|
@@ -760,9 +865,13 @@ const storage: PulseVaultStorage = {
|
|
|
760
865
|
async shutdown() {
|
|
761
866
|
/* optional teardown */
|
|
762
867
|
},
|
|
763
|
-
async reserveUpload({ artifactId, filename, ext, kind, relatedTo, checksum, name }) {
|
|
764
|
-
// Called by the TUS naming function. Return the file id for the datastore.
|
|
765
|
-
|
|
868
|
+
async reserveUpload({ artifactId, filename, ext, kind, relatedTo, checksum, name, appVersion, context }) {
|
|
869
|
+
// Called by the TUS naming function. Return the file id for the datastore. `context` is what
|
|
870
|
+
// `authorize` returned for this create (the capability token's context); keep it with the row.
|
|
871
|
+
await db.createArtifact({
|
|
872
|
+
artifactId, filename, ext, kind, relatedTo, checksum, name, appVersion, context,
|
|
873
|
+
status: "uploading", acknowledged: false, converted: false,
|
|
874
|
+
});
|
|
766
875
|
return `${kind}/${artifactId}${ext}`;
|
|
767
876
|
},
|
|
768
877
|
async resolve(artifactId): Promise<PulseVaultResolution | null> {
|
|
@@ -793,13 +902,36 @@ const storage: PulseVaultStorage = {
|
|
|
793
902
|
async getRelatedTo(artifactId) { return (await db.findArtifact(artifactId))?.relatedTo ?? null; },
|
|
794
903
|
async getChecksum(artifactId) { return (await db.findArtifact(artifactId))?.checksum ?? null; },
|
|
795
904
|
async getName(artifactId) { return (await db.findArtifact(artifactId))?.name ?? null; },
|
|
796
|
-
// Optional — needed
|
|
797
|
-
// upload started while it's unfinished, and
|
|
905
|
+
// Optional — needed by `retention` / `sweepAbandonedUploads` and by the completion replay.
|
|
906
|
+
// `updatedAt` is when the upload last received bytes (or started) while it's unfinished, and
|
|
907
|
+
// when it finished once ready (ms since epoch).
|
|
798
908
|
async *listArtifacts({ changedBefore } = {}) {
|
|
799
909
|
for (const row of await db.listArtifacts({ changedBefore })) {
|
|
800
910
|
yield { artifactId: row.id, kind: row.kind, relatedTo: row.relatedTo, ready: row.ready, updatedAt: row.updatedAt };
|
|
801
911
|
}
|
|
802
912
|
},
|
|
913
|
+
// Optional — the richer hook context, `getStatus`, `getPulse`, `reclaim`, `webReady` and the
|
|
914
|
+
// completion replay need these three.
|
|
915
|
+
async describeArtifact(artifactId) {
|
|
916
|
+
const row = await db.findArtifact(artifactId);
|
|
917
|
+
if (!row) return null;
|
|
918
|
+
return {
|
|
919
|
+
artifactId, kind: row.kind, ext: row.ext, filename: row.filename, relatedTo: row.relatedTo,
|
|
920
|
+
checksum: row.checksum, name: row.name, appVersion: row.appVersion, context: row.context,
|
|
921
|
+
ready: row.status === "ready", acknowledged: row.acknowledged, converted: row.converted,
|
|
922
|
+
outcome: row.outcome, updatedAt: row.updatedAt, readyAt: row.readyAt,
|
|
923
|
+
};
|
|
924
|
+
},
|
|
925
|
+
async patchArtifact(artifactId, { acknowledged, converted, outcome }) {
|
|
926
|
+
// Each field is optional; `outcome: null` clears it. Return false for an unknown id.
|
|
927
|
+
return (await db.updateArtifact(artifactId, { acknowledged, converted, outcome })).matched;
|
|
928
|
+
},
|
|
929
|
+
async *listRelated(artifactId) {
|
|
930
|
+
// Every artifact whose `relatedTo` is this one — from an index, not a scan.
|
|
931
|
+
for (const row of await db.listArtifacts({ relatedTo: artifactId })) {
|
|
932
|
+
yield { artifactId: row.id, kind: row.kind, relatedTo: artifactId, ready: row.ready, updatedAt: row.updatedAt };
|
|
933
|
+
}
|
|
934
|
+
},
|
|
803
935
|
};
|
|
804
936
|
```
|
|
805
937
|
|
package/dist/app.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { type PulseVaultAuthorize } from './routes/pulsevault.js';
|
|
|
3
3
|
import type { PulseVaultOnUploadComplete, PulseVaultOnArtifactEvent } from './lib/pulsevaultTus.js';
|
|
4
4
|
import type { PulseVaultValidatePayload } from './lib/magic.js';
|
|
5
5
|
import type { PulseVaultIssueViewLink } from './lib/view-links.js';
|
|
6
|
+
import type { PulseVaultReplayOptions, PulseVaultWebReadyOptions } from './lib/completion.js';
|
|
6
7
|
import { type PulseVaultRetentionOptions } from './lib/retention.js';
|
|
7
8
|
import type { PulseVaultStorage } from './storage/types.js';
|
|
8
9
|
import { type PulseVaultAllowedExtensionsInput } from './lib/options.js';
|
|
@@ -142,6 +143,23 @@ export type PulseVaultPluginOptions = {
|
|
|
142
143
|
* `sweepAbandonedUploads` from a cron job.
|
|
143
144
|
*/
|
|
144
145
|
retention?: PulseVaultRetentionOptions;
|
|
146
|
+
/** See the core's option of the same name: hold every create to the shape of a pulse. Defaults to `true`. */
|
|
147
|
+
pulseShape?: boolean;
|
|
148
|
+
/** See the core's option of the same name: a create takes over an idle unfinished upload of the same id. Defaults to `{ idleSeconds: 300 }`. */
|
|
149
|
+
reclaim?: {
|
|
150
|
+
idleSeconds?: number;
|
|
151
|
+
} | false;
|
|
152
|
+
/** See the core's option of the same name: a finished artifact can't be deleted through the routes. */
|
|
153
|
+
lockWhenReady?: boolean;
|
|
154
|
+
/** See the core's option of the same name: background web-ready conversion of finished videos. */
|
|
155
|
+
webReady?: PulseVaultWebReadyOptions | boolean;
|
|
156
|
+
/** See the core's option of the same name: re-fire `onUploadComplete` for completions the host never finished. */
|
|
157
|
+
replayCompletions?: PulseVaultReplayOptions | false;
|
|
158
|
+
/**
|
|
159
|
+
* Name of the decorator that exposes the core (`fastify.<name>.getStatus`, `.getPulse`,
|
|
160
|
+
* `.recordOutcome`, `.replayCompletions`). Defaults to `"pulseVaultCore"`.
|
|
161
|
+
*/
|
|
162
|
+
coreDecoratorName?: string;
|
|
145
163
|
/**
|
|
146
164
|
* @deprecated Use `validatePayload` instead — it now receives `ctx.kind`
|
|
147
165
|
* and runs for every artifact kind, including `"project"`. Still honored
|
|
@@ -183,4 +201,10 @@ export type { PulseVaultIssueViewLink, PulseVaultViewLink, PulseVaultViewLinkCon
|
|
|
183
201
|
export { createChecksumValidator, createS3ChecksumValidator, parseChecksumMetadata, } from './lib/checksum.js';
|
|
184
202
|
export type { ChecksumAlgorithm, ParsedChecksum } from './lib/checksum.js';
|
|
185
203
|
export type { PulseVaultRequest, PulseVaultLogger } from './lib/request.js';
|
|
204
|
+
export type { PulseVaultCore, PulseVaultArtifactStatus, PulseVaultPulse, } from './core.js';
|
|
205
|
+
export type { PulseVaultArtifactMeta, PulseVaultArtifactPatch, } from './storage/types.js';
|
|
206
|
+
export type { PulseVaultAuthorizeResult } from './lib/authorize.js';
|
|
207
|
+
export type { PulseVaultUploadCompleteContext } from './lib/pulsevaultTus.js';
|
|
208
|
+
export type { PulseVaultWebReadyOptions, PulseVaultReplayOptions } from './lib/completion.js';
|
|
209
|
+
export { MAX_CONTEXT_BYTES } from './lib/capability-token.js';
|
|
186
210
|
//# sourceMappingURL=app.d.ts.map
|
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,
|
|
1
|
+
{"version":3,"file":"app.d.ts","sourceRoot":"","sources":["../src/app.ts"],"names":[],"mappings":"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"}
|