@hostr/upload 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +298 -0
- package/dist/api/client.d.ts +71 -0
- package/dist/browser/index.d.ts +50 -0
- package/dist/browser.js +1061 -0
- package/dist/core/engine.d.ts +150 -0
- package/dist/core/events.d.ts +33 -0
- package/dist/core/retry.d.ts +20 -0
- package/dist/core/session.d.ts +28 -0
- package/dist/core/types.d.ts +44 -0
- package/dist/core/upload.d.ts +32 -0
- package/dist/hash/composite.d.ts +2 -0
- package/dist/hash/hasher.d.ts +2 -0
- package/dist/hash/hex.d.ts +2 -0
- package/dist/hash/md5.d.ts +24 -0
- package/dist/hash/sha256.d.ts +25 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +1012 -0
- package/dist/node/hasher.d.ts +2 -0
- package/dist/node/index.d.ts +43 -0
- package/dist/node.js +1089 -0
- package/dist/react-native/background-plan.d.ts +57 -0
- package/dist/react-native/background.d.ts +126 -0
- package/dist/react-native/fs.d.ts +28 -0
- package/dist/react-native/index.d.ts +69 -0
- package/dist/react-native/native-upload.d.ts +76 -0
- package/dist/react-native.js +896 -0
- package/package.json +48 -0
package/README.md
ADDED
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
# @hostr/upload
|
|
2
|
+
|
|
3
|
+
Resumable, presigned multipart uploads straight to R2 — bytes never transit
|
|
4
|
+
the API Worker. Four entry points, each bundled independently:
|
|
5
|
+
|
|
6
|
+
| Entry | Platform | Adapter-specific job |
|
|
7
|
+
|---|---|---|
|
|
8
|
+
| `@hostr/upload` | trusted backend only | `createUploadSession`, `mintUploadToken` — needs `HOSTR_API_KEY` |
|
|
9
|
+
| `@hostr/upload/browser` | web | XHR transport (for upload-progress events), `localStorage` session store |
|
|
10
|
+
| `@hostr/upload/node` | Node.js, scripts | `fetch` transport, filesystem session store |
|
|
11
|
+
| `@hostr/upload/react-native` | Expo / RN | native staged-parts transport; every upload survives backgrounding |
|
|
12
|
+
|
|
13
|
+
The engine, retry policy, and integrity checks are one shared core
|
|
14
|
+
(`packages/upload/src/core`); adapters only supply platform I/O (read bytes,
|
|
15
|
+
PUT bytes, persist a few KB of JSON). (The RN adapter is the exception: it
|
|
16
|
+
bypasses the engine entirely for OS-carried native transfers — see the
|
|
17
|
+
React Native section.)
|
|
18
|
+
|
|
19
|
+
**One rule for clients of a proxying backend:** take the hostr `endpoint`
|
|
20
|
+
from your backend's create-upload response (have your backend include its
|
|
21
|
+
own `HOSTR_API_BASE`), never from separate client config. An upload token is
|
|
22
|
+
only valid on the hostr instance that minted it, and endpoint drift fails as
|
|
23
|
+
a bare mid-flow `401` with no hint of the cause — returning the endpoint
|
|
24
|
+
with the session makes the drift structurally impossible. (Learned by the
|
|
25
|
+
first consumer integration, 2026-08-22.)
|
|
26
|
+
|
|
27
|
+
## Quickstart
|
|
28
|
+
|
|
29
|
+
### Browser
|
|
30
|
+
|
|
31
|
+
```js
|
|
32
|
+
import { createUploadSession, createUpload } from '@hostr/upload/browser';
|
|
33
|
+
|
|
34
|
+
// Trusted-backend call — holds HOSTR_API_KEY. Runs on your server, not in
|
|
35
|
+
// the bundle you ship to users; shown inline here only for brevity.
|
|
36
|
+
const session = await createUploadSession({
|
|
37
|
+
endpoint, apiKey, filename: file.name, contentType: file.type || 'video/mp4', size: file.size,
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
// From here on the browser holds only a scoped token — never the master key.
|
|
41
|
+
const upload = await createUpload({ endpoint, uploadToken: session.uploadToken, assetId: session.assetId, file });
|
|
42
|
+
|
|
43
|
+
upload.on('progress', p => {
|
|
44
|
+
console.log(`${p.partsDone}/${p.partsTotal} parts, ${(p.bytesConfirmed / p.totalBytes * 100).toFixed(1)}%`);
|
|
45
|
+
for (const [n, st] of Object.entries(p.partStates ?? {})) mark(Number(n), st); // 'inflight' | 'done', for a per-part grid
|
|
46
|
+
});
|
|
47
|
+
upload.on('statechange', s => console.log('state:', s)); // idle → running → (stalled ↔ running)* → completing → complete
|
|
48
|
+
upload.start();
|
|
49
|
+
|
|
50
|
+
const { etag, sha256 } = await upload.done;
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Session state (assetId, part progress, hash midstate) persists under
|
|
54
|
+
`localStorage` keyed by `name:size:lastModified`. Reload the page, pick the
|
|
55
|
+
same file again, and `createUpload` resumes it automatically — no explicit
|
|
56
|
+
"resume" call. See `apps/playground/upload.html` for the full flow, including
|
|
57
|
+
re-minting a token for a saved session with `mintUploadToken`.
|
|
58
|
+
|
|
59
|
+
### Node
|
|
60
|
+
|
|
61
|
+
```js
|
|
62
|
+
import { createUploadSession, createUpload } from '@hostr/upload/node';
|
|
63
|
+
|
|
64
|
+
const session = await createUploadSession({
|
|
65
|
+
endpoint, apiKey, filename: 'video.mp4', contentType: 'video/mp4', size, partSize: PART_SIZE,
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
const upload = await createUpload({
|
|
69
|
+
endpoint, uploadToken: session.uploadToken, assetId: session.assetId,
|
|
70
|
+
file: '/path/to/video.mp4', // filesystem path, not an open handle
|
|
71
|
+
stateDir: './upload-state', // survives a process restart; defaults to os.tmpdir()/hostr-upload
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
upload.start();
|
|
75
|
+
const { etag, sha256 } = await upload.done;
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
`infra/scripts/test-upload.mjs` is the canonical example — it drives the SDK
|
|
79
|
+
through a kill/resume cycle and a corrupted-part self-heal against a real API
|
|
80
|
+
and real R2.
|
|
81
|
+
|
|
82
|
+
### React Native
|
|
83
|
+
|
|
84
|
+
Requires `expo-file-system` >= 57 (Expo SDK 57): the adapter rides the modern
|
|
85
|
+
File/Directory API — `FileHandle` byte-window reads, native per-part `md5`,
|
|
86
|
+
`File.createUploadTask` background sessions — with no fallback to the removed
|
|
87
|
+
`expo-file-system/legacy` surface. Older SDKs should pin an older version of
|
|
88
|
+
this package.
|
|
89
|
+
|
|
90
|
+
There is **one upload path on RN**. Parts are staged natively and carried by
|
|
91
|
+
OS background-session tasks, so every upload keeps transferring if the app is
|
|
92
|
+
backgrounded — that is not a mode, it's a property. The engine-style surface
|
|
93
|
+
(`progress`/`statechange` events, `pause`/`resume`/`abort`, a `done` promise)
|
|
94
|
+
wraps it. RN uploads record no whole-file sha256; the composite etag —
|
|
95
|
+
verified per part and again at complete — is the integrity check.
|
|
96
|
+
|
|
97
|
+
```tsx
|
|
98
|
+
import { createUploadSession, mintUploadToken } from '@hostr/upload';
|
|
99
|
+
import { createUpload, asyncStorageStore, reconcileOnLaunch } from '@hostr/upload/react-native';
|
|
100
|
+
import AsyncStorage from '@react-native-async-storage/async-storage';
|
|
101
|
+
|
|
102
|
+
const store = asyncStorageStore(AsyncStorage); // REQUIRED — RN has no default persisted store
|
|
103
|
+
|
|
104
|
+
// Once, at app launch, before the user has picked anything — recovers any
|
|
105
|
+
// upload a previous launch left mid-flight (including after a force-kill).
|
|
106
|
+
useEffect(() => {
|
|
107
|
+
reconcileOnLaunch(store, { endpoint }).catch(() => {}); // best-effort; a real failure just waits for the next launch
|
|
108
|
+
}, []);
|
|
109
|
+
|
|
110
|
+
const session = await createUploadSession({ endpoint, apiKey, filename, contentType, size }); // trusted backend
|
|
111
|
+
const refreshToken = assetId => mintUploadToken({ endpoint, apiKey, assetId }).then(r => r.uploadToken);
|
|
112
|
+
|
|
113
|
+
const upload = await createUpload({
|
|
114
|
+
endpoint, uploadToken: session.uploadToken, assetId: session.assetId,
|
|
115
|
+
file, // { uri, size, modificationTime } from expo-document-picker / expo-image-picker
|
|
116
|
+
store,
|
|
117
|
+
onTokenExpired: refreshToken,
|
|
118
|
+
});
|
|
119
|
+
upload.on('progress', setProgress);
|
|
120
|
+
upload.start();
|
|
121
|
+
const { etag } = await upload.done; // resolved by the auto-reconcile that runs as the last part lands
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
For headless control (no events, no `done` promise — e.g. an upload queue
|
|
125
|
+
worker), `startBackgroundUpload` returns the underlying handle directly:
|
|
126
|
+
`reconcile()`, `pause()`, `abort()`, and session geometry. `reconcileOnLaunch`
|
|
127
|
+
is the mandatory recovery hook either way.
|
|
128
|
+
|
|
129
|
+
`apps/expo-playground/App.tsx` exercises the path end to end, with a manual
|
|
130
|
+
"Reconcile now" button for the force-kill recovery leg.
|
|
131
|
+
|
|
132
|
+
## Auth flow
|
|
133
|
+
|
|
134
|
+
```
|
|
135
|
+
trusted backend (holds HOSTR_API_KEY)
|
|
136
|
+
│ POST /v1/uploads ──createUploadSession()──▶ { assetId, uploadToken, tokenExpiresAt, partSize, expectedParts }
|
|
137
|
+
▼
|
|
138
|
+
client (browser / Node / RN) — holds only { endpoint, uploadToken, assetId }, never the master key
|
|
139
|
+
│ createUpload({ endpoint, uploadToken, assetId, file, ... })
|
|
140
|
+
▼
|
|
141
|
+
GET /v1/uploads/:id · POST /v1/uploads/:id/parts · POST /v1/uploads/:id/complete
|
|
142
|
+
— every call authenticates with uploadToken; the API accepts either the
|
|
143
|
+
master key or an HMAC token scoped to exactly that assetId (401 on any
|
|
144
|
+
other asset — verified by infra/scripts/test-upload.mjs's leg 4)
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
The split matters because `createUploadSession`/`mintUploadToken` are the
|
|
148
|
+
only two calls in this package that take `apiKey` — everything else takes
|
|
149
|
+
`uploadToken`. Never ship `apiKey` to an untrusted client; it is the same
|
|
150
|
+
credential that can create and delete any asset.
|
|
151
|
+
|
|
152
|
+
**Token TTL** defaults to 72 h, clamped 5 min–7 d server-side
|
|
153
|
+
(`clampTokenTtl` in `apps/api/src/limits.ts`) — pass `tokenTtlSec` to
|
|
154
|
+
`createUploadSession` or `mintUploadToken` to override, within that range.
|
|
155
|
+
|
|
156
|
+
**Token expiry mid-upload.** A large file can outlive a 72 h token. Pass
|
|
157
|
+
`onTokenExpired: async (assetId) => newToken` to `createUpload` /
|
|
158
|
+
`startBackgroundUpload`; the engine calls it on the first 401 from any API
|
|
159
|
+
call, swaps in the returned token, and retries — no restart, no lost
|
|
160
|
+
progress. The hook's server-side counterpart is `POST /v1/uploads/:id/token`
|
|
161
|
+
(`mintUploadToken`), which is **master-key-only**: the untrusted client can
|
|
162
|
+
never mint its own replacement, only ask the trusted backend to.
|
|
163
|
+
|
|
164
|
+
## Integrity: three layers
|
|
165
|
+
|
|
166
|
+
Each layer proves a different thing; none subsumes the others.
|
|
167
|
+
|
|
168
|
+
1. **Per-part MD5 vs R2's ETag.** Checked immediately after every `PUT` — a
|
|
169
|
+
2xx response whose returned ETag doesn't match the client's MD5 of the
|
|
170
|
+
bytes it just sent is treated as `corrupt`, not success, and the part is
|
|
171
|
+
re-sent (see `infra/scripts/test-upload.mjs` leg 3, which flips a byte on
|
|
172
|
+
the wire and watches the engine self-heal). Proves: *these specific bytes,
|
|
173
|
+
for this one part, landed uncorrupted.*
|
|
174
|
+
2. **Composite ETag at complete.** R2's multipart ETag is
|
|
175
|
+
`md5(concat(part MD5s)) + "-" + partCount` — computed client-side
|
|
176
|
+
(`compositeEtag`) from the same per-part MD5s and compared against what
|
|
177
|
+
`POST /complete` actually returns from R2's `CompleteMultipartUpload`.
|
|
178
|
+
Proves: *the parts assembled, in order, into one coherent object* — not
|
|
179
|
+
just that each part landed individually. This is also why multipart
|
|
180
|
+
ETags are **not** a portable content identity (CLAUDE.md's "Environment
|
|
181
|
+
gotchas"): the same file uploaded with a different part size produces a
|
|
182
|
+
different composite ETag.
|
|
183
|
+
3. **Whole-file SHA-256.** Hashed incrementally as bytes are read — off the
|
|
184
|
+
part-size grid entirely — and included in `POST /complete`, stored as
|
|
185
|
+
asset metadata. Proves: *the identity of the file itself*, independent of
|
|
186
|
+
how it happened to be chunked or stored. This is the layer
|
|
187
|
+
`infra/scripts/verify-upload.mjs` uses to independently re-derive from
|
|
188
|
+
the bytes actually sitting in R2, with no input from the SDK at all.
|
|
189
|
+
Opt out with `checksum: 'none'` (skips the hash lane; use when content
|
|
190
|
+
identity isn't needed and the file is large enough that the extra read
|
|
191
|
+
pass matters).
|
|
192
|
+
|
|
193
|
+
## Resume semantics
|
|
194
|
+
|
|
195
|
+
**What survives a kill:** the session record (assetId, token, `partSize`,
|
|
196
|
+
which parts are confirmed done, SHA-256 hash midstate) is persisted to the
|
|
197
|
+
adapter's `store` on every part landing and token refresh. The hash lane's
|
|
198
|
+
own checkpoints (every 8 MiB) only update that in-memory record — they don't
|
|
199
|
+
trigger a save of their own; they ride along on the next part-completion
|
|
200
|
+
save, or on a pause/stop (which flushes midstate explicitly so a kill mid-hash
|
|
201
|
+
doesn't restart it from byte 0). Relaunching `createUpload` against
|
|
202
|
+
the *same file* (same `name:size:lastModified` / `path:size:mtime` /
|
|
203
|
+
`uri:size:modificationTime` — a modified file gets a fresh session instead
|
|
204
|
+
of a corrupted resume) picks the session back up automatically.
|
|
205
|
+
|
|
206
|
+
**What doesn't survive:** a part that was mid-flight at the moment of the
|
|
207
|
+
kill is not resumed partway — it re-uploads from the start of that part.
|
|
208
|
+
SHA-256 progress since the last checkpoint (every 8 MiB) is lost and
|
|
209
|
+
re-hashed from the checkpoint forward, not from byte 0.
|
|
210
|
+
|
|
211
|
+
**`GET /v1/uploads/:id`'s `parts[]` (R2's own `ListParts`) is the truth on
|
|
212
|
+
every resume — never the persisted `done` map by itself.** The engine's
|
|
213
|
+
`reconcile()` step, run before dispatching any work:
|
|
214
|
+
|
|
215
|
+
- drops a locally-claimed-done part that R2 doesn't actually have (stale
|
|
216
|
+
claim — never sent, or R2 expired the multipart upload);
|
|
217
|
+
- keeps a part whose stored MD5 matches R2's ETag for it (still verified);
|
|
218
|
+
- re-uploads a part whose stored MD5 does **not** match R2's ETag (the
|
|
219
|
+
landed bytes aren't provably ours);
|
|
220
|
+
- for a part R2 holds but local state has no record of at all — lost
|
|
221
|
+
storage, reinstall, a different device picking up the same file — **re-reads
|
|
222
|
+
and re-hashes that byte range locally**, and adopts it as done only if the
|
|
223
|
+
hash matches R2's ETag. Lost local state never gets a free pass; it has to
|
|
224
|
+
re-earn trust by re-hashing, exactly like a normal upload would.
|
|
225
|
+
|
|
226
|
+
## Mobile
|
|
227
|
+
|
|
228
|
+
### Android
|
|
229
|
+
|
|
230
|
+
No second transport, and this package ships no native Android code. A JS
|
|
231
|
+
engine keeps running for as long as something keeps the host process
|
|
232
|
+
alive — on Android that means running the upload inside a **foreground
|
|
233
|
+
service** with a visible notification (Android's own requirement for
|
|
234
|
+
long-running background work; this is what YouTube/Drive do). Wire one up
|
|
235
|
+
with `react-native-background-actions`-style patterns, or Expo's own
|
|
236
|
+
foreground-service APIs, and call the normal foreground `createUpload` from
|
|
237
|
+
inside it — the engine itself is unmodified; only what keeps the process
|
|
238
|
+
alive changes.
|
|
239
|
+
|
|
240
|
+
### iOS background caveats
|
|
241
|
+
|
|
242
|
+
`startBackgroundUpload` hands each remaining part to
|
|
243
|
+
`expo-file-system`'s iOS **background session** upload task, which the OS
|
|
244
|
+
keeps carrying even if the app is backgrounded or its JS runtime is
|
|
245
|
+
suspended — Expo ships the `AppDelegate` glue for this, so no native wiring
|
|
246
|
+
is needed in an Expo app.
|
|
247
|
+
|
|
248
|
+
- **A user force-killing the app cancels the OS's in-flight background
|
|
249
|
+
transfers outright.** This is documented `URLSessionUploadTask` behavior,
|
|
250
|
+
true for every app on iOS (YouTube included) — not a hostr limitation, and
|
|
251
|
+
not preventable from JS. `reconcileOnLaunch` on the next app start is the
|
|
252
|
+
recovery path, not a workaround: it re-derives from `ListParts` what
|
|
253
|
+
actually landed and re-stages the rest with a fresh presign.
|
|
254
|
+
- **Scheduling is discretionary.** The OS decides when a background transfer
|
|
255
|
+
actually runs — battery state, network conditions, and Low Power Mode can
|
|
256
|
+
all delay it. There is no guaranteed latency.
|
|
257
|
+
- **Completion callbacks are an optimization, never load-bearing.** A task's
|
|
258
|
+
completion handler, while the app happens to be alive to receive it, marks
|
|
259
|
+
a part done a little sooner and skips a round trip — but `reconcile()`
|
|
260
|
+
(via `ListParts`) is the only path anything is actually allowed to depend
|
|
261
|
+
on, since a callback after a kill is simply never delivered.
|
|
262
|
+
- **No whole-file SHA-256.** `startBackgroundUpload` never runs the hash lane
|
|
263
|
+
(there's no in-process read pass over the whole file to drive it), so a
|
|
264
|
+
background-completed upload's asset record has no `sha256`; integrity
|
|
265
|
+
rests on the per-part MD5 and composite ETag checks alone (layers 1 and 2
|
|
266
|
+
above — see "Integrity: three layers").
|
|
267
|
+
|
|
268
|
+
## Bundle identity: import error classes from the entry you use
|
|
269
|
+
|
|
270
|
+
`NetworkError` and `ApiError` (and the engine's `IntegrityError`) are
|
|
271
|
+
exported from every entry that re-exports `api/client.ts` — but **each
|
|
272
|
+
subpath entry is bundled independently** (`@hostr/upload`, `/browser`,
|
|
273
|
+
`/node`, `/react-native` are four separate build outputs). A class
|
|
274
|
+
constructed inside one entry's bundle is not `instanceof`-equal to the "same"
|
|
275
|
+
class imported from a different entry.
|
|
276
|
+
|
|
277
|
+
This matters concretely if you supply a custom `transport` — e.g. to inject
|
|
278
|
+
failures in a test, as `infra/scripts/test-upload.mjs` does — and want the
|
|
279
|
+
engine's retry logic to recognize a thrown error as network-class:
|
|
280
|
+
|
|
281
|
+
```js
|
|
282
|
+
import { NetworkError } from '@hostr/upload/node'; // matches the entry createUpload was imported from
|
|
283
|
+
// import { NetworkError } from '@hostr/upload'; // WRONG here — different bundle, fails the instanceof check
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
Always import `NetworkError`/`ApiError` from the same subpath you imported
|
|
287
|
+
`createUpload` from.
|
|
288
|
+
|
|
289
|
+
## See also
|
|
290
|
+
|
|
291
|
+
- Root `README.md`, "Upload API" section — the HTTP surface this SDK talks
|
|
292
|
+
to (`POST /v1/uploads`, `GET /v1/uploads/:id`, `.../parts`,
|
|
293
|
+
`.../complete`, `.../token`, `DELETE`).
|
|
294
|
+
- `pnpm build:sdk` — builds this package and vendors the browser bundle into
|
|
295
|
+
`apps/playground/vendor/` for the playground to import.
|
|
296
|
+
- `pnpm verify:upload <assetId>` — independently re-derives the composite
|
|
297
|
+
ETag and SHA-256 from the bytes actually sitting in R2, with no input from
|
|
298
|
+
this SDK.
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/** Typed client for the hostr upload API (apps/api). No transport concerns
|
|
2
|
+
* beyond fetch — retries, backoff, and offline handling live in the engine. */
|
|
3
|
+
export declare class ApiError extends Error {
|
|
4
|
+
readonly status: number;
|
|
5
|
+
readonly code?: string;
|
|
6
|
+
constructor(status: number, code?: string);
|
|
7
|
+
}
|
|
8
|
+
export declare class NetworkError extends Error {
|
|
9
|
+
constructor(message?: string);
|
|
10
|
+
}
|
|
11
|
+
export type RemotePart = {
|
|
12
|
+
partNumber: number;
|
|
13
|
+
size: number;
|
|
14
|
+
etag: string;
|
|
15
|
+
};
|
|
16
|
+
export type RemoteUpload = {
|
|
17
|
+
assetId: string;
|
|
18
|
+
status: string;
|
|
19
|
+
key: string;
|
|
20
|
+
partSize: number;
|
|
21
|
+
expectedParts: number;
|
|
22
|
+
parts: RemotePart[];
|
|
23
|
+
};
|
|
24
|
+
export type CreateResult = {
|
|
25
|
+
assetId: string;
|
|
26
|
+
uploadToken: string;
|
|
27
|
+
tokenExpiresAt: number;
|
|
28
|
+
partSize: number;
|
|
29
|
+
expectedParts: number;
|
|
30
|
+
key: string;
|
|
31
|
+
};
|
|
32
|
+
export type CompleteResult = {
|
|
33
|
+
status: string;
|
|
34
|
+
etag?: string;
|
|
35
|
+
bytes?: number;
|
|
36
|
+
};
|
|
37
|
+
export declare class HostrApi {
|
|
38
|
+
#private;
|
|
39
|
+
constructor(endpoint: string, bearer: string, fetchFn?: typeof fetch);
|
|
40
|
+
setBearer(bearer: string): void;
|
|
41
|
+
getUpload(assetId: string): Promise<RemoteUpload>;
|
|
42
|
+
presignParts(assetId: string, partNumbers: number[], expiresIn?: number): Promise<{
|
|
43
|
+
urls: {
|
|
44
|
+
partNumber: number;
|
|
45
|
+
url: string;
|
|
46
|
+
}[];
|
|
47
|
+
expiresIn: number;
|
|
48
|
+
}>;
|
|
49
|
+
complete(assetId: string, body: {
|
|
50
|
+
sha256?: string;
|
|
51
|
+
}): Promise<CompleteResult>;
|
|
52
|
+
abort(assetId: string): Promise<void>;
|
|
53
|
+
}
|
|
54
|
+
export declare function createUploadSession(opts: {
|
|
55
|
+
endpoint: string;
|
|
56
|
+
apiKey: string;
|
|
57
|
+
filename: string;
|
|
58
|
+
contentType?: string;
|
|
59
|
+
size: number;
|
|
60
|
+
partSize?: number;
|
|
61
|
+
tokenTtlSec?: number;
|
|
62
|
+
}, fetchFn?: typeof fetch): Promise<CreateResult>;
|
|
63
|
+
export declare function mintUploadToken(opts: {
|
|
64
|
+
endpoint: string;
|
|
65
|
+
apiKey: string;
|
|
66
|
+
assetId: string;
|
|
67
|
+
tokenTtlSec?: number;
|
|
68
|
+
}, fetchFn?: typeof fetch): Promise<{
|
|
69
|
+
uploadToken: string;
|
|
70
|
+
tokenExpiresAt: number;
|
|
71
|
+
}>;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/** Browser adapter. This is a subpath entry (`@hostr/upload/browser`), never
|
|
2
|
+
* re-exported from the package root — pulling DOM-only types (XHR, window,
|
|
3
|
+
* localStorage) into the root entry would break non-browser bundlers. */
|
|
4
|
+
import type { FileSource, HttpTransport, StateStore, NetworkMonitor } from '../core/types.js';
|
|
5
|
+
import { type Upload } from '../core/upload.js';
|
|
6
|
+
export { createUploadSession, mintUploadToken } from '../api/client.js';
|
|
7
|
+
export { NetworkError, ApiError } from '../api/client.js';
|
|
8
|
+
export { IntegrityError } from '../core/engine.js';
|
|
9
|
+
export type { Upload } from '../core/upload.js';
|
|
10
|
+
/** Structural File-ish source: anything with name/size/lastModified and a
|
|
11
|
+
* slice(a, b).arrayBuffer() (a real DOM `File` satisfies this, and so does
|
|
12
|
+
* a plain stub) is Node-testable without touching the DOM. Identity is
|
|
13
|
+
* `name:size:lastModified` so a relaunch against the same file resolves to
|
|
14
|
+
* the same session, and a changed file (different size or mtime) starts a
|
|
15
|
+
* fresh one — mirrors nodeFileSource's `path:size:mtime`. */
|
|
16
|
+
export declare function browserFileSource(f: {
|
|
17
|
+
name: string;
|
|
18
|
+
size: number;
|
|
19
|
+
lastModified: number;
|
|
20
|
+
slice(a: number, b: number): {
|
|
21
|
+
arrayBuffer(): Promise<ArrayBuffer>;
|
|
22
|
+
};
|
|
23
|
+
}): FileSource;
|
|
24
|
+
/** Default transport: XHR, not `fetch` — `fetch` exposes no upload-progress
|
|
25
|
+
* events, and progress is the whole reason to reach for XHR here. */
|
|
26
|
+
export declare const xhrTransport: HttpTransport;
|
|
27
|
+
/** Persists session state under `localStorage` (or any Storage-ish backend,
|
|
28
|
+
* for tests). Defaults to `globalThis.localStorage`, resolved lazily inside
|
|
29
|
+
* the function body — not at module load — so importing this module in a
|
|
30
|
+
* non-browser environment (e.g. Node tests) never throws; it only throws if
|
|
31
|
+
* `localStorageStore()` is actually called with no `localStorage` global. */
|
|
32
|
+
export declare function localStorageStore(ls?: Storage): StateStore;
|
|
33
|
+
/** Wraps `navigator.onLine` + `window` `online`/`offline` events. Listeners
|
|
34
|
+
* are attached lazily inside `onChange`, not at module top level or even at
|
|
35
|
+
* factory-call time, so constructing (and even calling) this in Node —
|
|
36
|
+
* where `window` doesn't exist — never throws; `onChange` just becomes a
|
|
37
|
+
* no-op subscription in that case. */
|
|
38
|
+
export declare function browserNetworkMonitor(): NetworkMonitor;
|
|
39
|
+
export type CreateUploadOptions = {
|
|
40
|
+
endpoint: string;
|
|
41
|
+
uploadToken: string;
|
|
42
|
+
assetId: string;
|
|
43
|
+
file: File;
|
|
44
|
+
/** Defaults to a `localStorage`-backed store. */
|
|
45
|
+
store?: StateStore;
|
|
46
|
+
concurrency?: number;
|
|
47
|
+
checksum?: 'sha256' | 'none';
|
|
48
|
+
onTokenExpired?: (assetId: string) => Promise<string>;
|
|
49
|
+
};
|
|
50
|
+
export declare function createUpload(opts: CreateUploadOptions): Promise<Upload>;
|