@mieweb/pulsevault 0.2.0 → 0.4.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 (69) hide show
  1. package/CHANGELOG.md +107 -1
  2. package/OPERATIONS.md +24 -8
  3. package/PROTOCOL.md +187 -105
  4. package/README.md +103 -42
  5. package/dist/app.d.ts +28 -12
  6. package/dist/app.d.ts.map +1 -1
  7. package/dist/app.js +17 -4
  8. package/dist/app.js.map +1 -1
  9. package/dist/core.d.ts +18 -8
  10. package/dist/core.d.ts.map +1 -1
  11. package/dist/core.js +91 -23
  12. package/dist/core.js.map +1 -1
  13. package/dist/lib/authorize.d.ts +7 -1
  14. package/dist/lib/authorize.d.ts.map +1 -1
  15. package/dist/lib/capabilities.d.ts +30 -0
  16. package/dist/lib/capabilities.d.ts.map +1 -0
  17. package/dist/lib/capabilities.js +23 -0
  18. package/dist/lib/capabilities.js.map +1 -0
  19. package/dist/lib/capability-token.d.ts +41 -7
  20. package/dist/lib/capability-token.d.ts.map +1 -1
  21. package/dist/lib/capability-token.js +68 -13
  22. package/dist/lib/capability-token.js.map +1 -1
  23. package/dist/lib/deeplinks.d.ts +0 -13
  24. package/dist/lib/deeplinks.d.ts.map +1 -1
  25. package/dist/lib/deeplinks.js +0 -5
  26. package/dist/lib/deeplinks.js.map +1 -1
  27. package/dist/lib/options.d.ts +6 -1
  28. package/dist/lib/options.d.ts.map +1 -1
  29. package/dist/lib/options.js +9 -4
  30. package/dist/lib/options.js.map +1 -1
  31. package/dist/lib/protocol.d.ts +31 -0
  32. package/dist/lib/protocol.d.ts.map +1 -0
  33. package/dist/lib/protocol.js +55 -0
  34. package/dist/lib/protocol.js.map +1 -0
  35. package/dist/lib/pulsevaultTus.d.ts +7 -2
  36. package/dist/lib/pulsevaultTus.d.ts.map +1 -1
  37. package/dist/lib/pulsevaultTus.js +80 -6
  38. package/dist/lib/pulsevaultTus.js.map +1 -1
  39. package/dist/lib/retention.d.ts +54 -0
  40. package/dist/lib/retention.d.ts.map +1 -0
  41. package/dist/lib/retention.js +119 -0
  42. package/dist/lib/retention.js.map +1 -0
  43. package/dist/lib/view-links.d.ts +49 -0
  44. package/dist/lib/view-links.d.ts.map +1 -0
  45. package/dist/lib/view-links.js +35 -0
  46. package/dist/lib/view-links.js.map +1 -0
  47. package/dist/routes/pulsevault.d.ts +2 -2
  48. package/dist/routes/pulsevault.d.ts.map +1 -1
  49. package/dist/routes/pulsevault.js +51 -30
  50. package/dist/routes/pulsevault.js.map +1 -1
  51. package/dist/storage/local.d.ts +5 -1
  52. package/dist/storage/local.d.ts.map +1 -1
  53. package/dist/storage/local.js +55 -1
  54. package/dist/storage/local.js.map +1 -1
  55. package/dist/storage/s3.d.ts +5 -1
  56. package/dist/storage/s3.d.ts.map +1 -1
  57. package/dist/storage/s3.js +52 -5
  58. package/dist/storage/s3.js.map +1 -1
  59. package/dist/storage/types.d.ts +34 -6
  60. package/dist/storage/types.d.ts.map +1 -1
  61. package/package.json +16 -4
  62. package/protocol/openapi.json +1058 -0
  63. package/protocol/schemas/beat-manifest.schema.json +54 -0
  64. package/protocol/schemas/capabilities.schema.json +65 -0
  65. package/protocol/schemas/capability-token.schema.json +36 -0
  66. package/protocol/schemas/deep-link.schema.json +29 -0
  67. package/protocol/schemas/pulse-client.schema.json +8 -0
  68. package/protocol/schemas/upload-metadata.schema.json +45 -0
  69. package/protocol/schemas/view-link.schema.json +23 -0
