@portabyte/node 0.0.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/LICENSE +21 -0
- package/README.md +166 -0
- package/dist/index.cjs +462 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +194 -0
- package/dist/index.d.ts +194 -0
- package/dist/index.js +433 -0
- package/dist/index.js.map +1 -0
- package/package.json +46 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Portabyte
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
# Portabyte Node.js SDK
|
|
2
|
+
|
|
3
|
+
Upload, deliver, and manage files from trusted TypeScript server code. The SDK has no runtime package dependencies and uses your runtime's built-in `fetch`.
|
|
4
|
+
|
|
5
|
+
> Keep `pbt_sk_live_` API keys on your server. Do not use this SDK in browser code, mobile apps, or browser extensions.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
npm install @portabyte/node
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
The SDK requires Node.js 18 or later.
|
|
14
|
+
|
|
15
|
+
## Upload a file
|
|
16
|
+
|
|
17
|
+
Create a client with a project API key, then pass a file to `files.upload`. The SDK creates the upload session, transfers the bytes, confirms the asset, and returns the live file record.
|
|
18
|
+
|
|
19
|
+
```ts
|
|
20
|
+
import { readFile } from 'node:fs/promises';
|
|
21
|
+
import { Portabyte } from '@portabyte/node';
|
|
22
|
+
|
|
23
|
+
const portabyte = new Portabyte({
|
|
24
|
+
apiKey: process.env.PORTABYTE_API_KEY!,
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
const asset = await portabyte.files.upload({
|
|
28
|
+
file: await readFile('./summary.pdf'),
|
|
29
|
+
name: 'summary.pdf',
|
|
30
|
+
contentType: 'application/pdf',
|
|
31
|
+
visibility: 'public',
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
console.log(asset.publicUrl);
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
When your server receives a web `File`, such as through a route handler's `FormData`, pass it directly. The SDK reads its filename, MIME type, and size:
|
|
38
|
+
|
|
39
|
+
```ts
|
|
40
|
+
const asset = await portabyte.files.upload({
|
|
41
|
+
file,
|
|
42
|
+
path: 'reports/2026/may/summary.pdf',
|
|
43
|
+
visibility: 'private',
|
|
44
|
+
});
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Use `path` when you want one current file at a stable application-owned address. Uploading another confirmed file to that path replaces the current one.
|
|
48
|
+
|
|
49
|
+
## Deliver a file
|
|
50
|
+
|
|
51
|
+
Request a delivery URL with the file's ID:
|
|
52
|
+
|
|
53
|
+
```ts
|
|
54
|
+
const delivery = await portabyte.files.url(asset.id);
|
|
55
|
+
|
|
56
|
+
if (delivery.public) {
|
|
57
|
+
console.log(delivery.url);
|
|
58
|
+
} else {
|
|
59
|
+
console.log(delivery.expiresAt);
|
|
60
|
+
}
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Public files return stable URLs. Private files return short-lived signed URLs. Request a new private URL whenever an authorized recipient needs the file.
|
|
64
|
+
|
|
65
|
+
## Resume a large upload
|
|
66
|
+
|
|
67
|
+
Files of 32 MiB or larger use multipart upload automatically. For an upload that must survive a process restart, create a session and persist multipart state after each completed part:
|
|
68
|
+
|
|
69
|
+
```ts
|
|
70
|
+
const session = await portabyte.files.create({
|
|
71
|
+
name: 'recording.mp4',
|
|
72
|
+
contentType: 'video/mp4',
|
|
73
|
+
sizeBytes: video.size,
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
const asset = await portabyte.files.resume(session, {
|
|
77
|
+
file: video,
|
|
78
|
+
state: savedUploadState,
|
|
79
|
+
onStateChange: saveUploadState,
|
|
80
|
+
});
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Resume with the same byte size and MIME type. Multipart upload sessions expire after 12 hours.
|
|
84
|
+
|
|
85
|
+
## Manage files
|
|
86
|
+
|
|
87
|
+
Use the file ID to retrieve metadata, list a project page, or delete a file:
|
|
88
|
+
|
|
89
|
+
```ts
|
|
90
|
+
const file = await portabyte.files.get(asset.id);
|
|
91
|
+
const page = await portabyte.files.list({ limit: 20 });
|
|
92
|
+
await portabyte.files.remove(file.id);
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Pass `page.cursor` to `files.list` to retrieve the next page.
|
|
96
|
+
|
|
97
|
+
## Configure the client
|
|
98
|
+
|
|
99
|
+
| Option | Description | Default |
|
|
100
|
+
| ------------ | ------------------------------------------------------------ | --------------------------- |
|
|
101
|
+
| `apiKey` | Project API key that starts with `pbt_sk_live_` | Required |
|
|
102
|
+
| `baseUrl` | Control-plane API URL | `https://api.portabyte.dev` |
|
|
103
|
+
| `maxRetries` | Retries for idempotent requests | `2` |
|
|
104
|
+
| `timeoutMs` | Timeout for each request in milliseconds; set `0` to disable | `30000` |
|
|
105
|
+
| `fetch` | Custom `fetch` implementation | Runtime `fetch` |
|
|
106
|
+
|
|
107
|
+
The SDK retries `GET` requests and upload-byte requests after network failures, `429` responses, and `5xx` responses. It does not retry state-changing API calls.
|
|
108
|
+
|
|
109
|
+
## Upload directly from a browser
|
|
110
|
+
|
|
111
|
+
Keep your API key on your server. Prepare the upload on your server, return the browser-safe session to the client, then confirm it on your server after the browser has uploaded the bytes.
|
|
112
|
+
|
|
113
|
+
Before using this flow, set your frontend's origin once in **Console → Project → Settings → Upload defaults → Default CORS origin** (for example, `https://app.example.com`). The SDK uses that project default when `corsOrigin` is omitted. Pass `corsOrigin` only when a particular upload needs a different allowed origin.
|
|
114
|
+
|
|
115
|
+
```ts
|
|
116
|
+
// Your server route: POST /api/uploads/prepare
|
|
117
|
+
const upload = await portabyte.files.prepareBrowserUpload({
|
|
118
|
+
name: file.name,
|
|
119
|
+
contentType: file.type,
|
|
120
|
+
sizeBytes: file.size,
|
|
121
|
+
visibility: 'public',
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
// Return `upload` to the browser. Never return PORTABYTE_API_KEY.
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
```ts
|
|
128
|
+
// Browser code
|
|
129
|
+
await fetch(upload.uploadUrl, {
|
|
130
|
+
method: 'PUT',
|
|
131
|
+
headers: { 'Content-Type': file.type },
|
|
132
|
+
body: file,
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
// Tell your server the upload completed, then on the server:
|
|
136
|
+
const asset = await portabyte.files.confirm(upload.assetId);
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
For multipart sessions, use `uploadMode`, `partSize`, and `maxConcurrency` to upload parts through the same signed upload URL. Your server must still call `confirm` once the multipart upload completes.
|
|
140
|
+
|
|
141
|
+
## Handle errors
|
|
142
|
+
|
|
143
|
+
Failed requests throw `PortabyteError`, which includes the HTTP status, a stable error code, and a request ID when the API returns one:
|
|
144
|
+
|
|
145
|
+
```ts
|
|
146
|
+
import { PortabyteError } from '@portabyte/node';
|
|
147
|
+
|
|
148
|
+
try {
|
|
149
|
+
await portabyte.files.upload(uploadRequest);
|
|
150
|
+
} catch (error) {
|
|
151
|
+
if (error instanceof PortabyteError) {
|
|
152
|
+
console.error(error.status, error.code, error.requestId);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
## Learn more
|
|
158
|
+
|
|
159
|
+
- [Getting started](https://portabyte.dev/docs/getting-started/node-sdk)
|
|
160
|
+
- [REST API reference](https://portabyte.dev/docs/api-reference)
|
|
161
|
+
- [Browser uploads](https://portabyte.dev/docs/upload-delivery/browser-uploads)
|
|
162
|
+
- [Public and private files](https://portabyte.dev/docs/upload-delivery/public-and-private-files)
|
|
163
|
+
|
|
164
|
+
## License
|
|
165
|
+
|
|
166
|
+
[MIT](./LICENSE)
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,462 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
Portabyte: () => Portabyte,
|
|
24
|
+
PortabyteError: () => PortabyteError,
|
|
25
|
+
VERSION: () => VERSION
|
|
26
|
+
});
|
|
27
|
+
module.exports = __toCommonJS(index_exports);
|
|
28
|
+
|
|
29
|
+
// src/errors.ts
|
|
30
|
+
var PortabyteError = class extends Error {
|
|
31
|
+
status;
|
|
32
|
+
code;
|
|
33
|
+
requestId;
|
|
34
|
+
constructor(message, status, code, requestId) {
|
|
35
|
+
super(message);
|
|
36
|
+
this.name = "PortabyteError";
|
|
37
|
+
this.status = status;
|
|
38
|
+
this.code = code;
|
|
39
|
+
this.requestId = requestId;
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
// src/client.ts
|
|
44
|
+
var SDK_HEADER_NAME = "X-Portabyte-SDK";
|
|
45
|
+
var RetryableError = class extends Error {
|
|
46
|
+
constructor(retryAfterMs, fallback) {
|
|
47
|
+
super("retryable request failure");
|
|
48
|
+
this.retryAfterMs = retryAfterMs;
|
|
49
|
+
this.fallback = fallback;
|
|
50
|
+
}
|
|
51
|
+
retryAfterMs;
|
|
52
|
+
fallback;
|
|
53
|
+
};
|
|
54
|
+
var HttpClient = class {
|
|
55
|
+
constructor(options) {
|
|
56
|
+
this.options = options;
|
|
57
|
+
}
|
|
58
|
+
options;
|
|
59
|
+
async request(options) {
|
|
60
|
+
const idempotent = options.method === "GET";
|
|
61
|
+
return this.run(idempotent, async () => {
|
|
62
|
+
const response = await this.send(this.options.baseUrl + options.path, {
|
|
63
|
+
method: options.method,
|
|
64
|
+
headers: {
|
|
65
|
+
Authorization: `Bearer ${this.options.apiKey}`,
|
|
66
|
+
[SDK_HEADER_NAME]: this.options.sdkHeaderValue,
|
|
67
|
+
...options.body !== void 0 || options.method !== "GET" ? { "Content-Type": "application/json" } : {}
|
|
68
|
+
},
|
|
69
|
+
body: options.body === void 0 ? void 0 : JSON.stringify(options.body)
|
|
70
|
+
});
|
|
71
|
+
if (!response.ok) {
|
|
72
|
+
throw await responseError(response, idempotent);
|
|
73
|
+
}
|
|
74
|
+
if (response.status === 204) {
|
|
75
|
+
return void 0;
|
|
76
|
+
}
|
|
77
|
+
return await response.json();
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
// The gateway's CORS policy allows only Content-Type; the signed URL is
|
|
81
|
+
// the authorization, and the runtime sets the exact-match Content-Length.
|
|
82
|
+
async putBytes(uploadUrl, contentType, data) {
|
|
83
|
+
await this.run(true, async () => {
|
|
84
|
+
const response = await this.send(uploadUrl, {
|
|
85
|
+
method: "PUT",
|
|
86
|
+
headers: { "Content-Type": contentType },
|
|
87
|
+
body: data
|
|
88
|
+
});
|
|
89
|
+
if (!response.ok) {
|
|
90
|
+
throw await responseError(response, true);
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
async putBytesJSON(uploadUrl, contentType, data) {
|
|
95
|
+
return this.run(true, async () => {
|
|
96
|
+
const response = await this.send(uploadUrl, {
|
|
97
|
+
method: "PUT",
|
|
98
|
+
headers: { "Content-Type": contentType },
|
|
99
|
+
body: data
|
|
100
|
+
});
|
|
101
|
+
if (!response.ok) {
|
|
102
|
+
throw await responseError(response, true);
|
|
103
|
+
}
|
|
104
|
+
return await response.json();
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
async uploadJSON(uploadUrl, method, body, idempotent) {
|
|
108
|
+
return this.run(idempotent, async () => {
|
|
109
|
+
const response = await this.send(uploadUrl, {
|
|
110
|
+
method,
|
|
111
|
+
headers: { "Content-Type": "application/json" },
|
|
112
|
+
body: JSON.stringify(body)
|
|
113
|
+
});
|
|
114
|
+
if (!response.ok) {
|
|
115
|
+
throw await responseError(response, idempotent);
|
|
116
|
+
}
|
|
117
|
+
if (response.status === 204) {
|
|
118
|
+
return void 0;
|
|
119
|
+
}
|
|
120
|
+
return await response.json();
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
async send(url, init) {
|
|
124
|
+
try {
|
|
125
|
+
return await this.options.fetchImpl(url, {
|
|
126
|
+
...init,
|
|
127
|
+
signal: this.options.timeoutMs > 0 ? AbortSignal.timeout(this.options.timeoutMs) : void 0
|
|
128
|
+
});
|
|
129
|
+
} catch (cause) {
|
|
130
|
+
throw new RetryableError(
|
|
131
|
+
null,
|
|
132
|
+
new PortabyteError(
|
|
133
|
+
cause instanceof Error ? cause.message : "Network request failed.",
|
|
134
|
+
0,
|
|
135
|
+
"network_error"
|
|
136
|
+
)
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
async run(idempotent, send) {
|
|
141
|
+
for (let attempt = 0; ; attempt++) {
|
|
142
|
+
try {
|
|
143
|
+
return await send();
|
|
144
|
+
} catch (error) {
|
|
145
|
+
const retry = error instanceof RetryableError;
|
|
146
|
+
if (!retry || !idempotent || attempt >= this.options.maxRetries) {
|
|
147
|
+
throw retry ? error.fallback : error;
|
|
148
|
+
}
|
|
149
|
+
await sleep(error.retryAfterMs ?? backoffMs(attempt));
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
async function responseError(response, idempotent) {
|
|
155
|
+
const error = await errorFromResponse(response);
|
|
156
|
+
if (idempotent && (response.status === 429 || response.status >= 500)) {
|
|
157
|
+
const retryAfter = Number(response.headers.get("retry-after"));
|
|
158
|
+
const retryAfterMs = Number.isFinite(retryAfter) && retryAfter >= 0 ? retryAfter * 1e3 : null;
|
|
159
|
+
return new RetryableError(retryAfterMs, error);
|
|
160
|
+
}
|
|
161
|
+
return error;
|
|
162
|
+
}
|
|
163
|
+
function backoffMs(attempt) {
|
|
164
|
+
const baseMs = 500;
|
|
165
|
+
const capMs = 8e3;
|
|
166
|
+
return Math.floor(Math.random() * Math.min(capMs, baseMs * 2 ** attempt));
|
|
167
|
+
}
|
|
168
|
+
function sleep(ms) {
|
|
169
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
170
|
+
}
|
|
171
|
+
async function errorFromResponse(response) {
|
|
172
|
+
let code = "request_failed";
|
|
173
|
+
let message = `Request failed with status ${response.status}.`;
|
|
174
|
+
let requestId;
|
|
175
|
+
try {
|
|
176
|
+
const body = await response.json();
|
|
177
|
+
code = body.code ?? code;
|
|
178
|
+
message = body.message ?? message;
|
|
179
|
+
requestId = body.requestId;
|
|
180
|
+
} catch {
|
|
181
|
+
}
|
|
182
|
+
return new PortabyteError(message, response.status, code, requestId);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// src/assets.ts
|
|
186
|
+
var FilesAPI = class {
|
|
187
|
+
constructor(http) {
|
|
188
|
+
this.http = http;
|
|
189
|
+
}
|
|
190
|
+
http;
|
|
191
|
+
/** Creates a signed upload session. Prefer {@link upload}, which runs every step. */
|
|
192
|
+
async create(input) {
|
|
193
|
+
const body = {
|
|
194
|
+
name: input.name,
|
|
195
|
+
contentType: input.contentType,
|
|
196
|
+
sizeBytes: input.sizeBytes,
|
|
197
|
+
...input.path !== void 0 && { path: input.path },
|
|
198
|
+
...input.visibility !== void 0 && { visibility: input.visibility },
|
|
199
|
+
...input.corsOrigin !== void 0 && { corsOrigin: input.corsOrigin }
|
|
200
|
+
};
|
|
201
|
+
return this.http.request({
|
|
202
|
+
method: "POST",
|
|
203
|
+
path: this.path("assets"),
|
|
204
|
+
body
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Prepares a direct browser upload. Call this only from your trusted server,
|
|
209
|
+
* then return the result to the browser. The browser uploads bytes directly
|
|
210
|
+
* to uploadUrl; call {@link confirm} from your server once it reports success.
|
|
211
|
+
*/
|
|
212
|
+
async prepareBrowserUpload(input) {
|
|
213
|
+
const session = await this.create(input);
|
|
214
|
+
return {
|
|
215
|
+
assetId: session.id,
|
|
216
|
+
uploadUrl: session.uploadUrl,
|
|
217
|
+
uploadExpiresAt: session.uploadExpiresAt,
|
|
218
|
+
uploadMode: session.uploadMode,
|
|
219
|
+
...session.partSize !== void 0 && { partSize: session.partSize },
|
|
220
|
+
...session.maxConcurrency !== void 0 && {
|
|
221
|
+
maxConcurrency: session.maxConcurrency
|
|
222
|
+
}
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* Confirms a completed direct upload and returns its live asset record.
|
|
227
|
+
* Call this from your trusted server, never from a browser.
|
|
228
|
+
*/
|
|
229
|
+
async confirm(assetID) {
|
|
230
|
+
return this.http.request({
|
|
231
|
+
method: "POST",
|
|
232
|
+
path: this.path(`assets/${assetID}/uploaded`)
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
/** Uploads a File, Blob, or bytes end to end. */
|
|
236
|
+
async upload(request) {
|
|
237
|
+
const described = describeUpload(request);
|
|
238
|
+
const { file, multipart } = request;
|
|
239
|
+
const createOptions = {
|
|
240
|
+
...request.path !== void 0 && { path: request.path },
|
|
241
|
+
...request.visibility !== void 0 && { visibility: request.visibility },
|
|
242
|
+
...request.corsOrigin !== void 0 && { corsOrigin: request.corsOrigin }
|
|
243
|
+
};
|
|
244
|
+
const session = await this.create({ ...described, ...createOptions });
|
|
245
|
+
await this.transfer(
|
|
246
|
+
session,
|
|
247
|
+
file,
|
|
248
|
+
described.contentType,
|
|
249
|
+
multipart
|
|
250
|
+
);
|
|
251
|
+
try {
|
|
252
|
+
return await this.confirm(session.id);
|
|
253
|
+
} catch (error) {
|
|
254
|
+
if (error instanceof PortabyteError && error.status !== 0) {
|
|
255
|
+
await this.remove(session.id).catch(() => void 0);
|
|
256
|
+
}
|
|
257
|
+
throw error;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* Continues a previously-created session. Persist the create session and
|
|
262
|
+
* MultipartUploadState after each part to resume after an interruption.
|
|
263
|
+
*/
|
|
264
|
+
async resume(session, request) {
|
|
265
|
+
const contentType = request.contentType ?? (request.file instanceof Blob ? request.file.type : "");
|
|
266
|
+
if (fileSize(request.file) !== session.sizeBytes || contentType !== session.contentType) {
|
|
267
|
+
throw new PortabyteError(
|
|
268
|
+
"The selected file does not match this upload session.",
|
|
269
|
+
0,
|
|
270
|
+
"invalid_argument"
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
await this.transfer(session, request.file, contentType, request);
|
|
274
|
+
return this.confirm(session.id);
|
|
275
|
+
}
|
|
276
|
+
async list(options = {}) {
|
|
277
|
+
const params = new URLSearchParams();
|
|
278
|
+
if (options.cursor) params.set("cursor", options.cursor);
|
|
279
|
+
if (options.limit !== void 0) params.set("limit", String(options.limit));
|
|
280
|
+
const query = params.size > 0 ? `?${params}` : "";
|
|
281
|
+
return this.http.request({
|
|
282
|
+
method: "GET",
|
|
283
|
+
path: this.path(`assets${query}`)
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
async get(assetID) {
|
|
287
|
+
return this.http.request({
|
|
288
|
+
method: "GET",
|
|
289
|
+
path: this.path(`assets/${assetID}`)
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
async remove(assetID) {
|
|
293
|
+
await this.http.request({
|
|
294
|
+
method: "DELETE",
|
|
295
|
+
path: this.path(`assets/${assetID}`)
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
/**
|
|
299
|
+
* Cancels a multipart transfer and removes its pending asset. The gateway
|
|
300
|
+
* abort is best-effort: removing the pending asset guarantees the object
|
|
301
|
+
* can never be confirmed or delivered, even if its signed URL has expired.
|
|
302
|
+
*/
|
|
303
|
+
async cancel(session, state) {
|
|
304
|
+
if (session.uploadMode === "multipart" && state?.uploadId) {
|
|
305
|
+
await this.http.uploadJSON(
|
|
306
|
+
`${session.uploadUrl}/multipart/${state.uploadId}`,
|
|
307
|
+
"DELETE",
|
|
308
|
+
void 0,
|
|
309
|
+
false
|
|
310
|
+
).catch(() => void 0);
|
|
311
|
+
}
|
|
312
|
+
await this.remove(session.id);
|
|
313
|
+
}
|
|
314
|
+
/**
|
|
315
|
+
* Returns the URL the asset is served from: stable and cacheable for
|
|
316
|
+
* public assets, short-lived signed otherwise.
|
|
317
|
+
*/
|
|
318
|
+
async url(assetID) {
|
|
319
|
+
return this.http.request({
|
|
320
|
+
method: "GET",
|
|
321
|
+
path: this.path(`assets/${assetID}/url`)
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
path(suffix) {
|
|
325
|
+
return `/v1/${suffix}`;
|
|
326
|
+
}
|
|
327
|
+
async transfer(session, body, contentType, options) {
|
|
328
|
+
if (session.uploadMode === "single") {
|
|
329
|
+
await this.http.putBytes(session.uploadUrl, contentType, body);
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
await this.uploadMultipart(session, body, contentType, options);
|
|
333
|
+
}
|
|
334
|
+
async uploadMultipart(session, body, contentType, options = {}) {
|
|
335
|
+
if (!session.partSize || session.partSize < 5 * 1024 * 1024) {
|
|
336
|
+
throw new PortabyteError(
|
|
337
|
+
"Multipart upload session is missing a valid part size.",
|
|
338
|
+
0,
|
|
339
|
+
"invalid_upload"
|
|
340
|
+
);
|
|
341
|
+
}
|
|
342
|
+
const state = {
|
|
343
|
+
uploadId: options.state?.uploadId,
|
|
344
|
+
parts: [...options.state?.parts ?? []]
|
|
345
|
+
};
|
|
346
|
+
if (!state.uploadId) {
|
|
347
|
+
const started = await this.http.uploadJSON(
|
|
348
|
+
`${session.uploadUrl}/multipart`,
|
|
349
|
+
"POST",
|
|
350
|
+
{},
|
|
351
|
+
false
|
|
352
|
+
);
|
|
353
|
+
state.uploadId = started.uploadId;
|
|
354
|
+
await options.onStateChange?.({ ...state, parts: [...state.parts] });
|
|
355
|
+
}
|
|
356
|
+
const partCount = Math.ceil(session.sizeBytes / session.partSize);
|
|
357
|
+
const completed = new Map(
|
|
358
|
+
state.parts.map((part) => [part.partNumber, part])
|
|
359
|
+
);
|
|
360
|
+
const concurrency = Math.max(
|
|
361
|
+
1,
|
|
362
|
+
Math.min(options.concurrency ?? session.maxConcurrency ?? 3, 3)
|
|
363
|
+
);
|
|
364
|
+
let nextPart = 1;
|
|
365
|
+
const uploadNext = async () => {
|
|
366
|
+
for (; ; ) {
|
|
367
|
+
const partNumber = nextPart;
|
|
368
|
+
nextPart += 1;
|
|
369
|
+
if (partNumber > partCount) return;
|
|
370
|
+
if (completed.has(partNumber)) continue;
|
|
371
|
+
const start = (partNumber - 1) * session.partSize;
|
|
372
|
+
const end = Math.min(start + session.partSize, session.sizeBytes);
|
|
373
|
+
const part = await this.http.putBytesJSON(
|
|
374
|
+
`${session.uploadUrl}/multipart/${state.uploadId}/parts/${partNumber}`,
|
|
375
|
+
contentType,
|
|
376
|
+
sliceBody(body, start, end)
|
|
377
|
+
);
|
|
378
|
+
completed.set(part.partNumber, part);
|
|
379
|
+
state.parts = [...completed.values()].sort(
|
|
380
|
+
(left, right) => left.partNumber - right.partNumber
|
|
381
|
+
);
|
|
382
|
+
await options.onStateChange?.({ ...state, parts: [...state.parts] });
|
|
383
|
+
}
|
|
384
|
+
};
|
|
385
|
+
await Promise.all(
|
|
386
|
+
Array.from({ length: Math.min(concurrency, partCount) }, uploadNext)
|
|
387
|
+
);
|
|
388
|
+
await this.http.uploadJSON(
|
|
389
|
+
`${session.uploadUrl}/multipart/${state.uploadId}/complete`,
|
|
390
|
+
"POST",
|
|
391
|
+
{ parts: state.parts },
|
|
392
|
+
true
|
|
393
|
+
);
|
|
394
|
+
}
|
|
395
|
+
};
|
|
396
|
+
function describeUpload(request) {
|
|
397
|
+
const filename = request.name ?? fileName(request.file);
|
|
398
|
+
const mimeType = request.contentType ?? (request.file instanceof Blob ? request.file.type : "");
|
|
399
|
+
if (!filename) {
|
|
400
|
+
throw new PortabyteError(
|
|
401
|
+
"A file name is required when uploading a Blob or bytes.",
|
|
402
|
+
0,
|
|
403
|
+
"invalid_argument"
|
|
404
|
+
);
|
|
405
|
+
}
|
|
406
|
+
if (!mimeType) {
|
|
407
|
+
throw new PortabyteError(
|
|
408
|
+
"A content type is required when uploading bytes or a Blob without a type.",
|
|
409
|
+
0,
|
|
410
|
+
"invalid_argument"
|
|
411
|
+
);
|
|
412
|
+
}
|
|
413
|
+
return {
|
|
414
|
+
name: filename,
|
|
415
|
+
contentType: mimeType,
|
|
416
|
+
sizeBytes: request.file instanceof Blob ? request.file.size : request.file.byteLength
|
|
417
|
+
};
|
|
418
|
+
}
|
|
419
|
+
function fileName(file) {
|
|
420
|
+
if (!(file instanceof Blob)) return void 0;
|
|
421
|
+
const name = file.name;
|
|
422
|
+
return typeof name === "string" && name.length > 0 ? name : void 0;
|
|
423
|
+
}
|
|
424
|
+
function fileSize(file) {
|
|
425
|
+
return file instanceof Blob ? file.size : file.byteLength;
|
|
426
|
+
}
|
|
427
|
+
function sliceBody(body, start, end) {
|
|
428
|
+
return body instanceof Blob ? body.slice(start, end) : body.slice(start, end);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// src/index.ts
|
|
432
|
+
var VERSION = "0.0.1";
|
|
433
|
+
var DEFAULT_BASE_URL = "https://api.portabyte.dev";
|
|
434
|
+
var Portabyte = class {
|
|
435
|
+
/** Preferred API for application file uploads. */
|
|
436
|
+
files;
|
|
437
|
+
constructor(options) {
|
|
438
|
+
if (!options.apiKey.startsWith("pbt_sk_live_")) {
|
|
439
|
+
throw new PortabyteError(
|
|
440
|
+
'apiKey must be a server API key starting with "pbt_sk_live_".',
|
|
441
|
+
0,
|
|
442
|
+
"invalid_argument"
|
|
443
|
+
);
|
|
444
|
+
}
|
|
445
|
+
const http = new HttpClient({
|
|
446
|
+
baseUrl: (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\/$/, ""),
|
|
447
|
+
apiKey: options.apiKey,
|
|
448
|
+
fetchImpl: options.fetch ?? fetch,
|
|
449
|
+
maxRetries: options.maxRetries ?? 2,
|
|
450
|
+
timeoutMs: options.timeoutMs ?? 3e4,
|
|
451
|
+
sdkHeaderValue: `typescript/${VERSION}`
|
|
452
|
+
});
|
|
453
|
+
this.files = new FilesAPI(http);
|
|
454
|
+
}
|
|
455
|
+
};
|
|
456
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
457
|
+
0 && (module.exports = {
|
|
458
|
+
Portabyte,
|
|
459
|
+
PortabyteError,
|
|
460
|
+
VERSION
|
|
461
|
+
});
|
|
462
|
+
//# sourceMappingURL=index.cjs.map
|