@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
@@ -1,4 +1,5 @@
1
1
  import type { DataStore } from '@tus/server';
2
+ import type { WebReadyResult } from '../lib/web-ready.js';
2
3
  /**
3
4
  * How the GET route should serve a resolved artifact. Stream means
4
5
  * `@fastify/send` reads directly from a local path; Redirect issues an HTTP
@@ -82,6 +83,112 @@ export type ReserveUploadParams = {
82
83
  * an operator can tell which app build produced a file. Display metadata only, like `name`.
83
84
  */
84
85
  appVersion?: string;
86
+ /**
87
+ * Opaque host data the capability token carried (`issueCapabilityToken`'s `context`: an owner,
88
+ * a destination), as the `authorize` hook returned it for this create. Persisted verbatim so
89
+ * every later hook, status read and completion replay gets it back without a host lookup.
90
+ * JSON only; never interpreted by PulseVault.
91
+ */
92
+ context?: unknown;
93
+ };
94
+ /** Everything an adapter knows about one artifact, from `describeArtifact`. */
95
+ export type PulseVaultArtifactMeta = {
96
+ artifactId: string;
97
+ kind: UploadKind;
98
+ /**
99
+ * Lowercase extension of the stored file, including the leading dot: the uploaded file's,
100
+ * until a web-ready conversion turned another container into an MP4 (then `.mp4`).
101
+ */
102
+ ext: string;
103
+ /**
104
+ * The uploaded file's extension — `filename`'s, and the one tus's record of the upload is kept
105
+ * under — when a conversion stored the artifact under a different `ext`; absent otherwise. Use
106
+ * `uploadIdOf` to name the tus upload.
107
+ */
108
+ sourceExt?: string;
109
+ /** Original filename from `Upload-Metadata.filename`. */
110
+ filename: string;
111
+ relatedTo?: string;
112
+ checksum?: string;
113
+ name?: string;
114
+ appVersion?: string;
115
+ /** See `ReserveUploadParams.context`. */
116
+ context?: unknown;
117
+ /** `true` once the upload finished and is served. */
118
+ ready: boolean;
119
+ /**
120
+ * Whether the host's `onUploadComplete` has returned for this artifact: `false` from reserve,
121
+ * `true` once the core recorded the hook's return. A completion the host never recorded (a
122
+ * throw, a restart) is replayed from this flag. A sidecar written before the flag existed reads
123
+ * as `true` when finished (an upgrade replays nothing) and `false` while still uploading (its
124
+ * completion is still to come).
125
+ */
126
+ acknowledged: boolean;
127
+ /**
128
+ * Whether the web-ready conversion has finished for this artifact: `false` from reserve, `true`
129
+ * once the core recorded it. Only meaningful for a video on a server with `webReady`; the core
130
+ * derives the `processing` status from it. Legacy sidecars read as `true` when finished.
131
+ */
132
+ converted: boolean;
133
+ /** What the web-ready conversion did (`none`, `remuxed`, …, or `skipped` and why), once it ran. */
134
+ webReady?: WebReadyResult;
135
+ /** Whatever the host recorded with `recordOutcome` (where the upload went, why it was kept). */
136
+ outcome?: unknown;
137
+ /**
138
+ * A token for this reservation of the artifactId, new at every `reserveUpload`, so work that
139
+ * began on one upload can tell the id was removed and reserved again meanwhile (the
140
+ * `generation` patch condition). Required for `webReady` conversions: an adapter that offers
141
+ * `getLocalPath` but keeps no generation has its videos recorded `skipped`, never converted,
142
+ * since a queued conversion couldn't tell its upload from a later one of the same id. (The
143
+ * local adapter keeps one, and gives one at `markReady` to an upload reserved before it did.)
144
+ */
145
+ generation?: string;
146
+ /** Same meaning as `PulseVaultArtifactRecord.updatedAt`. */
147
+ updatedAt: number;
148
+ /**
149
+ * When the upload finished (ms since the epoch), recorded by `markReady` and never changed
150
+ * after — a stable order for "the newest related file", which `updatedAt` isn't (a later
151
+ * acknowledgement or outcome moves it). Absent on sidecars written before it was recorded.
152
+ */
153
+ readyAt?: number;
154
+ };
155
+ /**
156
+ * tus's id for an artifact's upload (`<kind>/<artifactId><ext>`), under the extension it was
157
+ * uploaded with — which a conversion to another container doesn't change.
158
+ */
159
+ export declare function uploadIdOf(meta: Pick<PulseVaultArtifactMeta, 'kind' | 'artifactId' | 'ext' | 'sourceExt'>): string;
160
+ /** The fields `patchArtifact` may change. Each is optional; absent means unchanged. */
161
+ export type PulseVaultArtifactPatch = {
162
+ acknowledged?: boolean;
163
+ converted?: boolean;
164
+ /** What the web-ready conversion did. */
165
+ webReady?: WebReadyResult;
166
+ /**
167
+ * The stored file's new extension, after a web-ready conversion wrote the artifact's bytes
168
+ * under it (`<kind>/<artifactId><ext>`): from then on the artifact is served from that file.
169
+ * Only adapters that store files on local disk support it; others throw.
170
+ */
171
+ ext?: string;
172
+ /**
173
+ * Apply the patch only while `converted` is still false, so a conversion's record never
174
+ * replaces one another pass already made (two instances converting the same artifact).
175
+ */
176
+ unlessConverted?: boolean;
177
+ /**
178
+ * Apply the patch only to this reservation of the artifactId (`PulseVaultArtifactMeta
179
+ * .generation`; `null` for one without a generation), not to an upload that reserved the same
180
+ * id after it was removed.
181
+ */
182
+ generation?: string | null;
183
+ /**
184
+ * A file to install as the artifact's stored bytes, under the same lock as the rest of the
185
+ * patch: renamed onto `<kind>/<artifactId><ext>` (with `ext` the new one when it changes),
186
+ * after which a previous stored file under another extension is deleted. Must be in the
187
+ * artifact's kind directory. Only adapters that store files on local disk support it.
188
+ */
189
+ file?: string;
190
+ /** `null` clears a recorded outcome. */
191
+ outcome?: unknown;
85
192
  };
