@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.
- package/CHANGELOG.md +107 -1
- package/OPERATIONS.md +24 -8
- package/PROTOCOL.md +187 -105
- package/README.md +103 -42
- package/dist/app.d.ts +28 -12
- package/dist/app.d.ts.map +1 -1
- package/dist/app.js +17 -4
- package/dist/app.js.map +1 -1
- package/dist/core.d.ts +18 -8
- package/dist/core.d.ts.map +1 -1
- package/dist/core.js +91 -23
- package/dist/core.js.map +1 -1
- package/dist/lib/authorize.d.ts +7 -1
- package/dist/lib/authorize.d.ts.map +1 -1
- package/dist/lib/capabilities.d.ts +30 -0
- package/dist/lib/capabilities.d.ts.map +1 -0
- package/dist/lib/capabilities.js +23 -0
- package/dist/lib/capabilities.js.map +1 -0
- package/dist/lib/capability-token.d.ts +41 -7
- package/dist/lib/capability-token.d.ts.map +1 -1
- package/dist/lib/capability-token.js +68 -13
- package/dist/lib/capability-token.js.map +1 -1
- package/dist/lib/deeplinks.d.ts +0 -13
- package/dist/lib/deeplinks.d.ts.map +1 -1
- package/dist/lib/deeplinks.js +0 -5
- package/dist/lib/deeplinks.js.map +1 -1
- package/dist/lib/options.d.ts +6 -1
- package/dist/lib/options.d.ts.map +1 -1
- package/dist/lib/options.js +9 -4
- package/dist/lib/options.js.map +1 -1
- package/dist/lib/protocol.d.ts +31 -0
- package/dist/lib/protocol.d.ts.map +1 -0
- package/dist/lib/protocol.js +55 -0
- package/dist/lib/protocol.js.map +1 -0
- package/dist/lib/pulsevaultTus.d.ts +7 -2
- package/dist/lib/pulsevaultTus.d.ts.map +1 -1
- package/dist/lib/pulsevaultTus.js +80 -6
- package/dist/lib/pulsevaultTus.js.map +1 -1
- package/dist/lib/retention.d.ts +54 -0
- package/dist/lib/retention.d.ts.map +1 -0
- package/dist/lib/retention.js +119 -0
- package/dist/lib/retention.js.map +1 -0
- package/dist/lib/view-links.d.ts +49 -0
- package/dist/lib/view-links.d.ts.map +1 -0
- package/dist/lib/view-links.js +35 -0
- package/dist/lib/view-links.js.map +1 -0
- package/dist/routes/pulsevault.d.ts +2 -2
- package/dist/routes/pulsevault.d.ts.map +1 -1
- package/dist/routes/pulsevault.js +51 -30
- package/dist/routes/pulsevault.js.map +1 -1
- package/dist/storage/local.d.ts +5 -1
- package/dist/storage/local.d.ts.map +1 -1
- package/dist/storage/local.js +55 -1
- package/dist/storage/local.js.map +1 -1
- package/dist/storage/s3.d.ts +5 -1
- package/dist/storage/s3.d.ts.map +1 -1
- package/dist/storage/s3.js +52 -5
- package/dist/storage/s3.js.map +1 -1
- package/dist/storage/types.d.ts +34 -6
- package/dist/storage/types.d.ts.map +1 -1
- package/package.json +16 -4
- package/protocol/openapi.json +1058 -0
- package/protocol/schemas/beat-manifest.schema.json +54 -0
- package/protocol/schemas/capabilities.schema.json +65 -0
- package/protocol/schemas/capability-token.schema.json +36 -0
- package/protocol/schemas/deep-link.schema.json +29 -0
- package/protocol/schemas/pulse-client.schema.json +8 -0
- package/protocol/schemas/upload-metadata.schema.json +45 -0
- 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 `>=
|
|
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,
|
|
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,
|
|
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
|
|
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
|
|
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
|
|
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 (
|
|
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
|
-
|
|
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":
|
|
474
|
-
"
|
|
475
|
-
"
|
|
476
|
-
"
|
|
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
|
|
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
|
|
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
|
|
735
|
-
// cleanup path when `validatePayload`
|
|
736
|
-
// the
|
|
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
|
|
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
|
-
|
|
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
|
-
- `
|
|
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":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,SAAS,CAAC;AAElD,OAAyB,EAAE,KAAK,mBAAmB,EAAE,MAAM,wBAAwB,CAAC;AACpF,OAAO,KAAK,EAAE,0BAA0B,EAAE,yBAAyB,EAAE,MAAM,wBAAwB,CAAC;AACpG,OAAO,KAAK,EAAE,yBAAyB,EAAE,MAAM,gBAAgB,CAAC;AAChE,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAC5D,OAAO,EASL,KAAK,gCAAgC,EACtC,MAAM,kBAAkB,CAAC;AAE1B;;;;;GAKG;AACH,MAAM,MAAM,sBAAsB,GAAG;IACnC,sEAAsE;IACtE,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IACzB;;;OAGG;IACH,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB,CAAC;AAEF,MAAM,MAAM,uBAAuB,GAAG;IACpC,wFAAwF;IACxF,OAAO,EAAE,iBAAiB,CAAC;IAC3B;;;;;;OAMG;IACH,MAAM,EAAE,MAAM,CAAC;IACf;;;OAGG;IACH,aAAa,EAAE,MAAM,CAAC;IACtB
|
|
1
|
+
{"version":3,"file":"app.d.ts","sourceRoot":"","sources":["../src/app.ts"],"names":[],"mappings":"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"}
|
package/dist/app.js
CHANGED
|
@@ -1,20 +1,31 @@
|
|
|
1
1
|
import fp from 'fastify-plugin';
|
|
2
2
|
import pulseVaultRoutes, {} from './routes/pulsevault.js';
|
|
3
|
-
import {
|
|
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,
|
|
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
|
-
|
|
9
|
-
export
|
|
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';
|
package/dist/core.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"core.d.ts","sourceRoot":"","sources":["../src/core.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAEjE,OAAO,EAIL,KAAK,0BAA0B,EAC/B,KAAK,yBAAyB,EAC/B,MAAM,wBAAwB,CAAC;AAChC,OAAO,KAAK,EAAE,yBAAyB,EAAE,MAAM,gBAAgB,CAAC;AAChE,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;
|
|
1
|
+
{"version":3,"file":"core.d.ts","sourceRoot":"","sources":["../src/core.ts"],"names":[],"mappings":"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"}
|