@mieweb/pulsevault 0.4.2 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/CHANGELOG.md +216 -1
  2. package/OPERATIONS.md +135 -44
  3. package/PROTOCOL.md +63 -4
  4. package/README.md +186 -34
  5. package/dist/app.d.ts +30 -5
  6. package/dist/app.d.ts.map +1 -1
  7. package/dist/app.js +24 -5
  8. package/dist/app.js.map +1 -1
  9. package/dist/augment.d.ts +3 -0
  10. package/dist/augment.d.ts.map +1 -1
  11. package/dist/augment.js.map +1 -1
  12. package/dist/core.d.ts +107 -8
  13. package/dist/core.d.ts.map +1 -1
  14. package/dist/core.js +381 -47
  15. package/dist/core.js.map +1 -1
  16. package/dist/lib/authorize.d.ts +28 -6
  17. package/dist/lib/authorize.d.ts.map +1 -1
  18. package/dist/lib/capability-token.d.ts +26 -0
  19. package/dist/lib/capability-token.d.ts.map +1 -1
  20. package/dist/lib/capability-token.js +73 -5
  21. package/dist/lib/capability-token.js.map +1 -1
  22. package/dist/lib/completion.d.ts +138 -0
  23. package/dist/lib/completion.d.ts.map +1 -0
  24. package/dist/lib/completion.js +375 -0
  25. package/dist/lib/completion.js.map +1 -0
  26. package/dist/lib/deeplinks.d.ts +18 -2
  27. package/dist/lib/deeplinks.d.ts.map +1 -1
  28. package/dist/lib/deeplinks.js +13 -2
  29. package/dist/lib/deeplinks.js.map +1 -1
  30. package/dist/lib/magic.d.ts +41 -1
  31. package/dist/lib/magic.d.ts.map +1 -1
  32. package/dist/lib/magic.js +104 -0
  33. package/dist/lib/magic.js.map +1 -1
  34. package/dist/lib/options.d.ts +2 -0
  35. package/dist/lib/options.d.ts.map +1 -1
  36. package/dist/lib/options.js +8 -2
  37. package/dist/lib/options.js.map +1 -1
  38. package/dist/lib/protocol-version.gen.js +1 -1
  39. package/dist/lib/pulsevaultTus.d.ts +28 -26
  40. package/dist/lib/pulsevaultTus.d.ts.map +1 -1
  41. package/dist/lib/pulsevaultTus.js +149 -32
  42. package/dist/lib/pulsevaultTus.js.map +1 -1
  43. package/dist/lib/web-ready.d.ts +138 -18
  44. package/dist/lib/web-ready.d.ts.map +1 -1
  45. package/dist/lib/web-ready.js +453 -91
  46. package/dist/lib/web-ready.js.map +1 -1
  47. package/dist/routes/pulsevault.d.ts +12 -1
  48. package/dist/routes/pulsevault.d.ts.map +1 -1
  49. package/dist/routes/pulsevault.js +92 -1
  50. package/dist/routes/pulsevault.js.map +1 -1
  51. package/dist/storage/local.d.ts +7 -1
  52. package/dist/storage/local.d.ts.map +1 -1
  53. package/dist/storage/local.js +408 -52
  54. package/dist/storage/local.js.map +1 -1
  55. package/dist/storage/s3.d.ts +7 -1
  56. package/dist/storage/s3.d.ts.map +1 -1
  57. package/dist/storage/s3.js +256 -14
  58. package/dist/storage/s3.js.map +1 -1
  59. package/dist/storage/types.d.ts +126 -0
  60. package/dist/storage/types.d.ts.map +1 -1
  61. package/dist/storage/types.js +7 -0
  62. package/dist/storage/types.js.map +1 -1
  63. package/package.json +2 -2
  64. package/protocol/openapi.json +270 -1
  65. package/protocol/schemas/artifact-status.schema.json +61 -0
  66. package/protocol/schemas/capability-token.schema.json +3 -0
  67. package/protocol/schemas/deep-link.schema.json +2 -2
  68. package/scripts/web-ready-migrate.mjs +35 -11
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/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.
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
- | `DELETE` | `/pulsevault/artifacts/:artifactId` | Delete a finalized upload (bytes + sidecar) |
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, conformAvailable
259
273
  /** @deprecated use validatePayload + ctx.kind === "project" */
260
274
  validateProjectPayload?: PulseVaultValidatePayload;
261
275
  /** @deprecated use onUploadComplete + ctx.kind === "project" */
