@mieweb/pulsevault 0.4.1 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/CHANGELOG.md +102 -1
  2. package/OPERATIONS.md +24 -10
  3. package/PROTOCOL.md +38 -1
  4. package/README.md +168 -29
  5. package/assets/README.md +33 -0
  6. package/assets/badge-app-store.svg +46 -0
  7. package/assets/badge-google-play.png +0 -0
  8. package/assets/pulse-logo-mono.svg +10 -0
  9. package/assets/pulse-logo.svg +14 -0
  10. package/dist/app.d.ts +24 -0
  11. package/dist/app.d.ts.map +1 -1
  12. package/dist/app.js +20 -2
  13. package/dist/app.js.map +1 -1
  14. package/dist/augment.d.ts +3 -0
  15. package/dist/augment.d.ts.map +1 -1
  16. package/dist/augment.js.map +1 -1
  17. package/dist/core.d.ts +89 -4
  18. package/dist/core.d.ts.map +1 -1
  19. package/dist/core.js +301 -40
  20. package/dist/core.js.map +1 -1
  21. package/dist/lib/authorize.d.ts +28 -6
  22. package/dist/lib/authorize.d.ts.map +1 -1
  23. package/dist/lib/capability-token.d.ts +26 -0
  24. package/dist/lib/capability-token.d.ts.map +1 -1
  25. package/dist/lib/capability-token.js +73 -5
  26. package/dist/lib/capability-token.js.map +1 -1
  27. package/dist/lib/completion.d.ts +135 -0
  28. package/dist/lib/completion.d.ts.map +1 -0
  29. package/dist/lib/completion.js +288 -0
  30. package/dist/lib/completion.js.map +1 -0
  31. package/dist/lib/options.d.ts.map +1 -1
  32. package/dist/lib/options.js +2 -0
  33. package/dist/lib/options.js.map +1 -1
  34. package/dist/lib/protocol-version.gen.js +1 -1
  35. package/dist/lib/pulsevaultTus.d.ts +28 -26
  36. package/dist/lib/pulsevaultTus.d.ts.map +1 -1
  37. package/dist/lib/pulsevaultTus.js +114 -31
  38. package/dist/lib/pulsevaultTus.js.map +1 -1
  39. package/dist/routes/pulsevault.d.ts +12 -1
  40. package/dist/routes/pulsevault.d.ts.map +1 -1
  41. package/dist/routes/pulsevault.js +92 -1
  42. package/dist/routes/pulsevault.js.map +1 -1
  43. package/dist/storage/local.d.ts +7 -1
  44. package/dist/storage/local.d.ts.map +1 -1
  45. package/dist/storage/local.js +173 -8
  46. package/dist/storage/local.js.map +1 -1
  47. package/dist/storage/s3.d.ts +7 -1
  48. package/dist/storage/s3.d.ts.map +1 -1
  49. package/dist/storage/s3.js +236 -14
  50. package/dist/storage/s3.js.map +1 -1
  51. package/dist/storage/types.d.ts +73 -0
  52. package/dist/storage/types.d.ts.map +1 -1
  53. package/package.json +6 -4
  54. package/protocol/openapi.json +247 -1
  55. package/protocol/schemas/artifact-status.schema.json +49 -0
  56. package/protocol/schemas/capability-token.schema.json +3 -0
@@ -24,7 +24,18 @@ export type CapabilityTokenClaims = {
24
24
  * capability token an upload uses.
25
25
  */
26
26
  use?: 'view';
27
+ /**
28
+ * Opaque host data (`issueCapabilityToken`'s `context`): who the upload is for, where it goes.
29
+ * Signed with the rest, so it can't be changed by whoever holds the token; stored with the
30
+ * artifact at create and handed back to every hook. JSON, at most `MAX_CONTEXT_BYTES` encoded.
31
+ */
32
+ ctx?: unknown;
27
33
  };
34
+ /**
35
+ * Cap on the encoded `context` claim. The token travels in a QR code and in every request's
36
+ * `Authorization` header, so a context is an owner and a destination, not a record.
37
+ */
38
+ export declare const MAX_CONTEXT_BYTES = 1024;
28
39
  export type IssueCapabilityTokenOptions = {
29
40
  /** Key id this token is signed under. Required so rotation has an explicit overlap window. */
30
41
  keyId: string;
@@ -32,6 +43,13 @@ export type IssueCapabilityTokenOptions = {
32
43
  issuer: string;
33
44
  /** Token lifetime in seconds. Defaults to 1800 (30 minutes). */
34
45
  expirySeconds?: number;
46
+ /**
47
+ * Opaque host data signed into the token (`ctx` claim): the owner, the destination — whatever
48
+ * the host would otherwise keep in a table keyed by artifactId. PulseVault stores it with the
49
+ * artifact at create and hands it to `authorize`, `onUploadComplete`, `getStatus` and
50
+ * `getPulse`, so most hosts need no lookup table of their own. JSON, at most 1 KiB encoded.
51
+ */
52
+ context?: unknown;
35
53
  };
36
54
  /**
37
55
  * Mint a stateless, HMAC-signed capability token scoped to one artifact (or
@@ -64,6 +82,13 @@ export type VerifyCapabilityTokenOptions = {
64
82
  issuer: string;
65
83
  /** Clock-skew tolerance in seconds applied to both `iat` and `exp` checks. Defaults to 30. */
66
84
  clockToleranceSeconds?: number;
85
+ /**
86
+ * Whether `createCapabilityAuthorize` holds a create to the shape of a pulse: a video only
87
+ * under the token's own `artifactId` (with no `relatedTo`), and a thumbnail, beat manifest or
88
+ * captions only under another id, `relatedTo` the token's. Defaults to `true`, matching the
89
+ * core's `pulseShape` option; set `false` there and here for non-Pulse uploads.
90
+ */
91
+ pulseShape?: boolean;
67
92
  };
