@mieweb/pulsevault 0.4.2 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/CHANGELOG.md +216 -1
  2. package/OPERATIONS.md +135 -44
  3. package/PROTOCOL.md +63 -4
  4. package/README.md +186 -34
  5. package/dist/app.d.ts +30 -5
  6. package/dist/app.d.ts.map +1 -1
  7. package/dist/app.js +24 -5
  8. package/dist/app.js.map +1 -1
  9. package/dist/augment.d.ts +3 -0
  10. package/dist/augment.d.ts.map +1 -1
  11. package/dist/augment.js.map +1 -1
  12. package/dist/core.d.ts +107 -8
  13. package/dist/core.d.ts.map +1 -1
  14. package/dist/core.js +381 -47
  15. package/dist/core.js.map +1 -1
  16. package/dist/lib/authorize.d.ts +28 -6
  17. package/dist/lib/authorize.d.ts.map +1 -1
  18. package/dist/lib/capability-token.d.ts +26 -0
  19. package/dist/lib/capability-token.d.ts.map +1 -1
  20. package/dist/lib/capability-token.js +73 -5
  21. package/dist/lib/capability-token.js.map +1 -1
  22. package/dist/lib/completion.d.ts +138 -0
  23. package/dist/lib/completion.d.ts.map +1 -0
  24. package/dist/lib/completion.js +375 -0
  25. package/dist/lib/completion.js.map +1 -0
  26. package/dist/lib/deeplinks.d.ts +18 -2
  27. package/dist/lib/deeplinks.d.ts.map +1 -1
  28. package/dist/lib/deeplinks.js +13 -2
  29. package/dist/lib/deeplinks.js.map +1 -1
  30. package/dist/lib/magic.d.ts +41 -1
  31. package/dist/lib/magic.d.ts.map +1 -1
  32. package/dist/lib/magic.js +104 -0
  33. package/dist/lib/magic.js.map +1 -1
  34. package/dist/lib/options.d.ts +2 -0
  35. package/dist/lib/options.d.ts.map +1 -1
  36. package/dist/lib/options.js +8 -2
  37. package/dist/lib/options.js.map +1 -1
  38. package/dist/lib/protocol-version.gen.js +1 -1
  39. package/dist/lib/pulsevaultTus.d.ts +28 -26
  40. package/dist/lib/pulsevaultTus.d.ts.map +1 -1
  41. package/dist/lib/pulsevaultTus.js +149 -32
  42. package/dist/lib/pulsevaultTus.js.map +1 -1
  43. package/dist/lib/web-ready.d.ts +138 -18
  44. package/dist/lib/web-ready.d.ts.map +1 -1
  45. package/dist/lib/web-ready.js +453 -91
  46. package/dist/lib/web-ready.js.map +1 -1
  47. package/dist/routes/pulsevault.d.ts +12 -1
  48. package/dist/routes/pulsevault.d.ts.map +1 -1
  49. package/dist/routes/pulsevault.js +92 -1
  50. package/dist/routes/pulsevault.js.map +1 -1
  51. package/dist/storage/local.d.ts +7 -1
  52. package/dist/storage/local.d.ts.map +1 -1
  53. package/dist/storage/local.js +408 -52
  54. package/dist/storage/local.js.map +1 -1
  55. package/dist/storage/s3.d.ts +7 -1
  56. package/dist/storage/s3.d.ts.map +1 -1
  57. package/dist/storage/s3.js +256 -14
  58. package/dist/storage/s3.js.map +1 -1
  59. package/dist/storage/types.d.ts +126 -0
  60. package/dist/storage/types.d.ts.map +1 -1
  61. package/dist/storage/types.js +7 -0
  62. package/dist/storage/types.js.map +1 -1
  63. package/package.json +2 -2
  64. package/protocol/openapi.json +270 -1
  65. package/protocol/schemas/artifact-status.schema.json +61 -0
  66. package/protocol/schemas/capability-token.schema.json +3 -0
  67. package/protocol/schemas/deep-link.schema.json +2 -2
  68. package/scripts/web-ready-migrate.mjs +35 -11
