@beam-network/sdk 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 +46 -0
- package/dist/client.d.ts +38 -0
- package/dist/client.js +273 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/models.d.ts +241 -0
- package/dist/models.js +31 -0
- package/dist/provider-signing.d.ts +30 -0
- package/dist/provider-signing.js +250 -0
- package/package.json +36 -0
package/README.md
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# BEAM SDK for TypeScript
|
|
2
|
+
|
|
3
|
+
Install:
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
npm install @beam-network/sdk
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
Example:
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
import { BeamClient, R2ProviderConfig, S3ProviderConfig } from "@beam-network/sdk";
|
|
13
|
+
|
|
14
|
+
const beam = new BeamClient({
|
|
15
|
+
apiKey: "bk_your_key"
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
const transfer = await beam.createTransfer({
|
|
19
|
+
sources: [
|
|
20
|
+
R2ProviderConfig.create({
|
|
21
|
+
bucket: "source-bucket",
|
|
22
|
+
key: "exports/report.parquet",
|
|
23
|
+
account_id: "cloudflare-account-id",
|
|
24
|
+
access_key_id: "r2-access-key",
|
|
25
|
+
secret_access_key: "r2-secret-key"
|
|
26
|
+
})
|
|
27
|
+
],
|
|
28
|
+
destinations: [
|
|
29
|
+
S3ProviderConfig.create({
|
|
30
|
+
bucket: "destination-bucket",
|
|
31
|
+
key: "imports/report.parquet",
|
|
32
|
+
region: "us-east-1",
|
|
33
|
+
access_key_id: "aws-access-key",
|
|
34
|
+
secret_access_key: "aws-secret-key"
|
|
35
|
+
})
|
|
36
|
+
],
|
|
37
|
+
name: "r2-to-s3-report"
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
const status = await beam.waitForTransfer(transfer.transfer_id);
|
|
41
|
+
console.log(status.status);
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
The main `createTransfer` API is provider-aware and strictly typed for S3, R2, and Hippius configs.
|
|
45
|
+
|
|
46
|
+
For low-level BeamCore `/transfers/create` calls with raw HTTP-style configs, use `createRawTransfer`.
|
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { AttachSignedUrlsResponse, BeamClientOptions, DistributeResponse, PreparedDestination, PreparedHttpSource, ProviderTransferCreateInput, RawTransferCreateInput, SignedChunkRoute, TransferCancelResponse, TransferCreateResponse, TransferPrepareResponse, TransferStatusInfo } from "./models.js";
|
|
2
|
+
export declare const BEAM_DEFAULT_URL = "https://beamcore.b1m.ai";
|
|
3
|
+
export declare class BeamApiError extends Error {
|
|
4
|
+
readonly status: number;
|
|
5
|
+
readonly body: string;
|
|
6
|
+
constructor(message: string, status: number, body: string);
|
|
7
|
+
}
|
|
8
|
+
export declare class BeamClient {
|
|
9
|
+
readonly baseUrl: string;
|
|
10
|
+
readonly apiKey: string;
|
|
11
|
+
private readonly fetchImpl;
|
|
12
|
+
constructor(options?: BeamClientOptions);
|
|
13
|
+
createRawTransfer(input: RawTransferCreateInput): Promise<TransferCreateResponse>;
|
|
14
|
+
createTransfer(input: ProviderTransferCreateInput): Promise<TransferPrepareResponse>;
|
|
15
|
+
transferStatus(transferId: string): Promise<TransferStatusInfo>;
|
|
16
|
+
distributeTransfer(transferId: string): Promise<DistributeResponse>;
|
|
17
|
+
cancelTransfer(transferId: string): Promise<TransferCancelResponse>;
|
|
18
|
+
prepareTransfer(input: {
|
|
19
|
+
sources: PreparedHttpSource[];
|
|
20
|
+
destinations: PreparedDestination[];
|
|
21
|
+
name?: string;
|
|
22
|
+
testMode?: boolean;
|
|
23
|
+
urlsExpiresAt?: string;
|
|
24
|
+
}): Promise<TransferPrepareResponse>;
|
|
25
|
+
attachSignedUrls(transferId: string, input: {
|
|
26
|
+
chunkRoutes: SignedChunkRoute[];
|
|
27
|
+
transferKey?: string;
|
|
28
|
+
urlsExpiresAt?: string;
|
|
29
|
+
}): Promise<AttachSignedUrlsResponse>;
|
|
30
|
+
prepareProviderTransfer(input: ProviderTransferCreateInput): Promise<TransferPrepareResponse>;
|
|
31
|
+
createAndDistribute(input: RawTransferCreateInput): Promise<TransferCreateResponse>;
|
|
32
|
+
waitForTransfer(transferId: string, options?: {
|
|
33
|
+
timeoutMs?: number;
|
|
34
|
+
pollIntervalMs?: number;
|
|
35
|
+
}): Promise<TransferStatusInfo>;
|
|
36
|
+
private request;
|
|
37
|
+
}
|
|
38
|
+
export declare function calculateOptimalChunkSize(totalSize: number): number;
|
package/dist/client.js
ADDED
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
import { abortMultipartUpload, createMultipartUpload, nowIso, prepareProviderDestination, prepareProviderSource, signAbortMultipartUpload, signCompleteMultipartUpload, signDestinationRoute } from "./provider-signing.js";
|
|
2
|
+
export const BEAM_DEFAULT_URL = "https://beamcore.b1m.ai";
|
|
3
|
+
export class BeamApiError extends Error {
|
|
4
|
+
status;
|
|
5
|
+
body;
|
|
6
|
+
constructor(message, status, body) {
|
|
7
|
+
super(message);
|
|
8
|
+
this.status = status;
|
|
9
|
+
this.body = body;
|
|
10
|
+
this.name = "BeamApiError";
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
export class BeamClient {
|
|
14
|
+
baseUrl;
|
|
15
|
+
apiKey;
|
|
16
|
+
fetchImpl;
|
|
17
|
+
constructor(options = {}) {
|
|
18
|
+
if (!options.apiKey?.trim()) {
|
|
19
|
+
throw new Error("apiKey is required.");
|
|
20
|
+
}
|
|
21
|
+
this.baseUrl = normalizeBaseUrl(resolveBaseUrl(options));
|
|
22
|
+
this.apiKey = options.apiKey;
|
|
23
|
+
this.fetchImpl = options.fetch ?? globalThis.fetch;
|
|
24
|
+
if (!this.fetchImpl) {
|
|
25
|
+
throw new Error("A fetch implementation is required.");
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
createRawTransfer(input) {
|
|
29
|
+
const body = compact({
|
|
30
|
+
sources: input.sources,
|
|
31
|
+
destinations: input.destinations,
|
|
32
|
+
total_size: input.totalSize,
|
|
33
|
+
chunk_size: input.chunkSize ?? calculateOptimalChunkSize(input.totalSize),
|
|
34
|
+
name: input.name,
|
|
35
|
+
merkle_root: input.merkleRoot,
|
|
36
|
+
chunk_hashes: input.chunkHashes,
|
|
37
|
+
callbacks: input.callbacks,
|
|
38
|
+
test_mode: input.testMode || undefined,
|
|
39
|
+
progressive_mode: input.progressiveMode || undefined
|
|
40
|
+
});
|
|
41
|
+
return this.request("POST", "/transfers/create", body);
|
|
42
|
+
}
|
|
43
|
+
createTransfer(input) {
|
|
44
|
+
return this.prepareProviderTransfer(input);
|
|
45
|
+
}
|
|
46
|
+
transferStatus(transferId) {
|
|
47
|
+
validateId(transferId, "transferId");
|
|
48
|
+
return this.request("GET", `/transfers/${encodeURIComponent(transferId)}/status`);
|
|
49
|
+
}
|
|
50
|
+
distributeTransfer(transferId) {
|
|
51
|
+
validateId(transferId, "transferId");
|
|
52
|
+
return this.request("POST", "/transfers/distribute", {
|
|
53
|
+
transfer_id: transferId
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
cancelTransfer(transferId) {
|
|
57
|
+
validateId(transferId, "transferId");
|
|
58
|
+
return this.request("DELETE", `/transfers/${encodeURIComponent(transferId)}`);
|
|
59
|
+
}
|
|
60
|
+
prepareTransfer(input) {
|
|
61
|
+
return this.request("POST", "/transfers/prepare", compact({
|
|
62
|
+
sources: input.sources,
|
|
63
|
+
destinations: input.destinations,
|
|
64
|
+
name: input.name,
|
|
65
|
+
test_mode: input.testMode || undefined,
|
|
66
|
+
urls_expires_at: input.urlsExpiresAt
|
|
67
|
+
}));
|
|
68
|
+
}
|
|
69
|
+
attachSignedUrls(transferId, input) {
|
|
70
|
+
validateId(transferId, "transferId");
|
|
71
|
+
return this.request("POST", "/transfers/attach-signed-urls", compact({
|
|
72
|
+
transfer_id: transferId,
|
|
73
|
+
chunk_routes: input.chunkRoutes,
|
|
74
|
+
transfer_key: input.transferKey,
|
|
75
|
+
urls_expires_at: input.urlsExpiresAt
|
|
76
|
+
}));
|
|
77
|
+
}
|
|
78
|
+
async prepareProviderTransfer(input) {
|
|
79
|
+
const expiresIn = input.expiresIn ?? 3600;
|
|
80
|
+
const preparedSources = await Promise.all(input.sources.map((source, index) => prepareProviderSource(source, { index, expiresIn, fetchImpl: this.fetchImpl })));
|
|
81
|
+
const preparedDestinations = input.destinations.map((destination, index) => prepareProviderDestination(destination, { index }));
|
|
82
|
+
const prepared = await this.prepareTransfer({
|
|
83
|
+
sources: preparedSources,
|
|
84
|
+
destinations: preparedDestinations,
|
|
85
|
+
name: input.name,
|
|
86
|
+
testMode: input.testMode
|
|
87
|
+
});
|
|
88
|
+
if (!prepared.success) {
|
|
89
|
+
return prepared;
|
|
90
|
+
}
|
|
91
|
+
const destinationsById = new Map();
|
|
92
|
+
preparedDestinations.forEach((preparedDestination, index) => {
|
|
93
|
+
destinationsById.set(preparedDestination.destination_id, input.destinations[index]);
|
|
94
|
+
});
|
|
95
|
+
const routes = [];
|
|
96
|
+
const multipartUploads = new Map();
|
|
97
|
+
try {
|
|
98
|
+
for (const chunk of prepared.chunk_plan) {
|
|
99
|
+
for (const target of chunk.destinations) {
|
|
100
|
+
const destination = destinationsById.get(target.destination_id);
|
|
101
|
+
if (!destination) {
|
|
102
|
+
throw new Error(`BeamCore returned unknown destination_id: ${target.destination_id}`);
|
|
103
|
+
}
|
|
104
|
+
const metadata = target.metadata ?? {};
|
|
105
|
+
const finalObjectKey = typeof metadata.final_object_key === "string"
|
|
106
|
+
? metadata.final_object_key
|
|
107
|
+
: target.object_key;
|
|
108
|
+
if (!finalObjectKey) {
|
|
109
|
+
throw new Error("destination signing target is missing object_key");
|
|
110
|
+
}
|
|
111
|
+
const multipartKey = `${prepared.transfer_id}:${target.destination_id}:${chunk.source_id}:${finalObjectKey}`;
|
|
112
|
+
let upload = multipartUploads.get(multipartKey);
|
|
113
|
+
if (!upload) {
|
|
114
|
+
const multipartCreatedAt = nowIso();
|
|
115
|
+
if (destination.provider === "hippius") {
|
|
116
|
+
upload = {
|
|
117
|
+
destination,
|
|
118
|
+
objectKey: finalObjectKey,
|
|
119
|
+
uploadId: "",
|
|
120
|
+
completeUrl: "",
|
|
121
|
+
abortUrl: "",
|
|
122
|
+
multipartGroupId: multipartKey,
|
|
123
|
+
multipartCreatedAt
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
else {
|
|
127
|
+
const uploadId = await createMultipartUpload(destination, finalObjectKey);
|
|
128
|
+
upload = {
|
|
129
|
+
destination,
|
|
130
|
+
objectKey: finalObjectKey,
|
|
131
|
+
uploadId,
|
|
132
|
+
completeUrl: await signCompleteMultipartUpload(destination, finalObjectKey, uploadId, expiresIn),
|
|
133
|
+
abortUrl: await signAbortMultipartUpload(destination, finalObjectKey, uploadId, expiresIn),
|
|
134
|
+
multipartGroupId: multipartKey,
|
|
135
|
+
multipartCreatedAt
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
multipartUploads.set(multipartKey, upload);
|
|
139
|
+
}
|
|
140
|
+
routes.push(await signDestinationRoute({
|
|
141
|
+
chunk,
|
|
142
|
+
target,
|
|
143
|
+
destination,
|
|
144
|
+
expiresIn,
|
|
145
|
+
uploadId: upload.uploadId,
|
|
146
|
+
partNumber: typeof metadata.part_number === "number"
|
|
147
|
+
? metadata.part_number
|
|
148
|
+
: chunk.source_chunk_index + 1,
|
|
149
|
+
multipartObjectKey: finalObjectKey,
|
|
150
|
+
completeUrl: upload.completeUrl,
|
|
151
|
+
abortUrl: upload.abortUrl,
|
|
152
|
+
transferId: prepared.transfer_id,
|
|
153
|
+
multipartGroupId: upload.multipartGroupId,
|
|
154
|
+
multipartCreatedAt: upload.multipartCreatedAt,
|
|
155
|
+
fetchImpl: this.fetchImpl
|
|
156
|
+
}));
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
catch (error) {
|
|
161
|
+
await abortCreatedUploads(multipartUploads);
|
|
162
|
+
throw error;
|
|
163
|
+
}
|
|
164
|
+
try {
|
|
165
|
+
const attached = await this.attachSignedUrls(prepared.transfer_id, {
|
|
166
|
+
chunkRoutes: routes,
|
|
167
|
+
transferKey: prepared.transfer_key
|
|
168
|
+
});
|
|
169
|
+
if (!attached.success) {
|
|
170
|
+
throw new Error(attached.error ?? attached.message ?? "attach signed URLs failed");
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
catch (error) {
|
|
174
|
+
await abortCreatedUploads(multipartUploads);
|
|
175
|
+
throw error;
|
|
176
|
+
}
|
|
177
|
+
if (input.distribute !== false) {
|
|
178
|
+
await this.distributeTransfer(prepared.transfer_id);
|
|
179
|
+
}
|
|
180
|
+
return prepared;
|
|
181
|
+
}
|
|
182
|
+
async createAndDistribute(input) {
|
|
183
|
+
const transfer = await this.createRawTransfer(input);
|
|
184
|
+
if (transfer.success) {
|
|
185
|
+
await this.distributeTransfer(transfer.transfer_id);
|
|
186
|
+
}
|
|
187
|
+
return transfer;
|
|
188
|
+
}
|
|
189
|
+
async waitForTransfer(transferId, options = {}) {
|
|
190
|
+
const timeoutMs = options.timeoutMs ?? 300_000;
|
|
191
|
+
const pollIntervalMs = options.pollIntervalMs ?? 2_000;
|
|
192
|
+
const startedAt = Date.now();
|
|
193
|
+
while (true) {
|
|
194
|
+
const status = await this.transferStatus(transferId);
|
|
195
|
+
if (status.status === "completed") {
|
|
196
|
+
return status;
|
|
197
|
+
}
|
|
198
|
+
if (status.status === "failed") {
|
|
199
|
+
throw new Error(`Transfer failed: ${status.error_message ?? "unknown error"}`);
|
|
200
|
+
}
|
|
201
|
+
if (status.status === "cancelled") {
|
|
202
|
+
throw new Error("Transfer cancelled");
|
|
203
|
+
}
|
|
204
|
+
if (Date.now() - startedAt >= timeoutMs) {
|
|
205
|
+
throw new Error(`Transfer ${transferId} did not complete within ${timeoutMs}ms`);
|
|
206
|
+
}
|
|
207
|
+
await sleep(pollIntervalMs);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
async request(method, path, body) {
|
|
211
|
+
const headers = {
|
|
212
|
+
Accept: "application/json",
|
|
213
|
+
"X-Api-Key": this.apiKey
|
|
214
|
+
};
|
|
215
|
+
let payload;
|
|
216
|
+
if (body !== undefined) {
|
|
217
|
+
headers["Content-Type"] = "application/json";
|
|
218
|
+
payload = JSON.stringify(body);
|
|
219
|
+
}
|
|
220
|
+
const response = await this.fetchImpl(`${this.baseUrl}${path}`, {
|
|
221
|
+
method,
|
|
222
|
+
headers,
|
|
223
|
+
body: payload
|
|
224
|
+
});
|
|
225
|
+
const text = await response.text();
|
|
226
|
+
if (!response.ok) {
|
|
227
|
+
throw new BeamApiError(`BEAM API request failed with ${response.status}`, response.status, text);
|
|
228
|
+
}
|
|
229
|
+
return (text ? JSON.parse(text) : {});
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
export function calculateOptimalChunkSize(totalSize) {
|
|
233
|
+
const mb = 1024 * 1024;
|
|
234
|
+
const gb = 1024 * mb;
|
|
235
|
+
const minChunk = 5 * mb;
|
|
236
|
+
const maxChunk = 5 * mb;
|
|
237
|
+
const targetChunks = 10;
|
|
238
|
+
if (totalSize <= minChunk) {
|
|
239
|
+
return totalSize;
|
|
240
|
+
}
|
|
241
|
+
if (totalSize < gb) {
|
|
242
|
+
return Math.max(minChunk, Math.min(maxChunk, Math.floor(totalSize / targetChunks)));
|
|
243
|
+
}
|
|
244
|
+
return maxChunk;
|
|
245
|
+
}
|
|
246
|
+
function resolveBaseUrl(options) {
|
|
247
|
+
if (options.baseUrl) {
|
|
248
|
+
return options.baseUrl;
|
|
249
|
+
}
|
|
250
|
+
return BEAM_DEFAULT_URL;
|
|
251
|
+
}
|
|
252
|
+
function normalizeBaseUrl(value) {
|
|
253
|
+
return value.replace(/\/+$/, "");
|
|
254
|
+
}
|
|
255
|
+
function compact(input) {
|
|
256
|
+
return Object.fromEntries(Object.entries(input).filter(([, value]) => value !== undefined));
|
|
257
|
+
}
|
|
258
|
+
function validateId(value, name) {
|
|
259
|
+
if (!value || value.includes("/") || value.includes("..")) {
|
|
260
|
+
throw new Error(`${name} is invalid`);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
async function abortCreatedUploads(uploads) {
|
|
264
|
+
await Promise.all([...uploads.values()].map(async (upload) => {
|
|
265
|
+
if (!upload.uploadId || upload.destination.provider === "hippius") {
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
await abortMultipartUpload(upload.destination, upload.objectKey, upload.uploadId);
|
|
269
|
+
}));
|
|
270
|
+
}
|
|
271
|
+
function sleep(ms) {
|
|
272
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
273
|
+
}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
package/dist/models.d.ts
ADDED
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
export interface BeamClientOptions {
|
|
2
|
+
apiKey?: string;
|
|
3
|
+
baseUrl?: string;
|
|
4
|
+
fetch?: typeof fetch;
|
|
5
|
+
}
|
|
6
|
+
export interface SourceConfig {
|
|
7
|
+
type: string;
|
|
8
|
+
bucket?: string;
|
|
9
|
+
key?: string;
|
|
10
|
+
region?: string;
|
|
11
|
+
project_id?: string;
|
|
12
|
+
account_id?: string;
|
|
13
|
+
endpoint_url?: string;
|
|
14
|
+
access_key_id?: string;
|
|
15
|
+
secret_access_key?: string;
|
|
16
|
+
url?: string;
|
|
17
|
+
headers?: Record<string, string>;
|
|
18
|
+
}
|
|
19
|
+
export interface DestConfig {
|
|
20
|
+
type: string;
|
|
21
|
+
bucket?: string;
|
|
22
|
+
key?: string;
|
|
23
|
+
region?: string;
|
|
24
|
+
project_id?: string;
|
|
25
|
+
account_id?: string;
|
|
26
|
+
endpoint_url?: string;
|
|
27
|
+
access_key_id?: string;
|
|
28
|
+
secret_access_key?: string;
|
|
29
|
+
url?: string;
|
|
30
|
+
headers?: Record<string, string>;
|
|
31
|
+
}
|
|
32
|
+
export interface CallbackConfig {
|
|
33
|
+
url: string;
|
|
34
|
+
headers?: Record<string, string>;
|
|
35
|
+
}
|
|
36
|
+
export interface TransferCreateRequest {
|
|
37
|
+
sources: SourceConfig[];
|
|
38
|
+
destinations: DestConfig[];
|
|
39
|
+
total_size: number;
|
|
40
|
+
chunk_size?: number;
|
|
41
|
+
name?: string;
|
|
42
|
+
merkle_root?: string;
|
|
43
|
+
chunk_hashes?: string[];
|
|
44
|
+
callbacks?: CallbackConfig[];
|
|
45
|
+
test_mode?: boolean;
|
|
46
|
+
progressive_mode?: boolean;
|
|
47
|
+
}
|
|
48
|
+
export interface RawTransferCreateInput {
|
|
49
|
+
sources: SourceConfig[];
|
|
50
|
+
destinations: DestConfig[];
|
|
51
|
+
totalSize: number;
|
|
52
|
+
chunkSize?: number;
|
|
53
|
+
name?: string;
|
|
54
|
+
merkleRoot?: string;
|
|
55
|
+
chunkHashes?: string[];
|
|
56
|
+
callbacks?: CallbackConfig[];
|
|
57
|
+
testMode?: boolean;
|
|
58
|
+
progressiveMode?: boolean;
|
|
59
|
+
}
|
|
60
|
+
export interface TransferCreateResponse {
|
|
61
|
+
success: boolean;
|
|
62
|
+
transfer_id: string;
|
|
63
|
+
total_chunks: number;
|
|
64
|
+
total_sources: number;
|
|
65
|
+
total_destinations: number;
|
|
66
|
+
source_urls?: Array<Record<string, string>>;
|
|
67
|
+
dest_urls?: Array<Record<string, string>>;
|
|
68
|
+
upload_ids?: Array<string | null>;
|
|
69
|
+
error?: string;
|
|
70
|
+
message?: string;
|
|
71
|
+
}
|
|
72
|
+
export interface TransferCancelResponse {
|
|
73
|
+
success: boolean;
|
|
74
|
+
message?: string;
|
|
75
|
+
}
|
|
76
|
+
export interface DistributeResponse {
|
|
77
|
+
success: boolean;
|
|
78
|
+
transfer_id: string;
|
|
79
|
+
orchestrators_assigned?: number;
|
|
80
|
+
message?: string;
|
|
81
|
+
error?: string;
|
|
82
|
+
}
|
|
83
|
+
export interface SourceStatusInfo {
|
|
84
|
+
source_index: number;
|
|
85
|
+
source_type: string;
|
|
86
|
+
bucket?: string;
|
|
87
|
+
key?: string;
|
|
88
|
+
region?: string;
|
|
89
|
+
}
|
|
90
|
+
export interface DestinationStatusInfo {
|
|
91
|
+
dest_index: number;
|
|
92
|
+
dest_type: string;
|
|
93
|
+
status: string;
|
|
94
|
+
chunks_delivered?: number;
|
|
95
|
+
chunks_pending?: number;
|
|
96
|
+
chunks_in_progress?: number;
|
|
97
|
+
chunks_completed?: number;
|
|
98
|
+
chunks_failed?: number;
|
|
99
|
+
bytes_delivered?: number;
|
|
100
|
+
location?: string;
|
|
101
|
+
}
|
|
102
|
+
export interface TransferStatusInfo {
|
|
103
|
+
transfer_id: string;
|
|
104
|
+
name?: string;
|
|
105
|
+
status: string;
|
|
106
|
+
total_size: number;
|
|
107
|
+
total_chunks: number;
|
|
108
|
+
total_sources?: number;
|
|
109
|
+
total_destinations?: number;
|
|
110
|
+
chunks_completed?: number;
|
|
111
|
+
destinations_completed?: number;
|
|
112
|
+
sources?: SourceStatusInfo[];
|
|
113
|
+
destinations?: DestinationStatusInfo[];
|
|
114
|
+
created_at?: string;
|
|
115
|
+
started_at?: string;
|
|
116
|
+
completed_at?: string;
|
|
117
|
+
error_message?: string;
|
|
118
|
+
}
|
|
119
|
+
export interface S3ProviderConfig {
|
|
120
|
+
provider: "s3";
|
|
121
|
+
id?: string;
|
|
122
|
+
bucket: string;
|
|
123
|
+
key: string;
|
|
124
|
+
region?: string;
|
|
125
|
+
access_key_id: string;
|
|
126
|
+
secret_access_key: string;
|
|
127
|
+
session_token?: string;
|
|
128
|
+
endpoint_url?: string;
|
|
129
|
+
}
|
|
130
|
+
export interface R2ProviderConfig {
|
|
131
|
+
provider: "r2";
|
|
132
|
+
id?: string;
|
|
133
|
+
bucket: string;
|
|
134
|
+
key: string;
|
|
135
|
+
access_key_id: string;
|
|
136
|
+
secret_access_key: string;
|
|
137
|
+
account_id?: string;
|
|
138
|
+
endpoint_url?: string;
|
|
139
|
+
}
|
|
140
|
+
export interface HippiusProviderConfig {
|
|
141
|
+
provider: "hippius";
|
|
142
|
+
id?: string;
|
|
143
|
+
bucket: string;
|
|
144
|
+
key: string;
|
|
145
|
+
api_token: string;
|
|
146
|
+
base_url?: string;
|
|
147
|
+
}
|
|
148
|
+
export type ProviderConfig = S3ProviderConfig | R2ProviderConfig | HippiusProviderConfig;
|
|
149
|
+
export type ProviderSourceConfig = ProviderConfig;
|
|
150
|
+
export type ProviderDestinationConfig = ProviderConfig;
|
|
151
|
+
export type S3ProviderConfigInput = Omit<S3ProviderConfig, "provider">;
|
|
152
|
+
export type R2ProviderConfigInput = Omit<R2ProviderConfig, "provider">;
|
|
153
|
+
export type HippiusProviderConfigInput = Omit<HippiusProviderConfig, "provider">;
|
|
154
|
+
export declare const S3ProviderConfig: Readonly<{
|
|
155
|
+
create(input: S3ProviderConfigInput): S3ProviderConfig;
|
|
156
|
+
}>;
|
|
157
|
+
export declare const R2ProviderConfig: Readonly<{
|
|
158
|
+
create(input: R2ProviderConfigInput): R2ProviderConfig;
|
|
159
|
+
}>;
|
|
160
|
+
export declare const HippiusProviderConfig: Readonly<{
|
|
161
|
+
create(input: HippiusProviderConfigInput): HippiusProviderConfig;
|
|
162
|
+
}>;
|
|
163
|
+
export interface ProviderTransferCreateInput {
|
|
164
|
+
sources: ProviderSourceConfig[];
|
|
165
|
+
destinations: ProviderDestinationConfig[];
|
|
166
|
+
name?: string;
|
|
167
|
+
testMode?: boolean;
|
|
168
|
+
expiresIn?: number;
|
|
169
|
+
/**
|
|
170
|
+
* Defaults to true. Set to false to only prepare and attach signed URLs.
|
|
171
|
+
*/
|
|
172
|
+
distribute?: boolean;
|
|
173
|
+
}
|
|
174
|
+
export interface PreparedHttpSource {
|
|
175
|
+
source_id: string;
|
|
176
|
+
type: "http";
|
|
177
|
+
provider?: string;
|
|
178
|
+
url: string;
|
|
179
|
+
size: number;
|
|
180
|
+
filename?: string;
|
|
181
|
+
headers?: Record<string, string>;
|
|
182
|
+
expires_at?: string;
|
|
183
|
+
metadata?: Record<string, unknown>;
|
|
184
|
+
}
|
|
185
|
+
export interface PreparedDestination {
|
|
186
|
+
destination_id: string;
|
|
187
|
+
provider: string;
|
|
188
|
+
mode?: "object_chunks" | "http_chunks";
|
|
189
|
+
logical_prefix?: string;
|
|
190
|
+
metadata?: Record<string, unknown>;
|
|
191
|
+
}
|
|
192
|
+
export interface ChunkDestinationSigningTarget {
|
|
193
|
+
destination_id: string;
|
|
194
|
+
provider?: string;
|
|
195
|
+
object_key?: string;
|
|
196
|
+
metadata?: Record<string, unknown>;
|
|
197
|
+
}
|
|
198
|
+
export interface ChunkSigningPlanItem {
|
|
199
|
+
chunk_index: number;
|
|
200
|
+
source_id: string;
|
|
201
|
+
source_chunk_index: number;
|
|
202
|
+
source_offset: number;
|
|
203
|
+
chunk_size: number;
|
|
204
|
+
source_url: string;
|
|
205
|
+
destinations: ChunkDestinationSigningTarget[];
|
|
206
|
+
}
|
|
207
|
+
export interface SignedChunkRoute {
|
|
208
|
+
source_id: string;
|
|
209
|
+
destination_id: string;
|
|
210
|
+
chunk_index: number;
|
|
211
|
+
source_url: string;
|
|
212
|
+
dest_url: string;
|
|
213
|
+
source_offset: number;
|
|
214
|
+
chunk_size: number;
|
|
215
|
+
expires_at?: string;
|
|
216
|
+
headers?: Record<string, string>;
|
|
217
|
+
metadata?: Record<string, unknown>;
|
|
218
|
+
}
|
|
219
|
+
export interface TransferPrepareResponse {
|
|
220
|
+
success: boolean;
|
|
221
|
+
transfer_id: string;
|
|
222
|
+
transfer_key?: string;
|
|
223
|
+
test_mode?: boolean;
|
|
224
|
+
chunk_size?: number;
|
|
225
|
+
total_size?: number;
|
|
226
|
+
total_sources?: number;
|
|
227
|
+
total_destinations?: number;
|
|
228
|
+
logical_chunks?: number;
|
|
229
|
+
total_chunks?: number;
|
|
230
|
+
chunk_plan: ChunkSigningPlanItem[];
|
|
231
|
+
error?: string;
|
|
232
|
+
message?: string;
|
|
233
|
+
}
|
|
234
|
+
export interface AttachSignedUrlsResponse {
|
|
235
|
+
success: boolean;
|
|
236
|
+
transfer_id: string;
|
|
237
|
+
total_routes?: number;
|
|
238
|
+
urls_expires_at?: string;
|
|
239
|
+
error?: string;
|
|
240
|
+
message?: string;
|
|
241
|
+
}
|
package/dist/models.js
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export const S3ProviderConfig = Object.freeze({
|
|
2
|
+
create(input) {
|
|
3
|
+
requireFields("s3", input, ["bucket", "key", "access_key_id", "secret_access_key"]);
|
|
4
|
+
return { provider: "s3", ...input };
|
|
5
|
+
}
|
|
6
|
+
});
|
|
7
|
+
export const R2ProviderConfig = Object.freeze({
|
|
8
|
+
create(input) {
|
|
9
|
+
requireFields("r2", input, ["bucket", "key", "access_key_id", "secret_access_key"]);
|
|
10
|
+
if (!hasText(input.account_id) && !hasText(input.endpoint_url)) {
|
|
11
|
+
throw new Error("r2 config requires account_id or endpoint_url.");
|
|
12
|
+
}
|
|
13
|
+
return { provider: "r2", ...input };
|
|
14
|
+
}
|
|
15
|
+
});
|
|
16
|
+
export const HippiusProviderConfig = Object.freeze({
|
|
17
|
+
create(input) {
|
|
18
|
+
requireFields("hippius", input, ["bucket", "key", "api_token"]);
|
|
19
|
+
return { provider: "hippius", ...input };
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
function requireFields(provider, input, fields) {
|
|
23
|
+
for (const field of fields) {
|
|
24
|
+
if (!hasText(input[field])) {
|
|
25
|
+
throw new Error(`${provider} config requires ${field}.`);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
function hasText(value) {
|
|
30
|
+
return typeof value === "string" && value.trim().length > 0;
|
|
31
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { ChunkDestinationSigningTarget, ChunkSigningPlanItem, PreparedDestination, PreparedHttpSource, ProviderDestinationConfig, ProviderSourceConfig, SignedChunkRoute } from "./models.js";
|
|
2
|
+
export declare function nowIso(): string;
|
|
3
|
+
export declare function expiresAtIso(expiresIn: number): string;
|
|
4
|
+
export declare function prepareProviderSource(source: ProviderSourceConfig, options?: {
|
|
5
|
+
index?: number;
|
|
6
|
+
expiresIn?: number;
|
|
7
|
+
fetchImpl?: typeof fetch;
|
|
8
|
+
}): Promise<PreparedHttpSource>;
|
|
9
|
+
export declare function prepareProviderDestination(destination: ProviderDestinationConfig, options?: {
|
|
10
|
+
index?: number;
|
|
11
|
+
}): PreparedDestination;
|
|
12
|
+
export declare function createMultipartUpload(destination: ProviderDestinationConfig, objectKey: string): Promise<string>;
|
|
13
|
+
export declare function signCompleteMultipartUpload(destination: ProviderDestinationConfig, objectKey: string, uploadId: string, expiresIn: number): Promise<string>;
|
|
14
|
+
export declare function signAbortMultipartUpload(destination: ProviderDestinationConfig, objectKey: string, uploadId: string, expiresIn: number): Promise<string>;
|
|
15
|
+
export declare function abortMultipartUpload(destination: ProviderDestinationConfig, objectKey: string, uploadId: string): Promise<void>;
|
|
16
|
+
export declare function signDestinationRoute(input: {
|
|
17
|
+
chunk: ChunkSigningPlanItem;
|
|
18
|
+
target: ChunkDestinationSigningTarget;
|
|
19
|
+
destination: ProviderDestinationConfig;
|
|
20
|
+
expiresIn: number;
|
|
21
|
+
uploadId?: string;
|
|
22
|
+
partNumber?: number;
|
|
23
|
+
multipartObjectKey?: string;
|
|
24
|
+
completeUrl?: string;
|
|
25
|
+
abortUrl?: string;
|
|
26
|
+
transferId?: string;
|
|
27
|
+
multipartGroupId?: string;
|
|
28
|
+
multipartCreatedAt?: string;
|
|
29
|
+
fetchImpl?: typeof fetch;
|
|
30
|
+
}): Promise<SignedChunkRoute>;
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
import { AbortMultipartUploadCommand, CompleteMultipartUploadCommand, CreateMultipartUploadCommand, GetObjectCommand, HeadObjectCommand, PutObjectCommand, S3Client, UploadPartCommand } from "@aws-sdk/client-s3";
|
|
2
|
+
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
|
|
3
|
+
export function nowIso() {
|
|
4
|
+
return new Date().toISOString();
|
|
5
|
+
}
|
|
6
|
+
export function expiresAtIso(expiresIn) {
|
|
7
|
+
return new Date(Date.now() + expiresIn * 1000).toISOString();
|
|
8
|
+
}
|
|
9
|
+
export async function prepareProviderSource(source, options = {}) {
|
|
10
|
+
const index = options.index ?? 0;
|
|
11
|
+
const expiresIn = options.expiresIn ?? 3600;
|
|
12
|
+
if (source.provider === "s3") {
|
|
13
|
+
const client = createS3Client(source);
|
|
14
|
+
const head = await client.send(new HeadObjectCommand({ Bucket: source.bucket, Key: source.key }));
|
|
15
|
+
const getUrl = await getSignedUrl(client, new GetObjectCommand({ Bucket: source.bucket, Key: source.key }), { expiresIn });
|
|
16
|
+
return {
|
|
17
|
+
source_id: source.id ?? `src_${index}`,
|
|
18
|
+
type: "http",
|
|
19
|
+
provider: "s3",
|
|
20
|
+
url: getUrl,
|
|
21
|
+
size: Number(head.ContentLength ?? 0),
|
|
22
|
+
filename: filename(source.key),
|
|
23
|
+
expires_at: expiresAtIso(expiresIn),
|
|
24
|
+
metadata: { bucket: source.bucket, key: source.key, region: source.region ?? "us-east-1" }
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
if (source.provider === "r2") {
|
|
28
|
+
const client = createR2Client(source);
|
|
29
|
+
const head = await client.send(new HeadObjectCommand({ Bucket: source.bucket, Key: source.key }));
|
|
30
|
+
const getUrl = await getSignedUrl(client, new GetObjectCommand({ Bucket: source.bucket, Key: source.key }), { expiresIn });
|
|
31
|
+
return {
|
|
32
|
+
source_id: source.id ?? `src_${index}`,
|
|
33
|
+
type: "http",
|
|
34
|
+
provider: "r2",
|
|
35
|
+
url: getUrl,
|
|
36
|
+
size: Number(head.ContentLength ?? 0),
|
|
37
|
+
filename: filename(source.key),
|
|
38
|
+
expires_at: expiresAtIso(expiresIn),
|
|
39
|
+
metadata: { bucket: source.bucket, key: source.key, endpoint_url: r2Endpoint(source) }
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
if (source.provider === "hippius") {
|
|
43
|
+
const fetchImpl = options.fetchImpl ?? globalThis.fetch;
|
|
44
|
+
const baseUrl = source.base_url ?? "https://api.hippius.com";
|
|
45
|
+
const size = await hippiusObjectSize(fetchImpl, baseUrl, source.api_token, source.bucket, source.key);
|
|
46
|
+
const url = await hippiusPresign(fetchImpl, baseUrl, source.api_token, source.bucket, source.key, "get", expiresIn);
|
|
47
|
+
return {
|
|
48
|
+
source_id: source.id ?? `src_${index}`,
|
|
49
|
+
type: "http",
|
|
50
|
+
provider: "hippius",
|
|
51
|
+
url,
|
|
52
|
+
size,
|
|
53
|
+
filename: filename(source.key),
|
|
54
|
+
expires_at: expiresAtIso(expiresIn),
|
|
55
|
+
metadata: { bucket: source.bucket, key: source.key, base_url: baseUrl }
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
return assertNever(source);
|
|
59
|
+
}
|
|
60
|
+
export function prepareProviderDestination(destination, options = {}) {
|
|
61
|
+
const index = options.index ?? 0;
|
|
62
|
+
const destinationId = destination.id ?? `dst_${index}`;
|
|
63
|
+
if (destination.provider === "s3") {
|
|
64
|
+
return {
|
|
65
|
+
destination_id: destinationId,
|
|
66
|
+
provider: "s3",
|
|
67
|
+
logical_prefix: destination.key,
|
|
68
|
+
metadata: { bucket: destination.bucket, key: destination.key, region: destination.region ?? "us-east-1" }
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
if (destination.provider === "r2") {
|
|
72
|
+
return {
|
|
73
|
+
destination_id: destinationId,
|
|
74
|
+
provider: "r2",
|
|
75
|
+
logical_prefix: destination.key,
|
|
76
|
+
metadata: { bucket: destination.bucket, key: destination.key, endpoint_url: r2Endpoint(destination) }
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
return {
|
|
80
|
+
destination_id: destinationId,
|
|
81
|
+
provider: "hippius",
|
|
82
|
+
logical_prefix: destination.key.replace(/\/+$/, ""),
|
|
83
|
+
metadata: { bucket: destination.bucket, key: destination.key, base_url: destination.base_url ?? "https://api.hippius.com" }
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
export async function createMultipartUpload(destination, objectKey) {
|
|
87
|
+
if (destination.provider === "s3") {
|
|
88
|
+
const client = createS3Client(destination);
|
|
89
|
+
const response = await client.send(new CreateMultipartUploadCommand({ Bucket: destination.bucket, Key: objectKey }));
|
|
90
|
+
if (!response.UploadId) {
|
|
91
|
+
throw new Error(`provider did not return UploadId for ${objectKey}`);
|
|
92
|
+
}
|
|
93
|
+
return response.UploadId;
|
|
94
|
+
}
|
|
95
|
+
if (destination.provider === "r2") {
|
|
96
|
+
const client = createR2Client(destination);
|
|
97
|
+
const response = await client.send(new CreateMultipartUploadCommand({ Bucket: destination.bucket, Key: objectKey }));
|
|
98
|
+
if (!response.UploadId) {
|
|
99
|
+
throw new Error(`provider did not return UploadId for ${objectKey}`);
|
|
100
|
+
}
|
|
101
|
+
return response.UploadId;
|
|
102
|
+
}
|
|
103
|
+
throw new Error(`multipart upload is not supported for ${destination.provider}`);
|
|
104
|
+
}
|
|
105
|
+
export async function signCompleteMultipartUpload(destination, objectKey, uploadId, expiresIn) {
|
|
106
|
+
if (destination.provider === "s3" || destination.provider === "r2") {
|
|
107
|
+
const client = destination.provider === "s3" ? createS3Client(destination) : createR2Client(destination);
|
|
108
|
+
return getSignedUrl(client, new CompleteMultipartUploadCommand({ Bucket: destination.bucket, Key: objectKey, UploadId: uploadId }), { expiresIn });
|
|
109
|
+
}
|
|
110
|
+
throw new Error(`multipart completion is not supported for ${destination.provider}`);
|
|
111
|
+
}
|
|
112
|
+
export async function signAbortMultipartUpload(destination, objectKey, uploadId, expiresIn) {
|
|
113
|
+
if (destination.provider === "s3" || destination.provider === "r2") {
|
|
114
|
+
const client = destination.provider === "s3" ? createS3Client(destination) : createR2Client(destination);
|
|
115
|
+
return getSignedUrl(client, new AbortMultipartUploadCommand({ Bucket: destination.bucket, Key: objectKey, UploadId: uploadId }), { expiresIn });
|
|
116
|
+
}
|
|
117
|
+
throw new Error(`multipart abort is not supported for ${destination.provider}`);
|
|
118
|
+
}
|
|
119
|
+
export async function abortMultipartUpload(destination, objectKey, uploadId) {
|
|
120
|
+
if (destination.provider === "s3") {
|
|
121
|
+
await createS3Client(destination).send(new AbortMultipartUploadCommand({ Bucket: destination.bucket, Key: objectKey, UploadId: uploadId }));
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
if (destination.provider === "r2") {
|
|
125
|
+
await createR2Client(destination).send(new AbortMultipartUploadCommand({ Bucket: destination.bucket, Key: objectKey, UploadId: uploadId }));
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
throw new Error(`multipart abort is not supported for ${destination.provider}`);
|
|
129
|
+
}
|
|
130
|
+
export async function signDestinationRoute(input) {
|
|
131
|
+
const objectKey = input.target.object_key;
|
|
132
|
+
if (!objectKey) {
|
|
133
|
+
throw new Error("destination signing target is missing object_key");
|
|
134
|
+
}
|
|
135
|
+
const uploadPartObjectKey = input.multipartObjectKey ?? objectKey;
|
|
136
|
+
const destUrl = await signDestinationUrl({
|
|
137
|
+
destination: input.destination,
|
|
138
|
+
objectKey,
|
|
139
|
+
uploadPartObjectKey,
|
|
140
|
+
expiresIn: input.expiresIn,
|
|
141
|
+
uploadId: input.uploadId,
|
|
142
|
+
partNumber: input.partNumber,
|
|
143
|
+
fetchImpl: input.fetchImpl
|
|
144
|
+
});
|
|
145
|
+
return {
|
|
146
|
+
source_id: input.chunk.source_id,
|
|
147
|
+
destination_id: input.target.destination_id,
|
|
148
|
+
chunk_index: input.chunk.chunk_index,
|
|
149
|
+
source_url: input.chunk.source_url,
|
|
150
|
+
dest_url: destUrl,
|
|
151
|
+
source_offset: input.chunk.source_offset,
|
|
152
|
+
chunk_size: input.chunk.chunk_size,
|
|
153
|
+
expires_at: expiresAtIso(input.expiresIn),
|
|
154
|
+
metadata: {
|
|
155
|
+
...(input.target.metadata ?? {}),
|
|
156
|
+
...(input.transferId ? { transfer_id: input.transferId } : {}),
|
|
157
|
+
...(input.multipartGroupId ? { multipart_group_id: input.multipartGroupId } : {}),
|
|
158
|
+
...(input.multipartCreatedAt ? { multipart_created_at: input.multipartCreatedAt } : {}),
|
|
159
|
+
...(input.uploadId ? { upload_id: input.uploadId } : {}),
|
|
160
|
+
...(input.partNumber ? { part_number: input.partNumber } : {}),
|
|
161
|
+
...(input.completeUrl ? { complete_url: input.completeUrl } : {}),
|
|
162
|
+
...(input.abortUrl ? { abort_url: input.abortUrl } : {})
|
|
163
|
+
}
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
async function signDestinationUrl(input) {
|
|
167
|
+
switch (input.destination.provider) {
|
|
168
|
+
case "s3":
|
|
169
|
+
case "r2": {
|
|
170
|
+
const client = input.destination.provider === "s3"
|
|
171
|
+
? createS3Client(input.destination)
|
|
172
|
+
: createR2Client(input.destination);
|
|
173
|
+
if (input.uploadId && input.partNumber) {
|
|
174
|
+
return getSignedUrl(client, new UploadPartCommand({
|
|
175
|
+
Bucket: input.destination.bucket,
|
|
176
|
+
Key: input.uploadPartObjectKey,
|
|
177
|
+
UploadId: input.uploadId,
|
|
178
|
+
PartNumber: input.partNumber
|
|
179
|
+
}), { expiresIn: input.expiresIn });
|
|
180
|
+
}
|
|
181
|
+
return getSignedUrl(client, new PutObjectCommand({ Bucket: input.destination.bucket, Key: input.objectKey }), { expiresIn: input.expiresIn });
|
|
182
|
+
}
|
|
183
|
+
case "hippius":
|
|
184
|
+
return hippiusPresign(input.fetchImpl ?? globalThis.fetch, input.destination.base_url ?? "https://api.hippius.com", input.destination.api_token, input.destination.bucket, input.objectKey, "put", input.expiresIn);
|
|
185
|
+
default:
|
|
186
|
+
return assertNever(input.destination);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
function createS3Client(source) {
|
|
190
|
+
return new S3Client({
|
|
191
|
+
region: source.region ?? "us-east-1",
|
|
192
|
+
endpoint: source.endpoint_url,
|
|
193
|
+
credentials: {
|
|
194
|
+
accessKeyId: source.access_key_id,
|
|
195
|
+
secretAccessKey: source.secret_access_key,
|
|
196
|
+
sessionToken: source.session_token
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
function createR2Client(source) {
|
|
201
|
+
return new S3Client({
|
|
202
|
+
region: "auto",
|
|
203
|
+
endpoint: r2Endpoint(source),
|
|
204
|
+
credentials: {
|
|
205
|
+
accessKeyId: source.access_key_id,
|
|
206
|
+
secretAccessKey: source.secret_access_key
|
|
207
|
+
}
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
function r2Endpoint(source) {
|
|
211
|
+
if (source.endpoint_url) {
|
|
212
|
+
return source.endpoint_url;
|
|
213
|
+
}
|
|
214
|
+
if (source.account_id) {
|
|
215
|
+
return `https://${source.account_id}.r2.cloudflarestorage.com`;
|
|
216
|
+
}
|
|
217
|
+
throw new Error("R2 signing requires account_id or endpoint_url");
|
|
218
|
+
}
|
|
219
|
+
async function hippiusPresign(fetchImpl, baseUrl, token, bucket, key, action, expiresIn) {
|
|
220
|
+
const url = new URL(`/api/objectstore/buckets/${bucket}/presigned-url/`, baseUrl);
|
|
221
|
+
url.searchParams.set("key", key);
|
|
222
|
+
url.searchParams.set("action", action);
|
|
223
|
+
url.searchParams.set("expires_in", String(expiresIn));
|
|
224
|
+
const response = await fetchImpl(url, { headers: { Authorization: `Token ${token}` } });
|
|
225
|
+
if (!response.ok) {
|
|
226
|
+
throw new Error(`Hippius presigned URL failed with ${response.status}: ${await response.text()}`);
|
|
227
|
+
}
|
|
228
|
+
return String((await response.json()).url);
|
|
229
|
+
}
|
|
230
|
+
async function hippiusObjectSize(fetchImpl, baseUrl, token, bucket, key) {
|
|
231
|
+
const url = new URL(`/api/objectstore/buckets/${bucket}/objects/`, baseUrl);
|
|
232
|
+
url.searchParams.set("prefix", key);
|
|
233
|
+
url.searchParams.set("max_keys", "1");
|
|
234
|
+
const response = await fetchImpl(url, { headers: { Authorization: `Token ${token}` } });
|
|
235
|
+
if (!response.ok) {
|
|
236
|
+
throw new Error(`Hippius object lookup failed with ${response.status}: ${await response.text()}`);
|
|
237
|
+
}
|
|
238
|
+
const payload = await response.json();
|
|
239
|
+
const contents = payload.Contents ?? payload.contents ?? [];
|
|
240
|
+
if (!contents.length) {
|
|
241
|
+
throw new Error(`Object not found: hippius://${bucket}/${key}`);
|
|
242
|
+
}
|
|
243
|
+
return Number(contents[0].Size ?? contents[0].size);
|
|
244
|
+
}
|
|
245
|
+
function filename(key) {
|
|
246
|
+
return key.split("/").filter(Boolean).at(-1) ?? key;
|
|
247
|
+
}
|
|
248
|
+
function assertNever(value) {
|
|
249
|
+
throw new Error(`Unsupported provider config: ${JSON.stringify(value)}`);
|
|
250
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@beam-network/sdk",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "TypeScript SDK for BEAM transfer creation and management.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"sideEffects": false,
|
|
8
|
+
"main": "./dist/index.js",
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"exports": {
|
|
11
|
+
".": {
|
|
12
|
+
"types": "./dist/index.d.ts",
|
|
13
|
+
"import": "./dist/index.js"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"dist",
|
|
18
|
+
"README.md"
|
|
19
|
+
],
|
|
20
|
+
"scripts": {
|
|
21
|
+
"build": "tsc -p tsconfig.json",
|
|
22
|
+
"test": "tsc -p tsconfig.json --noEmit && npm run test:functional",
|
|
23
|
+
"test:functional": "npm run build && node --test test/*.test.mjs",
|
|
24
|
+
"lint": "tsc -p tsconfig.json --noEmit"
|
|
25
|
+
},
|
|
26
|
+
"publishConfig": {
|
|
27
|
+
"access": "public"
|
|
28
|
+
},
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"@aws-sdk/client-s3": "^3.806.0",
|
|
31
|
+
"@aws-sdk/s3-request-presigner": "^3.806.0"
|
|
32
|
+
},
|
|
33
|
+
"devDependencies": {
|
|
34
|
+
"typescript": "^5.5.0"
|
|
35
|
+
}
|
|
36
|
+
}
|