86
193
  /** One artifact as `listArtifacts` reports it. */
87
194
  export type PulseVaultArtifactRecord = {
@@ -176,5 +283,24 @@ export interface PulseVaultStorage {
176
283
  listArtifacts?(opts?: {
177
284
  changedBefore?: number;
178
285
  }): AsyncIterable<PulseVaultArtifactRecord>;
286
+ /**
287
+ * Everything stored about one artifact, or `null` if the artifactId is unknown. Optional —
288
+ * the richer hook context, `getStatus`, `getPulse`, reclaiming idle uploads and replaying
289
+ * completions all need it (both built-in adapters have it).
290
+ */
291
+ describeArtifact?(artifactId: string): Promise<PulseVaultArtifactMeta | null>;
292
+ /**
293
+ * Change the bookkeeping of one artifact (`acknowledged`, `converted`, `webReady`, `ext`,
294
+ * `outcome`, or its stored `file`). Resolves `false`, changing nothing, when the artifactId is
295
+ * unknown or a condition (`unlessConverted`, `generation`) doesn't hold. Optional —
296
+ * completion replay, background web-ready and `recordOutcome` need it.
297
+ */
298
+ patchArtifact?(artifactId: string, patch: PulseVaultArtifactPatch): Promise<boolean>;
299
+ /**
300
+ * The artifacts that declared `relatedTo` this one (a pulse's captions, beat manifest and
301
+ * thumbnail), in flight or finished. Optional — `getPulse` and the poster route need it. Built
302
+ * from an index the adapter keeps at reserve time, so it never scans every artifact.
303
+ */
304
+ listRelated?(artifactId: string): AsyncIterable<PulseVaultArtifactRecord>;
179
305
  }
180
306
  //# sourceMappingURL=types.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/storage/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAE7C;;;;GAIG;AACH,MAAM,MAAM,oBAAoB,GAC5B;IACE,IAAI,EAAE,QAAQ,CAAC;IACf,qDAAqD;IACrD,IAAI,EAAE,MAAM,CAAC;IACb,mCAAmC;IACnC,QAAQ,EAAE,MAAM,CAAC;IACjB;;;;OAIG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB,GACD;IACE,IAAI,EAAE,UAAU,CAAC;IACjB,GAAG,EAAE,MAAM,CAAC;IACZ,uBAAuB;IACvB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB,CAAC;AAEN;;uCAEuC;AACvC,eAAO,MAAM,YAAY,wDAAyD,CAAC;AAEnF,MAAM,MAAM,UAAU,GAAG,CAAC,OAAO,YAAY,CAAC,CAAC,MAAM,CAAC,CAAC;AAEvD;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,UAAU,CAG1E;AAED,MAAM,MAAM,mBAAmB,GAAG;IAChC,4FAA4F;IAC5F,UAAU,EAAE,MAAM,CAAC;IACnB,oDAAoD;IACpD,QAAQ,EAAE,MAAM,CAAC;IACjB,yEAAyE;IACzE,GAAG,EAAE,MAAM,CAAC;IACZ,gFAAgF;IAChF,IAAI,EAAE,UAAU,CAAC;IACjB;;;;;;;;;;;;;;OAcG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;;;;OAMG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;;;;;;;OAQG;IACH,IAAI,CAAC,EAAE,MAAM,CAAC;IACd;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB,CAAC;AAEF,kDAAkD;AAClD,MAAM,MAAM,wBAAwB,GAAG;IACrC,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,UAAU,CAAC;IACjB,0FAA0F;IAC1F,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,mFAAmF;IACnF,KAAK,EAAE,OAAO,CAAC;IACf;;;OAGG;IACH,SAAS,EAAE,MAAM,CAAC;CACnB,CAAC;AAEF;;;GAGG;AACH,MAAM,WAAW,iBAAiB;IAChC,gDAAgD;IAChD,QAAQ,CAAC,SAAS,EAAE,SAAS,CAAC;IAE9B,sDAAsD;IACtD,UAAU,CAAC,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IAE7B;;;;OAIG;IACH,QAAQ,CAAC,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IAE3B;;;;OAIG;IACH,aAAa,CAAC,MAAM,EAAE,mBAAmB,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IAE5D;;;;;OAKG;IACH,OAAO,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,oBAAoB,GAAG,IAAI,CAAC,CAAC;IAElE;;;;;;;OAOG;IACH,SAAS,CAAC,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAE9C;;;;;;OAMG;IACH,MAAM,CAAC,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAE9C;;;;OAIG;IACH,OAAO,CAAC,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC,CAAC;IAEzD;;;;;OAKG;IACH,YAAY,CAAC,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IAE1D;;;;OAIG;IACH,WAAW,CAAC,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IAEzD;;;;;OAKG;IACH,OAAO,CAAC,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IAErD;;;;OAIG;IACH,aAAa,CAAC,CAAC,IAAI,CAAC,EAAE;QAAE,aAAa,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,aAAa,CAAC,wBAAwB,CAAC,CAAC;CAC5F"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/storage/types.ts"],"names":[],"mappings":"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"}
@@ -11,4 +11,11 @@ export function parseUploadKind(raw) {
11
11
  const lower = (raw ?? '').trim().toLowerCase();
12
12
  return UPLOAD_KINDS.includes(lower) ? lower : 'video';
13
13
  }
14
+ /**
15
+ * tus's id for an artifact's upload (`<kind>/<artifactId><ext>`), under the extension it was
16
+ * uploaded with — which a conversion to another container doesn't change.
17
+ */
18
+ export function uploadIdOf(meta) {
19
+ return `${meta.kind}/${meta.artifactId}${meta.sourceExt ?? meta.ext}`;
20
+ }
14
21
  //# sourceMappingURL=types.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/storage/types.ts"],"names":[],"mappings":"AA4BA;;uCAEuC;AACvC,MAAM,CAAC,MAAM,YAAY,GAAG,CAAC,OAAO,EAAE,SAAS,EAAE,UAAU,EAAE,WAAW,CAAU,CAAC;AAInF;;;;GAIG;AACH,MAAM,UAAU,eAAe,CAAC,GAA8B;IAC5D,MAAM,KAAK,GAAG,CAAC,GAAG,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;IAC/C,OAAQ,YAAkC,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,CAAE,KAAoB,CAAC,CAAC,CAAC,OAAO,CAAC;AAC/F,CAAC"}
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/storage/types.ts"],"names":[],"mappings":"AA6BA;;uCAEuC;AACvC,MAAM,CAAC,MAAM,YAAY,GAAG,CAAC,OAAO,EAAE,SAAS,EAAE,UAAU,EAAE,WAAW,CAAU,CAAC;AAInF;;;;GAIG;AACH,MAAM,UAAU,eAAe,CAAC,GAA8B;IAC5D,MAAM,KAAK,GAAG,CAAC,GAAG,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;IAC/C,OAAQ,YAAkC,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,CAAE,KAAoB,CAAC,CAAC,CAAC,OAAO,CAAC;AAC/F,CAAC;AA0HD;;;GAGG;AACH,MAAM,UAAU,UAAU,CAAC,IAA+E;IACxG,OAAO,GAAG,IAAI,CAAC,IAAI,IAAI,IAAI,CAAC,UAAU,GAAG,IAAI,CAAC,SAAS,IAAI,IAAI,CAAC,GAAG,EAAE,CAAC;AACxE,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mieweb/pulsevault",
3
- "version": "0.4.2",
3
+ "version": "0.6.0",
4
4
  "description": "Resumable video uploads via TUS for Fastify (plugin) or any Node backend (framework-agnostic core), with a pluggable storage adapter and a filesystem-first local-storage default.",
5
5
  "type": "module",
6
6
  "main": "./dist/app.js",
@@ -105,7 +105,7 @@
105
105
  "@tus/s3-store": "^2.0.6"
106
106
  },