@@ -0,0 +1,375 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { CONFORM_TARGET, ensureWebReady, validateWebReadyOptions, } from './web-ready.js';
4
+ import { consoleLogger } from './request.js';
5
+ import { uploadIdOf } from '../storage/types.js';
6
+ const DEFAULT_REPLAY_INTERVAL_SECONDS = 300;
7
+ /** The first replay after start: late enough for the host to finish booting. */
8
+ const FIRST_REPLAY_DELAY_MS = 5_000;
9
+ /** A request-shaped stand-in for replays, whose original request is long gone. */
10
+ const REPLAY_REQUEST = { headers: {} };
11
+ /** Fail at boot on a setting that can't work, rather than silently doing nothing later. */
12
+ export function validateCompletionOptions(opts) {
13
+ if (opts.webReady) {
14
+ if (typeof opts.storage.getLocalPath !== 'function') {
15
+ throw new TypeError('`webReady` needs a storage adapter with `getLocalPath` (the local adapter): ffmpeg rewrites the file in place');
16
+ }
17
+ if (typeof opts.storage.patchArtifact !== 'function' || typeof opts.storage.describeArtifact !== 'function') {
18
+ throw new TypeError('`webReady` needs a storage adapter with `patchArtifact` and `describeArtifact`');
19
+ }
20
+ if (typeof opts.webReady === 'object') {
21
+ const { concurrency } = opts.webReady;
22
+ if (concurrency !== undefined && !(Number.isInteger(concurrency) && concurrency >= 1)) {
23
+ throw new TypeError('`webReady.concurrency` must be a positive integer');
24
+ }
25
+ validateWebReadyOptions(opts.webReady);
26
+ }
27
+ }
28
+ if (opts.replay) {
29
+ const { intervalSeconds } = opts.replay;
30
+ if (intervalSeconds !== undefined && !(intervalSeconds > 0 && Number.isFinite(intervalSeconds))) {
31
+ throw new TypeError('`replayCompletions.intervalSeconds` must be a positive number of seconds');
32
+ }
33
+ // A replay that can find unacknowledged artifacts but can't record an acknowledgement would
34
+ // run the hook again on every pass, forever.
35
+ const { listArtifacts, describeArtifact, patchArtifact } = opts.storage;
36
+ if (typeof listArtifacts === 'function' && typeof describeArtifact === 'function' && typeof patchArtifact !== 'function') {
37
+ throw new TypeError('`replayCompletions` needs a storage adapter with `patchArtifact` to record what was replayed (or set `replayCompletions: false`)');
38
+ }
39
+ }
40
+ }
41
+ /** The hook context for an artifact, from its stored metadata. */
42
+ function contextFor(meta, upload, replay) {
43
+ return {
44
+ artifactId: upload.artifactId,
45
+ kind: upload.kind,
46
+ size: upload.size,
47
+ uploadId: upload.uploadId,
48
+ filename: meta?.filename ?? '',
49
+ // `filename`'s extension, as documented: the uploaded one, even once a conversion changed it.
50
+ ext: meta ? (meta.sourceExt ?? meta.ext) : '',
51
+ ...(meta?.relatedTo ? { relatedTo: meta.relatedTo } : {}),
52
+ ...(meta?.name ? { name: meta.name } : {}),
53
+ ...(meta?.appVersion ? { appVersion: meta.appVersion } : {}),
54
+ ...(meta?.context !== undefined ? { context: meta.context } : {}),
55
+ // What a conversion did, once one was recorded: a replayed hook gets it too, so a host that
56
+ // names the file from it (an `.mp4` once a WebM was conformed) gets it right the second time.
57
+ ...(meta?.converted && meta.webReady ? { webReady: meta.webReady } : {}),
58
+ replay,
59
+ };
60
+ }
61
+ /** A bounded set of concurrent jobs: `run` waits for a slot, `drain` waits for every job. */
62
+ function createQueue(concurrency) {
63
+ let active = 0;
64
+ const waiting = [];
65
+ const running = new Set();
66
+ const run = (job) => {
67
+ const task = (async () => {
68
+ if (active >= concurrency)
69
+ await new Promise((resolve) => waiting.push(resolve));
70
+ active++;
71
+ try {
72
+ await job();
73
+ }
74
+ finally {
75
+ active--;
76
+ waiting.shift()?.();
77
+ }
78
+ })();
79
+ running.add(task);
80
+ void task.finally(() => running.delete(task));
81
+ return task;
82
+ };
83
+ const drain = async () => {
84
+ while (running.size > 0)
85
+ await Promise.allSettled([...running]);
86
+ };
87
+ return { run, drain };
88
+ }
89
+ /**
90
+ * The completion pipeline shared by the core and the Fastify plugin. Everything it knows is on
91
+ * the artifact's sidecar: `converted` (the web-ready pass finished; written `false` at reserve)
92
+ * and `acknowledged` (the host's `onUploadComplete` returned). One rule settles a finished
93
+ * artifact, whether its final chunk just landed, a retried request finished it a second time,
94
+ * or a replay found it after a throw or a restart:
95
+ *
96
+ * 1. if a conversion applies and it isn't converted, convert (idempotent), then record it;
97
+ * 2. if it isn't acknowledged, run the hook — after 1 with `completeAfter`, otherwise at
98
+ * once — then record it.
99
+ *
100
+ * The only thing kept in memory is which artifacts are being settled right now, so the same
101
+ * artifact isn't settled twice at once.
102
+ */
103
+ export function createCompletionRunner(opts) {
104
+ const { storage, onUploadComplete, onArtifactEvent } = opts;
105
+ const logger = opts.logger ?? consoleLogger;
106
+ const webReady = opts.webReady
107
+ ? typeof opts.webReady === 'object'
108
+ ? opts.webReady
109
+ : {}
110
+ : undefined;
111
+ const completeAfter = webReady?.completeAfter === true;
112
+ const queue = createQueue(webReady?.concurrency ?? 1);
113
+ const replayEvery = opts.replay === false ? null : (opts.replay?.intervalSeconds ?? DEFAULT_REPLAY_INTERVAL_SECONDS) * 1000;
114
+ const converts = (kind) => webReady !== undefined && kind === 'video';
115
+ const describe = (artifactId) => storage.describeArtifact ? storage.describeArtifact(artifactId) : Promise.resolve(null);
116
+ const record = async (artifactId, patch) => {
117
+ try {
118
+ await storage.patchArtifact?.(artifactId, patch);
119
+ }
120
+ catch (err) {
121
+ // The work was done; a failed record means one extra pass over it, not a lost completion.
122
+ logger.error({ err, artifactId, patch }, 'pulsevault could not record a completion step');
123
+ }
124
+ };
125
+ /**
126
+ * Record a conversion's result, and install the file it wrote (`outputPath`, a temporary
127
+ * `.mp4` beside the stored file), in one patch under the adapter's lock: only for the upload
128
+ * the conversion began on (`generation`) and only while no other pass has recorded one
129
+ * (`unlessConverted`). Another container's original is deleted by the patch once the sidecar
130
+ * names the `.mp4`. When the patch doesn't apply — the artifact was removed, or reserved again,
131
+ * or converted elsewhere meanwhile — or fails, the file it wrote goes and nothing else
132
+ * changes. Until a record is written the original serves and the next pass converts again.
133
+ * `storedExt`: the extension of the file the conversion ran on, when it isn't the one the
134
+ * sidecar names (the local adapter found the converted `.mp4` under a sidecar written from a
135
+ * copy older than the conversion): the record names that file again. Resolves whether this
136
+ * record applied.
137
+ */
138
+ const recordConversion = async (artifactId, generation, result, outputPath, storedExt) => {
139
+ let applied = false;
140
+ try {
141
+ applied =
142
+ (await storage.patchArtifact?.(artifactId, {
143
+ converted: true,
144
+ webReady: result,
145
+ unlessConverted: true,
146
+ generation,
147
+ ...(outputPath
148
+ ? { file: outputPath, ext: CONFORM_TARGET.extension }
149
+ : storedExt
150
+ ? { ext: storedExt }
151
+ : {}),
152
+ })) ?? false;
153
+ }
154
+ catch (err) {
155
+ logger.error({ err, artifactId }, 'pulsevault could not record a web-ready conversion');
156
+ }
157
+ if (!applied && outputPath)
158
+ await fs.rm(outputPath, { force: true });
159
+ return applied;
160
+ };
161
+ /**
162
+ * Rule 1, for the reservation the work was scheduled for (`generation`; `undefined` when the
163
+ * adapter can't say). Never throws: a failed conversion serves the original bytes, and says why.
164
+ */
165
+ const convert = (ctx, generation) => new Promise((resolve) => {
166
+ void queue.run(async () => {
167
+ try {
168
+ // What was read before this job waited for its slot may be stale: the artifact may
169
+ // have been removed — and its id reserved again by another upload, which isn't this
170
+ // job's — or converted by another instance on the same disk, meanwhile.
171
+ const fresh = await describe(ctx.artifactId);
172
+ if (!fresh || !fresh.ready || (generation !== undefined && (fresh.generation ?? null) !== generation)) {
173
+ resolve(undefined);
174
+ return;
175
+ }
176
+ if (fresh.converted) {
177
+ resolve(fresh.webReady);
178
+ return;
179
+ }
180
+ const localPath = await storage.getLocalPath?.(ctx.artifactId);
181
+ // `install: false`: the file is installed by the record below, under the adapter's
182
+ // lock and conditions, never renamed over the stored file from here. Only with a
183
+ // generation: a conversion takes long enough for the id to be removed and uploaded
184
+ // again, and without one the record couldn't tell this upload from that one.
185
+ const { outputPath, ...result } = typeof localPath !== 'string'
186
+ ? { action: 'skipped', reason: 'no local path for this artifact', outputPath: undefined }
187
+ : !fresh.generation
188
+ ? { action: 'skipped', reason: 'the storage adapter keeps no generation for this upload', outputPath: undefined }
189
+ : await ensureWebReady(localPath, { ...webReady, logger, install: false });
190
+ const expected = generation !== undefined ? generation : (fresh.generation ?? null);
191
+ const localExt = typeof localPath === 'string' ? path.extname(localPath).toLowerCase() : '';
192
+ const storedExt = localExt && localExt !== fresh.ext ? localExt : undefined;
193
+ if (!(await recordConversion(ctx.artifactId, expected, result, outputPath, storedExt))) {
194
+ // Lost to another pass: its result, but only while the id is still this reservation's
195
+ // (a removal and a new upload of the id would hand back the new upload's result).
196
+ const now = await describe(ctx.artifactId);
197
+ resolve(now && (now.generation ?? null) === expected ? now.webReady : undefined);
198
+ return;
199
+ }
200
+ // The result is recorded: an observer that throws is the observer's failure, not the
201
+ // conversion's, and doesn't hide the result from the hook that follows it.
202
+ try {
203
+ await onArtifactEvent?.({
204
+ phase: 'processed',
205
+ artifactId: ctx.artifactId,
206
+ kind: ctx.kind,
207
+ size: ctx.size,
208
+ reason: result.reason,
209
+ webReady: result,
210
+ });
211
+ }
212
+ catch (err) {
213
+ logger.error({ err, artifactId: ctx.artifactId }, 'pulsevault onArtifactEvent (processed) failed');
214
+ }
215
+ resolve(result);
216
+ }
217
+ catch (err) {
218
+ logger.error({ err, artifactId: ctx.artifactId }, 'pulsevault web-ready conversion failed');
219
+ resolve(undefined);
220
+ }
221
+ });
222
+ });
223
+ /** Rule 2, recorded only on the reservation it ran for. Rethrows what the hook threw. */
224
+ const hook = async (request, ctx, generation) => {
225
+ if (onUploadComplete)
226
+ await onUploadComplete(request, ctx);
227
+ await record(ctx.artifactId, { acknowledged: true, ...(generation !== undefined ? { generation } : {}) });
228
+ };
229
+ /**
230
+ * One upload is settled by one caller at a time; a second caller joins the first. Keyed by the
231
+ * reservation too: a new upload of an id that was removed is never joined to the old one's.
232
+ */
233
+ const settling = new Map();
234
+ const settlingKey = (artifactId, generation) => `${artifactId}/${generation ?? ''}`;
235
+ /**
236
+ * Apply the rule to one finished artifact. The request that finished the upload waits on the
237
+ * result: it rejects when the hook threw, so the client gets a non-2xx — except with
238
+ * `completeAfter`, where the hook follows the conversion in the background and the request
239
+ * is answered at once. In the background (a replay, a hook after a conversion) a throw is
240
+ * logged and left for the next pass.
241
+ */
242
+ const settle = (request, meta, upload, replay) => {
243
+ const generation = meta ? (meta.generation ?? null) : undefined;
244
+ const key = settlingKey(upload.artifactId, generation);
245
+ const running = settling.get(key);
246
+ if (running)
247
+ return running;
248
+ const ctx = contextFor(meta, upload, replay);
249
+ // An adapter without `describeArtifact` can't say what was done: run everything (the hook
250
+ // is at-least-once anyway, and the conversion is idempotent).
251
+ const needsConversion = converts(upload.kind) && !(meta ? meta.converted : false);
252
+ const needsHook = meta ? !meta.acknowledged : true;
253
+ if (!needsConversion && !needsHook)
254
+ return Promise.resolve();
255
+ let run;
256
+ if (needsConversion && completeAfter) {
257
+ // Nothing for the request to wait on: the hook follows the conversion.
258
+ run = (async () => {
259
+ const result = await convert(ctx, generation);
260
+ if (!needsHook)
261
+ return;
262
+ try {
263
+ await hook(request, { ...ctx, ...(result ? { webReady: result } : {}) }, generation);
264
+ }
265
+ catch (err) {
266
+ logger.error({ err, artifactId: ctx.artifactId, kind: ctx.kind }, 'pulsevault onUploadComplete failed after web-ready; it will be tried again');
267
+ }
268
+ })();
269
+ settling.set(key, run);
270
+ void run.finally(() => settling.delete(key));
271
+ return Promise.resolve();
272
+ }
273
+ run = (async () => {
274
+ const conversion = needsConversion ? convert(ctx, generation) : Promise.resolve(undefined);
275
+ try {
276
+ if (needsHook)
277
+ await hook(request, ctx, generation);
278
+ }
279
+ finally {
280
+ // The settle covers the conversion too, so a replay doesn't queue it again meanwhile.
281
+ void conversion.finally(() => settling.delete(key));
282
+ }
283
+ })();
284
+ settling.set(key, run);
285
+ return run;
286
+ };
287
+ const complete = async (request, upload) => settle(request, await describe(upload.artifactId), upload, false);
288
+ /** The stored size of a finished upload, from tus's own record; `0` when it can't be read. */
289
+ const storedSize = async (uploadId) => {
290
+ try {
291
+ const upload = await storage.datastore.getUpload(uploadId);
292
+ return upload.size ?? upload.offset ?? 0;
293
+ }
294
+ catch {
295
+ return 0;
296
+ }
297
+ };
298
+ let replaying = null;
299
+ const replay = () => {
300
+ if (replaying)
301
+ return replaying;
302
+ replaying = (async () => {
303
+ const replayed = [];
304
+ if (typeof storage.listArtifacts !== 'function' || typeof storage.describeArtifact !== 'function') {
305
+ return replayed;
306
+ }
307
+ const pending = [];
308
+ for await (const record of storage.listArtifacts()) {
309
+ if (!record.ready)
310
+ continue;
311
+ let meta;
312
+ try {
313
+ meta = await storage.describeArtifact(record.artifactId);
314
+ }
315
+ catch (err) {
316
+ logger.error({ err, artifactId: record.artifactId }, 'pulsevault replay could not read an artifact');
317
+ continue;
318
+ }
319
+ if (!meta || !meta.ready || settling.has(settlingKey(meta.artifactId, meta.generation ?? null)))
320
+ continue;
321
+ if (meta.acknowledged && !(converts(meta.kind) && !meta.converted))
322
+ continue;
323
+ const uploadId = uploadIdOf(meta);
324
+ const upload = {
325
+ artifactId: meta.artifactId,
326
+ kind: meta.kind,
327
+ size: await storedSize(uploadId),
328
+ uploadId,
329
+ };
330
+ replayed.push(meta.artifactId);
331
+ pending.push(settle(REPLAY_REQUEST, meta, upload, true).catch((err) => {
332
+ logger.error({ err, artifactId: meta.artifactId, kind: meta.kind }, 'pulsevault onUploadComplete failed on replay; it will be tried again');
333
+ }));
334
+ }
335
+ await Promise.all(pending);
336
+ return replayed;
337
+ })().finally(() => {
338
+ replaying = null;
339
+ });
340
+ return replaying;
341
+ };
342
+ let timer = null;
343
+ let stopped = false;
344
+ const schedule = (delayMs) => {
345
+ if (stopped || replayEvery === null)
346
+ return;
347
+ timer = setTimeout(() => {
348
+ timer = null;
349
+ void replay()
350
+ .catch((err) => logger.error({ err }, 'pulsevault completion replay failed'))
351
+ .finally(() => schedule(replayEvery));
352
+ }, delayMs);
353
+ timer.unref();
354
+ };
355
+ return {
356
+ complete,
357
+ replay,
358
+ converts,
359
+ start: () => {
360
+ if (timer || stopped)
361
+ return;
362
+ schedule(Math.min(FIRST_REPLAY_DELAY_MS, replayEvery ?? FIRST_REPLAY_DELAY_MS));
363
+ },
364
+ stop: async () => {
365
+ stopped = true;
366
+ if (timer)
367
+ clearTimeout(timer);
368
+ timer = null;
369
+ await replaying?.catch(() => []);
370
+ await queue.drain();
371
+ await Promise.allSettled([...settling.values()]);
372
+ },
373
+ };
374
+ }
375
+ //# sourceMappingURL=completion.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"completion.js","sourceRoot":"","sources":["../../src/lib/completion.ts"],"names":[],"mappings":"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"}
@@ -16,10 +16,26 @@ export type UploadLinkOptions = {
16
16
  * step. Generate with `crypto.randomUUID()`.
17
17
  */
18
18
  artifactId: string;
19
+ /**
20
+ * Which of the two equivalent link forms to build (PROTOCOL.md §3):
21
+ * - `'pulsecam'` (default): `pulsecam://?<params>`, which every Pulse app opens.
22
+ * - `'https'`: `https://mieweb.github.io/pulse/open#<params>`, a Universal Link / App Link.
23
+ * Phone cameras open it, it opens Pulse when it's installed, and its page sends the person
24
+ * to the store when it isn't. The parameters ride in the fragment, so the link host never
25
+ * receives the token. Only Pulse releases that claim the link host open it directly; an
26
+ * older Pulse lands on the page, whose "Open it" button falls back to `pulsecam://`.
27
+ */
28
+ form?: 'pulsecam' | 'https';
19
29
  };