68
93
  /**
69
94
  * Verify a token minted by `issueCapabilityToken`. Returns the authorized
@@ -75,6 +100,7 @@ export type VerifyCapabilityTokenOptions = {
75
100
  */
76
101
  export declare function verifyCapabilityToken(token: string, lookupSecret: LookupSecret, opts: VerifyCapabilityTokenOptions): {
77
102
  artifactId: string;
103
+ context?: unknown;
78
104
  } | null;
79
105
  /**
80
106
  * Verify a view token minted by `issueViewToken`. Returns the `artifactId` it opens and its
@@ -1 +1 @@
1
- {"version":3,"file":"capability-token.d.ts","sourceRoot":"","sources":["../../src/lib/capability-token.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,mBAAmB,EAA8B,MAAM,gBAAgB,CAAC;AAEtF;;;;;;;;GAQG;AACH,MAAM,MAAM,qBAAqB,GAAG;IAClC,yFAAyF;IACzF,UAAU,EAAE,MAAM,CAAC;IACnB,sCAAsC;IACtC,GAAG,EAAE,MAAM,CAAC;IACZ,mCAAmC;IACnC,GAAG,EAAE,MAAM,CAAC;IACZ,2DAA2D;IAC3D,GAAG,EAAE,MAAM,CAAC;IACZ,+EAA+E;IAC/E,MAAM,EAAE,MAAM,CAAC;IACf;;;OAGG;IACH,GAAG,CAAC,EAAE,MAAM,CAAC;CACd,CAAC;AA+BF,MAAM,MAAM,2BAA2B,GAAG;IACxC,8FAA8F;IAC9F,KAAK,EAAE,MAAM,CAAC;IACd,qGAAqG;IACrG,MAAM,EAAE,MAAM,CAAC;IACf,gEAAgE;IAChE,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB,CAAC;AAEF;;;;;;;GAOG;AACH,wBAAgB,oBAAoB,CAClC,UAAU,EAAE,MAAM,EAClB,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,2BAA2B,GAChC,MAAM,CAWR;AAED,MAAM,MAAM,qBAAqB,GAAG;IAClC,0FAA0F;IAC1F,KAAK,EAAE,MAAM,CAAC;IACd,qCAAqC;IACrC,MAAM,EAAE,MAAM,CAAC;IACf,wFAAwF;IACxF,aAAa,EAAE,MAAM,CAAC;CACvB,CAAC;AAEF;;;;;GAKG;AACH,wBAAgB,cAAc,CAC5B,UAAU,EAAE,MAAM,EAClB,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,qBAAqB,GAC1B,MAAM,CAgBR;AAED,0GAA0G;AAC1G,MAAM,MAAM,YAAY,GAAG,CAAC,KAAK,EAAE,MAAM,KAAK,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC;AAExE,MAAM,MAAM,4BAA4B,GAAG;IACzC,qDAAqD;IACrD,MAAM,EAAE,MAAM,CAAC;IACf,8FAA8F;IAC9F,qBAAqB,CAAC,EAAE,MAAM,CAAC;CAChC,CAAC;AAoDF;;;;;;;GAOG;AACH,wBAAgB,qBAAqB,CACnC,KAAK,EAAE,MAAM,EACb,YAAY,EAAE,YAAY,EAC1B,IAAI,EAAE,4BAA4B,GACjC;IAAE,UAAU,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAG/B;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAC7B,KAAK,EAAE,MAAM,EACb,YAAY,EAAE,YAAY,EAC1B,IAAI,EAAE,4BAA4B,GACjC;IAAE,UAAU,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAE5C;AAgBD;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,wBAAgB,yBAAyB,CACvC,YAAY,EAAE,YAAY,EAC1B,IAAI,EAAE,4BAA4B,GACjC,mBAAmB,CAgBrB"}
1
+ {"version":3,"file":"capability-token.d.ts","sourceRoot":"","sources":["../../src/lib/capability-token.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,mBAAmB,EAA8B,MAAM,gBAAgB,CAAC;AAEtF;;;;;;;;GAQG;AACH,MAAM,MAAM,qBAAqB,GAAG;IAClC,yFAAyF;IACzF,UAAU,EAAE,MAAM,CAAC;IACnB,sCAAsC;IACtC,GAAG,EAAE,MAAM,CAAC;IACZ,mCAAmC;IACnC,GAAG,EAAE,MAAM,CAAC;IACZ,2DAA2D;IAC3D,GAAG,EAAE,MAAM,CAAC;IACZ,+EAA+E;IAC/E,MAAM,EAAE,MAAM,CAAC;IACf;;;OAGG;IACH,GAAG,CAAC,EAAE,MAAM,CAAC;IACb;;;;OAIG;IACH,GAAG,CAAC,EAAE,OAAO,CAAC;CACf,CAAC;AAIF;;;GAGG;AACH,eAAO,MAAM,iBAAiB,OAAO,CAAC;AAgDtC,MAAM,MAAM,2BAA2B,GAAG;IACxC,8FAA8F;IAC9F,KAAK,EAAE,MAAM,CAAC;IACd,qGAAqG;IACrG,MAAM,EAAE,MAAM,CAAC;IACf,gEAAgE;IAChE,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB;;;;;OAKG;IACH,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB,CAAC;AAEF;;;;;;;GAOG;AACH,wBAAgB,oBAAoB,CAClC,UAAU,EAAE,MAAM,EAClB,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,2BAA2B,GAChC,MAAM,CAaR;AAED,MAAM,MAAM,qBAAqB,GAAG;IAClC,0FAA0F;IAC1F,KAAK,EAAE,MAAM,CAAC;IACd,qCAAqC;IACrC,MAAM,EAAE,MAAM,CAAC;IACf,wFAAwF;IACxF,aAAa,EAAE,MAAM,CAAC;CACvB,CAAC;AAEF;;;;;GAKG;AACH,wBAAgB,cAAc,CAC5B,UAAU,EAAE,MAAM,EAClB,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,qBAAqB,GAC1B,MAAM,CAgBR;AAED,0GAA0G;AAC1G,MAAM,MAAM,YAAY,GAAG,CAAC,KAAK,EAAE,MAAM,KAAK,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC;AAExE,MAAM,MAAM,4BAA4B,GAAG;IACzC,qDAAqD;IACrD,MAAM,EAAE,MAAM,CAAC;IACf,8FAA8F;IAC9F,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B;;;;;OAKG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;CACtB,CAAC;AAiEF;;;;;;;GAOG;AACH,wBAAgB,qBAAqB,CACnC,KAAK,EAAE,MAAM,EACb,YAAY,EAAE,YAAY,EAC1B,IAAI,EAAE,4BAA4B,GACjC;IAAE,UAAU,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,OAAO,CAAA;CAAE,GAAG,IAAI,CAOlD;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAC7B,KAAK,EAAE,MAAM,EACb,YAAY,EAAE,YAAY,EAC1B,IAAI,EAAE,4BAA4B,GACjC;IAAE,UAAU,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAE5C;AAgBD;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,wBAAgB,yBAAyB,CACvC,YAAY,EAAE,YAAY,EAC1B,IAAI,EAAE,4BAA4B,GACjC,mBAAmB,CA2CrB"}
@@ -1,6 +1,31 @@
1
1
  import { createHmac, timingSafeEqual } from 'node:crypto';
2
2
  const DEFAULT_EXPIRY_SECONDS = 1800; // 30 minutes — long enough for one upload session.
3
3
  const DEFAULT_CLOCK_TOLERANCE_SECONDS = 30;
4
+ /**
5
+ * Cap on the encoded `context` claim. The token travels in a QR code and in every request's
6
+ * `Authorization` header, so a context is an owner and a destination, not a record.
7
+ */
8
+ export const MAX_CONTEXT_BYTES = 1024;
9
+ /** `JSON.stringify` a context, refusing anything that isn't plain JSON or is too large. */
10
+ function encodeContext(context) {
11
+ if (context === undefined) {
12
+ throw new TypeError('`context` must be JSON (an object, array, string, number, boolean or null)');
13
+ }
14
+ let encoded;
15
+ try {
16
+ encoded = JSON.stringify(context);
17
+ }
18
+ catch {
19
+ throw new TypeError('`context` must be JSON-serializable');
20
+ }
21
+ if (encoded === undefined) {
22
+ throw new TypeError('`context` must be JSON (an object, array, string, number, boolean or null)');
23
+ }
24
+ if (Buffer.byteLength(encoded, 'utf8') > MAX_CONTEXT_BYTES) {
25
+ throw new TypeError(`\`context\` must encode to at most ${MAX_CONTEXT_BYTES} bytes of JSON`);
26
+ }
27
+ return encoded;
28
+ }
4
29
  function base64urlEncode(input) {
5
30
  return Buffer.from(input, 'utf8').toString('base64url');
6
31
  }
