@siafoundation/sia-storage 0.0.14 → 0.1.1
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 +132 -5
- package/dist/chunk-QYQQIWFH.js +74 -0
- package/dist/chunk-QYQQIWFH.js.map +1 -0
- package/dist/cli.js +13 -0
- package/dist/index.d.ts +90 -2
- package/dist/index.js +309 -0
- package/dist/index.js.map +1 -1
- package/dist/index.node.cjs +30 -0
- package/dist/index.node.cjs.map +1 -1
- package/dist/index.node.d.ts +160 -6
- package/dist/next.d.ts +3 -0
- package/dist/next.js +12 -0
- package/dist/sia-storage-sw.js +2 -0
- package/dist/stream-worker.d.ts +51 -0
- package/dist/stream-worker.js +441 -0
- package/dist/stream-worker.js.map +1 -0
- package/dist/vite.d.ts +39 -0
- package/dist/vite.js +32 -0
- package/package.json +24 -8
- package/wasm/sia_storage_wasm.d.ts +237 -37
- package/wasm/sia_storage_wasm.js +462 -59
- package/wasm/sia_storage_wasm_bg.wasm +0 -0
- package/wasm/sia_storage_wasm_bg.wasm.d.ts +36 -10
package/README.md
CHANGED
|
@@ -70,11 +70,85 @@ await packed.add(fileB.stream())
|
|
|
70
70
|
for (const obj of await packed.finalize()) await sdk.pinObject(obj)
|
|
71
71
|
```
|
|
72
72
|
|
|
73
|
+
## Streaming files
|
|
74
|
+
|
|
75
|
+
Play video and audio without loading the whole file first, and save large files without holding them in memory. A service worker reads each object in ranges and answers the page with an ordinary URL, so `<video>` seeks on its own and a download goes to the browser's download manager. Where service workers are unavailable, the same calls read the object in the page instead, so the code is the same everywhere.
|
|
76
|
+
|
|
77
|
+
```ts
|
|
78
|
+
import { openStreams } from '@siafoundation/sia-storage'
|
|
79
|
+
|
|
80
|
+
// An Sdk, with the indexer URL and app metadata its Builder was made with.
|
|
81
|
+
const streams = openStreams(sdk, { indexerUrl, appMeta })
|
|
82
|
+
|
|
83
|
+
// Or a SharedSdk, with the indexer URL and seed it was connected with.
|
|
84
|
+
const sharedStreams = openStreams(sharedSdk, { indexerUrl, seed })
|
|
85
|
+
|
|
86
|
+
// Any element or fetch that takes a URL.
|
|
87
|
+
const video = await streams.url(object, { name: 'clip.mp4', type: 'video/mp4' })
|
|
88
|
+
videoElement.src = video.url
|
|
89
|
+
// When the element goes away:
|
|
90
|
+
video.release()
|
|
91
|
+
|
|
92
|
+
// From a click handler, so the browser allows the save.
|
|
93
|
+
button.onclick = () => streams.download(object, { name: 'clip.mp4' })
|
|
94
|
+
|
|
95
|
+
// When you're done with the SDK:
|
|
96
|
+
streams.close()
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### Serve the worker
|
|
100
|
+
|
|
101
|
+
The worker is one file, `sia-storage-sw.js`, served from your site's root. Use the line for your setup.
|
|
102
|
+
|
|
103
|
+
**Vite** (React, Vue, Svelte, Solid and others): add the plugin. It serves the worker in `vite dev`, emits it with the build, and excludes the SDK from dependency pre-bundling.
|
|
104
|
+
|
|
105
|
+
```js
|
|
106
|
+
// vite.config.js
|
|
107
|
+
import { siaStorage } from '@siafoundation/sia-storage/vite'
|
|
108
|
+
|
|
109
|
+
export default defineConfig({ plugins: [siaStorage()] })
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
**Next.js (App Router)**: add one route file.
|
|
113
|
+
|
|
114
|
+
```ts
|
|
115
|
+
// app/sia-storage-sw.js/route.ts
|
|
116
|
+
export { GET } from '@siafoundation/sia-storage/next'
|
|
117
|
+
export const dynamic = 'force-static'
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
**Anything else** (webpack, Rollup, esbuild, Parcel, the Next.js Pages Router, a static host): copy the worker into the folder served at your site's root. The file changes with each SDK version, so copy it on every install and leave it out of git.
|
|
121
|
+
|
|
122
|
+
```json
|
|
123
|
+
// package.json
|
|
124
|
+
"scripts": { "postinstall": "sia-storage-worker public" }
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
### Good to know
|
|
128
|
+
|
|
129
|
+
- **HTTPS or localhost.** Browsers only run service workers there. Anywhere else the page reads objects itself.
|
|
130
|
+
- **A site served from a sub-path**, such as `/app/`, serves the worker there too and says where, before the first `openStreams`: `enableStreaming({ workerUrl: '/app/sia-storage-sw.js' })`.
|
|
131
|
+
- **A site with its own service worker** can't add a second one, since a page has only one. Call `installStreams(self)` from `@siafoundation/sia-storage/stream-worker` inside yours, and point `enableStreaming` at it: `enableStreaming({ workerUrl: '/sw.js', type: 'module' })`.
|
|
132
|
+
- **A Content Security Policy** needs `worker-src 'self'`.
|
|
133
|
+
- **The credentials stay in memory.** The worker connects its own SDK, so it needs what yours was made with: a `SharedSdk`'s seed, or an `Sdk`'s app key, which it takes from `sdk.appKey()`. The page hands them over a message port, and the worker never writes them anywhere.
|
|
134
|
+
- **Keep the tab open** until a download finishes. If the browser restarts the worker, it gets the credentials back from the page.
|
|
135
|
+
- **The first download click** within a few seconds of `openStreams`, before the worker is ready, uses the save picker or an in-memory save instead.
|
|
136
|
+
|
|
137
|
+
## iCloud Private Relay
|
|
138
|
+
|
|
139
|
+
The SDK does not work through iCloud Private Relay. `detectPrivateRelay()` resolves true for a Safari visitor on it, so you can ask them to turn it off in iCloud settings. Other browsers resolve false without a request.
|
|
140
|
+
|
|
141
|
+
```ts
|
|
142
|
+
import { detectPrivateRelay } from '@siafoundation/sia-storage'
|
|
143
|
+
|
|
144
|
+
if (await detectPrivateRelay()) showRelayNotice()
|
|
145
|
+
```
|
|
146
|
+
|
|
73
147
|
## Framework notes
|
|
74
148
|
|
|
75
149
|
The browser build is WebAssembly — most bundlers handle it directly, a few need a small hint.
|
|
76
150
|
|
|
77
|
-
**Vite
|
|
151
|
+
**Vite**: the `siaStorage()` plugin from [Serve the worker](#serve-the-worker) covers this. Without it, production builds work as-is, and `vite dev` needs the package excluded from the dep pre-bundler so its `import.meta.url`-relative WASM path resolves correctly:
|
|
78
152
|
|
|
79
153
|
```js
|
|
80
154
|
// vite.config.js
|
|
@@ -113,6 +187,10 @@ Near-identical surfaces. Real differences:
|
|
|
113
187
|
|
|
114
188
|
- App identifier: `appMeta.appId: string (hex)` on browser, `appMeta.id: Buffer(32)` on Node.
|
|
115
189
|
- Numeric sizes are `number` on browser, `bigint` on Node. Byte arrays are `Uint8Array` on browser, `Buffer` on Node (`Buffer` is a `Uint8Array` subclass; both accept either as input).
|
|
190
|
+
- Sharing key seeds are hex `string` on browser, `Buffer` on Node. This covers `SharingKey.seed()`, `SharingKey.fromSeed(seed)`, and `SharedSdk.connect(indexerUrl, seed)`.
|
|
191
|
+
- `sdk.unshareObject(key, object)` takes a `PinnedObject` on browser and an object id `string` on Node.
|
|
192
|
+
- `sdk.hosts(query?)` and `sharedSdk.hosts(query?)` accept a `HostQuery` on browser; on Node they take no arguments.
|
|
193
|
+
- `openStreams` gives a browser page URLs that a service worker serves. Node reads objects with `sdk.download(object, { offset, length })`, which already streams a range, so it needs no worker. On Node, `enableStreaming()` resolves false, and `url` and `download` on an `openStreams` handle reject.
|
|
116
194
|
|
|
117
195
|
## API
|
|
118
196
|
|
|
@@ -125,10 +203,11 @@ Near-identical surfaces. Real differences:
|
|
|
125
203
|
| `validateRecoveryPhrase(phrase)` | Throws on invalid. |
|
|
126
204
|
| `setLogger(callback, level)` | Receive SDK logs. |
|
|
127
205
|
| `encodedSize(size, dataShards, parityShards)` | Encoded size after erasure coding. |
|
|
206
|
+
| `detectPrivateRelay()` | Whether a Safari visitor is on iCloud Private Relay, which the SDK does not work through. |
|
|
128
207
|
|
|
129
208
|
### `Sdk`
|
|
130
209
|
|
|
131
|
-
Returned from `Builder.register()` or `Builder.
|
|
210
|
+
Returned from `Builder.register()`, `Builder.connected()`, or `Builder.connectPreAuthorized()`.
|
|
132
211
|
|
|
133
212
|
| | |
|
|
134
213
|
|---|---|
|
|
@@ -138,9 +217,14 @@ Returned from `Builder.register()` or `Builder.connected()`.
|
|
|
138
217
|
| `uploadPacked(options?)` | `PackedUpload` for batching small files into shared slabs. |
|
|
139
218
|
| `object(key)` / `deleteObject(key)` / `pinObject(object)` | Object CRUD. |
|
|
140
219
|
| `updateObjectMetadata(object)` | Push local metadata changes to the indexer. |
|
|
141
|
-
| `
|
|
220
|
+
| `objectShareUrl(object, validUntil)` / `objectFromShareUrl(url)` | Create / consume share URLs. |
|
|
142
221
|
| `objectEvents(cursor?, limit)` | Paginated change feed. |
|
|
143
|
-
| `hosts()` / `slab(id)` / `account()` / `pruneSlabs()` | Indexer reads. |
|
|
222
|
+
| `hosts(query?)` / `slab(id)` / `account()` / `pruneSlabs()` | Indexer reads. |
|
|
223
|
+
| `createSharingKey(description, expiresAt?)` | Create a `SharingKey` for read-only sharing. |
|
|
224
|
+
| `sharingKeys(offset, limit)` / `sharingKey(key)` | List keys / fetch one key's `KeyRecord`. |
|
|
225
|
+
| `shareObject(key, object)` / `unshareObject(key, object)` | Attach / detach an object on a sharing key. |
|
|
226
|
+
| `sharedObjects(key, offset, limit)` | Objects attached to a sharing key. |
|
|
227
|
+
| `revokeSharingKey(key)` | Delete the key and detach all of its objects. |
|
|
144
228
|
|
|
145
229
|
### `Builder`
|
|
146
230
|
|
|
@@ -153,6 +237,9 @@ Returned from `Builder.register()` or `Builder.connected()`.
|
|
|
153
237
|
| `waitForApproval()` | Resolves once the user approves. |
|
|
154
238
|
| `register(phrase)` | Finish onboarding with a new recovery phrase → `Sdk`. |
|
|
155
239
|
| `connected(appKey)` | Reconnect with a saved `AppKey` → `Sdk \| null`. |
|
|
240
|
+
| `reconnecting()` | Whether the approved connect key already has an account. |
|
|
241
|
+
| `matchesExistingAppKey(phrase)` | Whether a phrase derives an already-registered app key. |
|
|
242
|
+
| `connectPreAuthorized(seed, phrase)` | Skip the approval flow with a pre-authorized key's 32-byte seed → `Sdk`. |
|
|
156
243
|
|
|
157
244
|
### `AppKey`
|
|
158
245
|
|
|
@@ -164,7 +251,7 @@ Returned from `Builder.register()` or `Builder.connected()`.
|
|
|
164
251
|
|
|
165
252
|
`new PinnedObject()` for new uploads, or `sdk.object(key)`.
|
|
166
253
|
|
|
167
|
-
`id()` · `size()` · `encodedSize()` · `slabs()` · `metadata()` · `updateMetadata(bytes)` · `createdAt()` · `updatedAt()` · `seal(appKey)` · `PinnedObject.open(appKey, sealed)`
|
|
254
|
+
`id()` · `size()` · `encodedSize()` · `slabs()` · `metadata()` · `updateMetadata(bytes)` · `truncate(length: bigint)` · `createdAt()` · `updatedAt()` · `seal(appKey)` · `PinnedObject.open(appKey, sealed)`
|
|
168
255
|
|
|
169
256
|
### `PackedUpload`
|
|
170
257
|
|
|
@@ -172,6 +259,46 @@ From `sdk.uploadPacked()`.
|
|
|
172
259
|
|
|
173
260
|
`add(stream)` · `finalize()` · `cancel()` · `remaining()` · `length()` · `slabs()`
|
|
174
261
|
|
|
262
|
+
### `SharingKey`
|
|
263
|
+
|
|
264
|
+
From `sdk.createSharingKey()`, or `SharingKey.fromSeed(seed)` to import one.
|
|
265
|
+
|
|
266
|
+
`publicKey` · `seed()` · `SharingKey.fromSeed(seed)`
|
|
267
|
+
|
|
268
|
+
### `KeyRecord`
|
|
269
|
+
|
|
270
|
+
The indexer's record for a sharing key, from `sdk.sharingKey()` or `sdk.sharingKeys()`.
|
|
271
|
+
|
|
272
|
+
`key` · `description` · `stats`
|
|
273
|
+
|
|
274
|
+
### `SharedSdk`
|
|
275
|
+
|
|
276
|
+
`SharedSdk.connect(indexerUrl, seed)` — read-only access for a recipient holding a
|
|
277
|
+
sharing key's seed. Downloads are paid for by the key's owner.
|
|
278
|
+
|
|
279
|
+
| | |
|
|
280
|
+
|---|---|
|
|
281
|
+
| `stats()` | The key's `KeyStats` snapshot. |
|
|
282
|
+
| `object(id)` / `objects(offset, limit)` | Read the objects the key grants access to. |
|
|
283
|
+
| `download(object, options?)` | Returns a `ReadableStream`. |
|
|
284
|
+
| `hosts(query?)` | Hosts serving this key's objects. |
|
|
285
|
+
|
|
286
|
+
### Streaming
|
|
287
|
+
|
|
288
|
+
| | |
|
|
289
|
+
|---|---|
|
|
290
|
+
| `openStreams(sdk, credentials)` | `Streams` for one SDK's objects. An `Sdk` takes `{ indexerUrl, appMeta }` and a `SharedSdk` takes `{ indexerUrl, seed }`, typed by which one you pass. |
|
|
291
|
+
| `streams.url(object, options)` | `{ url, blob?, release() }`. Streams where it can, else reads the whole object into `blob`. |
|
|
292
|
+
| `streams.download(object, options)` | Saves the object. Resolves `'streaming'`, `'saved'` or `'cancelled'`. Call it from a click handler. |
|
|
293
|
+
| `streams.close()` | Cancels this handle's streams. Other handles on the same SDK keep theirs. |
|
|
294
|
+
| `enableStreaming(options?)` | Registers the worker and resolves whether streaming is on. `openStreams` calls it for you, so call it only to pass `workerUrl`, `scope` or `type`. |
|
|
295
|
+
|
|
296
|
+
`options` for `url` and `download`: `name`, `type` (MIME), `onError(message)` for failures after streaming starts (logged when omitted), `onProgress(bytes)` when the page reads the object itself, and `signal` for `url`.
|
|
297
|
+
|
|
298
|
+
### `@siafoundation/sia-storage/stream-worker`
|
|
299
|
+
|
|
300
|
+
`installStreams(self, { wasm? })` adds streaming to a service worker of your own. `wasm` is the SDK's WebAssembly as a URL, bytes or module, for bundlers that don't emit it.
|
|
301
|
+
|
|
175
302
|
## License
|
|
176
303
|
|
|
177
304
|
MIT
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// src/stream/errors.ts
|
|
2
|
+
function streamErrorMessage(error) {
|
|
3
|
+
const message = error instanceof Error ? error.message : typeof error === "string" ? error : "Unknown streaming failure";
|
|
4
|
+
return message.replace(/\b(?:https?|wss?|sia):\/\/[^\s"'<>]+/gi, "[URL redacted]").replace(/(?:bearer\s+)[^\s,;]+/gi, "Bearer [redacted]").replace(
|
|
5
|
+
/((?:seed|share|token|key|authorization)\s*[=:]\s*)[^\s,;]+/gi,
|
|
6
|
+
"$1[redacted]"
|
|
7
|
+
).replace(/[a-f0-9]{64,}/gi, "[redacted]").replace(/[\r\n\t]/g, " ").slice(0, 1e3);
|
|
8
|
+
}
|
|
9
|
+
function reportStreamError(error, name, range) {
|
|
10
|
+
const message = streamErrorMessage(error);
|
|
11
|
+
const bytes = range ? ` bytes ${range.offset}-${range.offset + range.length - 1}` : "";
|
|
12
|
+
console.error(`[Sia stream] ${streamErrorMessage(name)}${bytes}: ${message}`);
|
|
13
|
+
return message;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// src/stream/protocol.ts
|
|
17
|
+
var STREAM_PATH = /\/__sia_stream__\/([^/]+)\/([^/]+)\/[^/]+$/;
|
|
18
|
+
function streamPath(scope, clientId, token) {
|
|
19
|
+
return `${scope}__sia_stream__/${clientId}/${token}/file`;
|
|
20
|
+
}
|
|
21
|
+
function parseStreamPath(pathname) {
|
|
22
|
+
const match = STREAM_PATH.exec(pathname);
|
|
23
|
+
return match ? { clientId: match[1], token: match[2] } : void 0;
|
|
24
|
+
}
|
|
25
|
+
var strings = (data, fields) => fields.every((field) => typeof data?.[field] === "string");
|
|
26
|
+
function isAppMetadata(value) {
|
|
27
|
+
return strings(value, ["appId", "name", "description", "serviceUrl"]);
|
|
28
|
+
}
|
|
29
|
+
function isConnection(value) {
|
|
30
|
+
const data = value;
|
|
31
|
+
if (data?.kind === "shared") return strings(data, ["indexerUrl", "seed"]);
|
|
32
|
+
return data?.kind === "app" && strings(data, ["indexerUrl", "appKey"]) && isAppMetadata(data.appMeta);
|
|
33
|
+
}
|
|
34
|
+
function isSourceReply(reply) {
|
|
35
|
+
const data = reply;
|
|
36
|
+
return data?.type === "source" && strings(data, ["session", "objectId", "name", "mime"]) && isConnection(data.connection) && (data.connection.kind === "shared" || typeof data.sealed === "object" && data.sealed !== null) && Number.isSafeInteger(data.size) && data.size >= 0;
|
|
37
|
+
}
|
|
38
|
+
function requestReply(target, message, { timeout, signal }) {
|
|
39
|
+
return new Promise((resolve, reject) => {
|
|
40
|
+
const { port1, port2 } = new MessageChannel();
|
|
41
|
+
const timer = timeout === void 0 ? void 0 : setTimeout(
|
|
42
|
+
() => settle(() => reject(new Error("No reply in time."))),
|
|
43
|
+
timeout
|
|
44
|
+
);
|
|
45
|
+
const abort = () => settle(() => reject(signal?.reason));
|
|
46
|
+
function settle(outcome) {
|
|
47
|
+
clearTimeout(timer);
|
|
48
|
+
signal?.removeEventListener("abort", abort);
|
|
49
|
+
port1.close();
|
|
50
|
+
port2.close();
|
|
51
|
+
outcome();
|
|
52
|
+
}
|
|
53
|
+
if (signal?.aborted) return abort();
|
|
54
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
55
|
+
port1.onmessage = ({ data }) => settle(() => resolve(data));
|
|
56
|
+
port1.onmessageerror = () => settle(() => reject(new Error("Unreadable reply.")));
|
|
57
|
+
try {
|
|
58
|
+
target.postMessage(message, [port2]);
|
|
59
|
+
} catch (error) {
|
|
60
|
+
settle(() => reject(error));
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export {
|
|
66
|
+
streamErrorMessage,
|
|
67
|
+
reportStreamError,
|
|
68
|
+
streamPath,
|
|
69
|
+
parseStreamPath,
|
|
70
|
+
isAppMetadata,
|
|
71
|
+
isSourceReply,
|
|
72
|
+
requestReply
|
|
73
|
+
};
|
|
74
|
+
//# sourceMappingURL=chunk-QYQQIWFH.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/stream/errors.ts","../src/stream/protocol.ts"],"sourcesContent":["/** Only sanitized strings cross the diagnostic boundary, never raw SDK objects. */\nexport function streamErrorMessage(error: unknown): string {\n const message =\n error instanceof Error\n ? error.message\n : typeof error === 'string'\n ? error\n : 'Unknown streaming failure'\n return message\n .replace(/\\b(?:https?|wss?|sia):\\/\\/[^\\s\"'<>]+/gi, '[URL redacted]')\n .replace(/(?:bearer\\s+)[^\\s,;]+/gi, 'Bearer [redacted]')\n .replace(\n /((?:seed|share|token|key|authorization)\\s*[=:]\\s*)[^\\s,;]+/gi,\n '$1[redacted]',\n )\n .replace(/[a-f0-9]{64,}/gi, '[redacted]')\n .replace(/[\\r\\n\\t]/g, ' ')\n .slice(0, 1000)\n}\n\nexport function reportStreamError(\n error: unknown,\n name: string,\n range: { offset: number; length: number } | undefined,\n) {\n const message = streamErrorMessage(error)\n // One string rather than an object, so it reads the same in every console.\n const bytes = range\n ? ` bytes ${range.offset}-${range.offset + range.length - 1}`\n : ''\n console.error(`[Sia stream] ${streamErrorMessage(name)}${bytes}: ${message}`)\n return message\n}\n","/**\n * Messages between a page and its streaming service worker.\n *\n * Stream URLs carry only the page's client id and a random token. When the\n * worker meets a token it does not know, it asks the page that made it for the\n * file and the share's credentials. The page and a worker can come from\n * different deployed versions, so the worker checks the page's reply before\n * using it.\n */\nimport type {\n AppMetadata,\n SealedObject,\n} from '../../wasm/sia_storage_wasm.js'\n\n/**\n * What the worker connects its own SDK with. A `SharedSdk` reconnects from its\n * sharing key's seed, and an `Sdk` from its app key, as hex, and app metadata.\n */\nexport type Connection =\n | { kind: 'shared'; indexerUrl: string; seed: string }\n | { kind: 'app'; indexerUrl: string; appKey: string; appMeta: AppMetadata }\n\nexport type SourceDetails = {\n // One per opened SDK, so the worker can release it as a whole.\n session: string\n connection: Connection\n objectId: string\n // An `Sdk`'s object, sealed with its app key, so the worker can open it\n // without a lookup. A `SharedSdk`'s object is looked up by `objectId`.\n sealed?: SealedObject\n name: string\n mime: string\n size: number\n}\n\n/** Page to worker. */\nexport type PageMessage =\n | { type: 'sia-client' }\n | { type: 'sia-release'; token: string }\n | { type: 'sia-release-share'; session: string }\n\n/** Worker to page. */\nexport type WorkerMessage =\n | { type: 'sia-source'; token: string }\n | {\n type: 'sia-stream-error'\n token: string\n message: string\n offset?: number\n length?: number\n }\n\n/** The worker's answer to `sia-client`. */\nexport type ClientReply = { clientId: string; supported: boolean }\n\n/** The page's answer to `sia-source`. */\nexport type SourceReply =\n | ({ type: 'source' } & SourceDetails)\n | { type: 'error' }\n\n// Stream URLs sit under the worker's scope, so a download's navigation, which\n// only a worker whose scope covers the URL can answer, reaches it on sites\n// served from a sub-path too.\nconst STREAM_PATH = /\\/__sia_stream__\\/([^/]+)\\/([^/]+)\\/[^/]+$/\n\n/** `scope` is the worker registration's scope URL, which ends in a slash. */\nexport function streamPath(scope: string, clientId: string, token: string) {\n return `${scope}__sia_stream__/${clientId}/${token}/file`\n}\n\nexport function parseStreamPath(pathname: string) {\n const match = STREAM_PATH.exec(pathname)\n return match ? { clientId: match[1]!, token: match[2]! } : undefined\n}\n\ntype Fields = Record<string, unknown> | null | undefined\n\nconst strings = (data: Fields, fields: string[]) =>\n fields.every((field) => typeof data?.[field] === 'string')\n\n/** Checked on both sides, so bad metadata fails in `openStreams`, not per request. */\nexport function isAppMetadata(value: unknown): value is AppMetadata {\n return strings(value as Fields, ['appId', 'name', 'description', 'serviceUrl'])\n}\n\nfunction isConnection(value: unknown): value is Connection {\n const data = value as Fields\n if (data?.kind === 'shared') return strings(data, ['indexerUrl', 'seed'])\n return (\n data?.kind === 'app' &&\n strings(data, ['indexerUrl', 'appKey']) &&\n isAppMetadata(data.appMeta)\n )\n}\n\nexport function isSourceReply(\n reply: unknown,\n): reply is { type: 'source' } & SourceDetails {\n const data = reply as Fields\n return (\n data?.type === 'source' &&\n strings(data, ['session', 'objectId', 'name', 'mime']) &&\n isConnection(data.connection) &&\n (data.connection.kind === 'shared' ||\n (typeof data.sealed === 'object' && data.sealed !== null)) &&\n Number.isSafeInteger(data.size) &&\n (data.size as number) >= 0\n )\n}\n\n/**\n * Posts `message` with a reply port and resolves with the first reply. Rejects\n * after `timeout` ms, or when `signal` aborts.\n */\nexport function requestReply(\n target: { postMessage(message: unknown, transfer: Transferable[]): void },\n message: unknown,\n { timeout, signal }: { timeout?: number; signal?: AbortSignal },\n): Promise<unknown> {\n return new Promise((resolve, reject) => {\n const { port1, port2 } = new MessageChannel()\n const timer =\n timeout === undefined\n ? undefined\n : setTimeout(\n () => settle(() => reject(new Error('No reply in time.'))),\n timeout,\n )\n const abort = () => settle(() => reject(signal?.reason))\n function settle(outcome: () => void) {\n clearTimeout(timer)\n signal?.removeEventListener('abort', abort)\n port1.close()\n port2.close()\n outcome()\n }\n if (signal?.aborted) return abort()\n signal?.addEventListener('abort', abort, { once: true })\n port1.onmessage = ({ data }) => settle(() => resolve(data))\n port1.onmessageerror = () =>\n settle(() => reject(new Error('Unreadable reply.')))\n try {\n target.postMessage(message, [port2])\n } catch (error) {\n settle(() => reject(error))\n }\n })\n}\n"],"mappings":";AACO,SAAS,mBAAmB,OAAwB;AACzD,QAAM,UACJ,iBAAiB,QACb,MAAM,UACN,OAAO,UAAU,WACf,QACA;AACR,SAAO,QACJ,QAAQ,0CAA0C,gBAAgB,EAClE,QAAQ,2BAA2B,mBAAmB,EACtD;AAAA,IACC;AAAA,IACA;AAAA,EACF,EACC,QAAQ,mBAAmB,YAAY,EACvC,QAAQ,aAAa,GAAG,EACxB,MAAM,GAAG,GAAI;AAClB;AAEO,SAAS,kBACd,OACA,MACA,OACA;AACA,QAAM,UAAU,mBAAmB,KAAK;AAExC,QAAM,QAAQ,QACV,UAAU,MAAM,MAAM,IAAI,MAAM,SAAS,MAAM,SAAS,CAAC,KACzD;AACJ,UAAQ,MAAM,gBAAgB,mBAAmB,IAAI,CAAC,GAAG,KAAK,KAAK,OAAO,EAAE;AAC5E,SAAO;AACT;;;AC+BA,IAAM,cAAc;AAGb,SAAS,WAAW,OAAe,UAAkB,OAAe;AACzE,SAAO,GAAG,KAAK,kBAAkB,QAAQ,IAAI,KAAK;AACpD;AAEO,SAAS,gBAAgB,UAAkB;AAChD,QAAM,QAAQ,YAAY,KAAK,QAAQ;AACvC,SAAO,QAAQ,EAAE,UAAU,MAAM,CAAC,GAAI,OAAO,MAAM,CAAC,EAAG,IAAI;AAC7D;AAIA,IAAM,UAAU,CAAC,MAAc,WAC7B,OAAO,MAAM,CAAC,UAAU,OAAO,OAAO,KAAK,MAAM,QAAQ;AAGpD,SAAS,cAAc,OAAsC;AAClE,SAAO,QAAQ,OAAiB,CAAC,SAAS,QAAQ,eAAe,YAAY,CAAC;AAChF;AAEA,SAAS,aAAa,OAAqC;AACzD,QAAM,OAAO;AACb,MAAI,MAAM,SAAS,SAAU,QAAO,QAAQ,MAAM,CAAC,cAAc,MAAM,CAAC;AACxE,SACE,MAAM,SAAS,SACf,QAAQ,MAAM,CAAC,cAAc,QAAQ,CAAC,KACtC,cAAc,KAAK,OAAO;AAE9B;AAEO,SAAS,cACd,OAC6C;AAC7C,QAAM,OAAO;AACb,SACE,MAAM,SAAS,YACf,QAAQ,MAAM,CAAC,WAAW,YAAY,QAAQ,MAAM,CAAC,KACrD,aAAa,KAAK,UAAU,MAC3B,KAAK,WAAW,SAAS,YACvB,OAAO,KAAK,WAAW,YAAY,KAAK,WAAW,SACtD,OAAO,cAAc,KAAK,IAAI,KAC7B,KAAK,QAAmB;AAE7B;AAMO,SAAS,aACd,QACA,SACA,EAAE,SAAS,OAAO,GACA;AAClB,SAAO,IAAI,QAAQ,CAAC,SAAS,WAAW;AACtC,UAAM,EAAE,OAAO,MAAM,IAAI,IAAI,eAAe;AAC5C,UAAM,QACJ,YAAY,SACR,SACA;AAAA,MACE,MAAM,OAAO,MAAM,OAAO,IAAI,MAAM,mBAAmB,CAAC,CAAC;AAAA,MACzD;AAAA,IACF;AACN,UAAM,QAAQ,MAAM,OAAO,MAAM,OAAO,QAAQ,MAAM,CAAC;AACvD,aAAS,OAAO,SAAqB;AACnC,mBAAa,KAAK;AAClB,cAAQ,oBAAoB,SAAS,KAAK;AAC1C,YAAM,MAAM;AACZ,YAAM,MAAM;AACZ,cAAQ;AAAA,IACV;AACA,QAAI,QAAQ,QAAS,QAAO,MAAM;AAClC,YAAQ,iBAAiB,SAAS,OAAO,EAAE,MAAM,KAAK,CAAC;AACvD,UAAM,YAAY,CAAC,EAAE,KAAK,MAAM,OAAO,MAAM,QAAQ,IAAI,CAAC;AAC1D,UAAM,iBAAiB,MACrB,OAAO,MAAM,OAAO,IAAI,MAAM,mBAAmB,CAAC,CAAC;AACrD,QAAI;AACF,aAAO,YAAY,SAAS,CAAC,KAAK,CAAC;AAAA,IACrC,SAAS,OAAO;AACd,aAAO,MAAM,OAAO,KAAK,CAAC;AAAA,IAC5B;AAAA,EACF,CAAC;AACH;","names":[]}
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/cli.ts
|
|
4
|
+
import { copyFileSync, mkdirSync } from "fs";
|
|
5
|
+
import { join } from "path";
|
|
6
|
+
var WORKER_FILE = "sia-storage-sw.js";
|
|
7
|
+
var dir = process.argv[2] ?? "public";
|
|
8
|
+
mkdirSync(dir, { recursive: true });
|
|
9
|
+
copyFileSync(
|
|
10
|
+
new URL(`./${WORKER_FILE}`, import.meta.url),
|
|
11
|
+
join(dir, WORKER_FILE)
|
|
12
|
+
);
|
|
13
|
+
console.log(`Copied ${WORKER_FILE} into ${dir}/`);
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,94 @@
|
|
|
1
|
-
|
|
1
|
+
import { AppMetadata, Sdk, SharedSdk, PinnedObject } from '../wasm/sia_storage_wasm.js';
|
|
2
|
+
export { Account, AppKey, AppMetadata, Builder, DownloadOptions, Host, HostQuery, KeyRecord, KeyStats, ObjectEvent, PackedUpload, PinnedObject, PinnedSlab, Sdk, SealedObject, Sector, ShardProgress, SharedSdk, SharingKey, Slab, UploadOptions, encodedSize, generateRecoveryPhrase, setLogger, validateRecoveryPhrase } from '../wasm/sia_storage_wasm.js';
|
|
3
|
+
|
|
4
|
+
declare function detectPrivateRelay(): Promise<boolean>;
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The page side of streaming.
|
|
8
|
+
*
|
|
9
|
+
* `openStreams` hands out same-origin URLs that the library's service worker
|
|
10
|
+
* answers with ranged reads of an object, so a `<video>` seeks without loading
|
|
11
|
+
* the whole file and a download goes to the browser's download manager instead
|
|
12
|
+
* of the page's memory. The worker connects its own SDK of the same kind, with
|
|
13
|
+
* credentials this page hands it over a message port and it keeps in memory.
|
|
14
|
+
* Where the worker is unavailable, the same calls read the object in the page.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
type StreamingOptions = {
|
|
18
|
+
/** Where the site serves the worker file. Defaults to `/sia-storage-sw.js`. */
|
|
19
|
+
workerUrl?: string;
|
|
20
|
+
/** The worker's scope. Defaults to the directory `workerUrl` is in. */
|
|
21
|
+
scope?: string;
|
|
22
|
+
/** `'module'` when `workerUrl` is an ES module worker of your own. */
|
|
23
|
+
type?: WorkerType;
|
|
24
|
+
};
|
|
25
|
+
/** What a `SharedSdk` was connected with. */
|
|
26
|
+
type SharedCredentials = {
|
|
27
|
+
indexerUrl: string;
|
|
28
|
+
seed: string;
|
|
29
|
+
};
|
|
30
|
+
/** What an `Sdk`'s `Builder` was made with. The key comes from `sdk.appKey()`. */
|
|
31
|
+
type AppCredentials = {
|
|
32
|
+
indexerUrl: string;
|
|
33
|
+
appMeta: AppMetadata;
|
|
34
|
+
};
|
|
35
|
+
/** The credentials `openStreams` takes for each kind of SDK. */
|
|
36
|
+
type CredentialsFor<S extends Sdk | SharedSdk> = S extends SharedSdk ? SharedCredentials : AppCredentials;
|
|
37
|
+
type FileOptions = {
|
|
38
|
+
/** Used for downloads and for the browser's media handling. */
|
|
39
|
+
name: string;
|
|
40
|
+
/** The MIME type. Defaults to `application/octet-stream`. */
|
|
41
|
+
type?: string;
|
|
42
|
+
/** A failure after streaming started. Without it, failures are logged. */
|
|
43
|
+
onError?: (message: string) => void;
|
|
44
|
+
/** Bytes read so far, when the page reads the object itself. */
|
|
45
|
+
onProgress?: (bytesDownloaded: number) => void;
|
|
46
|
+
};
|
|
47
|
+
/** A URL for an `<img>`, `<video>`, `<audio>`, `<iframe>` or `fetch`. */
|
|
48
|
+
type StreamedFile = {
|
|
49
|
+
url: string;
|
|
50
|
+
/** The whole file, when the page read it itself instead of streaming. */
|
|
51
|
+
blob?: Blob;
|
|
52
|
+
/** Frees the URL. Call it when the element showing it goes away. */
|
|
53
|
+
release(): void;
|
|
54
|
+
};
|
|
55
|
+
type Streams = {
|
|
56
|
+
/** A URL serving the object. Streams where it can, else reads it whole. */
|
|
57
|
+
url(object: PinnedObject, options: FileOptions & {
|
|
58
|
+
signal?: AbortSignal;
|
|
59
|
+
}): Promise<StreamedFile>;
|
|
60
|
+
/**
|
|
61
|
+
* Saves the object, from a click handler. Streams to the browser's download
|
|
62
|
+
* manager where it can, else opens the save picker, else saves a Blob.
|
|
63
|
+
* Awaits nothing before the picker opens, so the browser still counts it as
|
|
64
|
+
* part of the click.
|
|
65
|
+
*/
|
|
66
|
+
download(object: PinnedObject, options: FileOptions): Promise<'streaming' | 'saved' | 'cancelled'>;
|
|
67
|
+
/**
|
|
68
|
+
* Cancels the streams this handle made. Other handles on the same SDK keep
|
|
69
|
+
* theirs. Call it when you stop using the handle.
|
|
70
|
+
*/
|
|
71
|
+
close(): void;
|
|
72
|
+
};
|
|
73
|
+
/**
|
|
74
|
+
* Registers the streaming worker and resolves whether this page can stream.
|
|
75
|
+
* `openStreams` calls it with the defaults, so call it yourself only to pass
|
|
76
|
+
* options, and before the first `openStreams`. Later calls return the first
|
|
77
|
+
* call's answer, except when setup failed or timed out and a different worker
|
|
78
|
+
* has since taken control of the page. Then the next call tries again, with
|
|
79
|
+
* the first call's options.
|
|
80
|
+
*/
|
|
81
|
+
declare function enableStreaming(options?: StreamingOptions): Promise<boolean>;
|
|
82
|
+
/**
|
|
83
|
+
* Previews and downloads for one SDK's objects, from an `Sdk` or a `SharedSdk`.
|
|
84
|
+
*
|
|
85
|
+
* The worker cannot use this page's SDK, so it connects its own with
|
|
86
|
+
* `credentials`: the indexer URL and seed a `SharedSdk` was connected with, or
|
|
87
|
+
* the indexer URL and app metadata an `Sdk`'s `Builder` was made with.
|
|
88
|
+
*/
|
|
89
|
+
declare function openStreams<S extends Sdk | SharedSdk>(sdk: S, credentials: CredentialsFor<S>): Streams;
|
|
2
90
|
|
|
3
91
|
/** Initialize the WASM module. Safe to call multiple times. */
|
|
4
92
|
declare function initSia(): Promise<void>;
|
|
5
93
|
|
|
6
|
-
export { initSia };
|
|
94
|
+
export { type AppCredentials, type CredentialsFor, type FileOptions, type SharedCredentials, type StreamedFile, type StreamingOptions, type Streams, detectPrivateRelay, enableStreaming, initSia, openStreams };
|