20
30
  /**
21
- * Build a `pulsecam://` deep link that opens the Pulse app directly on the
22
- * upload screen for a specific artifact, pointed at this server.
31
+ * Where the https form of a pairing link points: the page on the link host the Pulse app
32
+ * claims. Fixed, because an app can only claim domains it was built with.
33
+ */
34
+ export declare const PULSE_OPEN_URL = "https://mieweb.github.io/pulse/open";
35
+ /**
36
+ * Build a pairing link that opens the Pulse app directly on the upload screen
37
+ * for a specific artifact, pointed at this server: `pulsecam://?<params>`, or
38
+ * with `form: 'https'`, `${PULSE_OPEN_URL}#<params>`.
23
39
  *
24
40
  * No `mode` param — there's only one mode, so it was always redundant. `v`
25
41
  * lets the app refuse/explain a future incompatible link shape instead of
@@ -1 +1 @@
1
- {"version":3,"file":"deeplinks.d.ts","sourceRoot":"","sources":["../../src/lib/deeplinks.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,iBAAiB,GAAG;IAC9B;;;;;;;OAOG;IACH,MAAM,EAAE,MAAM,CAAC;IACf,gJAAgJ;IAChJ,KAAK,CAAC,EAAE,MAAM,CAAC;IACf;;;;OAIG;IACH,UAAU,EAAE,MAAM,CAAC;CACpB,CAAC;AA4BF;;;;;;;;;;;;;GAaG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,iBAAiB,GAAG,MAAM,CAqB/D"}
1
+ {"version":3,"file":"deeplinks.d.ts","sourceRoot":"","sources":["../../src/lib/deeplinks.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,iBAAiB,GAAG;IAC9B;;;;;;;OAOG;IACH,MAAM,EAAE,MAAM,CAAC;IACf,gJAAgJ;IAChJ,KAAK,CAAC,EAAE,MAAM,CAAC;IACf;;;;OAIG;IACH,UAAU,EAAE,MAAM,CAAC;IACnB;;;;;;;;OAQG;IACH,IAAI,CAAC,EAAE,UAAU,GAAG,OAAO,CAAC;CAC7B,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,cAAc,wCAAwC,CAAC;AA4BpE;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,iBAAiB,GAAG,MAAM,CAyB/D"}
@@ -1,3 +1,8 @@
1
+ /**
2
+ * Where the https form of a pairing link points: the page on the link host the Pulse app
3
+ * claims. Fixed, because an app can only claim domains it was built with.
4
+ */
5
+ export const PULSE_OPEN_URL = 'https://mieweb.github.io/pulse/open';
1
6
  /** Deep-link wire format version. Bump when the param shape changes incompatibly. */
