@siafoundation/sia-storage 0.1.0 → 0.1.2
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 +93 -1
- 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 +89 -1
- package/dist/index.js +303 -0
- package/dist/index.js.map +1 -1
- package/dist/index.node.cjs +52 -12
- package/dist/index.node.cjs.map +1 -1
- package/dist/index.node.d.ts +32 -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 +26 -10
- package/wasm/sia_storage_wasm.d.ts +21 -12
- package/wasm/sia_storage_wasm.js +33 -32
- package/wasm/sia_storage_wasm_bg.wasm +0 -0
- package/wasm/sia_storage_wasm_bg.wasm.d.ts +8 -8
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
|
|
@@ -116,6 +190,7 @@ Near-identical surfaces. Real differences:
|
|
|
116
190
|
- Sharing key seeds are hex `string` on browser, `Buffer` on Node. This covers `SharingKey.seed()`, `SharingKey.fromSeed(seed)`, and `SharedSdk.connect(indexerUrl, seed)`.
|
|
117
191
|
- `sdk.unshareObject(key, object)` takes a `PinnedObject` on browser and an object id `string` on Node.
|
|
118
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.
|
|
119
194
|
|
|
120
195
|
## API
|
|
121
196
|
|
|
@@ -128,6 +203,7 @@ Near-identical surfaces. Real differences:
|
|
|
128
203
|
| `validateRecoveryPhrase(phrase)` | Throws on invalid. |
|
|
129
204
|
| `setLogger(callback, level)` | Receive SDK logs. |
|
|
130
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. |
|
|
131
207
|
|
|
132
208
|
### `Sdk`
|
|
133
209
|
|
|
@@ -207,6 +283,22 @@ sharing key's seed. Downloads are paid for by the key's owner.
|
|
|
207
283
|
| `download(object, options?)` | Returns a `ReadableStream`. |
|
|
208
284
|
| `hosts(query?)` | Hosts serving this key's objects. |
|
|
209
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
|
+
|
|
210
302
|
## License
|
|
211
303
|
|
|
212
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
|
+
import { AppMetadata, Sdk, SharedSdk, PinnedObject } from '../wasm/sia_storage_wasm.js';
|
|
1
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';
|
|
2
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;
|
|
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 };
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,305 @@
|
|
|
1
|
+
import {
|
|
2
|
+
isAppMetadata,
|
|
3
|
+
reportStreamError,
|
|
4
|
+
requestReply,
|
|
5
|
+
streamErrorMessage,
|
|
6
|
+
streamPath
|
|
7
|
+
} from "./chunk-QYQQIWFH.js";
|
|
8
|
+
|
|
1
9
|
// src/index.ts
|
|
2
10
|
import wasmInit from "../wasm/sia_storage_wasm.js";
|
|
11
|
+
|
|
12
|
+
// src/relay.ts
|
|
13
|
+
var RELAY_CHECK_URL = "https://check-relay.sia.storage";
|
|
14
|
+
var TIMEOUT = 5e3;
|
|
15
|
+
var answer;
|
|
16
|
+
function detectPrivateRelay() {
|
|
17
|
+
answer ??= typeof navigator === "undefined" ? Promise.resolve(false) : checkPrivateRelay(navigator.userAgent, fetch);
|
|
18
|
+
return answer;
|
|
19
|
+
}
|
|
20
|
+
async function checkPrivateRelay(userAgent, request) {
|
|
21
|
+
const safari = /Version\/\S+.*Safari\//.test(userAgent) && !/Chrome|Chromium|CriOS|FxiOS|Edg|OPR|OPiOS|Ddg\/|Android/.test(userAgent);
|
|
22
|
+
if (!safari) return false;
|
|
23
|
+
try {
|
|
24
|
+
const response = await request(RELAY_CHECK_URL, {
|
|
25
|
+
signal: AbortSignal.timeout(TIMEOUT)
|
|
26
|
+
});
|
|
27
|
+
const body = await response.json();
|
|
28
|
+
return body.relay === true;
|
|
29
|
+
} catch {
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// src/stream/fallback.ts
|
|
35
|
+
function chunks(stream) {
|
|
36
|
+
return stream.getReader();
|
|
37
|
+
}
|
|
38
|
+
async function readBlob(sdk, object, type, onProgress, signal) {
|
|
39
|
+
const reader = chunks(sdk.download(object));
|
|
40
|
+
const onAbort = () => void reader.cancel();
|
|
41
|
+
signal.addEventListener("abort", onAbort);
|
|
42
|
+
const parts = [];
|
|
43
|
+
let bytesDownloaded = 0;
|
|
44
|
+
try {
|
|
45
|
+
while (true) {
|
|
46
|
+
const { done, value } = await reader.read();
|
|
47
|
+
if (done) break;
|
|
48
|
+
parts.push(value);
|
|
49
|
+
bytesDownloaded += value.length;
|
|
50
|
+
onProgress(bytesDownloaded);
|
|
51
|
+
}
|
|
52
|
+
} finally {
|
|
53
|
+
signal.removeEventListener("abort", onAbort);
|
|
54
|
+
}
|
|
55
|
+
signal.throwIfAborted();
|
|
56
|
+
return new Blob(parts, type ? { type } : {});
|
|
57
|
+
}
|
|
58
|
+
function saveBlob(blob, name) {
|
|
59
|
+
const url = URL.createObjectURL(blob);
|
|
60
|
+
const a = document.createElement("a");
|
|
61
|
+
a.href = url;
|
|
62
|
+
a.download = name;
|
|
63
|
+
a.click();
|
|
64
|
+
setTimeout(() => URL.revokeObjectURL(url), 0);
|
|
65
|
+
}
|
|
66
|
+
async function saveToDisk(sdk, object, name, type, onProgress) {
|
|
67
|
+
const picker = window.showSaveFilePicker;
|
|
68
|
+
if (!picker) {
|
|
69
|
+
const blob = await readBlob(
|
|
70
|
+
sdk,
|
|
71
|
+
object,
|
|
72
|
+
type,
|
|
73
|
+
onProgress,
|
|
74
|
+
new AbortController().signal
|
|
75
|
+
);
|
|
76
|
+
saveBlob(blob, name);
|
|
77
|
+
return true;
|
|
78
|
+
}
|
|
79
|
+
let handle;
|
|
80
|
+
try {
|
|
81
|
+
handle = await picker.call(window, { suggestedName: name });
|
|
82
|
+
} catch (e) {
|
|
83
|
+
if (e instanceof DOMException && e.name === "AbortError") return false;
|
|
84
|
+
throw e;
|
|
85
|
+
}
|
|
86
|
+
let bytesDownloaded = 0;
|
|
87
|
+
const progress = new TransformStream({
|
|
88
|
+
transform(chunk, controller) {
|
|
89
|
+
bytesDownloaded += chunk.length;
|
|
90
|
+
onProgress(bytesDownloaded);
|
|
91
|
+
controller.enqueue(chunk);
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
const file = await handle.createWritable();
|
|
95
|
+
await sdk.download(object).pipeThrough(progress).pipeTo(file);
|
|
96
|
+
return true;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// src/stream/page.ts
|
|
100
|
+
var DEFAULT_WORKER_URL = "/sia-storage-sw.js";
|
|
101
|
+
var SETUP_TIMEOUT = 5e3;
|
|
102
|
+
var entries = /* @__PURE__ */ new Map();
|
|
103
|
+
var opened = /* @__PURE__ */ new Set();
|
|
104
|
+
var worker;
|
|
105
|
+
var initialization;
|
|
106
|
+
var initOptions = {};
|
|
107
|
+
var failedController;
|
|
108
|
+
function enableStreaming(options = {}) {
|
|
109
|
+
if (!initialization) {
|
|
110
|
+
initOptions = options;
|
|
111
|
+
} else if (failedController === void 0) {
|
|
112
|
+
return initialization;
|
|
113
|
+
} else {
|
|
114
|
+
const controller = navigator.serviceWorker.controller;
|
|
115
|
+
if (!controller || controller === failedController) return initialization;
|
|
116
|
+
}
|
|
117
|
+
failedController = void 0;
|
|
118
|
+
initialization = initialize(initOptions).catch(() => {
|
|
119
|
+
failedController = navigator.serviceWorker.controller;
|
|
120
|
+
return false;
|
|
121
|
+
});
|
|
122
|
+
return initialization;
|
|
123
|
+
}
|
|
124
|
+
async function initialize({
|
|
125
|
+
workerUrl = DEFAULT_WORKER_URL,
|
|
126
|
+
scope,
|
|
127
|
+
type
|
|
128
|
+
}) {
|
|
129
|
+
if (typeof window === "undefined" || !("serviceWorker" in navigator) || !window.isSecureContext) {
|
|
130
|
+
return false;
|
|
131
|
+
}
|
|
132
|
+
const { serviceWorker } = navigator;
|
|
133
|
+
serviceWorker.addEventListener("message", onWorkerMessage);
|
|
134
|
+
const deadline = AbortSignal.timeout(SETUP_TIMEOUT);
|
|
135
|
+
const existing = await within(serviceWorker.getRegistration(), deadline);
|
|
136
|
+
if (existing?.active && !serviceWorker.controller) return false;
|
|
137
|
+
const registration = await within(
|
|
138
|
+
serviceWorker.register(workerUrl, { ...scope && { scope }, ...type && { type } }).then(() => serviceWorker.ready),
|
|
139
|
+
deadline
|
|
140
|
+
);
|
|
141
|
+
if (!serviceWorker.controller) {
|
|
142
|
+
await within(
|
|
143
|
+
new Promise(
|
|
144
|
+
(resolve) => serviceWorker.addEventListener("controllerchange", resolve, {
|
|
145
|
+
once: true
|
|
146
|
+
})
|
|
147
|
+
),
|
|
148
|
+
deadline
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
const controller = serviceWorker.controller;
|
|
152
|
+
if (!controller) return false;
|
|
153
|
+
const request = { type: "sia-client" };
|
|
154
|
+
const reply = await requestReply(controller, request, {
|
|
155
|
+
signal: deadline
|
|
156
|
+
});
|
|
157
|
+
if (!reply.supported) return false;
|
|
158
|
+
worker = { clientId: reply.clientId, scope: registration.scope };
|
|
159
|
+
return true;
|
|
160
|
+
}
|
|
161
|
+
function within(promise, signal) {
|
|
162
|
+
return new Promise((resolve, reject) => {
|
|
163
|
+
signal.throwIfAborted();
|
|
164
|
+
signal.addEventListener("abort", () => reject(signal.reason), {
|
|
165
|
+
once: true
|
|
166
|
+
});
|
|
167
|
+
promise.then(resolve, reject);
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
function openStreams(sdk, credentials) {
|
|
171
|
+
const handle = openedFor(sdk, credentials);
|
|
172
|
+
opened.add(handle);
|
|
173
|
+
void enableStreaming();
|
|
174
|
+
return {
|
|
175
|
+
async url(object, options) {
|
|
176
|
+
await enableStreaming();
|
|
177
|
+
options.signal?.throwIfAborted();
|
|
178
|
+
const stream = streamUrl(handle, object, options);
|
|
179
|
+
if (stream) return stream;
|
|
180
|
+
const blob = await readBlob(
|
|
181
|
+
sdk,
|
|
182
|
+
object,
|
|
183
|
+
options.type,
|
|
184
|
+
options.onProgress ?? (() => {
|
|
185
|
+
}),
|
|
186
|
+
options.signal ?? new AbortController().signal
|
|
187
|
+
);
|
|
188
|
+
const url = URL.createObjectURL(blob);
|
|
189
|
+
return { url, blob, release: () => URL.revokeObjectURL(url) };
|
|
190
|
+
},
|
|
191
|
+
async download(object, options) {
|
|
192
|
+
const stream = streamUrl(handle, object, options);
|
|
193
|
+
if (stream) {
|
|
194
|
+
const frame = document.createElement("iframe");
|
|
195
|
+
frame.hidden = true;
|
|
196
|
+
frame.src = `${stream.url}?download=1`;
|
|
197
|
+
document.body.append(frame);
|
|
198
|
+
return "streaming";
|
|
199
|
+
}
|
|
200
|
+
const saved = await saveToDisk(
|
|
201
|
+
sdk,
|
|
202
|
+
object,
|
|
203
|
+
options.name,
|
|
204
|
+
options.type,
|
|
205
|
+
options.onProgress ?? (() => {
|
|
206
|
+
})
|
|
207
|
+
);
|
|
208
|
+
return saved ? "saved" : "cancelled";
|
|
209
|
+
},
|
|
210
|
+
close() {
|
|
211
|
+
releaseStreams(handle);
|
|
212
|
+
}
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
function openedFor(sdk, credentials) {
|
|
216
|
+
const { indexerUrl, seed, appMeta } = credentials ?? {};
|
|
217
|
+
const session = crypto.randomUUID();
|
|
218
|
+
if (typeof sdk.appKey === "function") {
|
|
219
|
+
if (typeof indexerUrl !== "string" || !isAppMetadata(appMeta)) {
|
|
220
|
+
throw new TypeError("openStreams(Sdk) needs { indexerUrl, appMeta }.");
|
|
221
|
+
}
|
|
222
|
+
const appKey = sdk.appKey();
|
|
223
|
+
const connection = {
|
|
224
|
+
kind: "app",
|
|
225
|
+
indexerUrl,
|
|
226
|
+
appKey: toHex(appKey.export()),
|
|
227
|
+
appMeta
|
|
228
|
+
};
|
|
229
|
+
return { session, connection, appKey };
|
|
230
|
+
}
|
|
231
|
+
if (typeof indexerUrl !== "string" || typeof seed !== "string") {
|
|
232
|
+
throw new TypeError("openStreams(SharedSdk) needs { indexerUrl, seed }.");
|
|
233
|
+
}
|
|
234
|
+
return { session, connection: { kind: "shared", indexerUrl, seed } };
|
|
235
|
+
}
|
|
236
|
+
function toHex(bytes) {
|
|
237
|
+
return Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
238
|
+
}
|
|
239
|
+
function streamUrl(handle, object, { name, type, onError }) {
|
|
240
|
+
if (!worker || !navigator.serviceWorker.controller || !opened.has(handle)) {
|
|
241
|
+
return void 0;
|
|
242
|
+
}
|
|
243
|
+
const token = crypto.randomUUID();
|
|
244
|
+
entries.set(token, {
|
|
245
|
+
handle,
|
|
246
|
+
objectId: object.id(),
|
|
247
|
+
sealed: handle.appKey && object.seal(handle.appKey),
|
|
248
|
+
size: object.size(),
|
|
249
|
+
name,
|
|
250
|
+
mime: type || "application/octet-stream",
|
|
251
|
+
onError
|
|
252
|
+
});
|
|
253
|
+
return {
|
|
254
|
+
url: streamPath(worker.scope, worker.clientId, token),
|
|
255
|
+
release() {
|
|
256
|
+
entries.delete(token);
|
|
257
|
+
const message = { type: "sia-release", token };
|
|
258
|
+
navigator.serviceWorker.controller?.postMessage(message);
|
|
259
|
+
}
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
function releaseStreams(handle) {
|
|
263
|
+
for (const [token, entry] of entries) {
|
|
264
|
+
if (entry.handle === handle) entries.delete(token);
|
|
265
|
+
}
|
|
266
|
+
if (!opened.delete(handle)) return;
|
|
267
|
+
const message = {
|
|
268
|
+
type: "sia-release-share",
|
|
269
|
+
session: handle.session
|
|
270
|
+
};
|
|
271
|
+
navigator.serviceWorker.controller?.postMessage(message);
|
|
272
|
+
}
|
|
273
|
+
function onWorkerMessage(event) {
|
|
274
|
+
if (event.source !== navigator.serviceWorker.controller) return;
|
|
275
|
+
const data = event.data;
|
|
276
|
+
if (data?.type === "sia-stream-error") {
|
|
277
|
+
const entry = entries.get(data.token);
|
|
278
|
+
if (!entry) return;
|
|
279
|
+
const { offset, length } = data;
|
|
280
|
+
const range = offset !== void 0 && length !== void 0 ? { offset, length } : void 0;
|
|
281
|
+
if (entry.onError) entry.onError(streamErrorMessage(data.message));
|
|
282
|
+
else reportStreamError(data.message, entry.name, range);
|
|
283
|
+
} else if (data?.type === "sia-source") {
|
|
284
|
+
const port = event.ports[0];
|
|
285
|
+
if (!port) return;
|
|
286
|
+
const entry = entries.get(data.token);
|
|
287
|
+
const reply = entry ? {
|
|
288
|
+
type: "source",
|
|
289
|
+
session: entry.handle.session,
|
|
290
|
+
connection: entry.handle.connection,
|
|
291
|
+
objectId: entry.objectId,
|
|
292
|
+
...entry.sealed && { sealed: entry.sealed },
|
|
293
|
+
size: entry.size,
|
|
294
|
+
mime: entry.mime,
|
|
295
|
+
name: entry.name
|
|
296
|
+
} : { type: "error" };
|
|
297
|
+
port.postMessage(reply);
|
|
298
|
+
port.close();
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// src/index.ts
|
|
3
303
|
import {
|
|
4
304
|
AppKey,
|
|
5
305
|
Builder,
|
|
@@ -30,9 +330,12 @@ export {
|
|
|
30
330
|
Sdk,
|
|
31
331
|
SharedSdk,
|
|
32
332
|
SharingKey,
|
|
333
|
+
detectPrivateRelay,
|
|
334
|
+
enableStreaming,
|
|
33
335
|
encodedSize,
|
|
34
336
|
generateRecoveryPhrase,
|
|
35
337
|
initSia,
|
|
338
|
+
openStreams,
|
|
36
339
|
setLogger,
|
|
37
340
|
validateRecoveryPhrase
|
|
38
341
|
};
|