107
107
  "pulseProtocol": {
108
- "version": "2.2",
108
+ "version": "2.5",
109
109
  "min": 2,
110
110
  "max": 2
111
111
  }
@@ -3,7 +3,7 @@
3
3
  "info": {
4
4
  "title": "Pulse upload protocol",
5
5
  "description": "The HTTP surface of the Pulse upload protocol, as implemented by @mieweb/pulsevault. Generated from the plugin's route schemas — see PROTOCOL.md for the full contract.",
6
- "version": "2.2"
6
+ "version": "2.5"
7
7
  },
8
8
  "components": {
9
9
  "schemas": {}
@@ -1047,6 +1047,275 @@
1047
1047
  }
1048
1048
  }
1049
1049
  }
1050
+ },
1051
+ "/pulsevault/artifacts/{artifactId}/status": {
1052
+ "get": {
1053
+ "summary": "Where an upload is: uploading, processing or ready",
1054
+ "tags": [
1055
+ "pulsevault"
1056
+ ],
1057
+ "description": "Reports the artifact's state (`unknown`, `uploading`, `processing` while a web-ready conversion rewrites it, `ready`), its kind and `relatedTo`, the bytes received against its declared length, whether the host's `onUploadComplete` has finished (`acknowledged`), and what the host recorded with `recordOutcome` (`outcome`). Runs the `authorize` hook with `phase: \"status\"`; `createCapabilityAuthorize` grants it to the pairing token and to a view token. Never cached.",
1058
+ "parameters": [
1059
+ {
1060
+ "schema": {
1061
+ "type": "string"
1062
+ },
1063
+ "in": "query",
1064
+ "name": "token",
1065
+ "required": false,
1066
+ "description": "Optional bearer token (the pairing token or a view token), forwarded to the `authorize` hook as `ctx.token` for browsers that can’t set a header."
1067
+ },
1068
+ {
1069
+ "schema": {
1070
+ "type": "string",
1071
+ "format": "uuid"
1072
+ },
1073
+ "in": "path",
1074
+ "name": "artifactId",
1075
+ "required": true,
1076
+ "description": "UUID of the upload to report on."
1077
+ }
1078
+ ],
1079
+ "responses": {
1080
+ "200": {
1081
+ "description": "Where an upload is (PROTOCOL.md §6.5, protocol 2.3; `webReady` 2.5): for a page waiting on it, so the host needs no table of its own. Authorized like opening the artifact. Never cached.",
1082
+ "content": {
1083
+ "application/json": {
1084
+ "schema": {
1085
+ "title": "GET /artifacts/<id>/status response",
1086
+ "description": "Where an upload is (PROTOCOL.md §6.5, protocol 2.3; `webReady` 2.5): for a page waiting on it, so the host needs no table of its own. Authorized like opening the artifact. Never cached.",
1087
+ "type": "object",
1088
+ "required": [
1089
+ "artifactId",
1090
+ "state"
1091
+ ],
1092
+ "properties": {
1093
+ "artifactId": {
1094
+ "type": "string",
1095
+ "format": "uuid",
1096
+ "description": "The artifact asked about."
1097
+ },
1098
+ "state": {
1099
+ "type": "string",
1100
+ "enum": [
1101
+ "unknown",
1102
+ "uploading",
1103
+ "processing",
1104
+ "ready"
1105
+ ],
1106
+ "description": "`unknown` for an id the server has never seen; `uploading` until the final byte; `processing` while the server converts the bytes for the web; `ready` once the artifact is served."
1107
+ },
1108
+ "kind": {
1109
+ "type": "string",
1110
+ "enum": [
1111
+ "video",
1112
+ "project",
1113
+ "captions",
1114
+ "thumbnail"
1115
+ ],
1116
+ "description": "Artifact kind. Absent when `state` is `unknown`."
1117
+ },
1118
+ "relatedTo": {
1119
+ "type": "string",
1120
+ "format": "uuid",
1121
+ "description": "The video this artifact belongs to, if it declared one."
1122
+ },
1123
+ "name": {
1124
+ "type": "string",
1125
+ "description": "The display name the client sent (`Upload-Metadata.name`), if any."
1126
+ },
1127
+ "bytesReceived": {
1128
+ "type": "number",
1129
+ "description": "Bytes received so far while `uploading`, or stored once finished, when the server knows."
1130
+ },
1131
+ "size": {
1132
+ "type": "number",
1133
+ "description": "The upload's declared length, when the server knows."
1134
+ },
1135
+ "acknowledged": {
1136
+ "type": "boolean",
1137
+ "description": "Whether the host has finished handling the completed upload (its `onUploadComplete` returned)."
1138
+ },
1139
+ "webReady": {
1140
+ "type": "object",
1141
+ "required": [
1142
+ "action",
1143
+ "reason"
1144
+ ],
1145
+ "description": "What the server's web-ready conversion did, once it ran (protocol 2.5): `none` (already in the served format), `remuxed`, `transcoded`, `conformed` (another container became an MP4), or `skipped` (the original bytes serve) — with a reason a person can read. Absent until it has run, and on servers that don't convert.",
1146
+ "properties": {
1147
+ "action": {
1148
+ "type": "string",
1149
+ "enum": [
1150
+ "none",
1151
+ "remuxed",
1152
+ "transcoded",
1153
+ "conformed",
1154
+ "skipped"
1155
+ ]
1156
+ },
1157
+ "reason": {
1158
+ "type": "string"
1159
+ }
1160
+ }
1161
+ },
1162
+ "outcome": {
1163
+ "description": "What the host recorded about where the upload went, or why it didn't. Any JSON; absent until the host records one."
1164
+ }
1165
+ }
1166
+ }
1167
+ }
1168
+ }
1169
+ },
1170
+ "400": {
1171
+ "description": "`artifactId` is not a valid UUID.",
1172
+ "content": {
1173
+ "application/json": {
1174
+ "schema": {
1175
+ "description": "`artifactId` is not a valid UUID.",
1176
+ "type": "object",
1177
+ "properties": {
1178
+ "ok": {
1179
+ "type": "boolean"
1180
+ },
1181
+ "error": {
1182
+ "type": "string"
1183
+ }
1184
+ },
1185
+ "required": [
1186
+ "ok",
1187
+ "error"
1188
+ ]
1189
+ }
1190
+ }
1191
+ }
1192
+ },
1193
+ "403": {
1194
+ "description": "Authorize hook rejected the request.",
1195
+ "content": {
1196
+ "application/json": {
1197
+ "schema": {
1198
+ "description": "Authorize hook rejected the request.",
1199
+ "type": "object",
1200
+ "properties": {
1201
+ "ok": {
1202
+ "type": "boolean"
1203
+ },
1204
+ "error": {
1205
+ "type": "string"
1206
+ }
1207
+ },
1208
+ "required": [
1209
+ "ok",
1210
+ "error"
1211
+ ]
1212
+ }
1213
+ }
1214
+ }
1215
+ }
1216
+ }
1217
+ }
1218
+ },
1219
+ "/pulsevault/artifacts/{artifactId}/poster": {
1220
+ "get": {
1221
+ "summary": "Serve a video's poster frame",
1222
+ "tags": [
1223
+ "pulsevault"
1224
+ ],
1225
+ "description": "Streams or redirects to the finished thumbnail `relatedTo` the video, exactly as `GET /artifacts/:artifactId` would serve it. Runs the `authorize` hook with `phase: \"resolve\"` on the video, so whoever may watch it may see its poster. 404 until the poster has landed.",
1226
+ "parameters": [
1227
+ {
1228
+ "schema": {
1229
+ "type": "string"
1230
+ },
1231
+ "in": "query",
1232
+ "name": "token",
1233
+ "required": false,
1234
+ "description": "Optional bearer token for pre-authenticated watch links, forwarded to the `authorize` hook as `ctx.token`."
1235
+ },
1236
+ {
1237
+ "schema": {
1238
+ "type": "string",
1239
+ "format": "uuid"
1240
+ },
1241
+ "in": "path",
1242
+ "name": "artifactId",
1243
+ "required": true,
1244
+ "description": "UUID of the video (the pulse's anchor artifact)."
1245
+ }
1246
+ ],
1247
+ "responses": {
1248
+ "400": {
1249
+ "description": "`artifactId` is not a valid UUID.",
1250
+ "content": {
1251
+ "application/json": {
1252
+ "schema": {
1253
+ "description": "`artifactId` is not a valid UUID.",
1254
+ "type": "object",
1255
+ "properties": {
1256
+ "ok": {
1257
+ "type": "boolean"
1258
+ },
1259
+ "error": {
1260
+ "type": "string"
1261
+ }
1262
+ },
1263
+ "required": [
1264
+ "ok",
1265
+ "error"
1266
+ ]
1267
+ }
1268
+ }
1269
+ }
1270
+ },
1271
+ "403": {
1272
+ "description": "Authorize hook rejected the request.",
1273
+ "content": {
1274
+ "application/json": {
1275
+ "schema": {
1276
+ "description": "Authorize hook rejected the request.",
1277
+ "type": "object",
1278
+ "properties": {
1279
+ "ok": {
1280
+ "type": "boolean"
1281
+ },
1282
+ "error": {
1283
+ "type": "string"
1284
+ }
1285
+ },
1286
+ "required": [
1287
+ "ok",
1288
+ "error"
1289
+ ]
1290
+ }
1291
+ }
1292
+ }
1293
+ },
1294
+ "404": {
1295
+ "description": "The video has no finished poster frame.",
1296
+ "content": {
1297
+ "application/json": {
1298
+ "schema": {
1299
+ "description": "The video has no finished poster frame.",
1300
+ "type": "object",
1301
+ "properties": {
1302
+ "ok": {
1303
+ "type": "boolean"
1304
+ },
1305
+ "error": {
1306
+ "type": "string"
1307
+ }
1308
+ },
1309
+ "required": [
1310
+ "ok",
1311
+ "error"
1312
+ ]
1313
+ }
1314
+ }
1315
+ }
1316
+ }
1317
+ }
1318
+ }
1050
1319
  }