2
7
  const LINK_VERSION = '1';
3
8
  /**
@@ -23,8 +28,9 @@ function isPrivateDevOrigin(url) {
23
28
  );
24
29
  }
25
30
  /**
26
- * Build a `pulsecam://` deep link that opens the Pulse app directly on the
27
- * upload screen for a specific artifact, pointed at this server.
31
+ * Build a pairing link that opens the Pulse app directly on the upload screen
32
+ * for a specific artifact, pointed at this server: `pulsecam://?<params>`, or
33
+ * with `form: 'https'`, `${PULSE_OPEN_URL}#<params>`.
28
34
  *
29
35
  * No `mode` param — there's only one mode, so it was always redundant. `v`
30
36
  * lets the app refuse/explain a future incompatible link shape instead of
@@ -37,6 +43,9 @@ function isPrivateDevOrigin(url) {
37
43
  * issued in the first place.
38
44
  */
39
45
  export function buildUploadLink(opts) {
46
+ if (opts.form !== undefined && opts.form !== 'pulsecam' && opts.form !== 'https') {
47
+ throw new Error(`buildUploadLink: \`form\` must be 'pulsecam' or 'https' (got "${String(opts.form)}")`);
48
+ }
40
49
  let server;
41
50
  try {
42
51
  server = new URL(opts.server);
@@ -55,6 +64,8 @@ export function buildUploadLink(opts) {
55
64
  });
56
65
  if (opts.token)
57
66
  params.set('token', opts.token);
67
+ if (opts.form === 'https')
68
+ return `${PULSE_OPEN_URL}#${params.toString()}`;
58
69
  return `pulsecam://?${params.toString()}`;
59
70
  }