@@ -40,6 +65,8 @@ export function issueCapabilityToken(artifactId, secret, opts) {
40
65
  exp: now + (opts.expirySeconds ?? DEFAULT_EXPIRY_SECONDS),
41
66
  kid: opts.keyId,
42
67
  issuer: opts.issuer,
68
+ // Round-tripped through JSON so the claim is exactly what a verifier will read back.
69
+ ...(opts.context !== undefined ? { ctx: JSON.parse(encodeContext(opts.context)) } : {}),
43
70
  };
44
71
  const payload = base64urlEncode(JSON.stringify(claims));
45
72
  return `${payload}.${sign(payload, secret)}`;
@@ -113,7 +140,21 @@ function verifyToken(token, lookupSecret, opts, use) {
113
140
  return null;
114
141
  if (claims.issuer !== opts.issuer)
115
142
  return null;
116
- return { artifactId: claims.artifactId, exp: claims.exp };
143
+ // The signature already covers `ctx`; the size cap is enforced on the way in, and checked
144
+ // again here so a token minted elsewhere with the same secret can't carry a huge context.
145
+ if (claims.ctx !== undefined) {
146
+ try {
147
+ encodeContext(claims.ctx);
148
+ }
149
+ catch {
150
+ return null;
151
+ }
152
+ }
153
+ return {
154
+ artifactId: claims.artifactId,
155
+ exp: claims.exp,
156
+ ...(claims.ctx !== undefined ? { context: claims.ctx } : {}),
157
+ };
117
158
  }