package/README.md CHANGED
@@ -27,7 +27,7 @@ The local storage adapter writes to a stable on-disk layout (see [Local storage]
27
27
 
28
28
  ## Requirements
29
29
 
30
- - Node.js `>=20`
30
+ - Node.js `>=22`
31
31
  - Fastify `^5.x` — only if you use the default `@mieweb/pulsevault` (Fastify
32
32
  plugin) entry point. The framework-agnostic `@mieweb/pulsevault/core` entry
33
33
  point (Express, Meteor, or plain `http.createServer`) has no Fastify
@@ -218,19 +218,22 @@ sequenceDiagram
218
218
  Org->>Pulse: ready for playback and captions fetch
219
219
  ```
220
220
 
221
+ A pulse uploads as **one video** — the artifact named by the pairing link — plus related artifacts that declare `relatedTo` it, all under the same session token: WebVTT captions (`kind=captions`, optional), a beat manifest (`kind=project`, `<draftId>-beats.pulse`) and a poster frame (`kind=thumbnail`). A **beat** is a timestamp range inside that video — where each recorded clip starts and ends (`{ segmentId, order, startMs, endMs }`), contiguous and summing to the video's `durationMs`. The beat manifest is groundwork for HLS / deep links into a pulse; it doesn't change how anything is uploaded. See `PROTOCOL.md` §8.
222
+
221
223
  ## Routes
222
224
 
223
225
  The plugin mounts the following routes under `prefix` (`@mieweb/pulsevault/core` mounts the identical set under `basePath` — see [Non-Fastify hosts](#non-fastify-hosts-express-meteor-plain-http)):
224
226
 
225
227
  | Method | Path | Description |
226
228
  | --- | --- | --- |
227
- | `GET` | `/pulsevault/capabilities` | Unauthenticated discovery: protocol version, `uploadUnit`, allowed extensions, limits |
229
+ | `GET` | `/pulsevault/capabilities` | Unauthenticated discovery: protocol version, artifact kinds, allowed extensions, limits |
228
230
  | `POST` | `/pulsevault/upload` | Create a TUS upload session |
229
- | `PATCH` / `HEAD` / `DELETE` \* | `/pulsevault/upload/:id` | Upload chunks, probe offset, cancel upload (TUS) |
231
+ | `PATCH` / `HEAD` / `DELETE` \* | `/pulsevault/upload/:id` | Upload chunks, probe offset, delete the upload (TUS) |
230
232
  | `GET` | `/pulsevault/artifacts/:artifactId` | Stream or redirect to the uploaded artifact (any kind) |
233
+ | `POST` | `/pulsevault/artifacts/:artifactId/view-link` | Mint a read-only view link to a finished artifact (only with [`issueViewLink`](#issueviewlink)) |
231
234
  | `DELETE` | `/pulsevault/artifacts/:artifactId` | Delete a finalized upload (bytes + sidecar) |
232
235
 
233
- \* `DELETE /pulsevault/upload/:id` is TUS's own "cancel in-flight upload" — distinct from `DELETE /pulsevault/artifacts/:artifactId`, which removes a finalized artifact.
236
+ \* `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.
234
237
 
235
238
  > `POST /reserve` is **not** part of the plugin. Your server implements it so you control auth, ownership, and any business logic tied to artifact creation.
236
239
 
@@ -243,7 +246,6 @@ type PulseVaultPluginOptions = {
243
246
  storage: PulseVaultStorage;
244
247
  prefix: string;
245
248
  maxUploadSize: number;
246
- uploadUnit?: "segment" | "merged"; // default: "segment" — see "Upload unit"
247
249
  decoratorName?: string; // default: "pulseVault"
248
250
  allowedExtensions?:
249
251
  | string[] // legacy — treated as video-only
@@ -275,19 +277,6 @@ URL prefix for all plugin routes. Set to `"/pulsevault"` for the standard namesp
275
277
 
276
278
  Maximum upload size in bytes. Use `Infinity` for no cap.
277
279
 
278
- ### Upload unit
279
-
280
- `uploadUnit: "segment" | "merged"` — purely advertised via `GET /pulsevault/capabilities`; **the plugin doesn't enforce either.** It tells the client which upload strategy this deployment expects:
281
-
282
- - `"segment"` (default): the client uploads each recorded clip individually (no merge/re-encode pass), plus one `kind=project` ordering manifest (`<draftId>-segments.pulse`). No captions or thumbnail. Lower client-side cost, finer-grained resumability.
283
- - `"merged"`: the client pre-merges a pulse's clips into one video before uploading, alongside its captions (`kind=captions`), a beat-timecode manifest (`kind=project`, `<draftId>-beats.pulse` — per-segment `startMs`/`endMs` on the merged timeline), and a poster `kind=thumbnail`. Simpler server-side mental model (one primary artifact per pulse), more client-side work.
284
-
285
- (A "beat" is a timecode range on the merged timeline — one recorded segment's placement within the merged video — not an upload unit. The old `"beat"` upload unit was renamed to `"segment"`.)
286
-
287
- A client reads `uploadUnit` at pairing time, before doing any merge or upload work, and branches accordingly. See `PROTOCOL.md` §8 for the full contract.
288
-
289
- Need both strategies live at once instead of one fixed value for the whole deployment? Pass `uploadUnit` to [`buildUploadLink`](#deep-link-helper) per session — it overrides `/capabilities` for that pairing only, with no server-side option to change.
290
-
291
280
  ### `decoratorName`
292
281
 
293
282
  Name of the Fastify decorator that exposes the storage adapter on the instance. Defaults to `"pulseVault"`. Override when registering the plugin more than once in the same process.
@@ -331,9 +320,9 @@ Upload filenames are keyed by UUID, so `immutable: true` is safe when `maxAge` i
331
320
 
332
321
  ### `authorize`
333
322
 
334
- Optional async hook called before TUS create/patch, before GET resolve, and before DELETE. Throw to reject — a `statusCode` or `status_code` number on the thrown error is used as the HTTP status (default `403`).
323
+ 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`).
335
324
 
336
- Phase mapping: `"create"` is the initial TUS `POST`; `"patch"` covers **every other request on the upload routes** — `PATCH` chunks, `HEAD` offset queries, **and the in-flight cancel `DELETE {prefix}/upload/<id>`**; `"resolve"` is `GET {prefix}/artifacts/<id>`; `"delete"` fires **only** for the finalized-artifact route `DELETE {prefix}/artifacts/<id>`. If you gate deletion on `phase === "delete"` alone, you are not gating upload cancellation — that arrives as `"patch"`.
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.
337
326
 
338
327
  ```ts
339
328
  type PulseVaultAuthorize = (
@@ -344,7 +333,7 @@ type PulseVaultAuthorize = (
344
333
  // `http.IncomingMessage`, etc.) — the same hook works under either.
345
334
  request: PulseVaultRequest,
346
335
  ctx: {
347
- phase: "create" | "patch" | "resolve" | "delete";
336
+ phase: "create" | "patch" | "resolve" | "delete" | "share";
348
337
  artifactId: string;
349
338
  kind: "video" | "project" | "captions"; // artifact kind; always present
350
339
  token?: string; // only on "resolve" phase
@@ -432,15 +421,18 @@ type PulseVaultOnUploadComplete = (
432
421
 
433
422
  ### `onArtifactEvent`
434
423
 
435
- Optional low-frequency hook — fired on authorize rejection (`create`/`delete`/`resolve` phases only, never per-chunk `patch`), upload completion, and payload-validation rejection. One hook covers both ops metrics and a compliance audit trail instead of hand-wiring both from the lower-level hooks above:
424
+ 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:
436
425
 
437
426
  ```ts
438
427
  type PulseVaultArtifactEvent = {
439
- phase: "authorize" | "complete" | "reject";
428
+ phase: "authorize" | "complete" | "reject" | "remove";
440
429
  artifactId: string;
441
- kind: "video" | "project" | "captions";
430
+ kind: "video" | "project" | "captions" | "thumbnail";
442
431
  size?: number;
443
- reason?: string; // present for "authorize" and "reject"
432
+ // present for "authorize" and "reject"; on "remove", "deleted" (DELETE /artifacts/:id or a
433
+ // TUS DELETE) or "abandoned" (the `retention` sweep)
434
+ reason?: string;
435
+ appVersion?: string; // the uploading app's version, on "complete"/"reject" (protocol 2.1)
444
436
  };
445
437
 
446
438
  onArtifactEvent: (event) => {
@@ -450,6 +442,46 @@ onArtifactEvent: (event) => {
450
442
 
451
443
  See `OPERATIONS.md` for a Prometheus-counter example and what to alert on.
452
444
 
445
+ ### `issueViewLink`
446
+
447
+ Optional — turns on read-only **view links** (protocol 2.2, `PROTOCOL.md` §6.4). The pairing token a client uploads with is a full capability: whoever holds it can upload to the artifact and delete it, so a watch link carrying it isn't safe to share. With this set, `POST {prefix}/artifacts/<id>/view-link` — authorized as `"share"` — calls it for a finished artifact and returns `{ token, expiresAt }`: a token that opens the artifact (and the artifacts `relatedTo` it) as `GET {prefix}/artifacts/<id>?token=…`, and does nothing else. `/capabilities` reports `viewLinks: true`.
448
+
449
+ You decide every link — how long it works, or whether this artifact gets one at all (return `null` to refuse; the client gets `403`). PulseVault sets no lifetime of its own. With capability tokens, `createViewLinkIssuer` signs view tokens that `createCapabilityAuthorize` accepts for opening artifacts only — never for uploading, deleting, or minting another link — and whose `expirySeconds` can be a number or decided per link:
450
+
451
+ ```ts
452
+ import { createViewLinkIssuer } from "@mieweb/pulsevault";
453
+
454
+ issueViewLink: createViewLinkIssuer({
455
+ keyId: "2026-06",
456
+ secret: keys["2026-06"],
457
+ issuer,
458
+ // Per link: e.g. a month for videos, a week for everything else.
459
+ expirySeconds: (_request, { kind }) => (kind === "video" ? 30 * 86_400 : 7 * 86_400),
460
+ }),
461
+ ```
462
+
463
+ View tokens are signed with a key derived from the same secret, so they need no new configuration, and a verifier that predates them (an older PulseVault sharing the secret) rejects one instead of treating it as an upload token. Rotating a key out invalidates the view links signed under it — the way to revoke them early, short of deleting the artifact.
464
+
465
+ ### `retention`
466
+
467
+ Optional cleanup of abandoned uploads — off unless set. A client that dies mid-upload (an app killed, a phone that never comes back) leaves an unfinished upload behind, and the related artifacts it finished before its video — captions, a beat manifest, a thumbnail — belong to a video that will never arrive. Nothing else removes them.
468
+
469
+ ```ts
470
+ retention: {
471
+ abandonedAfterSeconds: 24 * 60 * 60, // unfinished for a day = abandoned
472
+ sweepIntervalSeconds: 60 * 60, // default: sweep hourly
473
+ },
474
+ ```
475
+
476
+ Every `sweepIntervalSeconds` (at most 24 days) it removes, through `storage.remove`:
477
+
478
+ - an upload still unfinished `abandonedAfterSeconds` after its last write (the local adapter) or after it started (S3, which can't tell when bytes last arrived);
479
+ - a finished captions, beat manifest, project or thumbnail whose `relatedTo` artifact isn't finished — or is gone — that long after it finished.
480
+
481
+ Finished videos, and anything whose video finished, are never touched; this is not a retention *policy* for finished content (see `OPERATIONS.md` "Retention"). Keep `abandonedAfterSeconds` well above how long an upload may take — at least your capability tokens' lifetime, past which no upload can continue anyway (on S3 especially, where a long upload is judged by when it started). Each removal fires `onArtifactEvent` with `phase: "remove"`, `reason: "abandoned"`. An artifact that can't be checked or removed is logged and skipped. Needs a storage adapter with `listArtifacts` (both built-in adapters have it); invalid settings throw at registration.
482
+
483
+ 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
+
453
485
  ### `validateProjectPayload` / `onProjectUploadComplete` (deprecated)
454
486
 
455
487
  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.
@@ -470,10 +502,10 @@ When the final PATCH lands the plugin runs the following steps in order, for eve
470
502
 
471
503
  ```json
472
504
  {
473
- "protocolVersion": 1,
474
- "minSupportedVersion": 1,
475
- "maxSupportedVersion": 1,
476
- "uploadUnit": "segment",
505
+ "protocolVersion": 2,
506
+ "protocolRevision": "2.1",
507
+ "minSupportedVersion": 2,
508
+ "maxSupportedVersion": 2,
477
509
  "kinds": ["video", "project", "captions", "thumbnail"],
478
510
  "allowedExtensions": { "video": [".mp4"], "project": [".pulse", ".zip"], "captions": [".vtt"], "thumbnail": [".jpg", ".jpeg", ".png"] },
479
511
  "maxUploadSize": 5368709120,
@@ -483,7 +515,21 @@ When the final PATCH lands the plugin runs the following steps in order, for eve
483
515
 
484
516
  `checksum.algorithms` always lists what `createChecksumValidator`/`createS3ChecksumValidator` are capable of verifying — it does not detect whether this deployment's `validatePayload` actually calls one of them. A client sending a `checksum` metadata value is only verified if the operator wired in one of these helpers (or an equivalent check of their own).
485
517
 
486
- See `PROTOCOL.md` §2 for the full normative shape.
518
+ See `PROTOCOL.md` §2 for the full normative shape, and [Protocol versioning](#protocol-versioning) for how the numbers are kept.
519
+
520
+ ## Protocol versioning
521
+
522
+ The protocol this release implements is `pulseProtocol` in `package.json` — the one place it's written down (`PROTOCOL.md` §7):
523
+
524
+ ```json
525
+ "pulseProtocol": { "version": "2.1", "min": 2, "max": 2 }
526
+ ```
527
+
528
+ `version` is the spec revision, `major.minor`: the major changes only for breaking changes and is what pairing compares; the minor counts additions older clients can ignore. `min`/`max` are the protocol majors the server accepts. `/capabilities` and the `Protocol-Version` header come from it, and npm keeps it for every published version (`npm view @mieweb/pulsevault@<version> pulseProtocol`).
529
+
530
+ - **Clients say what they speak.** A client may send `Pulse-Client: Pulse/2.1.0 (45; ios); protocol=1-2`. If its newest protocol is older than `min`, uploads and artifact requests get `426 Upgrade Required` with the supported range; `/capabilities` always answers. Without the header nothing changes.
531
+ - **The protocol lives in `protocol/`.** `protocol/schemas/*.schema.json` define `/capabilities`, pairing links, `Upload-Metadata`, the beat manifest, capability-token claims and `Pulse-Client`. `protocol/openapi.json` is generated from the plugin's route schemas. The field tables in `PROTOCOL.md` are generated from the same files: edit a schema or a route, then `npm run protocol`.
532
+ - **CI enforces the rules** (`npm run protocol:check`, `scripts/check-protocol.mjs`): anything changed under `protocol/` needs a `pulseProtocol.version` bump, a breaking change needs a major bump ([oasdiff](https://github.com/oasdiff/oasdiff) decides for the HTTP routes), and `PROTOCOL.md` needs a history row for the new version. CI also runs the Pulse app's own contract suite against every PR's build; a PR that changes the server and the app together names the app branch with a `Pulse-Ref: <branch>` line in its description.
487
533
 
488
534
  ## Capability tokens
489
535
 
@@ -522,7 +568,7 @@ app.post("/pair", async (_req, reply) => {
522
568
  });
523
569
  ```
524
570
 
525
- A token authorizes either the artifact it names, or any artifact that declares that one as its `relatedTo` — so one token issued for a merged video also covers its captions, beat manifest and thumbnail (or, under `uploadUnit: "segment"`, every clip and the ordering manifest) in the same session, without minting a token per artifact. See `PROTOCOL.md` §5.4 for the full claim shape (`kid`/`iat`/`exp`/`issuer`/`artifactId`) and rationale.
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.
526
572
 
527
573
  ## Upload-Metadata protocol
528
574
 
@@ -531,12 +577,15 @@ The TUS `Upload-Metadata` header is a comma-separated list of `<key> <base64>` p
531
577
  | Key | Required | Description |
532
578
  |---|---|---|
533
579
  | `artifactId` | Yes (or `videoid`/`projectid`) | Server-generated UUID for this upload. |
534
- | `videoid` / `projectid` | Legacy aliases for `artifactId` | Accepted as synonyms indefinitely (protocol v1). Use `artifactId` for new code. |
580
+ | `videoid` / `projectid` | Legacy aliases for `artifactId` | Accepted as synonyms until a protocol major removes them. Use `artifactId` for new code. |
535
581
  | `filename` | Yes | Original filename. The extension is validated against the kind's allowed list. |
536
582
  | `kind` | No | `video` (default), `project`, or `captions`. Determines the storage subdir and which hooks fire. |
537
583
  | `relatedTo` | No | UUID of another artifact this one belongs to (e.g. a video's captions). Lets one capability token authorize a whole session. |
538
584
  | `checksum` | No | `<algorithm>:<hex digest>` of the finished file, verified by `createChecksumValidator`/`createS3ChecksumValidator` if configured. |
539
585
  | `name` | No | Free-form UTF-8 display title (e.g. the draft name typed on the capture device). Trimmed + length-capped, persisted to the sidecar, and readable via `storage.getName(artifactId)`. Display-only — never used for storage paths or authorization; escape it for your output context. |
586
+ | `appVersion` | No | Version of the app that uploaded it, e.g. `2.1.0 (45)` (protocol 2.1). Trimmed, capped at 64 characters, persisted to the sidecar and reported on the `complete`/`reject` events. Display-only, like `name`. |
587
+
588
+ The full list, with types, is `protocol/schemas/upload-metadata.schema.json` (and the generated table in `PROTOCOL.md` §4.1).
540
589
 
541
590
  > Base64 the **UTF-8 bytes** of each value. `filename`/`kind`/`artifactId` are ASCII, but a free-form `name` can carry accents or emoji — a browser `btoa()` (Latin-1) corrupts those, so encode via `Buffer`/`TextEncoder` (or your platform's UTF-8-safe base64) for the `name` value.
542
591
 
@@ -731,9 +780,10 @@ const storage: PulseVaultStorage = {
731
780
  await db.updateArtifact(artifactId, { status: "ready" });
732
781
  },
733
782
  async remove(artifactId) {
734
- // Called from DELETE /artifacts/:artifactId and from the plugin's
735
- // cleanup path when `validatePayload` rejects an upload. Return false if
736
- // the artifactId was already absent.
783
+ // Called from DELETE /artifacts/:artifactId, for a TUS DELETE of the
784
+ // artifact's upload, from the plugin's cleanup path when `validatePayload`
785
+ // rejects an upload, and by the `retention` sweep. Return false if the
786
+ // artifactId was already absent.
737
787
  const result = await db.deleteArtifact(artifactId);
738
788
  return result.deleted;
739
789
  },
@@ -743,6 +793,13 @@ const storage: PulseVaultStorage = {
743
793
  async getRelatedTo(artifactId) { return (await db.findArtifact(artifactId))?.relatedTo ?? null; },
744
794
  async getChecksum(artifactId) { return (await db.findArtifact(artifactId))?.checksum ?? null; },
745
795
  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).
798
+ async *listArtifacts({ changedBefore } = {}) {
799
+ for (const row of await db.listArtifacts({ changedBefore })) {
800
+ yield { artifactId: row.id, kind: row.kind, relatedTo: row.relatedTo, ready: row.ready, updatedAt: row.updatedAt };
801
+ }
802
+ },
746
803
  };
747
804
  ```
748
805
 
@@ -761,20 +818,20 @@ const uploadLink = buildUploadLink({
761
818
  server: "https://example.com/pulsevault",
762
819
  artifactId: randomUUID(), // generate server-side; skip POST /reserve on the app
763
820
  token: "secret", // optional — forwarded to your authorize hook; see Capability tokens
764
- uploadUnit: "merged", // optional — see "Upload unit" below
765
821
  });
766
- // pulsecam://?v=1&artifactId=...&server=https%3A%2F%2Fexample.com%2Fpulsevault&token=secret&uploadUnit=merged
822
+ // pulsecam://?v=1&artifactId=...&server=https%3A%2F%2Fexample.com%2Fpulsevault&token=secret
767
823
  ```
768
824
 
769
- `uploadUnit` on the link is a **per-session override** of whatever `GET /capabilities` currently reports (PROTOCOL.md §3, §8). Omit it and nothing changes — the client falls back to `/capabilities` exactly as before. Set it when you want "segment" and "merged" sessions live at the same time (staged rollout, A/B test, per-tenant policy) instead of one fixed value for the whole deployment — `/capabilities` can only ever report one current value, and a client reading it separately from opening the link is racing whatever the server happened to be serving at that moment, not the value this specific session was paired under.
770
-
771
825
  ## Tests
772
826
 
773
827
  ```sh
774
- npm test
828
+ npm test # the node:test suite against the built plugin
829
+ npm run protocol:check # protocol/openapi.json and PROTOCOL.md are up to date
830
+ npm run protocol:compat # protocol changes follow the versioning rules (needs oasdiff on PATH)
831
+ npm run e2e # the auth demo end to end (needs Postgres; see scripts/e2e-tus.mjs)
775
832
  ```
776
833
 
777
- Runs a Node `--test` suite against the built plugin. Coverage includes:
834
+ CI (`.github/workflows/ci.yml`) runs all of these, plus the Pulse app's contract suite against the PR's build. The node:test suite covers:
778
835
 
779
836
  - TUS create/HEAD/PATCH resume, collision handling, extension rejection, range GETs
780
837
  - Ready-gate (`GET` returns 404 while uploading)
@@ -790,7 +847,11 @@ Runs a Node `--test` suite against the built plugin. Coverage includes:
790
847
  - `allowedExtensions` object form
791
848
  - `createChecksumValidator`/`createS3ChecksumValidator`: matching digest accepted, mismatch rejected with cleanup
792
849
  - `issueCapabilityToken`/`verifyCapabilityToken`/`createCapabilityAuthorize`: round-trip, tampered signature/payload, expiry + clock tolerance, unknown `kid`, issuer mismatch, key-rotation overlap, `relatedTo`-based session authorization — both as fast unit tests (no server) and wired into real HTTP requests
793
- - `GET /capabilities`
850
+ - TUS `DELETE` removing the whole artifact, in flight or finished (local and S3), authorized as `delete`
851
+ - `retention` / `sweepAbandonedUploads` on both adapters: abandoned uploads and the related artifacts of a video that never finished are removed, finished pulses are kept, nothing newer than the cutoff is touched, the option's own timer, and boot-time validation
852
+ - View links: `issueViewToken`/`verifyViewToken`/`createViewLinkIssuer`, a view token opening its artifact and related ones but never uploading, deleting or minting another link, per-link lifetimes, refusal, and only finished artifacts
853
+ - `GET /capabilities`, and every protocol schema checked against what the code actually produces (`/capabilities`, pairing links, token claims, upload metadata, view links)
854
+ - Protocol versioning: the version read from `package.json`, `Pulse-Client` parsing, `426` for clients that are too old (core and plugin), `appVersion` stored and reported
794
855
  - S3/R2 backend (`createS3Storage`): full resumable upload → presigned-redirect playback, `createS3Mp4Sniffer`, `createS3ChecksumValidator`, `DELETE`, `kind=project`/`captions`, run against an in-process zero-dependency S3 mock (`test/mock-s3.mjs`, no cloud credentials needed)
795
856
  - `@mieweb/pulsevault/core` (the framework-agnostic entry point): the same protocol suite re-run against a bare `http.createServer` wrapping `core.handler`, plus an Express-specific smoke test proving `app.use(prefix, handler)` composition
796
857
 
package/dist/app.d.ts CHANGED
@@ -2,6 +2,8 @@ import type { FastifyPluginAsync } from 'fastify';
2
2
  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
+ import type { PulseVaultIssueViewLink } from './lib/view-links.js';
6
+ import { type PulseVaultRetentionOptions } from './lib/retention.js';
5
7
  import type { PulseVaultStorage } from './storage/types.js';
6
8
  import { type PulseVaultAllowedExtensionsInput } from './lib/options.js';
7
9
  /**
@@ -40,15 +42,6 @@ export type PulseVaultPluginOptions = {
40
42
  * explicit cap for their deployment. Use `Infinity` for no cap.
41
43
  */
42
44
  maxUploadSize: number;
43
- /**
44
- * Which upload strategy this deployment expects: `"segment"` uploads each
45
- * clip individually (no client-side merge/re-encode pass) plus a manifest
46
- * artifact for ordering, while `"merged"` expects one pre-merged video per
47
- * pulse (plus its captions, beat manifest and thumbnail). Purely advertised
48
- * via `GET /capabilities` for the client to branch on — `pulsevault` doesn't
49
- * enforce either. Defaults to `"segment"`.
50
- */
51
- uploadUnit?: 'segment' | 'merged';
52
45
  /**
53
46
  * Fastify instance decorator name under which the storage adapter is
54
47
  * exposed. Defaults to `"pulseVault"`. Override when registering this
@@ -130,6 +123,25 @@ export type PulseVaultPluginOptions = {
130
123
  * compliance audit trail; see `OPERATIONS.md` for wiring examples.
131
124
  */
132
125
  onArtifactEvent?: PulseVaultOnArtifactEvent;
126
+ /**
127
+ * Optional read-only view links (protocol 2.2). When set, `POST {prefix}/artifacts/<id>/view-link`
128
+ * — authorized as `"share"` — calls this for a finished artifact and returns the link it mints:
129
+ * a token that opens the artifact (and the artifacts `relatedTo` it) and nothing more, so it's
130
+ * safe to share, unlike the capability token an upload uses. The host decides each link —
131
+ * return `null` to refuse one — and `/capabilities` reports `viewLinks: true`. For capability
132
+ * tokens, use `createViewLinkIssuer`, whose `expirySeconds` can be decided per link.
133
+ */
134
+ issueViewLink?: PulseVaultIssueViewLink;
135
+ /**
136
+ * Optional cleanup of abandoned uploads, off unless set. Every `sweepIntervalSeconds` (default
137
+ * an hour) it removes an upload still unfinished `abandonedAfterSeconds` after it started — a
138
+ * client that died mid-upload — and a finished artifact whose `relatedTo` artifact isn't
139
+ * finished (or is gone) that long after it finished: the captions, beat manifest or thumbnail
140
+ * of a video that never arrived. Finished videos are never touched. Needs a storage adapter with
141
+ * `listArtifacts` (both built-in ones). To schedule it yourself instead, call
142
+ * `sweepAbandonedUploads` from a cron job.
143
+ */
144
+ retention?: PulseVaultRetentionOptions;
133
145
  /**
134
146
  * @deprecated Use `validatePayload` instead — it now receives `ctx.kind`
135
147
  * and runs for every artifact kind, including `"project"`. Still honored
@@ -153,7 +165,7 @@ export { createLocalStorage } from './storage/local.js';
153
165
  export type { LocalStorage, LocalStorageOptions } from './storage/local.js';
154
166
  export { createS3Storage } from './storage/s3.js';
155
167
  export type { S3Storage, S3StorageOptions } from './storage/s3.js';
156
- export type { PulseVaultResolution, PulseVaultStorage, ReserveUploadParams, UploadKind, } from './storage/types.js';
168
+ export type { PulseVaultArtifactRecord, PulseVaultResolution, PulseVaultStorage, ReserveUploadParams, UploadKind, } from './storage/types.js';
157
169
  export type { PulseVaultAuthorize, PulseVaultAuthorizeContext, PulseVaultAuthorizePhase, } from './routes/pulsevault.js';
158
170
  export type { PulseVaultOnUploadComplete, PulseVaultOnArtifactEvent, PulseVaultArtifactEvent, } from './lib/pulsevaultTus.js';
159
171
  export { sniffMp4, createMp4Sniffer, createS3Mp4Sniffer } from './lib/magic.js';
@@ -162,8 +174,12 @@ export { ensureWebReady, scanMoovPosition } from './lib/web-ready.js';
162
174
  export type { WebReadyAction, WebReadyOptions, WebReadyResult, MoovPosition } from './lib/web-ready.js';
163
175
  export { buildUploadLink } from './lib/deeplinks.js';
164
176
  export type { UploadLinkOptions } from './lib/deeplinks.js';
165
- export { issueCapabilityToken, verifyCapabilityToken, createCapabilityAuthorize, } from './lib/capability-token.js';
166
- export type { CapabilityTokenClaims, IssueCapabilityTokenOptions, VerifyCapabilityTokenOptions, LookupSecret, } from './lib/capability-token.js';
177
+ export { issueCapabilityToken, verifyCapabilityToken, issueViewToken, verifyViewToken, createCapabilityAuthorize, } from './lib/capability-token.js';
178
+ export type { CapabilityTokenClaims, IssueCapabilityTokenOptions, IssueViewTokenOptions, VerifyCapabilityTokenOptions, LookupSecret, } from './lib/capability-token.js';
179
+ export { createViewLinkIssuer } from './lib/view-links.js';
180
+ export { sweepAbandonedUploads } from './lib/retention.js';
181
+ export type { PulseVaultRetentionOptions } from './lib/retention.js';
182
+ export type { PulseVaultIssueViewLink, PulseVaultViewLink, PulseVaultViewLinkContext, ViewLinkIssuerOptions, } from './lib/view-links.js';
167
183
  export { createChecksumValidator, createS3ChecksumValidator, parseChecksumMetadata, } from './lib/checksum.js';
168
184
  export type { ChecksumAlgorithm, ParsedChecksum } from './lib/checksum.js';
169
185
  export type { PulseVaultRequest, PulseVaultLogger } from './lib/request.js';
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
@@ -1,20 +1,31 @@
1
1
  import fp from 'fastify-plugin';
2
2
  import pulseVaultRoutes, {} from './routes/pulsevault.js';
3
- import { normalizeAllowedExtensions, validateBasePath, validateMaxUploadSize, validateUploadUnit, validateAllowedExtensions, warnIfUsingDeprecatedProjectHooks, composeValidatePayload, composeOnUploadComplete, } from './lib/options.js';
3
+ import { startRetentionSweep, validateRetentionOptions, } from './lib/retention.js';
4
+ import { normalizeAllowedExtensions, validateBasePath, validateMaxUploadSize, rejectRemovedOptions, validateAllowedExtensions, warnIfUsingDeprecatedProjectHooks, composeValidatePayload, composeOnUploadComplete, } from './lib/options.js';
4
5
  const DEFAULT_DECORATOR_NAME = 'pulseVault';
5
6
  const app = async (fastify, opts) => {
7
+ rejectRemovedOptions(opts);
6
8
  validateBasePath(opts.prefix, 'prefix');
7
9
  validateMaxUploadSize(opts.maxUploadSize);
8
- validateUploadUnit(opts.uploadUnit);
9
10
  validateAllowedExtensions(opts.allowedExtensions);
11
+ validateRetentionOptions(opts.retention, opts.storage);
10
12
  warnIfUsingDeprecatedProjectHooks(opts);
11
13
  // Register the shutdown hook *before* awaiting initialize() so any partial
12
14
  // state the adapter allocates mid-init still gets cleaned up if Fastify
13
15
  // later tears the plugin down.
16
+ let retentionSweep = null;
14
17
  fastify.addHook('onClose', async () => {
18
+ // Waits for a sweep in progress, so it never runs against shut-down storage.
19
+ await retentionSweep?.stop();
15
20
  await opts.storage.shutdown?.();
16
21
  });
17
22
  await opts.storage.initialize?.();
23
+ if (opts.retention) {
24
+ const { onArtifactEvent } = opts;
25
+ retentionSweep = startRetentionSweep(opts.storage, opts.retention, fastify.log, async ({ artifactId, kind }) => {
26
+ await onArtifactEvent?.({ phase: 'remove', artifactId, kind, reason: 'abandoned' });
27
+ });
28
+ }
18
29
  const decoratorName = opts.decoratorName ?? DEFAULT_DECORATOR_NAME;
19
30
  const allowedExtensions = normalizeAllowedExtensions(opts.allowedExtensions);
20
31
  fastify.decorate(decoratorName, opts.storage);
@@ -22,13 +33,13 @@ const app = async (fastify, opts) => {
22
33
  prefix: opts.prefix,
23
34
  storage: opts.storage,
24
35
  maxUploadSize: opts.maxUploadSize,
25
- uploadUnit: opts.uploadUnit ?? 'segment',
26
36
  allowedExtensions,
27
37
  cache: opts.cache,
28
38
  authorize: opts.authorize,
29
39
  validatePayload: composeValidatePayload(opts.validatePayload, opts.validateProjectPayload),
30
40
  onUploadComplete: composeOnUploadComplete(opts.onUploadComplete, opts.onProjectUploadComplete),
31
41
  onArtifactEvent: opts.onArtifactEvent,
42
+ issueViewLink: opts.issueViewLink,
32
43
  });
33
44
  };
34
45
  export default fp(app, {
@@ -40,6 +51,8 @@ export { createS3Storage } from './storage/s3.js';
40
51
  export { sniffMp4, createMp4Sniffer, createS3Mp4Sniffer } from './lib/magic.js';
41
52
  export { ensureWebReady, scanMoovPosition } from './lib/web-ready.js';
42
53
  export { buildUploadLink } from './lib/deeplinks.js';
43
- export { issueCapabilityToken, verifyCapabilityToken, createCapabilityAuthorize, } from './lib/capability-token.js';
54
+ export { issueCapabilityToken, verifyCapabilityToken, issueViewToken, verifyViewToken, createCapabilityAuthorize, } from './lib/capability-token.js';
55
+ export { createViewLinkIssuer } from './lib/view-links.js';
56
+ export { sweepAbandonedUploads } from './lib/retention.js';
44
57
  export { createChecksumValidator, createS3ChecksumValidator, parseChecksumMetadata, } from './lib/checksum.js';
45
58
  //# 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":"AACA,OAAO,EAAE,MAAM,gBAAgB,CAAC;AAChC,OAAO,gBAAgB,EAAE,EAA4B,MAAM,wBAAwB,CAAC;AAIpF,OAAO,EACL,0BAA0B,EAC1B,gBAAgB,EAChB,qBAAqB,EACrB,kBAAkB,EAClB,yBAAyB,EACzB,iCAAiC,EACjC,sBAAsB,EACtB,uBAAuB,GAExB,MAAM,kBAAkB,CAAC;AAmJ1B,MAAM,sBAAsB,GAAG,YAAY,CAAC;AAE5C,MAAM,GAAG,GAAgD,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,EAAE;IAC/E,gBAAgB,CAAC,IAAI,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC;IACxC,qBAAqB,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC;IAC1C,kBAAkB,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;IACpC,yBAAyB,CAAC,IAAI,CAAC,iBAAiB,CAAC,CAAC;IAClD,iCAAiC,CAAC,IAAI,CAAC,CAAC;IAExC,2EAA2E;IAC3E,wEAAwE;IACxE,+BAA+B;IAC/B,OAAO,CAAC,OAAO,CAAC,SAAS,EAAE,KAAK,IAAI,EAAE;QACpC,MAAM,IAAI,CAAC,OAAO,CAAC,QAAQ,EAAE,EAAE,CAAC;IAClC,CAAC,CAAC,CAAC;IACH,MAAM,IAAI,CAAC,OAAO,CAAC,UAAU,EAAE,EAAE,CAAC;IAElC,MAAM,aAAa,GAAG,IAAI,CAAC,aAAa,IAAI,sBAAsB,CAAC;IACnE,MAAM,iBAAiB,GAAG,0BAA0B,CAAC,IAAI,CAAC,iBAAiB,CAAC,CAAC;IAE7E,OAAO,CAAC,QAAQ,CAAC,aAAa,EAAE,IAAI,CAAC,OAAO,CAAC,CAAC;IAE9C,MAAM,OAAO,CAAC,QAAQ,CAAC,gBAAgB,EAAE;QACvC,MAAM,EAAE,IAAI,CAAC,MAAM;QACnB,OAAO,EAAE,IAAI,CAAC,OAAO;QACrB,aAAa,EAAE,IAAI,CAAC,aAAa;QACjC,UAAU,EAAE,IAAI,CAAC,UAAU,IAAI,SAAS;QACxC,iBAAiB;QACjB,KAAK,EAAE,IAAI,CAAC,KAAK;QACjB,SAAS,EAAE,IAAI,CAAC,SAAS;QACzB,eAAe,EAAE,sBAAsB,CAAC,IAAI,CAAC,eAAe,EAAE,IAAI,CAAC,sBAAsB,CAAC;QAC1F,gBAAgB,EAAE,uBAAuB,CAAC,IAAI,CAAC,gBAAgB,EAAE,IAAI,CAAC,uBAAuB,CAAC;QAC9F,eAAe,EAAE,IAAI,CAAC,eAAe;KACtC,CAAC,CAAC;AACL,CAAC,CAAC;AAEF,eAAe,EAAE,CAAC,GAAG,EAAE;IACrB,IAAI,EAAE,YAAY;IAClB,OAAO,EAAE,KAAK;CACf,CAAC,CAAC;AAEH,OAAO,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAC;AAExD,OAAO,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAkBlD,OAAO,EAAE,QAAQ,EAAE,gBAAgB,EAAE,kBAAkB,EAAE,MAAM,gBAAgB,CAAC;AAEhF,OAAO,EAAE,cAAc,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAEtE,OAAO,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AAErD,OAAO,EACL,oBAAoB,EACpB,qBAAqB,EACrB,yBAAyB,GAC1B,MAAM,2BAA2B,CAAC;AAOnC,OAAO,EACL,uBAAuB,EACvB,yBAAyB,EACzB,qBAAqB,GACtB,MAAM,mBAAmB,CAAC"}
1
+ {"version":3,"file":"app.js","sourceRoot":"","sources":["../src/app.ts"],"names":[],"mappings":"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"}
package/dist/core.d.ts CHANGED
@@ -2,11 +2,13 @@ import type { IncomingMessage, ServerResponse } from 'node:http';
2
2
  import { type PulseVaultOnUploadComplete, type PulseVaultOnArtifactEvent } from './lib/pulsevaultTus.js';
3
3
  import type { PulseVaultValidatePayload } from './lib/magic.js';
4
4
  import type { PulseVaultAuthorize } from './lib/authorize.js';
5
+ import type { PulseVaultIssueViewLink } from './lib/view-links.js';
6
+ import { type PulseVaultRetentionOptions } from './lib/retention.js';
5
7
  import { type PulseVaultLogger } from './lib/request.js';
6
8
  import type { PulseVaultStorage } from './storage/types.js';
7
9
  import { type PulseVaultAllowedExtensionsInput } from './lib/options.js';
8
- /** Wire protocol version this release implements. See `/capabilities` and `PROTOCOL.md`. */
9
- export declare const PROTOCOL_VERSION = 1;
10
+ import { PROTOCOL_VERSION } from './lib/capabilities.js';
11
+ export { PROTOCOL_VERSION };
10
12
  export type PulseVaultCoreCacheOptions = {
11
13
  cacheControl?: boolean;
12
14
  maxAge?: string | number;
@@ -38,8 +40,6 @@ export type PulseVaultCoreOptions = {
38
40
  stripBasePath?: boolean;
39
41
  /** Max TUS upload size in bytes. Required — consumers must choose an explicit cap. Use `Infinity` for no cap. */
40
42
  maxUploadSize: number;
41
- /** Which upload strategy this deployment expects. Purely advertised via `GET /capabilities`. Defaults to `"segment"`. */
42
- uploadUnit?: 'segment' | 'merged';
43
43
  /** File extensions allowed per artifact kind. See the Fastify plugin's `allowedExtensions` for the full shape. */
44
44
  allowedExtensions?: PulseVaultAllowedExtensionsInput;
45
45
  /** Cache-control options forwarded to `@fastify/send` for the GET route. */
@@ -52,6 +52,10 @@ export type PulseVaultCoreOptions = {
52
52
  onUploadComplete?: PulseVaultOnUploadComplete;
53
53
  /** Optional low-frequency event hook. See the Fastify plugin's `onArtifactEvent` option for semantics. */
54
54
  onArtifactEvent?: PulseVaultOnArtifactEvent;
55
+ /** Optional read-only view links. See the Fastify plugin's `issueViewLink` option for semantics. */
56
+ issueViewLink?: PulseVaultIssueViewLink;
57
+ /** Optional cleanup of abandoned uploads. See the Fastify plugin's `retention` option for semantics. */
58
+ retention?: PulseVaultRetentionOptions;
55
59
  /** Logger for internal diagnostics (authorize rejections, tus handler failures). Defaults to `console`. */
56
60
  logger?: PulseVaultLogger;
57
61
  /** @deprecated Use `validatePayload` instead — see the Fastify plugin's option of the same name. */
@@ -69,7 +73,7 @@ export type PulseVaultCore = {
69
73
  * raw `http.createServer((req, res) => core.handler(req, res))` handler.
70
74
  */
71
75
  handler: (req: IncomingMessage, res: ServerResponse, next?: ConnectNext) => Promise<void>;
72
- /** One-time teardown — calls `storage.shutdown?.()`. */
76
+ /** One-time teardown — stops the `retention` sweep, then calls `storage.shutdown?.()`. */
73
77
  shutdown: () => Promise<void>;
74
78
  /** Handles a TUS create/patch/head/delete request directly (any method under `/upload`). */
75
79
  handleTus: (req: IncomingMessage, res: ServerResponse) => Promise<void>;
@@ -77,6 +81,8 @@ export type PulseVaultCore = {
77
81
  handleCapabilities: (req: IncomingMessage, res: ServerResponse) => Promise<void>;
78
82
  /** Resolves and streams/redirects `GET /artifacts/:artifactId`. */
79
83
  handleArtifactGet: (req: IncomingMessage, res: ServerResponse, artifactId: string, token: string | undefined) => Promise<void>;
84
+ /** Handles `POST /artifacts/:artifactId/view-link` (404 unless `issueViewLink` is configured). */
85
+ handleViewLink: (req: IncomingMessage, res: ServerResponse, artifactId: string) => Promise<void>;
80
86
  /** Handles `DELETE /artifacts/:artifactId`. */
81
87
  handleArtifactDelete: (req: IncomingMessage, res: ServerResponse, artifactId: string) => Promise<void>;
82
88
  };
@@ -93,7 +99,7 @@ export { createLocalStorage } from './storage/local.js';
93
99
  export type { LocalStorage, LocalStorageOptions } from './storage/local.js';
94
100
  export { createS3Storage } from './storage/s3.js';
95
101
  export type { S3Storage, S3StorageOptions } from './storage/s3.js';
96
- export type { PulseVaultResolution, PulseVaultStorage, ReserveUploadParams, UploadKind, } from './storage/types.js';
102
+ export type { PulseVaultArtifactRecord, PulseVaultResolution, PulseVaultStorage, ReserveUploadParams, UploadKind, } from './storage/types.js';
97
103
  export type { PulseVaultAuthorize, PulseVaultAuthorizeContext, PulseVaultAuthorizePhase, } from './lib/authorize.js';
98
104
  export type { PulseVaultOnUploadComplete, PulseVaultOnArtifactEvent, PulseVaultArtifactEvent, } from './lib/pulsevaultTus.js';
99
105
  export { sniffMp4, createMp4Sniffer, createS3Mp4Sniffer } from './lib/magic.js';
@@ -102,8 +108,12 @@ export { ensureWebReady, scanMoovPosition } from './lib/web-ready.js';
102
108
  export type { WebReadyAction, WebReadyOptions, WebReadyResult, MoovPosition } from './lib/web-ready.js';
103
109
  export { buildUploadLink } from './lib/deeplinks.js';
104
110
  export type { UploadLinkOptions } from './lib/deeplinks.js';
105
- export { issueCapabilityToken, verifyCapabilityToken, createCapabilityAuthorize, } from './lib/capability-token.js';
106
- export type { CapabilityTokenClaims, IssueCapabilityTokenOptions, VerifyCapabilityTokenOptions, LookupSecret, } from './lib/capability-token.js';
111
+ export { issueCapabilityToken, verifyCapabilityToken, issueViewToken, verifyViewToken, createCapabilityAuthorize, } from './lib/capability-token.js';
112
+ export type { CapabilityTokenClaims, IssueCapabilityTokenOptions, IssueViewTokenOptions, VerifyCapabilityTokenOptions, LookupSecret, } from './lib/capability-token.js';
113
+ export { createViewLinkIssuer } from './lib/view-links.js';
114
+ export { sweepAbandonedUploads } from './lib/retention.js';
115
+ export type { PulseVaultRetentionOptions } from './lib/retention.js';
116
+ export type { PulseVaultIssueViewLink, PulseVaultViewLink, PulseVaultViewLinkContext, ViewLinkIssuerOptions, } from './lib/view-links.js';
107
117
  export { createChecksumValidator, createS3ChecksumValidator, parseChecksumMetadata, } from './lib/checksum.js';
108
118
  export type { ChecksumAlgorithm, ParsedChecksum } from './lib/checksum.js';
109
119
  export { type PulseVaultRequest, type PulseVaultLogger } from './lib/request.js';
@@ -1 +1 @@
1
- {"version":3,"file":"core.d.ts","sourceRoot":"","sources":["../src/core.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"core.d.ts","sourceRoot":"","sources":["../src/core.ts"],"names":[],"mappings":"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"}