60
71
  //# sourceMappingURL=deeplinks.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"deeplinks.js","sourceRoot":"","sources":["../../src/lib/deeplinks.ts"],"names":[],"mappings":"AAoBA,qFAAqF;AACrF,MAAM,YAAY,GAAG,GAAG,CAAC;AAEzB;;;;;;;;GAQG;AACH,SAAS,kBAAkB,CAAC,GAAQ;IAClC,IAAI,GAAG,CAAC,QAAQ,KAAK,OAAO;QAAE,OAAO,KAAK,CAAC;IAC3C,MAAM,IAAI,GAAG,GAAG,CAAC,QAAQ,CAAC;IAC1B,OAAO,CACL,IAAI,KAAK,WAAW;QACpB,IAAI,KAAK,WAAW;QACpB,qBAAqB,CAAC,IAAI,CAAC,IAAI,CAAC;QAChC,sBAAsB,CAAC,IAAI,CAAC,IAAI,CAAC;QACjC,qCAAqC,CAAC,IAAI,CAAC,IAAI,CAAC;QAChD,mDAAmD,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,0BAA0B;QAC5F,sBAAsB,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,aAAa;KAChD,CAAC;AACJ,CAAC;AAED;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,eAAe,CAAC,IAAuB;IACrD,IAAI,MAAW,CAAC;IAChB,IAAI,CAAC;QACH,MAAM,GAAG,IAAI,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IAChC,CAAC;IAAC,MAAM,CAAC;QACP,MAAM,IAAI,KAAK,CAAC,mDAAmD,IAAI,CAAC,MAAM,EAAE,CAAC,CAAC;IACpF,CAAC;IACD,IAAI,MAAM,CAAC,QAAQ,KAAK,QAAQ,IAAI,CAAC,kBAAkB,CAAC,MAAM,CAAC,EAAE,CAAC;QAChE,MAAM,IAAI,KAAK,CACb,sDAAsD,IAAI,CAAC,MAAM,OAAO;YACtE,uFAAuF,CAC1F,CAAC;IACJ,CAAC;IAED,MAAM,MAAM,GAAG,IAAI,eAAe,CAAC;QACjC,CAAC,EAAE,YAAY;QACf,UAAU,EAAE,IAAI,CAAC,UAAU;QAC3B,MAAM,EAAE,IAAI,CAAC,MAAM;KACpB,CAAC,CAAC;IACH,IAAI,IAAI,CAAC,KAAK;QAAE,MAAM,CAAC,GAAG,CAAC,OAAO,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC;IAChD,OAAO,eAAe,MAAM,CAAC,QAAQ,EAAE,EAAE,CAAC;AAC5C,CAAC"}
