@molecule/api-media-streaming-hls 1.0.0 → 1.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/README.md +278 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +10 -0
- package/dist/provider.d.ts.map +1 -1
- package/dist/provider.js +52 -7
- package/package.json +6 -5
package/README.md
ADDED
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
AUTO-GENERATED — DO NOT EDIT THIS FILE.
|
|
3
|
+
Generated by `mlcl sync-docs` from the package's src/index.ts JSDoc + mlcl/registry.json.
|
|
4
|
+
Edits here are overwritten on the next commit (molecule's pre-commit hook regenerates).
|
|
5
|
+
To change this document, edit the module-level JSDoc in src/index.ts.
|
|
6
|
+
Generated: 2026-09-18T06:08:25.367Z
|
|
7
|
+
-->
|
|
8
|
+
|
|
9
|
+
# @molecule/api-media-streaming-hls
|
|
10
|
+
|
|
11
|
+
> **Auto-generated, AI-first package reference** for the [molecule.dev](https://molecule.dev) ecosystem.
|
|
12
|
+
> It is written to be read by coding agents as much as by people, and is generated from this
|
|
13
|
+
> package's source — edit `src/index.ts` JSDoc, not this file.
|
|
14
|
+
|
|
15
|
+
HLS media streaming provider for molecule.dev.
|
|
16
|
+
|
|
17
|
+
Provides HLS (HTTP Live Streaming) support via ffmpeg for media segmentation
|
|
18
|
+
and transcoding, with pure-TypeScript M3U8 playlist generation. Requires
|
|
19
|
+
ffmpeg to be installed on the host system.
|
|
20
|
+
|
|
21
|
+
## Quick Start
|
|
22
|
+
|
|
23
|
+
```typescript
|
|
24
|
+
import { setProvider, createStream } from '@molecule/api-media-streaming'
|
|
25
|
+
import { provider } from '@molecule/api-media-streaming-hls'
|
|
26
|
+
|
|
27
|
+
setProvider(provider)
|
|
28
|
+
|
|
29
|
+
const manifest = await createStream('/path/to/video.mp4', {
|
|
30
|
+
segmentDuration: 6,
|
|
31
|
+
protocol: 'hls',
|
|
32
|
+
})
|
|
33
|
+
console.log(manifest.manifestUri) // '/hls-…/index.m3u8'
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Type
|
|
37
|
+
|
|
38
|
+
`provider`
|
|
39
|
+
|
|
40
|
+
## Installation
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
npm install @molecule/api-media-streaming-hls @molecule/api-media-streaming
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## API
|
|
47
|
+
|
|
48
|
+
### Interfaces
|
|
49
|
+
|
|
50
|
+
#### `HlsConfig`
|
|
51
|
+
|
|
52
|
+
Configuration options for the HLS streaming provider.
|
|
53
|
+
|
|
54
|
+
```typescript
|
|
55
|
+
interface HlsConfig {
|
|
56
|
+
/** Path to the ffmpeg binary. Defaults to `'ffmpeg'` (resolved via PATH). */
|
|
57
|
+
ffmpegPath?: string
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Path to the ffprobe binary. RESERVED for future use — the current
|
|
61
|
+
* provider never invokes ffprobe (segment durations are taken from
|
|
62
|
+
* `segmentDuration`, not probed). Setting this has no effect today.
|
|
63
|
+
*/
|
|
64
|
+
ffprobePath?: string
|
|
65
|
+
|
|
66
|
+
/** Base directory where stream output files are written. Defaults to `os.tmpdir()`. */
|
|
67
|
+
outputBasePath?: string
|
|
68
|
+
|
|
69
|
+
/** Default segment duration in seconds. Defaults to `6`. */
|
|
70
|
+
segmentDuration?: number
|
|
71
|
+
|
|
72
|
+
/** HLS playlist version. Defaults to `3`. */
|
|
73
|
+
hlsVersion?: number
|
|
74
|
+
}
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
#### `M3u8PlaylistOptions`
|
|
78
|
+
|
|
79
|
+
Options for generating an M3U8 media playlist.
|
|
80
|
+
|
|
81
|
+
```typescript
|
|
82
|
+
interface M3u8PlaylistOptions {
|
|
83
|
+
/** HLS playlist version. Defaults to `3`. */
|
|
84
|
+
version?: number
|
|
85
|
+
|
|
86
|
+
/** Target segment duration in seconds. Defaults to the maximum segment duration. */
|
|
87
|
+
targetDuration?: number
|
|
88
|
+
|
|
89
|
+
/** Whether this is a VOD (complete) or live (in-progress) playlist. Defaults to `'vod'`. */
|
|
90
|
+
playlistType?: 'vod' | 'event'
|
|
91
|
+
|
|
92
|
+
/** Media sequence number for the first segment. Defaults to `0`. */
|
|
93
|
+
mediaSequence?: number
|
|
94
|
+
}
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### Functions
|
|
98
|
+
|
|
99
|
+
#### `assertSafePathComponent(value, label)`
|
|
100
|
+
|
|
101
|
+
Asserts that a caller-supplied value is a safe single path component.
|
|
102
|
+
|
|
103
|
+
Rejects empty strings, the relative segments `.` and `..`, anything
|
|
104
|
+
containing a path separator or NUL byte, and anything outside the
|
|
105
|
+
`[A-Za-z0-9._-]` allow-list (which also rejects shell metacharacters).
|
|
106
|
+
|
|
107
|
+
```typescript
|
|
108
|
+
function assertSafePathComponent(value: string, label: string): string
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
- `value` — The caller-supplied component (e.g. a stream id or profile name).
|
|
112
|
+
- `label` — Human-readable name of the field, used in the error message.
|
|
113
|
+
|
|
114
|
+
**Returns:** The validated component, unchanged.
|
|
115
|
+
|
|
116
|
+
#### `assertSegmentIndex(index)`
|
|
117
|
+
|
|
118
|
+
Asserts that a caller-supplied segment index is a non-negative integer.
|
|
119
|
+
|
|
120
|
+
```typescript
|
|
121
|
+
function assertSegmentIndex(index: number): number
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
- `index` — The caller-supplied segment index.
|
|
125
|
+
|
|
126
|
+
**Returns:** The validated index, unchanged.
|
|
127
|
+
|
|
128
|
+
#### `createProvider(config)`
|
|
129
|
+
|
|
130
|
+
Creates an HLS streaming provider.
|
|
131
|
+
|
|
132
|
+
```typescript
|
|
133
|
+
function createProvider(config?: HlsConfig): StreamingProvider
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
- `config` — Optional provider configuration.
|
|
137
|
+
|
|
138
|
+
**Returns:** A `StreamingProvider` backed by HLS / ffmpeg.
|
|
139
|
+
|
|
140
|
+
#### `generateMasterPlaylist(variants)`
|
|
141
|
+
|
|
142
|
+
Generates an M3U8 master playlist for adaptive bitrate streaming.
|
|
143
|
+
|
|
144
|
+
```typescript
|
|
145
|
+
function generateMasterPlaylist(variants: TranscodeVariant[]): string
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
- `variants` — The transcoded variant streams.
|
|
149
|
+
|
|
150
|
+
**Returns:** The master M3U8 playlist content as a string.
|
|
151
|
+
|
|
152
|
+
#### `generateMediaPlaylist(segments, options)`
|
|
153
|
+
|
|
154
|
+
Generates an M3U8 media playlist from a list of stream segments.
|
|
155
|
+
|
|
156
|
+
```typescript
|
|
157
|
+
function generateMediaPlaylist(segments: StreamSegment[], options?: M3u8PlaylistOptions): string
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
- `segments` — Ordered list of stream segments.
|
|
161
|
+
- `options` — Playlist generation options.
|
|
162
|
+
|
|
163
|
+
**Returns:** The M3U8 playlist content as a string.
|
|
164
|
+
|
|
165
|
+
#### `resolveWithinBase(base, parts)`
|
|
166
|
+
|
|
167
|
+
Resolves `parts` against `base` and asserts the result stays within `base`.
|
|
168
|
+
|
|
169
|
+
Defense-in-depth on top of {@link assertSafePathComponent}: even if a
|
|
170
|
+
component slipped through, the resolved absolute path is rejected unless it
|
|
171
|
+
is `base` itself or a descendant of it.
|
|
172
|
+
|
|
173
|
+
```typescript
|
|
174
|
+
function resolveWithinBase(base: string, parts?: string[]): string
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
- `base` — The intended base directory.
|
|
178
|
+
- `parts` — Path segments to append.
|
|
179
|
+
|
|
180
|
+
**Returns:** The resolved absolute path, guaranteed to be inside `base`.
|
|
181
|
+
|
|
182
|
+
### Constants
|
|
183
|
+
|
|
184
|
+
#### `provider`
|
|
185
|
+
|
|
186
|
+
The provider implementation with default configuration.
|
|
187
|
+
|
|
188
|
+
```typescript
|
|
189
|
+
const provider: StreamingProvider
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
## Core Interface
|
|
193
|
+
|
|
194
|
+
Implements `@molecule/api-media-streaming` interface.
|
|
195
|
+
|
|
196
|
+
## Bond Wiring
|
|
197
|
+
|
|
198
|
+
Setup function to register this provider with the core interface:
|
|
199
|
+
|
|
200
|
+
```typescript
|
|
201
|
+
import { setProvider } from '@molecule/api-media-streaming'
|
|
202
|
+
import { provider } from '@molecule/api-media-streaming-hls'
|
|
203
|
+
|
|
204
|
+
export function setupMediaStreamingHls(): void {
|
|
205
|
+
setProvider(provider)
|
|
206
|
+
}
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
## Injection Notes
|
|
210
|
+
|
|
211
|
+
### Requirements
|
|
212
|
+
|
|
213
|
+
Peer dependencies:
|
|
214
|
+
|
|
215
|
+
- `@molecule/api-media-streaming` ^1.0.1
|
|
216
|
+
|
|
217
|
+
### Runtime Dependencies
|
|
218
|
+
|
|
219
|
+
- `@molecule/api-media-streaming`
|
|
220
|
+
|
|
221
|
+
- **String inputs must be local absolute file paths.** `createStream()` and
|
|
222
|
+
`transcode()` accept `Buffer | string`; a string is passed straight to
|
|
223
|
+
ffmpeg's `-i`, and ffmpeg natively fetches `http(s)`, `tcp`, `tls`, and
|
|
224
|
+
more — so an unvalidated string would make the bond an SSRF / file-read
|
|
225
|
+
primitive acting with your server's network position. Strings that
|
|
226
|
+
contain `://`, carry a `scheme:` prefix, or are not absolute paths are
|
|
227
|
+
rejected with a thrown error. For remote media, fetch the bytes yourself
|
|
228
|
+
(with your own SSRF guard) and pass a `Buffer`. As defense-in-depth the
|
|
229
|
+
ffmpeg protocol whitelist is trimmed to local protocols (`file,crypto`) —
|
|
230
|
+
network protocols are never enabled.
|
|
231
|
+
- **Requires the `ffmpeg` binary on the host** (resolved via PATH, or set
|
|
232
|
+
`createProvider({ ffmpegPath })`). A missing binary fails at first
|
|
233
|
+
`createStream()`/`transcode()` call with `spawn ffmpeg ENOENT` — verify
|
|
234
|
+
with `ffmpeg -version` before shipping.
|
|
235
|
+
- **The default output directory is `os.tmpdir()`** — volatile and served by
|
|
236
|
+
nothing. Pass `createProvider({ outputBasePath })` pointing at a directory
|
|
237
|
+
your server exposes (see the core remarks), or serve bytes through
|
|
238
|
+
`getSegment()` / `generateManifest()` endpoints.
|
|
239
|
+
- `createStream()` also caches every segment `Buffer` in an in-process map
|
|
240
|
+
(never evicted) so `getSegment()` is fast; memory grows by the full video
|
|
241
|
+
size per stream. `getSegment()` disk fallback looks ONLY under
|
|
242
|
+
`outputBasePath/<streamId>/` — a per-call `createStream(..., { outputPath })`
|
|
243
|
+
override writes segments where the fallback cannot find them after a
|
|
244
|
+
restart, and `transcode()` ignores `outputPath` entirely (always writes
|
|
245
|
+
under `outputBasePath`).
|
|
246
|
+
- `ffprobePath` in `HlsConfig` is currently RESERVED — no ffprobe call exists
|
|
247
|
+
yet; segment durations come from the requested `segmentDuration`.
|
|
248
|
+
|
|
249
|
+
## E2E Tests
|
|
250
|
+
|
|
251
|
+
Integration checklist — drive the real UI (live preview, no mocks), adapt
|
|
252
|
+
each item to this app's actual upload/player screens, and check every box
|
|
253
|
+
off one by one. A box you can't check is an integration bug to fix — not a
|
|
254
|
+
skip. You can't judge real transcode QUALITY or a live A/V feed in the
|
|
255
|
+
sandbox; verify the pipeline + playback WIRING you own:
|
|
256
|
+
|
|
257
|
+
- [ ] Uploading/ingesting a media asset produces a real PLAYABLE stream: the
|
|
258
|
+
returned `manifestUri` (`.m3u8` for HLS / `.mpd` for DASH) loads in the
|
|
259
|
+
app's video player and actually plays — frames advance and the player
|
|
260
|
+
fetches segments (watch the network panel), never a broken/blank player.
|
|
261
|
+
- [ ] The stream is served from/through the APP'S OWN origin — an
|
|
262
|
+
`outputPath` under a directory the server exposes, or endpoints that return
|
|
263
|
+
`generateManifest(segments)` and stream `getSegment(streamId, index)` bytes.
|
|
264
|
+
The player must NOT hotlink a raw expiring provider URL, and no manifest or
|
|
265
|
+
segment request may 404.
|
|
266
|
+
- [ ] Processing STATE is observable and playback is gated on it: an asset
|
|
267
|
+
moves pending → processing → ready (StreamStatus), the UI reflects that,
|
|
268
|
+
and the player mounts only once status is 'ready' — never a dead player on
|
|
269
|
+
a still-transcoding asset.
|
|
270
|
+
- [ ] If adaptive bitrate is exposed, `transcode()` produced multiple
|
|
271
|
+
renditions: the master manifest (`masterManifestUri`) lists more than one
|
|
272
|
+
`variant` and the player can switch quality across them.
|
|
273
|
+
- [ ] If the app exposes a poster/thumbnail for an asset, it generates and
|
|
274
|
+
renders before playback (no blank tile).
|
|
275
|
+
- [ ] SECURITY — private media is AUTHORIZED on playback: the manifest and
|
|
276
|
+
segment endpoints check the requester (or hand out a signed/expiring URL),
|
|
277
|
+
so a user CANNOT fetch another user's stream by guessing its `id`/URL; and
|
|
278
|
+
provider keys stay server-side (never shipped to the client bundle).
|
package/dist/index.d.ts
CHANGED
|
@@ -20,6 +20,16 @@
|
|
|
20
20
|
* ```
|
|
21
21
|
*
|
|
22
22
|
* @remarks
|
|
23
|
+
* - **String inputs must be local absolute file paths.** `createStream()` and
|
|
24
|
+
* `transcode()` accept `Buffer | string`; a string is passed straight to
|
|
25
|
+
* ffmpeg's `-i`, and ffmpeg natively fetches `http(s)`, `tcp`, `tls`, and
|
|
26
|
+
* more — so an unvalidated string would make the bond an SSRF / file-read
|
|
27
|
+
* primitive acting with your server's network position. Strings that
|
|
28
|
+
* contain `://`, carry a `scheme:` prefix, or are not absolute paths are
|
|
29
|
+
* rejected with a thrown error. For remote media, fetch the bytes yourself
|
|
30
|
+
* (with your own SSRF guard) and pass a `Buffer`. As defense-in-depth the
|
|
31
|
+
* ffmpeg protocol whitelist is trimmed to local protocols (`file,crypto`) —
|
|
32
|
+
* network protocols are never enabled.
|
|
23
33
|
* - **Requires the `ffmpeg` binary on the host** (resolved via PATH, or set
|
|
24
34
|
* `createProvider({ ffmpegPath })`). A missing binary fails at first
|
|
25
35
|
* `createStream()`/`transcode()` call with `spawn ffmpeg ENOENT` — verify
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmDG;AAEH,cAAc,oBAAoB,CAAA;AAClC,cAAc,WAAW,CAAA;AACzB,cAAc,eAAe,CAAA;AAC7B,cAAc,YAAY,CAAA;AAC1B,cAAc,eAAe,CAAA"}
|
package/dist/index.js
CHANGED
|
@@ -20,6 +20,16 @@
|
|
|
20
20
|
* ```
|
|
21
21
|
*
|
|
22
22
|
* @remarks
|
|
23
|
+
* - **String inputs must be local absolute file paths.** `createStream()` and
|
|
24
|
+
* `transcode()` accept `Buffer | string`; a string is passed straight to
|
|
25
|
+
* ffmpeg's `-i`, and ffmpeg natively fetches `http(s)`, `tcp`, `tls`, and
|
|
26
|
+
* more — so an unvalidated string would make the bond an SSRF / file-read
|
|
27
|
+
* primitive acting with your server's network position. Strings that
|
|
28
|
+
* contain `://`, carry a `scheme:` prefix, or are not absolute paths are
|
|
29
|
+
* rejected with a thrown error. For remote media, fetch the bytes yourself
|
|
30
|
+
* (with your own SSRF guard) and pass a `Buffer`. As defense-in-depth the
|
|
31
|
+
* ffmpeg protocol whitelist is trimmed to local protocols (`file,crypto`) —
|
|
32
|
+
* network protocols are never enabled.
|
|
23
33
|
* - **Requires the `ffmpeg` binary on the host** (resolved via PATH, or set
|
|
24
34
|
* `createProvider({ ffmpegPath })`). A missing binary fails at first
|
|
25
35
|
* `createStream()`/`transcode()` call with `spawn ffmpeg ENOENT` — verify
|
package/dist/provider.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"provider.d.ts","sourceRoot":"","sources":["../src/provider.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAQH,OAAO,KAAK,EACV,iBAAiB,EAOlB,MAAM,+BAA+B,CAAA;AAGtC,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAA;
|
|
1
|
+
{"version":3,"file":"provider.d.ts","sourceRoot":"","sources":["../src/provider.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAQH,OAAO,KAAK,EACV,iBAAiB,EAOlB,MAAM,+BAA+B,CAAA;AAGtC,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAA;AAoG3C;;;;;GAKG;AACH,eAAO,MAAM,cAAc,GAAI,SAAQ,SAAc,KAAG,iBAmKvD,CAAA;AAED;;GAEG;AACH,eAAO,MAAM,QAAQ,EAAE,iBAAoC,CAAA"}
|
package/dist/provider.js
CHANGED
|
@@ -10,11 +10,48 @@
|
|
|
10
10
|
import { execFile } from 'node:child_process';
|
|
11
11
|
import { mkdir, readdir, readFile, writeFile } from 'node:fs/promises';
|
|
12
12
|
import { tmpdir } from 'node:os';
|
|
13
|
-
import { join } from 'node:path';
|
|
13
|
+
import { isAbsolute, join } from 'node:path';
|
|
14
14
|
import { promisify } from 'node:util';
|
|
15
15
|
import { generateMasterPlaylist, generateMediaPlaylist } from './m3u8.js';
|
|
16
16
|
import { assertSafePathComponent, assertSegmentIndex, resolveWithinBase } from './validate.js';
|
|
17
17
|
const execFileAsync = promisify(execFile);
|
|
18
|
+
/**
|
|
19
|
+
* Matches a URL scheme prefix (`scheme:`) the way ffmpeg's own protocol
|
|
20
|
+
* handler resolution does — including scheme-only forms like `http:host/x`
|
|
21
|
+
* that carry no `//`.
|
|
22
|
+
*/
|
|
23
|
+
const URL_SCHEME_PREFIX = /^[a-z][a-z0-9+.-]*:/i;
|
|
24
|
+
/**
|
|
25
|
+
* Asserts that a caller-supplied string input is a LOCAL ABSOLUTE FILE PATH.
|
|
26
|
+
*
|
|
27
|
+
* String inputs are passed verbatim as ffmpeg's `-i` argument, and ffmpeg
|
|
28
|
+
* natively speaks `http`, `https`, `tcp`, `tls`, `concat`, `gopher`, and
|
|
29
|
+
* more. Forwarding an unvalidated string therefore turns this bond into an
|
|
30
|
+
* SSRF / file-read primitive: `http://169.254.169.254/…` (cloud metadata),
|
|
31
|
+
* `http://intra-host/…`, or any other URL the host can reach, executed by
|
|
32
|
+
* ffmpeg with the server's network position. We reject:
|
|
33
|
+
*
|
|
34
|
+
* - anything containing `://` (absolute URLs),
|
|
35
|
+
* - anything with a `scheme:` prefix (ffmpeg accepts `http:host/x` too),
|
|
36
|
+
* - anything that is not an absolute path (`/…`) — a relative path would
|
|
37
|
+
* resolve against ffmpeg's CWD, not the caller's.
|
|
38
|
+
*
|
|
39
|
+
* Applications that need remote media must fetch the bytes themselves (with
|
|
40
|
+
* their own SSRF guard) and pass a `Buffer`.
|
|
41
|
+
*
|
|
42
|
+
* Module-private: not part of the package's public export surface.
|
|
43
|
+
*
|
|
44
|
+
* @param inputPath - The caller-supplied string input.
|
|
45
|
+
* @returns The validated path, unchanged.
|
|
46
|
+
* @throws {Error} When the string is not a local absolute file path.
|
|
47
|
+
*/
|
|
48
|
+
const assertLocalInputPath = (inputPath) => {
|
|
49
|
+
if (inputPath.includes('://') || URL_SCHEME_PREFIX.test(inputPath) || !isAbsolute(inputPath)) {
|
|
50
|
+
throw new Error(`Invalid media input: string inputs must be local absolute file paths (got ${JSON.stringify(inputPath)}). ` +
|
|
51
|
+
'Fetch remote media yourself (with an SSRF guard) and pass a Buffer instead — ffmpeg URLs are rejected to prevent server-side request forgery.');
|
|
52
|
+
}
|
|
53
|
+
return inputPath;
|
|
54
|
+
};
|
|
18
55
|
let streamCounter = 0;
|
|
19
56
|
/**
|
|
20
57
|
* Generates a unique stream identifier.
|
|
@@ -34,7 +71,10 @@ const generateStreamId = () => {
|
|
|
34
71
|
*/
|
|
35
72
|
const prepareInput = async (input, dir) => {
|
|
36
73
|
if (typeof input === 'string') {
|
|
37
|
-
|
|
74
|
+
// SSRF guard: a string is treated as a local absolute FILE path only —
|
|
75
|
+
// ffmpeg speaks http/tcp/… natively, so an unvalidated string is a
|
|
76
|
+
// fetch-anything primitive. Remote media must arrive as a Buffer.
|
|
77
|
+
return assertLocalInputPath(input);
|
|
38
78
|
}
|
|
39
79
|
const inputPath = join(dir, 'input.tmp');
|
|
40
80
|
await writeFile(inputPath, input);
|
|
@@ -78,10 +118,13 @@ export const createProvider = (config = {}) => {
|
|
|
78
118
|
await mkdir(outputDir, { recursive: true });
|
|
79
119
|
const inputPath = await prepareInput(input, outputDir);
|
|
80
120
|
await execFileAsync(ffmpegPath, [
|
|
81
|
-
//
|
|
82
|
-
//
|
|
121
|
+
// Defense-in-depth behind the string-input SSRF guard: restrict ffmpeg
|
|
122
|
+
// to LOCAL protocols only (file, crypto). Network protocols (http,
|
|
123
|
+
// https, tcp, tls) are deliberately NOT whitelisted — a string input
|
|
124
|
+
// must be a local absolute file path (see assertLocalInputPath), so
|
|
125
|
+
// nothing legitimate ever needs network access here. [P5BONDS DiD]
|
|
83
126
|
'-protocol_whitelist',
|
|
84
|
-
'file,crypto
|
|
127
|
+
'file,crypto',
|
|
85
128
|
'-i',
|
|
86
129
|
inputPath,
|
|
87
130
|
'-codec',
|
|
@@ -128,9 +171,11 @@ export const createProvider = (config = {}) => {
|
|
|
128
171
|
await mkdir(profileDir, { recursive: true });
|
|
129
172
|
const codec = profile.codec ?? 'h264';
|
|
130
173
|
const args = [
|
|
131
|
-
//
|
|
174
|
+
// Defense-in-depth behind the string-input SSRF guard: local
|
|
175
|
+
// protocols only — see the createStream whitelist comment.
|
|
176
|
+
// [P5BONDS DiD]
|
|
132
177
|
'-protocol_whitelist',
|
|
133
|
-
'file,crypto
|
|
178
|
+
'file,crypto',
|
|
134
179
|
'-i',
|
|
135
180
|
inputPath,
|
|
136
181
|
'-c:v',
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@molecule/api-media-streaming-hls",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "HLS media streaming provider for molecule.dev — ffmpeg-based segmentation, transcoding, and M3U8 playlist generation",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -17,7 +17,8 @@
|
|
|
17
17
|
}
|
|
18
18
|
},
|
|
19
19
|
"files": [
|
|
20
|
-
"dist"
|
|
20
|
+
"dist",
|
|
21
|
+
"README.md"
|
|
21
22
|
],
|
|
22
23
|
"keywords": [
|
|
23
24
|
"molecule",
|
|
@@ -30,13 +31,13 @@
|
|
|
30
31
|
],
|
|
31
32
|
"license": "Apache-2.0",
|
|
32
33
|
"peerDependencies": {
|
|
33
|
-
"@molecule/api-media-streaming": "^1.0.
|
|
34
|
+
"@molecule/api-media-streaming": "^1.0.1"
|
|
34
35
|
},
|
|
35
36
|
"devDependencies": {
|
|
36
|
-
"@molecule/api-media-streaming": "1.0.
|
|
37
|
+
"@molecule/api-media-streaming": "1.0.1",
|
|
37
38
|
"@types/node": "26.1.2",
|
|
38
39
|
"typescript": "6.0.3",
|
|
39
|
-
"vitest": "4.1.
|
|
40
|
+
"vitest": "4.1.11"
|
|
40
41
|
},
|
|
41
42
|
"repository": {
|
|
42
43
|
"type": "git",
|