@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 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** — production builds work as-is. For `vite dev`, exclude the package from the dep pre-bundler so its `import.meta.url`-relative WASM path resolves correctly:
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.connected()`.
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
- | `shareObject(object, validUntil)` / `sharedObject(url)` | Create / consume share URLs. |
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
- export { Account, AppKey, AppMetadata, Builder, DownloadOptions, Host, HostQuery, ObjectEvent, PackedUpload, PinnedObject, PinnedSlab, Sdk, SealedObject, Sector, ShardProgress, Slab, UploadOptions, encodedSize, generateRecoveryPhrase, setLogger, validateRecoveryPhrase } from '../wasm/sia_storage_wasm.js';
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 };