@@ -292,8 +306,9 @@ import "@mieweb/pulsevault/augment";
292
306
  File extensions accepted per artifact kind. Three accepted forms:
293
307
 
294
308
  ```ts
295
- // 1. Omit entirely — uses all three defaults:
296
- // video: [".mp4"] project: [".pulse", ".zip"] captions: [".vtt"]
309
+ // 1. Omit entirely — uses the defaults:
310
+ // video: [".mp4", ".mov", ".m4v", ".webm", ".mkv", ".3gp", ".avi"] project: [".pulse", ".zip"]
311
+ // captions: [".vtt"] thumbnail: [".jpg", ".jpeg", ".png"]
297
312
 
298
313
  // 2. Flat array (legacy) — video-only; project/captions keep their defaults:
299
314
  allowedExtensions: [".mp4"]
@@ -302,7 +317,7 @@ allowedExtensions: [".mp4"]
302
317
  allowedExtensions: { video: [".mp4"], project: [".pulse"], captions: [".vtt"] }
303
318
  ```
304
319
 
305
- All extensions must include the leading dot and are matched case-insensitively. The `kind` field in `Upload-Metadata` determines which list is checked.
320
+ All extensions must include the leading dot and are matched case-insensitively. The `kind` field in `Upload-Metadata` determines which list is checked. The default video list is the containers [`webReady`](#webready) conforms to MP4 (`CONFORM_VIDEO_EXTENSIONS`); the extension only decides what's accepted at `create`, so pair it with `createVideoValidator()` to check the bytes themselves.
306
321
 
307
322
  ### `cache`
308
323
 
@@ -322,7 +337,7 @@ Upload filenames are keyed by UUID, so `immutable: true` is safe when `maxAge` i
322
337
 
323
338
  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
339
 
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>`; `"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.
340
+ 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
341
 
327
342
  ```ts
328
343
  type PulseVaultAuthorize = (
@@ -333,15 +348,22 @@ type PulseVaultAuthorize = (
333
348
  // `http.IncomingMessage`, etc.) — the same hook works under either.
334
349
  request: PulseVaultRequest,
335
350
  ctx: {
336
- phase: "create" | "patch" | "resolve" | "delete" | "share";
351
+ phase: "create" | "patch" | "resolve" | "status" | "delete" | "share";
337
352
  artifactId: string;
338
- kind: "video" | "project" | "captions"; // artifact kind; always present
339
- token?: string; // only on "resolve" phase
340
- relatedTo?: string; // the session-anchor artifact this one belongs to, if any
353
+ kind: "video" | "project" | "captions" | "thumbnail"; // artifact kind; always present
354
+ token?: string; // only on the "resolve" and "status" phases
355
+ relatedTo?: string; // the video this artifact belongs to, if it declared one
356
+ name?: string; // Upload-Metadata.name, when sent
357
+ appVersion?: string; // Upload-Metadata.appVersion, when sent
358
+ filename?: string; // Upload-Metadata.filename
359
+ ext?: string; // its lowercase extension, with the dot
360
+ context?: unknown; // the token's context, read back from storage (absent on "create")
341
361
  },
342
- ) => void | Promise<void>;
362
+ ) => void | { context?: unknown } | Promise<void | { context?: unknown }>;
343
363
  ```
344
364
 
365
+ 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.
366
+
345
367
  ```ts
346
368
  await app.register(pulseVault, {
347
369
  // ...
@@ -395,6 +417,8 @@ await app.register(pulseVault, {
395
417
  });
396
418
  ```
397
419
 
420
+ For videos from anywhere (a WebM from a browser recorder, an MKV, an AVI), check them by what they are with **`createVideoValidator({ maxDurationSeconds?, probeTimeoutSeconds? })`**: ffprobe must find a video stream with a duration above zero that isn't a picture (PNG, JPEG, GIF, HEIC, a one-frame clip), or the upload is refused with `422` and a message a host can show as it is (`That file isn't a video.`, `That video is longer than the limit of 10 minutes.`, or `That video couldn't be checked in time.` when ffprobe runs past `probeTimeoutSeconds`, default 60). It checks `kind: "video"` only and lets other kinds through; without ffprobe it falls back to `sniffVideo(path)` (the container's first bytes: `ftyp`, EBML or RIFF AVI) and logs once. Local storage only.
421
+
398
422
  `createMp4Sniffer` reads the first 12 bytes and verifies the ISOBMFF `ftyp` header (MP4, MOV, M4V, 3GP). Uploads that pass the extension check but contain non-video bytes are rejected with 422 and the disk is cleaned up. The lower-level `sniffMp4(path)` is also exported if you want to drive your own validator.
