@mieweb/pulsevault 0.0.2 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +184 -0
- package/LICENSE +15 -39
- package/OPERATIONS.md +223 -0
- package/PROTOCOL.md +312 -0
- package/README.md +532 -122
- package/augment.d.ts +1 -0
- package/augment.js +3 -0
- package/core.d.ts +1 -0
- package/core.js +7 -0
- package/dist/app.d.ts +90 -34
- package/dist/app.d.ts.map +1 -1
- package/dist/app.js +23 -33
- package/dist/app.js.map +1 -1
- package/dist/augment.d.ts +14 -7
- package/dist/augment.d.ts.map +1 -1
- package/dist/augment.js.map +1 -1
- package/dist/core.d.ts +108 -0
- package/dist/core.d.ts.map +1 -0
- package/dist/core.js +431 -0
- package/dist/core.js.map +1 -0
- package/dist/lib/authorize.d.ts +19 -0
- package/dist/lib/authorize.d.ts.map +1 -0
- package/dist/lib/authorize.js +2 -0
- package/dist/lib/authorize.js.map +1 -0
- package/dist/lib/capability-token.d.ts +80 -0
- package/dist/lib/capability-token.d.ts.map +1 -0
- package/dist/lib/capability-token.js +126 -0
- package/dist/lib/capability-token.js.map +1 -0
- package/dist/lib/checksum.d.ts +45 -0
- package/dist/lib/checksum.d.ts.map +1 -0
- package/dist/lib/checksum.js +102 -0
- package/dist/lib/checksum.js.map +1 -0
- package/dist/lib/deeplinks.d.ts +37 -21
- package/dist/lib/deeplinks.d.ts.map +1 -1
- package/dist/lib/deeplinks.js +54 -13
- package/dist/lib/deeplinks.js.map +1 -1
- package/dist/lib/magic.d.ts +30 -5
- package/dist/lib/magic.d.ts.map +1 -1
- package/dist/lib/magic.js +51 -14
- package/dist/lib/magic.js.map +1 -1
- package/dist/lib/options.d.ts +39 -0
- package/dist/lib/options.d.ts.map +1 -0
- package/dist/lib/options.js +103 -0
- package/dist/lib/options.js.map +1 -0
- package/dist/lib/pulsevaultTus.d.ts +52 -26
- package/dist/lib/pulsevaultTus.d.ts.map +1 -1
- package/dist/lib/pulsevaultTus.js +111 -43
- package/dist/lib/pulsevaultTus.js.map +1 -1
- package/dist/lib/request.d.ts +24 -0
- package/dist/lib/request.d.ts.map +1 -0
- package/dist/lib/request.js +16 -0
- package/dist/lib/request.js.map +1 -0
- package/dist/lib/uuid.d.ts.map +1 -1
- package/dist/lib/uuid.js +1 -1
- package/dist/lib/uuid.js.map +1 -1
- package/dist/routes/pulsevault.d.ts +18 -16
- package/dist/routes/pulsevault.d.ts.map +1 -1
- package/dist/routes/pulsevault.js +120 -251
- package/dist/routes/pulsevault.js.map +1 -1
- package/dist/storage/local.d.ts +31 -10
- package/dist/storage/local.d.ts.map +1 -1
- package/dist/storage/local.js +166 -82
- package/dist/storage/local.js.map +1 -1
- package/dist/storage/s3.d.ts +109 -0
- package/dist/storage/s3.d.ts.map +1 -0
- package/dist/storage/s3.js +432 -0
- package/dist/storage/s3.js.map +1 -0
- package/dist/storage/types.d.ts +69 -17
- package/dist/storage/types.d.ts.map +1 -1
- package/package.json +35 -7
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project are documented here. The format follows
|
|
4
|
+
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/); this project follows
|
|
5
|
+
semantic versioning once it reaches `1.0.0` — pre-1.0, minor bumps may include
|
|
6
|
+
breaking changes, called out explicitly below.
|
|
7
|
+
|
|
8
|
+
## [Unreleased]
|
|
9
|
+
|
|
10
|
+
This release reworks the upload contract for genuine multi-tenant use — any
|
|
11
|
+
third party can implement a compatible server from `PROTOCOL.md` alone, not
|
|
12
|
+
just by reading this package's source. It bundles several breaking changes
|
|
13
|
+
into one release rather than spreading them across several; see "Upgrading"
|
|
14
|
+
in `OPERATIONS.md` for the migration path.
|
|
15
|
+
|
|
16
|
+
### Security
|
|
17
|
+
|
|
18
|
+
The `videoid` → `artifactId` / generic-route rework above introduced two
|
|
19
|
+
related authorization bugs in `core.ts`'s `PATCH`/`HEAD` handling. Both are
|
|
20
|
+
fixed in this same release; upgrade before this reaches a published version
|
|
21
|
+
if you've evaluated against an intermediate build.
|
|
22
|
+
|
|
23
|
+
- **`authorize` was never actually invoked for `PATCH`/`HEAD`/in-flight-`DELETE`
|
|
24
|
+
under `{prefix}/upload/<id>`**, regardless of configuration — including the
|
|
25
|
+
built-in `createCapabilityAuthorize`. The helper that recovered an
|
|
26
|
+
artifactId from the tus resource URL for the `authorize` context assumed
|
|
27
|
+
the wrong shape for the current `<kind>/<artifactId><ext>` tus id and never
|
|
28
|
+
successfully extracted a UUID, so the "no artifactId, so allow" fallback
|
|
29
|
+
silently took over on every such request. Since an artifactId is not a
|
|
30
|
+
secret (it's carried in the pairing deep link/QR code by design, per
|
|
31
|
+
`PROTOCOL.md` §3), this let anyone who had seen a pairing link write bytes
|
|
32
|
+
into that upload with no token at all. Fixed by resolving the artifactId
|
|
33
|
+
through the same parser the actual upload-completion path uses, and by
|
|
34
|
+
making authorization-context resolution failure a hard reject rather than
|
|
35
|
+
a silent allow (`PROTOCOL.md` §5.2).
|
|
36
|
+
- **A crafted multi-segment `PATCH` URL could write bytes to a different
|
|
37
|
+
artifact than the one `authorize()` checked.** The `authorize`-context URL
|
|
38
|
+
parser took the first path segment after `/upload/`, while `@tus/server`'s
|
|
39
|
+
own request routing (which decides what's actually read/written) takes the
|
|
40
|
+
URL's last segment — a divergence exploitable via extra path segments
|
|
41
|
+
(accepted by the `/upload/*` wildcard route). A party holding a valid
|
|
42
|
+
token for their own artifact could append a second, victim artifactId as a
|
|
43
|
+
trailing segment: `authorize()` saw and approved their own id, while the
|
|
44
|
+
request body landed on the victim's file. Fixed by resolving the
|
|
45
|
+
authorization-context artifactId via the exact same last-segment
|
|
46
|
+
extraction `@tus/server` itself uses (`PROTOCOL.md` §4.4, a new normative
|
|
47
|
+
requirement on server implementations generally, not just this package).
|
|
48
|
+
- Both are covered by new regression tests in `test/plugin.test.mjs` that
|
|
49
|
+
fail against the pre-fix code and pass against the fix.
|
|
50
|
+
|
|
51
|
+
### Breaking
|
|
52
|
+
|
|
53
|
+
- **Renamed `videoid` → `artifactId`** across the storage interface
|
|
54
|
+
(`ReserveUploadParams`, `PulseVaultStorage` methods), the `authorize`
|
|
55
|
+
context (`PulseVaultAuthorizeContext`), the `validatePayload`/
|
|
56
|
+
`onUploadComplete` hook contexts, the `request.pulseVault` TypeScript
|
|
57
|
+
augmentation (`./augment.ts` — a separate breaking change from the wire
|
|
58
|
+
rename, since it's the published type surface, not the HTTP contract), and
|
|
59
|
+
the deep-link query parameter built by `buildUploadLink`. `videoid`/
|
|
60
|
+
`projectid` remain accepted as legacy aliases in `Upload-Metadata` — only
|
|
61
|
+
the *primary* name changed, not backward compatibility for existing
|
|
62
|
+
clients' requests.
|
|
63
|
+
- **Collapsed the per-kind routes into one generic route.** `GET/DELETE
|
|
64
|
+
/:videoid` and `GET/DELETE /project/:projectid` are gone; replaced by
|
|
65
|
+
`GET/DELETE /artifacts/:artifactId`, which resolves the kind from storage
|
|
66
|
+
rather than the URL. This also means `kind=captions` artifacts (new, see
|
|
67
|
+
below) don't need a third route pair.
|
|
68
|
+
- **`validateProjectPayload`/`onProjectUploadComplete` are deprecated** (not
|
|
69
|
+
yet removed) in favor of the now-generic `validatePayload`/
|
|
70
|
+
`onUploadComplete`, which receive `ctx.kind` and run for every artifact
|
|
71
|
+
kind. Passing either deprecated option now emits a one-time
|
|
72
|
+
`DeprecationWarning` at plugin registration.
|
|
73
|
+
- **`allowedExtensions` requires a `captions` default to be considered** if
|
|
74
|
+
you were relying on the exact shape of the normalized object internally
|
|
75
|
+
(public consumers passing `allowedExtensions` as documented are unaffected
|
|
76
|
+
— the new key just has a default like `video`/`project` already did).
|
|
77
|
+
|
|
78
|
+
### Added
|
|
79
|
+
|
|
80
|
+
- **`kind: "captions"`** artifact type (default extension `.srt`), running
|
|
81
|
+
through the same generic `validatePayload`/`onUploadComplete` hooks as
|
|
82
|
+
every other kind.
|
|
83
|
+
- **`relatedTo`** — an optional `Upload-Metadata` key (and matching
|
|
84
|
+
`ReserveUploadParams`/storage field) linking one artifact to another (e.g.
|
|
85
|
+
a video's captions, or a beat belonging to a pulse manifest's session).
|
|
86
|
+
Storage adapters expose it via the new optional `getRelatedTo` method.
|
|
87
|
+
- **`checksum`** — an optional `Upload-Metadata` key (`<algorithm>:<hex
|
|
88
|
+
digest>`) verified post-upload via the new `createChecksumValidator`
|
|
89
|
+
(local storage) / `createS3ChecksumValidator` (S3/R2) helpers, chainable
|
|
90
|
+
with `createMp4Sniffer`. This is at-rest integrity verification on the
|
|
91
|
+
finished file, not a per-chunk check — `@tus/server`'s installed version
|
|
92
|
+
doesn't implement the TUS Checksum extension, which would have covered
|
|
93
|
+
chunks in flight.
|
|
94
|
+
- **`src/lib/capability-token.ts`**: `issueCapabilityToken`,
|
|
95
|
+
`verifyCapabilityToken`, `createCapabilityAuthorize` — a stateless,
|
|
96
|
+
HMAC-signed capability-token scheme with `kid` (key rotation with an
|
|
97
|
+
overlap window), `iat` (clock-skew tolerance), and `issuer` (cross-deployment
|
|
98
|
+
replay protection) claims. Fully optional — bring your own `authorize` if
|
|
99
|
+
you have existing auth.
|
|
100
|
+
- **`GET {prefix}/capabilities`** — unauthenticated discovery route reporting
|
|
101
|
+
`protocolVersion`, `minSupportedVersion`/`maxSupportedVersion`,
|
|
102
|
+
`uploadUnit`, `kinds`, `allowedExtensions`, `maxUploadSize`, and supported
|
|
103
|
+
checksum algorithms. Lets independently-versioned deployments and clients
|
|
104
|
+
detect compatibility before pairing.
|
|
105
|
+
- **`uploadUnit` plugin option** (`"beat" | "merged"`, default `"beat"`) —
|
|
106
|
+
purely advertised via `/capabilities`; the operator declares which upload
|
|
107
|
+
strategy this deployment expects, the client branches on it. Not enforced
|
|
108
|
+
by the plugin.
|
|
109
|
+
- **`buildUploadLink({ uploadUnit })`** (PROTOCOL.md §3, §8) — optional
|
|
110
|
+
per-session override of the deployment-wide `uploadUnit`, carried on the
|
|
111
|
+
pairing link itself instead of only `/capabilities`. Lets an operator run
|
|
112
|
+
`"beat"` and `"merged"` sessions concurrently (staged rollout, A/B test,
|
|
113
|
+
per-tenant policy) without racing a client's separate `/capabilities` fetch
|
|
114
|
+
against a single, deployment-wide value. Fully backward compatible: omit it
|
|
115
|
+
and a client falls back to `/capabilities` exactly as before.
|
|
116
|
+
- **`onArtifactEvent` plugin option** — one low-frequency hook (authorize
|
|
117
|
+
rejection, completion, validation rejection — never per chunk) covering
|
|
118
|
+
both ops metrics and a compliance audit trail from a single integration
|
|
119
|
+
point.
|
|
120
|
+
- **`Protocol-Version` response header** on every route this plugin mounts.
|
|
121
|
+
- **`@mieweb/pulsevault/core`** — a framework-agnostic entry point with no
|
|
122
|
+
Fastify dependency. `createPulseVaultCore(...)` returns a connect-style
|
|
123
|
+
`handler(req, res, next?)` usable directly as Express middleware
|
|
124
|
+
(`app.use(prefix, handler)`), Meteor middleware
|
|
125
|
+
(`WebApp.connectHandlers.use(prefix, handler)`), or a bare
|
|
126
|
+
`http.createServer` callback — same options, hooks, and storage adapters as
|
|
127
|
+
the Fastify plugin (which is now itself a thin adapter over this core, so
|
|
128
|
+
behavior can't drift between the two). See "Non-Fastify hosts" in
|
|
129
|
+
`README.md`, and `examples/express-demo`/`examples/meteor-demo` for full
|
|
130
|
+
runnable servers verified against the real frameworks.
|
|
131
|
+
- As part of this, `PulseVaultAuthorize`/`PulseVaultValidatePayload`/
|
|
132
|
+
`PulseVaultOnUploadComplete`'s `request` parameter is now typed as the
|
|
133
|
+
generic `PulseVaultRequest` (just `{ headers }`) instead of
|
|
134
|
+
`FastifyRequest` — `FastifyRequest` still satisfies it structurally, so
|
|
135
|
+
untyped/inferred hook callbacks (the documented pattern) are unaffected;
|
|
136
|
+
only an explicit `(request: FastifyRequest, ctx) => ...` annotation on
|
|
137
|
+
one of these three hooks would need loosening.
|
|
138
|
+
- Bounded, insertion-order-evicting in-memory metadata cache in both storage
|
|
139
|
+
adapters (`metaCacheLimit` option, default 10,000 entries) — previously
|
|
140
|
+
unbounded for the life of the process.
|
|
141
|
+
- `S3Storage.readAll` — full-object read, used by `createS3ChecksumValidator`.
|
|
142
|
+
- `S3Storage.digestAll` — streams a finalized object through a hash digest
|
|
143
|
+
without buffering the whole thing into memory first; the streaming
|
|
144
|
+
counterpart to `readAll`, now used internally by
|
|
145
|
+
`createS3ChecksumValidator` (see "Fixed" below).
|
|
146
|
+
|
|
147
|
+
### Fixed
|
|
148
|
+
|
|
149
|
+
- `maxUploadSize` enforcement and the in-progress-upload 404 behavior now
|
|
150
|
+
have explicit test coverage proving bytes are rejected/hidden at the right
|
|
151
|
+
point, not just implied by the implementation.
|
|
152
|
+
- **Meteor compatibility.** Meteor's bundler doesn't resolve `package.json`
|
|
153
|
+
`"exports"` subpath maps, which broke both this package's own `"./core"`
|
|
154
|
+
entry and, transitively, `@tus/server`'s dependency on `srvx` (which as of
|
|
155
|
+
`@tus/server@2.1.0` ships *only* subpath exports, no legacy `main`
|
|
156
|
+
fallback). Fixed two ways: `@tus/server`/`@tus/file-store` are pinned to
|
|
157
|
+
`2.0.0` (the last release before the `srvx` migration — everything added
|
|
158
|
+
in `2.1.0`–`2.4.1` was either the `srvx` migration itself, follow-up
|
|
159
|
+
fixes for regressions it introduced, or one unused `exposedHeaders`
|
|
160
|
+
option, so nothing this package relies on is lost); and plain root-level
|
|
161
|
+
`core.js`/`core.d.ts`/`augment.js`/`augment.d.ts` files now ship alongside
|
|
162
|
+
the `"exports"` map, so resolvers that ignore `"exports"` (Meteor's
|
|
163
|
+
included) still find these entry points via ordinary relative-path
|
|
164
|
+
resolution. Verified against a real Meteor 3.4 app: `WebApp.connectHandlers`
|
|
165
|
+
resolves `@mieweb/pulsevault/core` cleanly and a full TUS
|
|
166
|
+
create → PATCH → GET round-trip works.
|
|
167
|
+
- **Unbounded memory use during checksum validation.** `createChecksumValidator`
|
|
168
|
+
read the whole finalized file into a single `Buffer` via `fs.readFile`
|
|
169
|
+
before hashing; combined with the documented-supported `maxUploadSize:
|
|
170
|
+
Infinity`, a large upload could exhaust process memory just to verify its
|
|
171
|
+
checksum. Now streams the file through the hash via `createReadStream` +
|
|
172
|
+
a stream pipeline. `createS3ChecksumValidator` had the same issue via
|
|
173
|
+
`S3Storage.readAll`'s full-object buffer — now uses the new streaming
|
|
174
|
+
`S3Storage.digestAll` instead.
|
|
175
|
+
- **Silent TOCTOU on S3-compatible backends without `IfNoneMatch` support.**
|
|
176
|
+
`reserveUpload`'s conditional-write collision guard falls back to a
|
|
177
|
+
weaker check-then-write on backends that reject `IfNoneMatch` (some
|
|
178
|
+
S3-compatible stores, and the `s3rver` mock this repo's own test suite
|
|
179
|
+
runs against) — this reopens the race between concurrent/retried creates
|
|
180
|
+
for the same artifactId. There's no way to close it without an external
|
|
181
|
+
lock, so this is now at least surfaced: a one-time `console.warn` fires
|
|
182
|
+
per process the first time a deployment falls into this degraded mode.
|
|
183
|
+
|
|
184
|
+
[Unreleased]: https://github.com/mieweb/pulsevault/compare/v0.0.1...HEAD
|
package/LICENSE
CHANGED
|
@@ -1,45 +1,21 @@
|
|
|
1
|
-
|
|
1
|
+
MIT License
|
|
2
2
|
|
|
3
|
-
Copyright (c) 2026 Medical Informatics Engineering, LLC
|
|
3
|
+
Copyright (c) 2026 Medical Informatics Engineering, LLC
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
7
11
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
Permission is hereby granted, free of charge, to any person or organization
|
|
11
|
-
obtaining a copy of this Software, to use, copy, modify, merge, and distribute
|
|
12
|
-
the Software for non-commercial purposes, subject to the following conditions:
|
|
13
|
-
|
|
14
|
-
1. Attribution Required: The above copyright notice, this permission notice,
|
|
15
|
-
and clear attribution to Medical Informatics Engineering, LLC. shall be
|
|
16
|
-
included in all copies or substantial portions of the Software.
|
|
17
|
-
|
|
18
|
-
2. Open Source Requirement: If you distribute the Software or any derivative
|
|
19
|
-
works, you must do so under an open source license approved by the Open
|
|
20
|
-
Source Initiative (OSI) and make the source code publicly available.
|
|
21
|
-
|
|
22
|
-
3. Non-Commercial Use Only: This license does not grant permission to use the
|
|
23
|
-
Software for commercial purposes. "Commercial purposes" means any use
|
|
24
|
-
intended for or directed toward commercial advantage or monetary
|
|
25
|
-
compensation.
|
|
26
|
-
|
|
27
|
-
COMMERCIAL USE
|
|
28
|
-
|
|
29
|
-
For commercial use, including but not limited to:
|
|
30
|
-
- Use in proprietary/closed-source products
|
|
31
|
-
- Use in products or services sold for profit
|
|
32
|
-
- Use by for-profit organizations for internal business operations
|
|
33
|
-
|
|
34
|
-
You must obtain a separate commercial license from Medical Informatics
|
|
35
|
-
Engineering, LLC. Contact: https://www.mieweb.com or helpdesk@mieweb.com
|
|
36
|
-
|
|
37
|
-
NO WARRANTY
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
38
14
|
|
|
39
15
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
40
16
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
41
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/OPERATIONS.md
ADDED
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
# Operations
|
|
2
|
+
|
|
3
|
+
Deployment, scaling, and maintenance guidance for running `@mieweb/pulsevault`
|
|
4
|
+
in production. See `README.md` for the quick-start and plugin options, and
|
|
5
|
+
`PROTOCOL.md` for the wire contract.
|
|
6
|
+
|
|
7
|
+
## Upgrading to this release
|
|
8
|
+
|
|
9
|
+
This release renames `videoid` → `artifactId` and collapses the per-kind
|
|
10
|
+
routes into one generic route (see `CHANGELOG.md`). **No on-disk data
|
|
11
|
+
migration is required**: the local and S3 storage layouts never stored the
|
|
12
|
+
id as a field inside the sidecar JSON — the id is the filename — so existing
|
|
13
|
+
uploaded artifacts are immediately readable through the new
|
|
14
|
+
`GET /artifacts/:artifactId` route with zero changes to the files on disk.
|
|
15
|
+
|
|
16
|
+
What *does* need updating:
|
|
17
|
+
|
|
18
|
+
- Any of your own code calling the old routes directly (`GET/DELETE
|
|
19
|
+
/:videoid`, `GET/DELETE /project/:projectid`) must move to `GET/DELETE
|
|
20
|
+
/artifacts/:artifactId` — the old routes return `404`, not a redirect.
|
|
21
|
+
- Any code reading `request.pulseVault.videoid` (if you imported
|
|
22
|
+
`@mieweb/pulsevault/augment`) must read `.artifactId` instead.
|
|
23
|
+
- If you use `validateProjectPayload`/`onProjectUploadComplete`, you'll see a
|
|
24
|
+
one-time `DeprecationWarning` at startup. They still work this release —
|
|
25
|
+
migrate to `validatePayload`/`onUploadComplete` with a `ctx.kind ===
|
|
26
|
+
"project"` branch when convenient, before a future major version removes
|
|
27
|
+
the deprecated options.
|
|
28
|
+
- Clients still sending `Upload-Metadata: videoid ...` (instead of
|
|
29
|
+
`artifactId`) keep working — the alias is accepted indefinitely for
|
|
30
|
+
protocol version 1 (see `PROTOCOL.md` §4.1) — but new client code should
|
|
31
|
+
send `artifactId`.
|
|
32
|
+
|
|
33
|
+
There is no automated migration script because there is nothing on disk to
|
|
34
|
+
migrate. If a future release ever does change the storage layout, that
|
|
35
|
+
release's `OPERATIONS.md` entry will include one.
|
|
36
|
+
|
|
37
|
+
## Horizontal scaling
|
|
38
|
+
|
|
39
|
+
**The local storage adapter (`createLocalStorage`) requires either
|
|
40
|
+
sticky-session load-balancer routing or a shared filesystem (NFS/EFS/etc.)
|
|
41
|
+
across instances.** TUS resumable uploads need subsequent `PATCH`/`HEAD`
|
|
42
|
+
requests to reach an instance that can see the partial upload's bytes and
|
|
43
|
+
`@tus/file-store` offset metadata. If you run multiple instances behind a
|
|
44
|
+
load balancer without either of these, a retried `PATCH` that lands on a
|
|
45
|
+
different instance will silently fail to resume — the client's `HEAD` will
|
|
46
|
+
see no offset and effectively restart from byte zero, with no error
|
|
47
|
+
surfaced anywhere.
|
|
48
|
+
|
|
49
|
+
Two ways to actually support multiple instances:
|
|
50
|
+
|
|
51
|
+
1. **Sticky sessions**: configure your load balancer to route by client IP
|
|
52
|
+
or a session cookie so all requests for one upload land on the same
|
|
53
|
+
instance.
|
|
54
|
+
2. **Shared filesystem**: point every instance's `workspaceDir` at the same
|
|
55
|
+
NFS/EFS mount. Verify your mount's durability/consistency guarantees
|
|
56
|
+
under concurrent writes from multiple instances before relying on this in
|
|
57
|
+
production.
|
|
58
|
+
|
|
59
|
+
**The S3/R2 adapter (`createS3Storage`) has no such requirement** — every
|
|
60
|
+
instance talks to the same bucket, so it scales horizontally with zero
|
|
61
|
+
additional configuration. Prefer it for any multi-instance deployment unless
|
|
62
|
+
you have a specific reason to use local disk.
|
|
63
|
+
|
|
64
|
+
### S3-compatible backend collision-guard fallback
|
|
65
|
+
|
|
66
|
+
`reserveUpload`'s collision guard normally uses `PutObjectCommand`'s
|
|
67
|
+
`IfNoneMatch: "*"` to atomically reject a second create for an artifactId
|
|
68
|
+
that already has an upload. Some S3-compatible backends (older/less-complete
|
|
69
|
+
implementations; the `s3rver` mock this package's own test suite runs
|
|
70
|
+
against) don't support conditional writes and reject that header outright.
|
|
71
|
+
On those backends, `reserveUpload` falls back to a weaker check-then-write —
|
|
72
|
+
functionally the same guard, but with the original race reopened: two
|
|
73
|
+
truly concurrent (or retried) creates for the same artifactId can both pass
|
|
74
|
+
the check before either writes, and the second silently clobbers the
|
|
75
|
+
first's metadata. There's no way to close this without an external lock,
|
|
76
|
+
since it's a limitation of the backend, not this package. If your bucket
|
|
77
|
+
provider doesn't support `IfNoneMatch`, this fallback logs a one-time
|
|
78
|
+
`console.warn` per process the first time it's used — treat that warning as
|
|
79
|
+
a signal to either move to a backend that supports conditional writes, or
|
|
80
|
+
serialize artifactId creation yourself (e.g. in your own `/reserve`
|
|
81
|
+
endpoint) if concurrent creates for the same id are a real possibility in
|
|
82
|
+
your deployment.
|
|
83
|
+
|
|
84
|
+
## Resource limits and abuse prevention
|
|
85
|
+
|
|
86
|
+
`pulsevault` does not implement rate limiting or a concurrent-upload cap
|
|
87
|
+
itself — that's left to the operator, consistent with this project's general
|
|
88
|
+
principle that policy decisions belong to the deployment, not the library.
|
|
89
|
+
A minimal example using `@fastify/rate-limit`, scoped to just the upload
|
|
90
|
+
routes:
|
|
91
|
+
|
|
92
|
+
```ts
|
|
93
|
+
import rateLimit from "@fastify/rate-limit";
|
|
94
|
+
|
|
95
|
+
await app.register(rateLimit, {
|
|
96
|
+
max: 20,
|
|
97
|
+
timeWindow: "1 minute",
|
|
98
|
+
// Scope to the pulsevault prefix only — don't rate-limit your whole app
|
|
99
|
+
// with upload-sized limits.
|
|
100
|
+
allowList: (req) => !req.url.startsWith("/pulsevault/upload"),
|
|
101
|
+
});
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Under `@mieweb/pulsevault/core` (Express, Meteor, plain `http`), use the equivalent middleware for your host — e.g. `express-rate-limit` mounted ahead of `pulseVault.handler`, scoped the same way.
|
|
105
|
+
|
|
106
|
+
## Monitoring and audit logging
|
|
107
|
+
|
|
108
|
+
Wire `onArtifactEvent` once to get both ops metrics and a compliance audit
|
|
109
|
+
trail from the same hook — it fires at low-frequency, audit-worthy moments
|
|
110
|
+
only (never per chunk): authorize rejection (on `create`/`delete`/`resolve`,
|
|
111
|
+
not `patch`), successful completion, and validation rejection.
|
|
112
|
+
|
|
113
|
+
**Plain structured logging** (zero new dependencies):
|
|
114
|
+
|
|
115
|
+
```ts
|
|
116
|
+
onArtifactEvent: (event) => {
|
|
117
|
+
app.log.info(event, "pulsevault artifact event");
|
|
118
|
+
},
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
**Prometheus counters**:
|
|
122
|
+
|
|
123
|
+
```ts
|
|
124
|
+
import { Counter } from "prom-client";
|
|
125
|
+
const artifactEvents = new Counter({
|
|
126
|
+
name: "pulsevault_artifact_events_total",
|
|
127
|
+
help: "pulsevault artifact lifecycle events",
|
|
128
|
+
labelNames: ["phase", "kind"],
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
onArtifactEvent: (event) => {
|
|
132
|
+
artifactEvents.inc({ phase: event.phase, kind: event.kind });
|
|
133
|
+
if (event.phase === "reject" || event.phase === "authorize") {
|
|
134
|
+
app.log.warn(event, "pulsevault artifact event");
|
|
135
|
+
}
|
|
136
|
+
},
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
`onArtifactEvent` isn't request-scoped (no Fastify instance to hang a logger
|
|
140
|
+
off of either way), so the examples above read the same under
|
|
141
|
+
`@mieweb/pulsevault/core` — just close over whatever logger (or `console`)
|
|
142
|
+
your app already uses instead of `app.log`. The core's own internal
|
|
143
|
+
diagnostics (authorize-rejection/error logging separate from
|
|
144
|
+
`onArtifactEvent`) accept an explicit `logger` option on
|
|
145
|
+
`createPulseVaultCore(...)` for the same reason — see "Non-Fastify hosts" in
|
|
146
|
+
`README.md`.
|
|
147
|
+
|
|
148
|
+
What to alert on: a sustained rise in `reject`/`authorize`-rejection events
|
|
149
|
+
(misconfigured auth, or an attacker probing), disk usage on the local
|
|
150
|
+
adapter's `workspaceDir` approaching capacity, and any `5xx` rate on the
|
|
151
|
+
upload routes.
|
|
152
|
+
|
|
153
|
+
## Backup and restore (local storage)
|
|
154
|
+
|
|
155
|
+
The entire state of the local adapter lives under `workspaceDir`:
|
|
156
|
+
`.pulsevault/` (sidecars), `video/`, `project/`, `captions/` (bytes). Back it
|
|
157
|
+
up as a normal filesystem tree — there's no separate database to keep in
|
|
158
|
+
sync. To restore, copy the tree back and restart; in-progress uploads at
|
|
159
|
+
backup time will resume correctly via the normal TUS `HEAD`-then-resume path
|
|
160
|
+
once the client retries, or will simply sit as abandoned partial uploads
|
|
161
|
+
(see "Retention" below) if the client never retries.
|
|
162
|
+
|
|
163
|
+
## Retention
|
|
164
|
+
|
|
165
|
+
`pulsevault` has no built-in retention/expiry feature — this is intentionally
|
|
166
|
+
left to the operator (see `PROTOCOL.md` §1 on lifecycle ownership). A sample
|
|
167
|
+
cron script for the local adapter, deleting artifacts whose sidecar has been
|
|
168
|
+
`"uploading"` for longer than a cutoff (abandoned uploads) or that are older
|
|
169
|
+
than a retention window (compliance-driven deletion):
|
|
170
|
+
|
|
171
|
+
```ts
|
|
172
|
+
import { readdir, readFile, stat } from "node:fs/promises";
|
|
173
|
+
import path from "node:path";
|
|
174
|
+
import { createLocalStorage } from "@mieweb/pulsevault";
|
|
175
|
+
|
|
176
|
+
const storage = createLocalStorage({ workspaceDir: "./data" });
|
|
177
|
+
const sidecarDir = path.join(storage.workspaceRoot, ".pulsevault");
|
|
178
|
+
const ABANDONED_AFTER_MS = 24 * 60 * 60 * 1000; // 24h stuck "uploading"
|
|
179
|
+
const RETAIN_FOR_MS = 90 * 24 * 60 * 60 * 1000; // 90 days
|
|
180
|
+
|
|
181
|
+
for (const file of await readdir(sidecarDir)) {
|
|
182
|
+
if (!file.endsWith(".json")) continue;
|
|
183
|
+
const artifactId = file.slice(0, -".json".length);
|
|
184
|
+
const sidecarPath = path.join(sidecarDir, file);
|
|
185
|
+
const [sidecar, stats] = await Promise.all([
|
|
186
|
+
readFile(sidecarPath, "utf8").then(JSON.parse),
|
|
187
|
+
stat(sidecarPath),
|
|
188
|
+
]);
|
|
189
|
+
const ageMs = Date.now() - stats.mtimeMs;
|
|
190
|
+
const stuckUploading = sidecar.status === "uploading" && ageMs > ABANDONED_AFTER_MS;
|
|
191
|
+
const pastRetention = sidecar.status === "ready" && ageMs > RETAIN_FOR_MS;
|
|
192
|
+
if (stuckUploading || pastRetention) {
|
|
193
|
+
await storage.remove(artifactId);
|
|
194
|
+
console.log(`removed ${artifactId} (${stuckUploading ? "abandoned" : "retention"})`);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
Run this on whatever schedule your retention policy requires. For S3/R2
|
|
200
|
+
storage, prefer your bucket provider's native lifecycle-policy feature (S3
|
|
201
|
+
Lifecycle Rules, R2 Object Lifecycle) over a custom script where available.
|
|
202
|
+
|
|
203
|
+
## Secrets management
|
|
204
|
+
|
|
205
|
+
If you use `createCapabilityAuthorize`, you supply the HMAC secret(s)
|
|
206
|
+
yourself via `lookupSecret`. For local development, reading from an
|
|
207
|
+
environment variable is fine. For production, read from your organization's
|
|
208
|
+
actual secrets manager instead of a raw env var — e.g.:
|
|
209
|
+
|
|
210
|
+
```ts
|
|
211
|
+
import { SecretsManagerClient, GetSecretValueCommand } from "@aws-sdk/client-secrets-manager";
|
|
212
|
+
|
|
213
|
+
const client = new SecretsManagerClient({});
|
|
214
|
+
const keys: Record<string, string> = {};
|
|
215
|
+
async function loadKey(kid: string) {
|
|
216
|
+
const res = await client.send(new GetSecretValueCommand({ SecretId: `pulsevault/${kid}` }));
|
|
217
|
+
keys[kid] = res.SecretString!;
|
|
218
|
+
}
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
Rotate by adding the new `kid` to your lookup table alongside the old one,
|
|
222
|
+
switching issuance to the new `kid`, and removing the old entry only after
|
|
223
|
+
its longest-lived outstanding token has expired.
|