1051
1320
  },
1052
1321
  "tags": [
@@ -0,0 +1,61 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://github.com/mieweb/pulsevault/blob/main/protocol/schemas/artifact-status.schema.json",
4
+ "title": "GET /artifacts/<id>/status response",
5
+ "description": "Where an upload is (PROTOCOL.md §6.5, protocol 2.3; `webReady` 2.5): for a page waiting on it, so the host needs no table of its own. Authorized like opening the artifact. Never cached.",
6
+ "type": "object",
7
+ "required": ["artifactId", "state"],
8
+ "properties": {
9
+ "artifactId": {
10
+ "type": "string",
11
+ "format": "uuid",
12
+ "description": "The artifact asked about."
13
+ },
14
+ "state": {
15
+ "type": "string",
16
+ "enum": ["unknown", "uploading", "processing", "ready"],
17
+ "description": "`unknown` for an id the server has never seen; `uploading` until the final byte; `processing` while the server converts the bytes for the web; `ready` once the artifact is served."
18
+ },
19
+ "kind": {
20
+ "type": "string",
21
+ "enum": ["video", "project", "captions", "thumbnail"],
22
+ "description": "Artifact kind. Absent when `state` is `unknown`."
23
+ },
24
+ "relatedTo": {
25
+ "type": "string",
26
+ "format": "uuid",
27
+ "description": "The video this artifact belongs to, if it declared one."
28
+ },
29
+ "name": {
30
+ "type": "string",
31
+ "description": "The display name the client sent (`Upload-Metadata.name`), if any."
32
+ },
33
+ "bytesReceived": {
34
+ "type": "number",
35
+ "description": "Bytes received so far while `uploading`, or stored once finished, when the server knows."
36
+ },
37
+ "size": {
38
+ "type": "number",
39
+ "description": "The upload's declared length, when the server knows."
40
+ },
41
+ "acknowledged": {
42
+ "type": "boolean",
43
+ "description": "Whether the host has finished handling the completed upload (its `onUploadComplete` returned)."
44
+ },
45
+ "webReady": {
46
+ "type": "object",
47
+ "required": ["action", "reason"],
48
+ "description": "What the server's web-ready conversion did, once it ran (protocol 2.5): `none` (already in the served format), `remuxed`, `transcoded`, `conformed` (another container became an MP4), or `skipped` (the original bytes serve) — with a reason a person can read. Absent until it has run, and on servers that don't convert.",
49
+ "properties": {
50
+ "action": {
51
+ "type": "string",
52
+ "enum": ["none", "remuxed", "transcoded", "conformed", "skipped"]
53
+ },
54
+ "reason": { "type": "string" }
55
+ }
56
+ },
57
+ "outcome": {
58
+ "description": "What the host recorded about where the upload went, or why it didn't. Any JSON; absent until the host records one."
59
+ }
60
+ }
61
+ }
@@ -31,6 +31,9 @@
31
31
  "type": "string",