399
423
 
400
424
  `createChecksumValidator`/`createS3ChecksumValidator` parse `ctx.checksum` with the also-exported `parseChecksumMetadata(raw)`, which returns `{ algorithm, digest }` or `null` for a missing/malformed value — use it directly if you're writing your own checksum check instead of the ship-ready validators.
@@ -414,25 +438,42 @@ Optional async hook fired once the final byte is written, `validatePayload` has
414
438
 
415
439
  ```ts
416
440
  type PulseVaultOnUploadComplete = (
417
- request: PulseVaultRequest, // see the note under `authorize` above
418
- ctx: { artifactId: string; kind: "video" | "project" | "captions"; size: number; uploadId: string },
441
+ request: PulseVaultRequest, // see the note under `authorize` above; `{ headers: {} }` on a replay
442
+ ctx: {
443
+ artifactId: string;
444
+ kind: "video" | "project" | "captions" | "thumbnail";
445
+ size: number;
446
+ uploadId: string;
447
+ filename: string; // Upload-Metadata.filename
448
+ ext: string; // its lowercase extension, with the dot
449
+ relatedTo?: string; // the video a thumbnail, beat manifest or captions belongs to
450
+ name?: string; // Upload-Metadata.name (the draft's title), when sent
451
+ appVersion?: string; // Upload-Metadata.appVersion, when sent
452
+ context?: unknown; // the capability token's context (see Capability tokens)
453
+ replay: boolean; // true when PulseVault is firing a completion it never recorded
454
+ webReady?: WebReadyResult; // with `webReady.completeAfter`: what the conversion did
455
+ },
419
456
  ) => void | Promise<void>;
420
457
  ```
421
458
 
459
+ **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.
460
+
422
461
  ### `onArtifactEvent`
423
462
 
424
463
  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
464
 