1
+ {"version":3,"file":"deeplinks.js","sourceRoot":"","sources":["../../src/lib/deeplinks.ts"],"names":[],"mappings":"AA8BA;;;GAGG;AACH,MAAM,CAAC,MAAM,cAAc,GAAG,qCAAqC,CAAC;AAEpE,qFAAqF;AACrF,MAAM,YAAY,GAAG,GAAG,CAAC;AAEzB;;;;;;;;GAQG;AACH,SAAS,kBAAkB,CAAC,GAAQ;IAClC,IAAI,GAAG,CAAC,QAAQ,KAAK,OAAO;QAAE,OAAO,KAAK,CAAC;IAC3C,MAAM,IAAI,GAAG,GAAG,CAAC,QAAQ,CAAC;IAC1B,OAAO,CACL,IAAI,KAAK,WAAW;QACpB,IAAI,KAAK,WAAW;QACpB,qBAAqB,CAAC,IAAI,CAAC,IAAI,CAAC;QAChC,sBAAsB,CAAC,IAAI,CAAC,IAAI,CAAC;QACjC,qCAAqC,CAAC,IAAI,CAAC,IAAI,CAAC;QAChD,mDAAmD,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,0BAA0B;QAC5F,sBAAsB,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,aAAa;KAChD,CAAC;AACJ,CAAC;AAED;;;;;;;;;;;;;;GAcG;AACH,MAAM,UAAU,eAAe,CAAC,IAAuB;IACrD,IAAI,IAAI,CAAC,IAAI,KAAK,SAAS,IAAI,IAAI,CAAC,IAAI,KAAK,UAAU,IAAI,IAAI,CAAC,IAAI,KAAK,OAAO,EAAE,CAAC;QACjF,MAAM,IAAI,KAAK,CAAC,iEAAiE,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAC1G,CAAC;IACD,IAAI,MAAW,CAAC;IAChB,IAAI,CAAC;QACH,MAAM,GAAG,IAAI,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IAChC,CAAC;IAAC,MAAM,CAAC;QACP,MAAM,IAAI,KAAK,CAAC,mDAAmD,IAAI,CAAC,MAAM,EAAE,CAAC,CAAC;IACpF,CAAC;IACD,IAAI,MAAM,CAAC,QAAQ,KAAK,QAAQ,IAAI,CAAC,kBAAkB,CAAC,MAAM,CAAC,EAAE,CAAC;QAChE,MAAM,IAAI,KAAK,CACb,sDAAsD,IAAI,CAAC,MAAM,OAAO;YACtE,uFAAuF,CAC1F,CAAC;IACJ,CAAC;IAED,MAAM,MAAM,GAAG,IAAI,eAAe,CAAC;QACjC,CAAC,EAAE,YAAY;QACf,UAAU,EAAE,IAAI,CAAC,UAAU;QAC3B,MAAM,EAAE,IAAI,CAAC,MAAM;KACpB,CAAC,CAAC;IACH,IAAI,IAAI,CAAC,KAAK;QAAE,MAAM,CAAC,GAAG,CAAC,OAAO,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC;IAChD,IAAI,IAAI,CAAC,IAAI,KAAK,OAAO;QAAE,OAAO,GAAG,cAAc,IAAI,MAAM,CAAC,QAAQ,EAAE,EAAE,CAAC;IAC3E,OAAO,eAAe,MAAM,CAAC,QAAQ,EAAE,EAAE,CAAC;AAC5C,CAAC"}
@@ -1,4 +1,4 @@
1
- import type { PulseVaultRequest } from './request.js';
1
+ import { type PulseVaultLogger, type PulseVaultRequest } from './request.js';
2
2
  import type { LocalStorage } from '../storage/local.js';