32
32
  "enum": ["view"],
33
33
  "description": "`view` on a read-only view token (§5.5), which only opens artifacts and is signed with a key derived from the secret. Absent on the capability token an upload uses."
34
+ },
35
+ "ctx": {
36
+ "description": "Opaque host data signed into the token (protocol 2.3): who the upload is for, where it goes. The server stores it with the artifact the token creates and hands it back to its own hooks; a client never reads it. Any JSON, at most 1 KiB encoded."
34
37
  }
35
38
  }
36
39
  }
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "$schema": "https://json-schema.org/draft/2020-12/schema",
3
3
  "$id": "https://github.com/mieweb/pulsevault/blob/main/protocol/schemas/deep-link.schema.json",
4
- "title": "Pairing link query parameters",
5
- "description": "The query parameters of a `pulsecam://?…` pairing link (PROTOCOL.md §3), as built by `buildUploadLink`. Clients must ignore parameters they don't recognize.",
4
+ "title": "Pairing link parameters",
5
+ "description": "The parameters of a pairing link (PROTOCOL.md §3), as built by `buildUploadLink`: the query string of `pulsecam://?…`, or the fragment of `https://mieweb.github.io/pulse/open#…`. Clients must ignore parameters they don't recognize.",
6
6
  "type": "object",