426
465
  ```ts
427
466
  type PulseVaultArtifactEvent = {
428
- phase: "authorize" | "complete" | "reject" | "remove";
467
+ phase: "authorize" | "complete" | "reject" | "remove" | "processed";
429
468
  artifactId: string;
430
469
  kind: "video" | "project" | "captions" | "thumbnail";
431
470
  size?: number;
432
471
  // present for "authorize" and "reject"; on "remove", "deleted" (DELETE /artifacts/:id or a
433
- // TUS DELETE) or "abandoned" (the `retention` sweep)
472
+ // TUS DELETE), "abandoned" (the `retention` sweep) or "reclaimed" (a create took over an
473
+ // idle unfinished upload); on "processed", what the web-ready conversion did
434
474
  reason?: string;
435
475
  appVersion?: string; // the uploading app's version, on "complete"/"reject" (protocol 2.1)
476
+ webReady?: WebReadyResult; // on "processed": { action, reason }
436
477
  };
437
478
 
438
479
  onArtifactEvent: (event) => {
@@ -482,6 +523,65 @@ Finished videos, and anything whose video finished, are never touched; this is n
482
523
 
483
524
  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
525
 
526
+ ### `pulseShape`
527
+
528
+ 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.
529
+
530
+ The Pulse app has always uploaded in this shape. For uploads that aren't pulses, pass `pulseShape: false` here **and** to `createCapabilityAuthorize`.
531
+
532
+ ### `reclaim`
533
+
534
+ `{ 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.
535
+
536
+ ### `lockWhenReady`
537
+
538
+ 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.
539
+
540
+ ### `webReady`
541
+
542
+ Off by default. `true`, or `ensureWebReady`'s options plus:
543
+
544
+ ```ts
545
+ webReady: {
546
+ concurrency: 1, // conversions at once; a transcode is CPU-bound
547
+ completeAfter: false, // true: run onUploadComplete only once the conversion has finished
548
+ maxEdge: 1920, // longest edge of the served video
549
+ timeoutSeconds: undefined, // default 60 + 10 × duration in 1080p30 seconds (4K120 counts 16×), at most 1 h: a hang guard
550
+ probeTimeoutSeconds: 60, // the same guard for each ffprobe run over the upload
551
+ transcode: true, crf: 23, preset: "veryfast", ffmpegPath: "ffmpeg", ffprobePath: "ffprobe",
552
+ },
553
+ ```
554
+
555
+ Every finished video is conformed in the background, after the final `PATCH` is answered, to the one format the Pulse app records (`CONFORM_TARGET`: faststart MP4, H.264 8-bit SDR, longest edge at most `maxEdge`, rotation applied, AAC or silent; orientation kept). A file already in it — every Pulse upload — keeps its bytes (`none`); a `moov` at the end is a lossless remux (`remuxed`); an off-target stream is re-encoded (`transcoded`: HEVC, 10-bit HDR tone-mapped to SDR, 4K scaled, Opus/PCM to AAC); another container (`.mov`, `.webm`, `.mkv`, …) becomes a new `.mp4` served as `video/mp4` at the same artifact URL (`conformed`). See `OPERATIONS.md` for the table. While it runs the artifact's status is `processing` (the original bytes serve meanwhile, revalidated on every request instead of the configured `cache`; every rewrite is atomic), and when it's done the status reports `webReady: { action, reason }` — `skipped` with the reason when ffmpeg is missing, a run failed or timed out, and the original keeps serving — and `onArtifactEvent` fires `processed` with the same result. 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.
556
+
557
+ ### `replayCompletions`
558
+
559
+ 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).
560
+
561
+ ### Status, outcome and pulses
562
+
563
+ The core behind the routes is `fastify.pulseVaultCore` (rename it with `coreDecoratorName`; non-Fastify hosts have it as the object `createPulseVaultCore` returns):
564
+
565
+ ```ts
566
+ // Where an upload is — what GET /artifacts/:id/status reports — for a host that polls server-side.
567
+ await app.pulseVaultCore.getStatus(videoId);
568
+ // → { artifactId, state: "unknown" | "uploading" | "processing" | "ready", kind, relatedTo?, name?,
569
+ // bytesReceived?, size?, acknowledged, webReady?: { action, reason }, outcome?, context? }
570
+ // `context` (the token's) is for the host — an owner check is a comparison on it; the route leaves it out.
571
+
572
+ // Where it went, or why it didn't: any JSON, reported as `outcome` by the status route. `null` clears it.
573
+ await app.pulseVaultCore.recordOutcome(videoId, { state: "done", note: "Posted to Huddle" });
574
+
575
+ // Whether videos are conformed here (`webReady` on, ffmpeg and ffprobe installed) — for a health check.
576
+ await app.pulseVaultCore.conformAvailable();
577
+
578
+ // A video and the finished files relatedTo it, by kind — from the adapters' relation index, never a scan.
579
+ const { video, thumbnail, captions, manifest } = await app.pulseVaultCore.getPulse(videoId);
580
+ // One per kind; when a file was re-sent, the one that finished last (`readyAt`) wins.
581
+ ```
582
+
583
+ 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.
584
+
485
585
  ### `validateProjectPayload` / `onProjectUploadComplete` (deprecated)
486
586
 
487
587
  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 +592,7 @@ When the final PATCH lands the plugin runs the following steps in order, for eve
492
592
 
493
593
  1. **`validatePayload`** (optional, runs for every kind) — throws → `storage.remove(artifactId)`, HTTP 4xx (default 422).
494
594
  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.
595
+ 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
596
 
497
597
  (`validateProjectPayload`/`onProjectUploadComplete`, if passed, run instead of the generic hooks specifically for `kind=project` — see the deprecation note above.)
498
598
 
@@ -507,7 +607,7 @@ When the final PATCH lands the plugin runs the following steps in order, for eve
507
607
  "minSupportedVersion": 2,
508
608
  "maxSupportedVersion": 2,
509
609
  "kinds": ["video", "project", "captions", "thumbnail"],
510
- "allowedExtensions": { "video": [".mp4"], "project": [".pulse", ".zip"], "captions": [".vtt"], "thumbnail": [".jpg", ".jpeg", ".png"] },
610
+ "allowedExtensions": { "video": [".mp4", ".mov", ".m4v", ".webm", ".mkv", ".3gp", ".avi"], "project": [".pulse", ".zip"], "captions": [".vtt"], "thumbnail": [".jpg", ".jpeg", ".png"] },
511
611
  "maxUploadSize": 5368709120,
512
612
  "checksum": { "algorithms": ["sha256", "sha1", "md5"] }
513
613
  }
@@ -568,7 +668,19 @@ app.post("/pair", async (_req, reply) => {
568
668
  });
569
669
  ```
570
670
 
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.
671
+ 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.
672
+
673
+ **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:
674
+
675
+ ```ts
676
+ const token = issueCapabilityToken(artifactId, keys["2026-06"], {
677
+ keyId: "2026-06",
678
+ issuer,
679
+ context: { userId: user.id, destination: { kind: "huddle", teamId } }, // any JSON, at most 1 KiB encoded
680
+ });
681
+ ```
682
+
683
+ `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
684
 