3
3
  import type { S3Storage } from '../storage/s3.js';
4
4
  import type { UploadKind } from '../storage/types.js';
@@ -72,4 +72,44 @@ export declare function createMp4Sniffer(storage: LocalStorage): PulseVaultValid
72
72
  * ```
73
73
  */
74
74
  export declare function createS3Mp4Sniffer(storage: S3Storage): PulseVaultValidatePayload;
75
+ /**
76
+ * Whether a file's first bytes open one of the video containers PulseVault accepts: ISO base
77
+ * media (`ftyp`: MP4, MOV, M4V, 3GP), EBML (WebM, MKV) or RIFF `AVI `. A ~12-byte sniff, for
78
+ * when ffprobe isn't installed: it tells a video container from a renamed document, not a
79
+ * playable video from a broken one.
80
+ */
81
+ export declare function sniffVideo(filePath: string): Promise<boolean>;
82
+ export type VideoValidatorOptions = {
83
+ /** Refuse a video longer than this many seconds. Default: no limit. */
84
+ maxDurationSeconds?: number;
85
+ /** Path to the ffprobe binary. Default `"ffprobe"` (resolved via PATH). */
86
+ ffprobePath?: string;
87
+ /**
88
+ * Wall-clock limit for each ffprobe run, in seconds, so a malformed file can't hold the
89
+ * final `PATCH` open. Default `60`.
90
+ */
91
+ probeTimeoutSeconds?: number;
92
+ /** Where the one-time "ffprobe not found" warning goes. Defaults to `console`. */
93
+ logger?: PulseVaultLogger;
94
+ };
95
+ /**
96
+ * Build a `validatePayload` hook that checks a video by what it is, not its file name: ffprobe
97
+ * must find a video stream with a duration above zero (and, with `maxDurationSeconds`, not
98
+ * longer than that). Without ffprobe on the host it falls back to `sniffVideo` and logs once.
99
+ * Other kinds pass untouched. Needs the bytes on local disk (the local adapter).
100
+ *
101
+ * A refusal is a `422` whose message a host can show as it is: "That file isn't a video.",
102
+ * "That video is longer than the limit of 10 minutes." or, when ffprobe runs past
103
+ * `probeTimeoutSeconds` (a malformed file), "That video couldn't be checked in time."
104
+ *
105
+ * Usage:
106
+ * ```ts
107
+ * await app.register(pulseVault, {
108
+ * storage: createLocalStorage({ workspaceDir: "./data" }),
109
+ * validatePayload: createVideoValidator({ maxDurationSeconds: 600 }),
110
+ * // ...
111
+ * });
112
+ * ```
113
+ */
114
+ export declare function createVideoValidator(options?: VideoValidatorOptions): PulseVaultValidatePayload;
75
115
  //# sourceMappingURL=magic.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"magic.d.ts","sourceRoot":"","sources":["../../src/lib/magic.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AACtD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AACxD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAClD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,qBAAqB,CAAC;AAEtD;;;;;;;;;;;GAWG;AACH,MAAM,MAAM,yBAAyB,GAAG,CACtC,OAAO,EAAE,iBAAiB,EAC1B,GAAG,EAAE;IACH,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,wHAAwH;IACxH,IAAI,EAAE,UAAU,CAAC;IACjB,4EAA4E;IAC5E,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,0FAA0F;IAC1F,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB,KACE,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;AAE1B;;;;;;;;;GASG;AACH,wBAAsB,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAejE;AAaD;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,YAAY,GAAG,yBAAyB,CAgBjF;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,SAAS,GAAG,yBAAyB,CAehF"}
1
+ {"version":3,"file":"magic.d.ts","sourceRoot":"","sources":["../../src/lib/magic.ts"],"names":[],"mappings":"AACA,OAAO,EAAiB,KAAK,gBAAgB,EAAE,KAAK,iBAAiB,EAAE,MAAM,cAAc,CAAC;AAE5F,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AACxD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAClD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,qBAAqB,CAAC;AAEtD;;;;;;;;;;;GAWG;AACH,MAAM,MAAM,yBAAyB,GAAG,CACtC,OAAO,EAAE,iBAAiB,EAC1B,GAAG,EAAE;IACH,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,wHAAwH;IACxH,IAAI,EAAE,UAAU,CAAC;IACjB,4EAA4E;IAC5E,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,0FAA0F;IAC1F,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB,KACE,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;AAE1B;;;;;;;;;GASG;AACH,wBAAsB,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAejE;AAaD;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,YAAY,GAAG,yBAAyB,CAgBjF;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,SAAS,GAAG,yBAAyB,CAehF;AAED;;;;;GAKG;AACH,wBAAsB,UAAU,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAiBnE;AAED,MAAM,MAAM,qBAAqB,GAAG;IAClC,uEAAuE;IACvE,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,2EAA2E;IAC3E,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;OAGG;IACH,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,kFAAkF;IAClF,MAAM,CAAC,EAAE,gBAAgB,CAAC;CAC3B,CAAC;AAcF;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,GAAE,qBAA0B,GAAG,yBAAyB,CA6CnG"}