7
7
  "required": ["v", "artifactId", "server"],
8
8
  "properties": {
@@ -1,17 +1,20 @@
1
1
  #!/usr/bin/env node
2
- // Make every already-uploaded video artifact in a local-storage workspace
3
- // web-playable, in place. New uploads are handled by the `onUploadComplete`
4
- // web-ready hook (see examples/fastify-demo/server.mjs); this script is the
5
- // one-time backfill for artifacts that landed before the hook existed.
2
+ // Conform every already-uploaded video artifact in a local-storage workspace
3
+ // to the format PulseVault serves (`CONFORM_TARGET`). New uploads are handled
4
+ // by the `webReady` option; this script is the one-time backfill for
5
+ // artifacts that landed before it was on. Stop the server first: it caches
6
+ // each artifact's extension, which a container change rewrites.
6
7
  //
7
8
  // For each ready `kind: "video"` sidecar in `<workspaceDir>/.pulsevault/`:
8
- // - moov atom at the end of the file → lossless faststart remux (sub-second)
9
- // - non-H.264 video codec (e.g. HEVC) → one-time libx264 transcode
10
- // - already web-ready / not MP4 bytes → untouched
9
+ // - already in the target format → untouched
10
+ // - moov atom at the end of an MP4 → lossless faststart remux
11
+ // - off-target video or audio → one ffmpeg run (H.264/AAC, scaled, tone-mapped)
12
+ // - another container (WebM, MKV, MOV, …) → a new `<id>.mp4`; the sidecar
13
+ // switches to it and the original is deleted
11
14
  //
12
15
  // Rewrites are atomic (tmp file + rename), so interrupting the script never
13
16
  // corrupts an artifact — rerun it and it picks up where it left off (already
14
- // fixed files report `none` and are skipped for free).
17
+ // fixed files report `none`).
15
18
  //
16
19
  // Usage:
17
20
  // node scripts/web-ready-migrate.mjs <workspaceDir> [--dry-run] [--no-transcode]
@@ -46,7 +49,7 @@ try {
46
49
  process.exit(1);
47
50
  }
48
51
 
49
- const counts = { none: 0, remuxed: 0, transcoded: 0, skipped: 0, missing: 0 };
52
+ const counts = { none: 0, remuxed: 0, transcoded: 0, conformed: 0, skipped: 0, missing: 0 };
50
53
  let processed = 0;
51
54
 
52
55
  for (const sidecarFile of sidecarFiles) {
@@ -84,8 +87,29 @@ for (const sidecarFile of sidecarFiles) {
84
87
  continue;
85
88
  }
86
89
 
87
- const result = await ensureWebReady(filePath, { transcode, logger: console });
90
+ const { outputPath, ...result } = await ensureWebReady(filePath, { transcode, logger: console });
88
91
  counts[result.action] += 1;
92
+ // Record what was done on the sidecar, as the server does; a new container also switches the
93
+ // artifact to its `.mp4` (keeping the uploaded extension as `sourceExt`), then the original
94
+ // goes. Atomic tmp + rename, like the adapter's own sidecar writes.
95
+ // A rerun finds a converted file already in the format: keep what the conversion recorded,
96
+ // and finish what an interrupted run left: the original, once the sidecar names the `.mp4`.
97
+ if (result.action === "none" && sidecar.webReady) {
98
+ if (sidecar.sourceExt && sidecar.sourceExt !== sidecar.ext) {
99
+ await fs.rm(path.join(root, kind, `${artifactId}${sidecar.sourceExt}`), { force: true });
100
+ }
101
+ continue;
102
+ }
103
+ const next = { ...sidecar, converted: true, webReady: result };
104
+ if (outputPath) {
105
+ next.sourceExt = sidecar.sourceExt ?? sidecar.ext;
106
+ next.ext = path.extname(outputPath);
107
+ }
108
+ const sidecarPath = path.join(sidecarDir, sidecarFile);
109
+ const tmpPath = `${sidecarPath}.migrate.tmp`;
110
+ await fs.writeFile(tmpPath, JSON.stringify(next), "utf8");
111
+ await fs.rename(tmpPath, sidecarPath);
112
+ if (outputPath) await fs.rm(filePath, { force: true });
89
113
  if (result.action !== "none") {
90
114
  console.log(`${result.action.padEnd(11)} ${artifactId} (${result.reason})`);
91
115
  }
@@ -96,7 +120,7 @@ if (dryRun) {
96
120
  } else {
97
121
  console.log(
98
122
  `\nDone: ${processed} ready video artifact(s) — ` +
99
- `${counts.remuxed} remuxed, ${counts.transcoded} transcoded, ` +
123
+ `${counts.remuxed} remuxed, ${counts.transcoded} transcoded, ${counts.conformed} conformed, ` +
100
124
  `${counts.none} already web-ready, ${counts.skipped} skipped, ${counts.missing} missing.`,
101
125
  );
102
126
  }