118
159
  /**
119
160
  * Verify a token minted by `issueCapabilityToken`. Returns the authorized
@@ -125,7 +166,12 @@ function verifyToken(token, lookupSecret, opts, use) {
125
166
  */
126
167
  export function verifyCapabilityToken(token, lookupSecret, opts) {
127
168
  const verified = verifyToken(token, lookupSecret, opts, 'upload');
128
- return verified && { artifactId: verified.artifactId };
169
+ if (!verified)
170
+ return null;
171
+ return {
172
+ artifactId: verified.artifactId,
173
+ ...(verified.context !== undefined ? { context: verified.context } : {}),
174
+ };
129
175
  }
130
176
  /**
131
177
  * Verify a view token minted by `issueViewToken`. Returns the `artifactId` it opens and its
@@ -171,19 +217,41 @@ function extractToken(request, ctx) {
171
217
  * can't be used to change anything or to extend itself.
172
218
  */
173
219
  export function createCapabilityAuthorize(lookupSecret, opts) {
220
+ const pulseShape = opts.pulseShape ?? true;
174
221
  return async (request, ctx) => {
175
222
  const token = extractToken(request, ctx);
176
223
  if (!token) {
177
224
  throw Object.assign(new Error('Missing capability token'), { statusCode: 401 });
178
225
  }
179
- const verified = verifyCapabilityToken(token, lookupSecret, opts) ??
180
- (ctx.phase === 'resolve' ? verifyViewToken(token, lookupSecret, opts) : null);
226
+ const readOnly = ctx.phase === 'resolve' || ctx.phase === 'status';
227
+ const capability = verifyCapabilityToken(token, lookupSecret, opts);
228
+ const verified = capability ?? (readOnly ? verifyViewToken(token, lookupSecret, opts) : null);
181
229
  if (!verified) {
182
230
  throw Object.assign(new Error('Invalid or expired capability token'), { statusCode: 403 });
183
231
  }
184
- if (ctx.artifactId !== verified.artifactId && ctx.relatedTo !== verified.artifactId) {
232
+ if (pulseShape && ctx.phase === 'create') {
233
+ // The shape of a pulse: the token names the video, and only the video is created under
234
+ // that id. Everything else is created under its own id, `relatedTo` the token's. Without
235
+ // this, a token could create any number of videos under ids of the client's choosing, or
236
+ // take the video's own id with a thumbnail so the real video can never land.
237
+ const ok = ctx.kind === 'video'
238
+ ? ctx.artifactId === verified.artifactId && ctx.relatedTo === undefined
239
+ : ctx.relatedTo === verified.artifactId && ctx.artifactId !== verified.artifactId;
240
+ if (!ok) {
241
+ throw Object.assign(new Error(ctx.kind === 'video'
242
+ ? 'A video is uploaded under the token’s own artifactId, with no relatedTo'
243
+ : `A ${ctx.kind} is uploaded under its own artifactId, relatedTo the token’s`), { statusCode: 403 });
244
+ }
245
+ }
246
+ else if (ctx.artifactId !== verified.artifactId && ctx.relatedTo !== verified.artifactId) {
185
247
  throw Object.assign(new Error('Token does not authorize this artifact'), { statusCode: 403 });
186
248
  }
249
+ // The token's context is stored with the artifact it creates, so every later hook, status
250
+ // read and replay gets it without a host lookup.
251
+ if (ctx.phase === 'create' && capability?.context !== undefined) {
252
+ return { context: capability.context };
253
+ }
254
+ return undefined;
187
255
  };
188
256
  }
189
257
  //# sourceMappingURL=capability-token.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"capability-token.js","sourceRoot":"","sources":["../../src/lib/capability-token.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"capability-token.js","sourceRoot":"","sources":["../../src/lib/capability-token.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,135 @@
1
+ import { type WebReadyOptions, type WebReadyResult } from './web-ready.js';
2
+ import { type PulseVaultLogger, type PulseVaultRequest } from './request.js';
3
+ import type { PulseVaultStorage, UploadKind } from '../storage/types.js';
4
+ /**
5
+ * What `onUploadComplete` is told about a finished upload. Everything the client sent in
6
+ * `Upload-Metadata` and everything the token carried, so a host can map the file to its own
7
+ * record without reading storage back.
8
+ */
9
+ export type PulseVaultUploadCompleteContext = {
10
+ artifactId: string;
11
+ kind: UploadKind;
12
+ /** Bytes stored. `0` on a replay when the adapter can't tell. */
13
+ size: number;
14
+ /** tus's upload id (`<kind>/<artifactId><ext>`). */
15
+ uploadId: string;
16
+ /** Original filename from `Upload-Metadata.filename`. */
17
+ filename: string;
18
+ /** Lowercase extension of `filename`, with the leading dot. */
19
+ ext: string;
20
+ /** The video this artifact belongs to (`Upload-Metadata.relatedTo`), for a thumbnail, beat manifest or captions. */
21
+ relatedTo?: string;
22
+ /** The display name from `Upload-Metadata.name` (the draft's title), when sent. */
23
+ name?: string;
24
+ /** The uploading app's version, when sent. */
25
+ appVersion?: string;
26
+ /** The host data the capability token carried (`issueCapabilityToken`'s `context`). */
27
+ context?: unknown;
28
+ /**
29
+ * `true` when PulseVault is firing this completion again: the hook threw, or the process
30
+ * stopped, before it recorded the first one. Hosts must treat completion as at-least-once —
31
+ * check their own record before writing it again.
32
+ */
33
+ replay: boolean;
34
+ /** With `webReady.completeAfter`, what the conversion did — the hook runs once it has finished. */
35
+ webReady?: WebReadyResult;
36
+ };
37
+ export type PulseVaultOnUploadComplete = (request: PulseVaultRequest, ctx: PulseVaultUploadCompleteContext) => void | Promise<void>;
38
+ /**
39
+ * Fired at low-frequency, audit-worthy moments — never per chunk — so an
40
+ * operator can wire one hook to get both ops metrics and a compliance audit
41
+ * trail without hand-rolling both from the lower-level hooks. `remove` fires
42
+ * after PulseVault removed an artifact: a `DELETE /artifacts/:id` or a TUS
43
+ * `DELETE` (`reason: "deleted"`), the `retention` sweep (`reason: "abandoned"`),
44
+ * or a create that reclaimed an idle, unfinished upload (`reason: "reclaimed"`)
45
+ * — so a host keeping its own index of artifacts can drop it. `processed`
46
+ * fires when a web-ready conversion has finished, with what it did.
47
+ */
48
+ export type PulseVaultArtifactEvent = {
49
+ phase: 'authorize' | 'complete' | 'reject' | 'remove' | 'processed';
50
+ artifactId: string;
51
+ kind: UploadKind;
52
+ size?: number;
53
+ reason?: string;
54
+ /** The uploading app's version, from `Upload-Metadata.appVersion` (`complete`/`reject` only). */
55
+ appVersion?: string;
56
+ /** What the conversion did (`processed` only). */
57
+ webReady?: WebReadyResult;
58
+ };
59
+ export type PulseVaultOnArtifactEvent = (event: PulseVaultArtifactEvent) => void | Promise<void>;
60
+ /**
61
+ * Web-ready conversion of every finished video: a lossless faststart remux, or an H.264
62
+ * transcode for a codec browsers can't play (`ensureWebReady`), run after the final `PATCH` is
63
+ * answered so the client never waits on ffmpeg. Local storage only; on an adapter without
64
+ * `getLocalPath` the option is refused at boot.
65
+ */
66
+ export type PulseVaultWebReadyOptions = WebReadyOptions & {
67
+ /** How many conversions run at once. A transcode is CPU-bound; defaults to 1. */
68
+ concurrency?: number;
69
+ /**
70
+ * Call `onUploadComplete` only once the conversion has finished, with `ctx.webReady` saying
71
+ * what it did — so a host that publishes from the hook never publishes a video that's still
72
+ * being rewritten. Defaults to `false`: the hook runs at once and the conversion follows.
73
+ */
74
+ completeAfter?: boolean;
75
+ };
76
+ export type PulseVaultReplayOptions = {
77
+ /**
78
+ * How often PulseVault settles finished artifacts it isn't done with: the host's
79
+ * `onUploadComplete` never returned (it threw, or the process stopped first), or a conversion
80
+ * never finished. The first pass runs shortly after start. Defaults to 300 seconds.
81
+ */
82
+ intervalSeconds?: number;
83
+ };
84
+ export type CompletionRunnerOptions = {
85
+ storage: PulseVaultStorage;
86
+ onUploadComplete?: PulseVaultOnUploadComplete;
87
+ onArtifactEvent?: PulseVaultOnArtifactEvent;
88
+ webReady?: PulseVaultWebReadyOptions | boolean;
89
+ replay?: PulseVaultReplayOptions | false;
90
+ logger?: PulseVaultLogger;
91
+ };
92
+ export type FinishedUpload = {
93
+ artifactId: string;
94
+ kind: UploadKind;
95
+ size: number;
96
+ uploadId: string;
97
+ };
98
+ export type CompletionRunner = {
99
+ /**
100
+ * Settle an upload whose bytes are stored and marked ready. Resolves once the final request
101
+ * may be answered: after `onUploadComplete` when it runs inline (and rejects when the hook
102
+ * threw, so the client gets a non-2xx), or at once when the hook waits for the conversion.
103
+ */
104
+ complete(request: PulseVaultRequest, upload: FinishedUpload): Promise<void>;
105
+ /**
106
+ * One pass over storage: settle every finished artifact that still needs something — a hook
107
+ * that never returned, a conversion that never finished. Resolves the artifactIds it settled.
108
+ * Needs `storage.listArtifacts` and `describeArtifact`; resolves `[]` without them.
109
+ */
110
+ replay(): Promise<string[]>;
111
+ /** Whether a web-ready conversion applies to an artifact of this kind: `webReady` is on and it's a video. */
112
+ converts(kind: UploadKind): boolean;
113
+ /** Start the periodic replay (no-op when replay is off). The timer never keeps the process alive. */
114
+ start(): void;
115
+ /** Stop the periodic replay and wait for conversions and hooks in progress. */
116
+ stop(): Promise<void>;
117
+ };
118
+ /** Fail at boot on a setting that can't work, rather than silently doing nothing later. */
119
+ export declare function validateCompletionOptions(opts: Pick<CompletionRunnerOptions, 'storage' | 'webReady' | 'replay'>): void;
120
+ /**
121
+ * The completion pipeline shared by the core and the Fastify plugin. Everything it knows is on
122
+ * the artifact's sidecar: `converted` (the web-ready pass finished; written `false` at reserve)
123
+ * and `acknowledged` (the host's `onUploadComplete` returned). One rule settles a finished
124
+ * artifact, whether its final chunk just landed, a retried request finished it a second time,
125
+ * or a replay found it after a throw or a restart:
126
+ *
127
+ * 1. if a conversion applies and it isn't converted, convert (idempotent), then record it;
128
+ * 2. if it isn't acknowledged, run the hook — after 1 with `completeAfter`, otherwise at
129
+ * once — then record it.
130
+ *
131
+ * The only thing kept in memory is which artifacts are being settled right now, so the same
132
+ * artifact isn't settled twice at once.
133
+ */
134
+ export declare function createCompletionRunner(opts: CompletionRunnerOptions): CompletionRunner;
135
+ //# sourceMappingURL=completion.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"completion.d.ts","sourceRoot":"","sources":["../../src/lib/completion.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,288 @@
1
+ import { ensureWebReady } from './web-ready.js';
2
+ import { consoleLogger } from './request.js';
3
+ const DEFAULT_REPLAY_INTERVAL_SECONDS = 300;
4
+ /** The first replay after start: late enough for the host to finish booting. */
5
+ const FIRST_REPLAY_DELAY_MS = 5_000;
6
+ /** A request-shaped stand-in for replays, whose original request is long gone. */
7
+ const REPLAY_REQUEST = { headers: {} };
8
+ /** Fail at boot on a setting that can't work, rather than silently doing nothing later. */
9
+ export function validateCompletionOptions(opts) {
10
+ if (opts.webReady) {
11
+ if (typeof opts.storage.getLocalPath !== 'function') {
12
+ throw new TypeError('`webReady` needs a storage adapter with `getLocalPath` (the local adapter): ffmpeg rewrites the file in place');
13
+ }
14
+ if (typeof opts.storage.patchArtifact !== 'function') {
15
+ throw new TypeError('`webReady` needs a storage adapter with `patchArtifact`');
16
+ }
17
+ if (typeof opts.webReady === 'object') {
18
+ const { concurrency } = opts.webReady;
19
+ if (concurrency !== undefined && !(Number.isInteger(concurrency) && concurrency >= 1)) {
20
+ throw new TypeError('`webReady.concurrency` must be a positive integer');
21
+ }
22
+ }
23
+ }
24
+ if (opts.replay) {
25
+ const { intervalSeconds } = opts.replay;
26
+ if (intervalSeconds !== undefined && !(intervalSeconds > 0 && Number.isFinite(intervalSeconds))) {
27
+ throw new TypeError('`replayCompletions.intervalSeconds` must be a positive number of seconds');
28
+ }
29
+ // A replay that can find unacknowledged artifacts but can't record an acknowledgement would
30
+ // run the hook again on every pass, forever.
31
+ const { listArtifacts, describeArtifact, patchArtifact } = opts.storage;
32
+ if (typeof listArtifacts === 'function' && typeof describeArtifact === 'function' && typeof patchArtifact !== 'function') {
33
+ throw new TypeError('`replayCompletions` needs a storage adapter with `patchArtifact` to record what was replayed (or set `replayCompletions: false`)');
34
+ }
35
+ }
36
+ }
37
+ /** The hook context for an artifact, from its stored metadata. */
38
+ function contextFor(meta, upload, replay) {
39
+ return {
40
+ artifactId: upload.artifactId,
41
+ kind: upload.kind,
42
+ size: upload.size,
43
+ uploadId: upload.uploadId,
44
+ filename: meta?.filename ?? '',
45
+ ext: meta?.ext ?? '',
46
+ ...(meta?.relatedTo ? { relatedTo: meta.relatedTo } : {}),
47
+ ...(meta?.name ? { name: meta.name } : {}),
48
+ ...(meta?.appVersion ? { appVersion: meta.appVersion } : {}),
49
+ ...(meta?.context !== undefined ? { context: meta.context } : {}),
50
+ replay,
51
+ };
52
+ }
53
+ /** A bounded set of concurrent jobs: `run` waits for a slot, `drain` waits for every job. */
54
+ function createQueue(concurrency) {
55
+ let active = 0;
56
+ const waiting = [];
57
+ const running = new Set();
58
+ const run = (job) => {
59
+ const task = (async () => {
60
+ if (active >= concurrency)
61
+ await new Promise((resolve) => waiting.push(resolve));
62
+ active++;
63
+ try {
64
+ await job();
65
+ }
66
+ finally {
67
+ active--;
68
+ waiting.shift()?.();
69
+ }
70
+ })();
71
+ running.add(task);
72
+ void task.finally(() => running.delete(task));
73
+ return task;
74
+ };
75
+ const drain = async () => {
76
+ while (running.size > 0)
77
+ await Promise.allSettled([...running]);
78
+ };
79
+ return { run, drain };
80
+ }
81
+ /**
82
+ * The completion pipeline shared by the core and the Fastify plugin. Everything it knows is on
83
+ * the artifact's sidecar: `converted` (the web-ready pass finished; written `false` at reserve)
84
+ * and `acknowledged` (the host's `onUploadComplete` returned). One rule settles a finished
85
+ * artifact, whether its final chunk just landed, a retried request finished it a second time,
86
+ * or a replay found it after a throw or a restart:
87
+ *
88
+ * 1. if a conversion applies and it isn't converted, convert (idempotent), then record it;
89
+ * 2. if it isn't acknowledged, run the hook — after 1 with `completeAfter`, otherwise at
90
+ * once — then record it.
91
+ *
92
+ * The only thing kept in memory is which artifacts are being settled right now, so the same
93
+ * artifact isn't settled twice at once.
94
+ */
95
+ export function createCompletionRunner(opts) {
96
+ const { storage, onUploadComplete, onArtifactEvent } = opts;
97
+ const logger = opts.logger ?? consoleLogger;
98
+ const webReady = opts.webReady
99
+ ? typeof opts.webReady === 'object'
100
+ ? opts.webReady
101
+ : {}
102
+ : undefined;
103
+ const completeAfter = webReady?.completeAfter === true;
104
+ const queue = createQueue(webReady?.concurrency ?? 1);
105
+ const replayEvery = opts.replay === false ? null : (opts.replay?.intervalSeconds ?? DEFAULT_REPLAY_INTERVAL_SECONDS) * 1000;
106
+ const converts = (kind) => webReady !== undefined && kind === 'video';
107
+ const describe = (artifactId) => storage.describeArtifact ? storage.describeArtifact(artifactId) : Promise.resolve(null);
108
+ const record = async (artifactId, patch) => {
109
+ try {
110
+ await storage.patchArtifact?.(artifactId, patch);
111
+ }
112
+ catch (err) {
113
+ // The work was done; a failed record means one extra pass over it, not a lost completion.
114
+ logger.error({ err, artifactId, patch }, 'pulsevault could not record a completion step');
115
+ }
116
+ };
117
+ /** Rule 1. Never throws: a failed conversion serves the original bytes and is tried again later. */
118
+ const convert = (ctx) => new Promise((resolve) => {
119
+ void queue.run(async () => {
120
+ try {
121
+ const localPath = await storage.getLocalPath?.(ctx.artifactId);
122
+ const result = typeof localPath === 'string'
123
+ ? await ensureWebReady(localPath, { ...webReady, logger })
124
+ : { action: 'skipped', reason: 'no local path for this artifact' };
125
+ await record(ctx.artifactId, { converted: true });
126
+ await onArtifactEvent?.({
127
+ phase: 'processed',
128
+ artifactId: ctx.artifactId,
129
+ kind: ctx.kind,
130
+ size: ctx.size,
131
+ reason: result.reason,
132
+ webReady: result,
133
+ });
134
+ resolve(result);
135
+ }
136
+ catch (err) {
137
+ logger.error({ err, artifactId: ctx.artifactId }, 'pulsevault web-ready conversion failed');
138
+ resolve(undefined);
139
+ }
140
+ });
141
+ });
142
+ /** Rule 2. Rethrows what the hook threw. */
143
+ const hook = async (request, ctx) => {
144
+ if (onUploadComplete)
145
+ await onUploadComplete(request, ctx);
146
+ await record(ctx.artifactId, { acknowledged: true });
147
+ };
148
+ /** One artifact is settled by one caller at a time; a second caller joins the first. */
149
+ const settling = new Map();
150
+ /**
151
+ * Apply the rule to one finished artifact. The request that finished the upload waits on the
152
+ * result: it rejects when the hook threw, so the client gets a non-2xx — except with
153
+ * `completeAfter`, where the hook follows the conversion in the background and the request
154
+ * is answered at once. In the background (a replay, a hook after a conversion) a throw is
155
+ * logged and left for the next pass.
156
+ */
157
+ const settle = (request, meta, upload, replay) => {
158
+ const running = settling.get(upload.artifactId);
159
+ if (running)
160
+ return running;
161
+ const ctx = contextFor(meta, upload, replay);
162
+ // An adapter without `describeArtifact` can't say what was done: run everything (the hook
163
+ // is at-least-once anyway, and the conversion is idempotent).
164
+ const needsConversion = converts(upload.kind) && !(meta ? meta.converted : false);
165
+ const needsHook = meta ? !meta.acknowledged : true;
166
+ if (!needsConversion && !needsHook)
167
+ return Promise.resolve();
168
+ let run;
169
+ if (needsConversion && completeAfter) {
170
+ // Nothing for the request to wait on: the hook follows the conversion.
171
+ run = (async () => {
172
+ const result = await convert(ctx);
173
+ if (!needsHook)
174
+ return;
175
+ try {
176
+ await hook(request, { ...ctx, ...(result ? { webReady: result } : {}) });
177
+ }
178
+ catch (err) {
179
+ logger.error({ err, artifactId: ctx.artifactId, kind: ctx.kind }, 'pulsevault onUploadComplete failed after web-ready; it will be tried again');
180
+ }
181
+ })();
182
+ settling.set(upload.artifactId, run);
183
+ void run.finally(() => settling.delete(upload.artifactId));
184
+ return Promise.resolve();
185
+ }
186
+ run = (async () => {
187
+ const conversion = needsConversion ? convert(ctx) : Promise.resolve(undefined);
188
+ try {
189
+ if (needsHook)
190
+ await hook(request, ctx);
191
+ }
192
+ finally {
193
+ // The settle covers the conversion too, so a replay doesn't queue it again meanwhile.
194
+ void conversion.finally(() => settling.delete(upload.artifactId));
195
+ }
196
+ })();
197
+ settling.set(upload.artifactId, run);
198
+ return run;
199
+ };
200
+ const complete = async (request, upload) => settle(request, await describe(upload.artifactId), upload, false);
201
+ /** The stored size of a finished upload, from tus's own record; `0` when it can't be read. */
202
+ const storedSize = async (uploadId) => {
203
+ try {
204
+ const upload = await storage.datastore.getUpload(uploadId);
205
+ return upload.size ?? upload.offset ?? 0;
206
+ }
207
+ catch {
208
+ return 0;
209
+ }
210
+ };
211
+ let replaying = null;
212
+ const replay = () => {
213
+ if (replaying)
214
+ return replaying;
215
+ replaying = (async () => {
216
+ const replayed = [];
217
+ if (typeof storage.listArtifacts !== 'function' || typeof storage.describeArtifact !== 'function') {
218
+ return replayed;
219
+ }
220
+ const pending = [];
221
+ for await (const record of storage.listArtifacts()) {
222
+ if (!record.ready || settling.has(record.artifactId))
223
+ continue;
224
+ let meta;
225
+ try {
226
+ meta = await storage.describeArtifact(record.artifactId);
227
+ }
228
+ catch (err) {
229
+ logger.error({ err, artifactId: record.artifactId }, 'pulsevault replay could not read an artifact');
230
+ continue;
231
+ }
232
+ if (!meta || !meta.ready || settling.has(meta.artifactId))
233
+ continue;
234
+ if (meta.acknowledged && !(converts(meta.kind) && !meta.converted))
235
+ continue;
236
+ const uploadId = `${meta.kind}/${meta.artifactId}${meta.ext}`;
237
+ const upload = {
238
+ artifactId: meta.artifactId,
239
+ kind: meta.kind,
240
+ size: await storedSize(uploadId),
241
+ uploadId,
242
+ };
243
+ replayed.push(meta.artifactId);
244
+ pending.push(settle(REPLAY_REQUEST, meta, upload, true).catch((err) => {
245
+ logger.error({ err, artifactId: meta.artifactId, kind: meta.kind }, 'pulsevault onUploadComplete failed on replay; it will be tried again');
246
+ }));
247
+ }
248
+ await Promise.all(pending);
249
+ return replayed;
250
+ })().finally(() => {
251
+ replaying = null;
252
+ });
253
+ return replaying;
254
+ };
255
+ let timer = null;
256
+ let stopped = false;
257
+ const schedule = (delayMs) => {
258
+ if (stopped || replayEvery === null)
259
+ return;
260
+ timer = setTimeout(() => {
261
+ timer = null;
262
+ void replay()
263
+ .catch((err) => logger.error({ err }, 'pulsevault completion replay failed'))
264
+ .finally(() => schedule(replayEvery));
265
+ }, delayMs);
266
+ timer.unref();
267
+ };
268
+ return {
269
+ complete,
270
+ replay,
271
+ converts,
272
+ start: () => {
273
+ if (timer || stopped)
274
+ return;
275
+ schedule(Math.min(FIRST_REPLAY_DELAY_MS, replayEvery ?? FIRST_REPLAY_DELAY_MS));
276
+ },
277
+ stop: async () => {
278
+ stopped = true;
279
+ if (timer)
280
+ clearTimeout(timer);
281
+ timer = null;
282
+ await replaying?.catch(() => []);
283
+ await queue.drain();
284
+ await Promise.allSettled([...settling.values()]);
285
+ },
286
+ };
287
+ }
288
+ //# sourceMappingURL=completion.js.map