573
685
  ## Upload-Metadata protocol
574
686
 
@@ -612,7 +724,9 @@ The local adapter writes uploads into flat kind-scoped subdirectories. Downstrea
612
724
 
613
725
  ```text
614
726
  <workspaceRoot>/
615
- .pulsevault/<id>.json # sidecar: { version, ext, filename, status, kind, relatedTo, checksum, name }
727
+ .pulsevault/<id>.json # sidecar: { version, ext, filename, status, kind, relatedTo, checksum, name,
728
+ # appVersion, context, acknowledged, converted, readyAt, outcome }
729
+ .pulsevault/related/<videoId>/<id> # relation index: an empty marker per artifact relatedTo the video
616
730
  video/<id><ext> # video upload bytes (kind="video")
617
731
  video/<id><ext>.json # @tus/file-store offset/metadata sidecar
618
732
  project/<id><ext> # project bundle bytes (kind="project")
@@ -623,7 +737,7 @@ The local adapter writes uploads into flat kind-scoped subdirectories. Downstrea
623
737
 
624
738
  `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
739
 
626
- 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.
740
+ 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
741
 
628
742
  **Horizontal scaling**: this adapter requires sticky-session routing or a shared filesystem across instances — see `OPERATIONS.md`.
629
743
 
@@ -760,9 +874,13 @@ const storage: PulseVaultStorage = {
760
874
  async shutdown() {
761
875
  /* optional teardown */
762
876
  },
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
- await db.createArtifact({ artifactId, filename, kind, relatedTo, checksum, name, status: "uploading" });
877
+ async reserveUpload({ artifactId, filename, ext, kind, relatedTo, checksum, name, appVersion, context }) {
878
+ // Called by the TUS naming function. Return the file id for the datastore. `context` is what
879
+ // `authorize` returned for this create (the capability token's context); keep it with the row.
880
+ await db.createArtifact({
881
+ artifactId, filename, ext, kind, relatedTo, checksum, name, appVersion, context,
882
+ status: "uploading", acknowledged: false, converted: false,
883
+ });
766
884
  return `${kind}/${artifactId}${ext}`;
767
885
  },
768
886
  async resolve(artifactId): Promise<PulseVaultResolution | null> {
@@ -793,19 +911,42 @@ const storage: PulseVaultStorage = {
793
911
  async getRelatedTo(artifactId) { return (await db.findArtifact(artifactId))?.relatedTo ?? null; },
794
912
  async getChecksum(artifactId) { return (await db.findArtifact(artifactId))?.checksum ?? null; },
795
913
  async getName(artifactId) { return (await db.findArtifact(artifactId))?.name ?? null; },
796
- // Optional — needed only by `retention` / `sweepAbandonedUploads`. `updatedAt` is when the
797
- // upload started while it's unfinished, and when it finished once ready (ms since epoch).
914
+ // Optional — needed by `retention` / `sweepAbandonedUploads` and by the completion replay.
915
+ // `updatedAt` is when the upload last received bytes (or started) while it's unfinished, and
916
+ // when it finished once ready (ms since epoch).
798
917
  async *listArtifacts({ changedBefore } = {}) {
799
918
  for (const row of await db.listArtifacts({ changedBefore })) {
800
919
  yield { artifactId: row.id, kind: row.kind, relatedTo: row.relatedTo, ready: row.ready, updatedAt: row.updatedAt };
801
920
  }
802
921
  },
922
+ // Optional — the richer hook context, `getStatus`, `getPulse`, `reclaim`, `webReady` and the
923
+ // completion replay need these three.
924
+ async describeArtifact(artifactId) {
925
+ const row = await db.findArtifact(artifactId);
926
+ if (!row) return null;
927
+ return {
928
+ artifactId, kind: row.kind, ext: row.ext, filename: row.filename, relatedTo: row.relatedTo,
929
+ checksum: row.checksum, name: row.name, appVersion: row.appVersion, context: row.context,
930
+ ready: row.status === "ready", acknowledged: row.acknowledged, converted: row.converted,
931
+ outcome: row.outcome, updatedAt: row.updatedAt, readyAt: row.readyAt,
932
+ };
933
+ },
934
+ async patchArtifact(artifactId, { acknowledged, converted, outcome }) {
935
+ // Each field is optional; `outcome: null` clears it. Return false for an unknown id.
936
+ return (await db.updateArtifact(artifactId, { acknowledged, converted, outcome })).matched;
937
+ },
938
+ async *listRelated(artifactId) {
939
+ // Every artifact whose `relatedTo` is this one — from an index, not a scan.
940
+ for (const row of await db.listArtifacts({ relatedTo: artifactId })) {
941
+ yield { artifactId: row.id, kind: row.kind, relatedTo: artifactId, ready: row.ready, updatedAt: row.updatedAt };
942
+ }
943
+ },
803
944
  };
804
945
  ```
805
946
 
806
947
  ## Deep link helper
807
948
 
808
- Use this to generate a `pulsecam://` deep link for pairing the Pulse mobile app with your server. Typically encoded as a QR code on a pairing page.
949
+ Use this to generate a pairing link for the Pulse mobile app and your server. Typically encoded as a QR code on a pairing page.
809
950
 
810
951
  ```ts
811
952
  import { buildUploadLink } from "@mieweb/pulsevault";
@@ -820,8 +961,19 @@ const uploadLink = buildUploadLink({
820
961
  token: "secret", // optional — forwarded to your authorize hook; see Capability tokens
821
962
  });
822
963
  // pulsecam://?v=1&artifactId=...&server=https%3A%2F%2Fexample.com%2Fpulsevault&token=secret
964
+
965
+ // The https form, a Universal Link / App Link (PROTOCOL.md §3):
966
+ const httpsLink = buildUploadLink({
967
+ server: "https://example.com/pulsevault",
968
+ artifactId: randomUUID(),
969
+ token: "secret",
970
+ form: "https",
971
+ });
972
+ // https://mieweb.github.io/pulse/open#v=1&artifactId=...&server=https%3A%2F%2Fexample.com%2Fpulsevault&token=secret
823
973
  ```
824
974
 
975
+ The `https` form is what a phone camera opens: Pulse opens straight from it when installed, and its page sends the person to the store when not. The parameters are after `#`, which browsers never send, so the link host never sees the token. Pulse releases from before the link host was claimed land on the page instead and open Pulse from its button, so `pulsecam://` stays the default.
976
+
825
977
  ## Pulse logo and store badges
826
978
 
827
979
  The package ships the Pulse logo for the button that opens Pulse
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`, `.conformAvailable`). 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
@@ -162,17 +180,18 @@ export type PulseVaultPluginOptions = {
162
180
  declare const _default: FastifyPluginAsync<PulseVaultPluginOptions>;
163
181
  export default _default;
164
182
  export { createLocalStorage } from './storage/local.js';
183
+ export { uploadIdOf } from './storage/types.js';
165
184
  export type { LocalStorage, LocalStorageOptions } from './storage/local.js';
166
185
  export { createS3Storage } from './storage/s3.js';
167
186
  export type { S3Storage, S3StorageOptions } from './storage/s3.js';
168
187
  export type { PulseVaultArtifactRecord, PulseVaultResolution, PulseVaultStorage, ReserveUploadParams, UploadKind, } from './storage/types.js';
169
188
  export type { PulseVaultAuthorize, PulseVaultAuthorizeContext, PulseVaultAuthorizePhase, } from './routes/pulsevault.js';
170
189
  export type { PulseVaultOnUploadComplete, PulseVaultOnArtifactEvent, PulseVaultArtifactEvent, } from './lib/pulsevaultTus.js';
171
- export { sniffMp4, createMp4Sniffer, createS3Mp4Sniffer } from './lib/magic.js';
172
- export type { PulseVaultValidatePayload } from './lib/magic.js';
173
- export { ensureWebReady, scanMoovPosition } from './lib/web-ready.js';
174
- export type { WebReadyAction, WebReadyOptions, WebReadyResult, MoovPosition } from './lib/web-ready.js';
175
- export { buildUploadLink } from './lib/deeplinks.js';
190
+ export { sniffMp4, createMp4Sniffer, createS3Mp4Sniffer, sniffVideo, createVideoValidator } from './lib/magic.js';
191
+ export type { PulseVaultValidatePayload, VideoValidatorOptions } from './lib/magic.js';
192
+ export { ensureWebReady, scanMoovPosition, probeVideo, webReadyAvailable, CONFORM_TARGET, CONFORM_VIDEO_EXTENSIONS, } from './lib/web-ready.js';
193
+ export type { WebReadyAction, WebReadyOptions, WebReadyResult, MoovPosition, VideoProbe } from './lib/web-ready.js';
194
+ export { buildUploadLink, PULSE_OPEN_URL } from './lib/deeplinks.js';
176
195
  export type { UploadLinkOptions } from './lib/deeplinks.js';
177
196
  export { issueCapabilityToken, verifyCapabilityToken, issueViewToken, verifyViewToken, createCapabilityAuthorize, } from './lib/capability-token.js';
178
197
  export type { CapabilityTokenClaims, IssueCapabilityTokenOptions, IssueViewTokenOptions, VerifyCapabilityTokenOptions, LookupSecret, } from './lib/capability-token.js';
@@ -183,4 +202,10 @@ export type { PulseVaultIssueViewLink, PulseVaultViewLink, PulseVaultViewLinkCon
183
202
  export { createChecksumValidator, createS3ChecksumValidator, parseChecksumMetadata, } from './lib/checksum.js';
184
203
  export type { ChecksumAlgorithm, ParsedChecksum } from './lib/checksum.js';
185
204
  export type { PulseVaultRequest, PulseVaultLogger } from './lib/request.js';
205
+ export type { PulseVaultCore, PulseVaultArtifactStatus, PulseVaultPulse, } from './core.js';
206
+ export type { PulseVaultArtifactMeta, PulseVaultArtifactPatch, } from './storage/types.js';
207
+ export type { PulseVaultAuthorizeResult } from './lib/authorize.js';
208
+ export type { PulseVaultUploadCompleteContext } from './lib/pulsevaultTus.js';
209
+ export type { PulseVaultWebReadyOptions, PulseVaultReplayOptions } from './lib/completion.js';
210
+ export { MAX_CONTEXT_BYTES } from './lib/capability-token.js';
186
211
  //# 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":"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"}
1
+ {"version":3,"file":"app.d.ts","sourceRoot":"","sources":["../src/app.ts"],"names":[],"mappings":"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"}
package/dist/app.js CHANGED
@@ -3,6 +3,7 @@ import pulseVaultRoutes, {} from './routes/pulsevault.js';
3
3
  import { startRetentionSweep, validateRetentionOptions, } from './lib/retention.js';
4
4
  import { normalizeAllowedExtensions, validateBasePath, validateMaxUploadSize, rejectRemovedOptions, validateAllowedExtensions, warnIfUsingDeprecatedProjectHooks, composeValidatePayload, composeOnUploadComplete, } from './lib/options.js';
5
5
  const DEFAULT_DECORATOR_NAME = 'pulseVault';
6
+ const DEFAULT_CORE_DECORATOR_NAME = 'pulseVaultCore';
6
7
  const app = async (fastify, opts) => {
7
8
  rejectRemovedOptions(opts);
8
9
  validateBasePath(opts.prefix, 'prefix');
@@ -14,10 +15,15 @@ const app = async (fastify, opts) => {
14
15
  // state the adapter allocates mid-init still gets cleaned up if Fastify
15
16
  // later tears the plugin down.
16
17
  let retentionSweep = null;
18
+ let core = null;
17
19
  fastify.addHook('onClose', async () => {
18
- // Waits for a sweep in progress, so it never runs against shut-down storage.
20
+ // Waits for a sweep in progress, so it never runs against shut-down storage. The core's
21
+ // shutdown stops its completion queue and replay timer (and shuts storage down itself).
19
22
  await retentionSweep?.stop();
20
- await opts.storage.shutdown?.();
23
+ if (core)
24
+ await core.shutdown();
25
+ else
26
+ await opts.storage.shutdown?.();
21
27
  });
22
28
  await opts.storage.initialize?.();
23
29
  if (opts.retention) {
@@ -40,19 +46,32 @@ const app = async (fastify, opts) => {
40
46
  onUploadComplete: composeOnUploadComplete(opts.onUploadComplete, opts.onProjectUploadComplete),
41
47
  onArtifactEvent: opts.onArtifactEvent,
42
48
  issueViewLink: opts.issueViewLink,
49
+ pulseShape: opts.pulseShape,
50
+ reclaim: opts.reclaim,
51
+ lockWhenReady: opts.lockWhenReady,
52
+ webReady: opts.webReady,
53
+ replayCompletions: opts.replayCompletions,
54
+ onCore: (built) => {
55
+ core = built;
56
+ },
43
57
  });
58
+ if (!core)
59
+ throw new Error('pulsevault: the routes plugin did not hand back its core');
60
+ fastify.decorate(opts.coreDecoratorName ?? DEFAULT_CORE_DECORATOR_NAME, core);
44
61
  };
45
62
  export default fp(app, {
46
63
  name: 'pulsevault',
47
64
  fastify: '5.x',
48
65
  });
49
66
  export { createLocalStorage } from './storage/local.js';
67
+ export { uploadIdOf } from './storage/types.js';
50
68
  export { createS3Storage } from './storage/s3.js';
51
- export { sniffMp4, createMp4Sniffer, createS3Mp4Sniffer } from './lib/magic.js';
52
- export { ensureWebReady, scanMoovPosition } from './lib/web-ready.js';
53
- export { buildUploadLink } from './lib/deeplinks.js';
69
+ export { sniffMp4, createMp4Sniffer, createS3Mp4Sniffer, sniffVideo, createVideoValidator } from './lib/magic.js';
70
+ export { ensureWebReady, scanMoovPosition, probeVideo, webReadyAvailable, CONFORM_TARGET, CONFORM_VIDEO_EXTENSIONS, } from './lib/web-ready.js';
71
+ export { buildUploadLink, PULSE_OPEN_URL } from './lib/deeplinks.js';
54
72
  export { issueCapabilityToken, verifyCapabilityToken, issueViewToken, verifyViewToken, createCapabilityAuthorize, } from './lib/capability-token.js';
55
73
  export { createViewLinkIssuer } from './lib/view-links.js';
56
74
  export { sweepAbandonedUploads } from './lib/retention.js';
57
75
  export { createChecksumValidator, createS3ChecksumValidator, parseChecksumMetadata, } from './lib/checksum.js';
76
+ export { MAX_CONTEXT_BYTES } from './lib/capability-token.js';
58
77
  //# sourceMappingURL=app.js.map
package/dist/app.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"app.js","sourceRoot":"","sources":["../src/app.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"app.js","sourceRoot":"","sources":["../src/app.ts"],"names":[],"mappings":"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"}
package/dist/augment.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import type { PulseVaultStorage, UploadKind } from './storage/types.js';
2
+ import type { PulseVaultCore } from './core.js';
2
3
  /**
3
4
  * Opt-in TypeScript augmentation for the default `pulseVault` decorator and
4
5
  * the per-request `request.pulseVault` context that the plugin sets when it
@@ -32,6 +33,8 @@ import type { PulseVaultStorage, UploadKind } from './storage/types.js';
32
33
  declare module 'fastify' {
33
34
  interface FastifyInstance {
34
35
  pulseVault: PulseVaultStorage;
36
+ /** The core behind the routes: `getStatus`, `getPulse`, `recordOutcome`, `replayCompletions`. */
37
+ pulseVaultCore: PulseVaultCore;
35
38
  }
36
39
  interface FastifyRequest {
37
40
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"augment.d.ts","sourceRoot":"","sources":["../src/augment.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAExE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,OAAO,QAAQ,SAAS,CAAC;IACvB,UAAU,eAAe;QACvB,UAAU,EAAE,iBAAiB,CAAC;KAC/B;IACD,UAAU,cAAc;QACtB;;;;;;WAMG;QACH,UAAU,CAAC,EAAE;YAAE,UAAU,EAAE,MAAM,CAAC;YAAC,IAAI,EAAE,UAAU,CAAC;YAAC,SAAS,CAAC,EAAE,MAAM,CAAA;SAAE,CAAC;KAC3E;CACF;AAED,OAAO,EAAE,CAAC"}
1
+ {"version":3,"file":"augment.d.ts","sourceRoot":"","sources":["../src/augment.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AACxE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAEhD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,OAAO,QAAQ,SAAS,CAAC;IACvB,UAAU,eAAe;QACvB,UAAU,EAAE,iBAAiB,CAAC;QAC9B,iGAAiG;QACjG,cAAc,EAAE,cAAc,CAAC;KAChC;IACD,UAAU,cAAc;QACtB;;;;;;WAMG;QACH,UAAU,CAAC,EAAE;YAAE,UAAU,EAAE,MAAM,CAAC;YAAC,IAAI,EAAE,UAAU,CAAC;YAAC,SAAS,CAAC,EAAE,MAAM,CAAA;SAAE,CAAC;KAC3E;CACF;AAED,OAAO,EAAE,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"augment.js","sourceRoot":"","sources":["../src/augment.ts"],"names":[],"mappings":"AAgDA,OAAO,EAAE,CAAC"}
1
+ {"version":3,"file":"augment.js","sourceRoot":"","sources":["../src/augment.ts"],"names":[],"mappings":"AAmDA,OAAO,EAAE,CAAC"}