@beam-network/sdk 0.1.0 → 0.2.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 CHANGED
@@ -34,13 +34,61 @@ const transfer = await beam.createTransfer({
34
34
  secret_access_key: "aws-secret-key"
35
35
  })
36
36
  ],
37
- name: "r2-to-s3-report"
37
+ name: "r2-to-s3-report",
38
+ testMode: true
38
39
  });
39
40
 
40
41
  const status = await beam.waitForTransfer(transfer.transfer_id);
41
42
  console.log(status.status);
42
43
  ```
43
44
 
44
- The main `createTransfer` API is provider-aware and strictly typed for S3, R2, and Hippius configs.
45
+ The main `createTransfer` API is provider-aware and strictly typed for S3, R2, S3-compatible, and Hippius configs.
46
+ Use `testMode: true` to create a BeamCore test-mode transfer.
47
+
48
+ ## S3-Compatible Providers
49
+
50
+ Use `S3CompatibleProviderConfig` for S3-compatible providers with custom
51
+ endpoints, such as Wasabi, MinIO, Backblaze B2 S3 API, DigitalOcean Spaces,
52
+ Scaleway Object Storage, and self-hosted S3-compatible systems.
53
+
54
+ ```ts
55
+ import {
56
+ BeamClient,
57
+ S3CompatibleProviderConfig
58
+ } from "@beam-network/sdk";
59
+
60
+ const beam = new BeamClient({ apiKey: process.env.BEAM_API_KEY! });
61
+
62
+ await beam.prepareProviderTransfer({
63
+ sources: [
64
+ S3CompatibleProviderConfig.create({
65
+ provider: "wasabi",
66
+ bucket: "my-bucket",
67
+ key: "input/file.bin",
68
+ region: "us-east-1",
69
+ endpoint_url: "https://s3.us-east-1.wasabisys.com",
70
+ access_key_id: process.env.WASABI_ACCESS_KEY_ID!,
71
+ secret_access_key: process.env.WASABI_SECRET_ACCESS_KEY!
72
+ })
73
+ ],
74
+ destinations: [
75
+ S3CompatibleProviderConfig.create({
76
+ provider: "minio",
77
+ bucket: "archive",
78
+ key: "file.bin",
79
+ endpoint_url: "https://minio.example.com",
80
+ force_path_style: true,
81
+ access_key_id: process.env.MINIO_ACCESS_KEY_ID!,
82
+ secret_access_key: process.env.MINIO_SECRET_ACCESS_KEY!
83
+ })
84
+ ],
85
+ name: "S3-compatible transfer"
86
+ });
87
+ ```
88
+
89
+ Use `S3ProviderConfig` for ordinary AWS S3 if preferred, and keep using
90
+ `R2ProviderConfig` for existing Cloudflare R2 code. Use
91
+ `S3CompatibleProviderConfig` when a provider needs a custom S3-compatible
92
+ endpoint.
45
93
 
46
94
  For low-level BeamCore `/transfers/create` calls with raw HTTP-style configs, use `createRawTransfer`.
package/dist/client.js CHANGED
@@ -112,7 +112,7 @@ export class BeamClient {
112
112
  let upload = multipartUploads.get(multipartKey);
113
113
  if (!upload) {
114
114
  const multipartCreatedAt = nowIso();
115
- if (destination.provider === "hippius") {
115
+ if (isHippiusDestination(destination)) {
116
116
  upload = {
117
117
  destination,
118
118
  objectKey: finalObjectKey,
@@ -262,12 +262,15 @@ function validateId(value, name) {
262
262
  }
263
263
  async function abortCreatedUploads(uploads) {
264
264
  await Promise.all([...uploads.values()].map(async (upload) => {
265
- if (!upload.uploadId || upload.destination.provider === "hippius") {
265
+ if (!upload.uploadId || isHippiusDestination(upload.destination)) {
266
266
  return;
267
267
  }
268
268
  await abortMultipartUpload(upload.destination, upload.objectKey, upload.uploadId);
269
269
  }));
270
270
  }
271
+ function isHippiusDestination(destination) {
272
+ return destination.provider === "hippius" && "api_token" in destination;
273
+ }
271
274
  function sleep(ms) {
272
275
  return new Promise((resolve) => setTimeout(resolve, ms));
273
276
  }
package/dist/models.d.ts CHANGED
@@ -54,6 +54,9 @@ export interface RawTransferCreateInput {
54
54
  merkleRoot?: string;
55
55
  chunkHashes?: string[];
56
56
  callbacks?: CallbackConfig[];
57
+ /**
58
+ * Maps to BeamCore test_mode.
59
+ */
57
60
  testMode?: boolean;
58
61
  progressiveMode?: boolean;
59
62
  }
@@ -137,6 +140,20 @@ export interface R2ProviderConfig {
137
140
  account_id?: string;
138
141
  endpoint_url?: string;
139
142
  }
143
+ export interface S3CompatibleProviderConfig {
144
+ provider: string;
145
+ driver?: "s3-compatible";
146
+ id?: string;
147
+ bucket: string;
148
+ key: string;
149
+ region?: string;
150
+ endpoint_url?: string;
151
+ access_key_id: string;
152
+ secret_access_key: string;
153
+ session_token?: string;
154
+ force_path_style?: boolean;
155
+ account_id?: string;
156
+ }
140
157
  export interface HippiusProviderConfig {
141
158
  provider: "hippius";
142
159
  id?: string;
@@ -145,11 +162,12 @@ export interface HippiusProviderConfig {
145
162
  api_token: string;
146
163
  base_url?: string;
147
164
  }
148
- export type ProviderConfig = S3ProviderConfig | R2ProviderConfig | HippiusProviderConfig;
165
+ export type ProviderConfig = S3ProviderConfig | R2ProviderConfig | S3CompatibleProviderConfig | HippiusProviderConfig;
149
166
  export type ProviderSourceConfig = ProviderConfig;
150
167
  export type ProviderDestinationConfig = ProviderConfig;
151
168
  export type S3ProviderConfigInput = Omit<S3ProviderConfig, "provider">;
152
169
  export type R2ProviderConfigInput = Omit<R2ProviderConfig, "provider">;
170
+ export type S3CompatibleProviderConfigInput = Omit<S3CompatibleProviderConfig, "driver">;
153
171
  export type HippiusProviderConfigInput = Omit<HippiusProviderConfig, "provider">;
154
172
  export declare const S3ProviderConfig: Readonly<{
155
173
  create(input: S3ProviderConfigInput): S3ProviderConfig;
@@ -157,6 +175,9 @@ export declare const S3ProviderConfig: Readonly<{
157
175
  export declare const R2ProviderConfig: Readonly<{
158
176
  create(input: R2ProviderConfigInput): R2ProviderConfig;
159
177
  }>;
178
+ export declare const S3CompatibleProviderConfig: Readonly<{
179
+ create(input: S3CompatibleProviderConfigInput): S3CompatibleProviderConfig;
180
+ }>;
160
181
  export declare const HippiusProviderConfig: Readonly<{
161
182
  create(input: HippiusProviderConfigInput): HippiusProviderConfig;
162
183
  }>;
@@ -164,6 +185,9 @@ export interface ProviderTransferCreateInput {
164
185
  sources: ProviderSourceConfig[];
165
186
  destinations: ProviderDestinationConfig[];
166
187
  name?: string;
188
+ /**
189
+ * Maps to BeamCore test_mode.
190
+ */
167
191
  testMode?: boolean;
168
192
  expiresIn?: number;
169
193
  /**
package/dist/models.js CHANGED
@@ -13,6 +13,25 @@ export const R2ProviderConfig = Object.freeze({
13
13
  return { provider: "r2", ...input };
14
14
  }
15
15
  });
16
+ export const S3CompatibleProviderConfig = Object.freeze({
17
+ create(input) {
18
+ requireFields(input.provider || "s3-compatible", input, [
19
+ "provider",
20
+ "bucket",
21
+ "key",
22
+ "access_key_id",
23
+ "secret_access_key"
24
+ ]);
25
+ const provider = input.provider.trim().toLowerCase();
26
+ if (provider === "r2" && !hasText(input.endpoint_url) && !hasText(input.account_id)) {
27
+ throw new Error("r2 config requires account_id or endpoint_url.");
28
+ }
29
+ if (provider !== "s3" && provider !== "r2" && !hasText(input.endpoint_url)) {
30
+ throw new Error(`${provider} config requires endpoint_url.`);
31
+ }
32
+ return { ...input, provider, driver: "s3-compatible" };
33
+ }
34
+ });
16
35
  export const HippiusProviderConfig = Object.freeze({
17
36
  create(input) {
18
37
  requireFields("hippius", input, ["bucket", "key", "api_token"]);
@@ -1,4 +1,5 @@
1
- import type { ChunkDestinationSigningTarget, ChunkSigningPlanItem, PreparedDestination, PreparedHttpSource, ProviderDestinationConfig, ProviderSourceConfig, SignedChunkRoute } from "./models.js";
1
+ import type { ChunkDestinationSigningTarget, ChunkSigningPlanItem, PreparedDestination, PreparedHttpSource, ProviderDestinationConfig, ProviderSourceConfig, R2ProviderConfig, S3CompatibleProviderConfig, S3ProviderConfig, SignedChunkRoute } from "./models.js";
2
+ export type AnyS3CompatibleProviderConfig = S3ProviderConfig | R2ProviderConfig | S3CompatibleProviderConfig;
2
3
  export declare function nowIso(): string;
3
4
  export declare function expiresAtIso(expiresIn: number): string;
4
5
  export declare function prepareProviderSource(source: ProviderSourceConfig, options?: {
@@ -28,3 +29,7 @@ export declare function signDestinationRoute(input: {
28
29
  multipartCreatedAt?: string;
29
30
  fetchImpl?: typeof fetch;
30
31
  }): Promise<SignedChunkRoute>;
32
+ export declare function isS3CompatibleProvider(config: ProviderSourceConfig | ProviderDestinationConfig): config is AnyS3CompatibleProviderConfig;
33
+ export declare function s3CompatibleEndpoint(source: AnyS3CompatibleProviderConfig): string | undefined;
34
+ export declare function s3CompatibleRegion(source: AnyS3CompatibleProviderConfig): string;
35
+ export declare function s3CompatibleForcePathStyle(source: AnyS3CompatibleProviderConfig, endpoint?: string | undefined): boolean | undefined;
@@ -9,37 +9,23 @@ export function expiresAtIso(expiresIn) {
9
9
  export async function prepareProviderSource(source, options = {}) {
10
10
  const index = options.index ?? 0;
11
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);
12
+ if (isS3CompatibleProvider(source)) {
13
+ const endpoint = s3CompatibleEndpoint(source);
14
+ const client = createS3CompatibleClient(source, endpoint);
29
15
  const head = await client.send(new HeadObjectCommand({ Bucket: source.bucket, Key: source.key }));
30
16
  const getUrl = await getSignedUrl(client, new GetObjectCommand({ Bucket: source.bucket, Key: source.key }), { expiresIn });
31
17
  return {
32
18
  source_id: source.id ?? `src_${index}`,
33
19
  type: "http",
34
- provider: "r2",
20
+ provider: source.provider,
35
21
  url: getUrl,
36
22
  size: Number(head.ContentLength ?? 0),
37
23
  filename: filename(source.key),
38
24
  expires_at: expiresAtIso(expiresIn),
39
- metadata: { bucket: source.bucket, key: source.key, endpoint_url: r2Endpoint(source) }
25
+ metadata: s3CompatibleMetadata(source, endpoint)
40
26
  };
41
27
  }
42
- if (source.provider === "hippius") {
28
+ if (isHippiusProvider(source)) {
43
29
  const fetchImpl = options.fetchImpl ?? globalThis.fetch;
44
30
  const baseUrl = source.base_url ?? "https://api.hippius.com";
45
31
  const size = await hippiusObjectSize(fetchImpl, baseUrl, source.api_token, source.bucket, source.key);
@@ -55,45 +41,33 @@ export async function prepareProviderSource(source, options = {}) {
55
41
  metadata: { bucket: source.bucket, key: source.key, base_url: baseUrl }
56
42
  };
57
43
  }
58
- return assertNever(source);
44
+ return unsupportedProviderConfig(source);
59
45
  }
60
46
  export function prepareProviderDestination(destination, options = {}) {
61
47
  const index = options.index ?? 0;
62
48
  const destinationId = destination.id ?? `dst_${index}`;
63
- if (destination.provider === "s3") {
49
+ if (isS3CompatibleProvider(destination)) {
50
+ const endpoint = s3CompatibleEndpoint(destination);
64
51
  return {
65
52
  destination_id: destinationId,
66
- provider: "s3",
53
+ provider: destination.provider,
67
54
  logical_prefix: destination.key,
68
- metadata: { bucket: destination.bucket, key: destination.key, region: destination.region ?? "us-east-1" }
55
+ metadata: s3CompatibleMetadata(destination, endpoint)
69
56
  };
70
57
  }
71
- if (destination.provider === "r2") {
58
+ if (isHippiusProvider(destination)) {
72
59
  return {
73
60
  destination_id: destinationId,
74
- provider: "r2",
75
- logical_prefix: destination.key,
76
- metadata: { bucket: destination.bucket, key: destination.key, endpoint_url: r2Endpoint(destination) }
61
+ provider: "hippius",
62
+ logical_prefix: destination.key.replace(/\/+$/, ""),
63
+ metadata: { bucket: destination.bucket, key: destination.key, base_url: destination.base_url ?? "https://api.hippius.com" }
77
64
  };
78
65
  }
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
- };
66
+ return unsupportedProviderConfig(destination);
85
67
  }
86
68
  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);
69
+ if (isS3CompatibleProvider(destination)) {
70
+ const client = createS3CompatibleClient(destination);
97
71
  const response = await client.send(new CreateMultipartUploadCommand({ Bucket: destination.bucket, Key: objectKey }));
98
72
  if (!response.UploadId) {
99
73
  throw new Error(`provider did not return UploadId for ${objectKey}`);
@@ -103,26 +77,22 @@ export async function createMultipartUpload(destination, objectKey) {
103
77
  throw new Error(`multipart upload is not supported for ${destination.provider}`);
104
78
  }
105
79
  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);
80
+ if (isS3CompatibleProvider(destination)) {
81
+ const client = createS3CompatibleClient(destination);
108
82
  return getSignedUrl(client, new CompleteMultipartUploadCommand({ Bucket: destination.bucket, Key: objectKey, UploadId: uploadId }), { expiresIn });
109
83
  }
110
84
  throw new Error(`multipart completion is not supported for ${destination.provider}`);
111
85
  }
112
86
  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);
87
+ if (isS3CompatibleProvider(destination)) {
88
+ const client = createS3CompatibleClient(destination);
115
89
  return getSignedUrl(client, new AbortMultipartUploadCommand({ Bucket: destination.bucket, Key: objectKey, UploadId: uploadId }), { expiresIn });
116
90
  }
117
91
  throw new Error(`multipart abort is not supported for ${destination.provider}`);
118
92
  }
119
93
  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 }));
94
+ if (isS3CompatibleProvider(destination)) {
95
+ await createS3CompatibleClient(destination).send(new AbortMultipartUploadCommand({ Bucket: destination.bucket, Key: objectKey, UploadId: uploadId }));
126
96
  return;
127
97
  }
128
98
  throw new Error(`multipart abort is not supported for ${destination.provider}`);
@@ -164,57 +134,88 @@ export async function signDestinationRoute(input) {
164
134
  };
165
135
  }
166
136
  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 });
137
+ if (isS3CompatibleProvider(input.destination)) {
138
+ const client = createS3CompatibleClient(input.destination);
139
+ if (input.uploadId && input.partNumber) {
140
+ return getSignedUrl(client, new UploadPartCommand({
141
+ Bucket: input.destination.bucket,
142
+ Key: input.uploadPartObjectKey,
143
+ UploadId: input.uploadId,
144
+ PartNumber: input.partNumber
145
+ }), { expiresIn: input.expiresIn });
182
146
  }
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);
147
+ return getSignedUrl(client, new PutObjectCommand({ Bucket: input.destination.bucket, Key: input.objectKey }), { expiresIn: input.expiresIn });
187
148
  }
149
+ if (isHippiusProvider(input.destination)) {
150
+ 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);
151
+ }
152
+ return unsupportedProviderConfig(input.destination);
188
153
  }
189
- function createS3Client(source) {
154
+ function createS3CompatibleClient(source, endpoint = s3CompatibleEndpoint(source)) {
190
155
  return new S3Client({
191
- region: source.region ?? "us-east-1",
192
- endpoint: source.endpoint_url,
156
+ region: s3CompatibleRegion(source),
157
+ endpoint,
158
+ forcePathStyle: s3CompatibleForcePathStyle(source, endpoint),
193
159
  credentials: {
194
160
  accessKeyId: source.access_key_id,
195
161
  secretAccessKey: source.secret_access_key,
196
- sessionToken: source.session_token
162
+ sessionToken: "session_token" in source ? source.session_token : undefined
197
163
  }
198
164
  });
199
165
  }
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
- });
166
+ export function isS3CompatibleProvider(config) {
167
+ if (config.provider === "s3" || config.provider === "r2") {
168
+ return true;
169
+ }
170
+ if ("driver" in config && config.driver === "s3-compatible") {
171
+ return true;
172
+ }
173
+ return "access_key_id" in config && "secret_access_key" in config;
209
174
  }
210
- function r2Endpoint(source) {
211
- if (source.endpoint_url) {
175
+ export function s3CompatibleEndpoint(source) {
176
+ if (hasText(source.endpoint_url)) {
212
177
  return source.endpoint_url;
213
178
  }
214
- if (source.account_id) {
215
- return `https://${source.account_id}.r2.cloudflarestorage.com`;
179
+ if (source.provider === "r2") {
180
+ if ("account_id" in source && hasText(source.account_id)) {
181
+ return `https://${source.account_id}.r2.cloudflarestorage.com`;
182
+ }
183
+ throw new Error("r2 config requires account_id or endpoint_url.");
184
+ }
185
+ if (source.provider !== "s3") {
186
+ throw new Error(`${source.provider} config requires endpoint_url.`);
187
+ }
188
+ return undefined;
189
+ }
190
+ export function s3CompatibleRegion(source) {
191
+ if ("region" in source && hasText(source.region)) {
192
+ return source.region;
193
+ }
194
+ if (source.provider === "r2") {
195
+ return "auto";
196
+ }
197
+ return "us-east-1";
198
+ }
199
+ export function s3CompatibleForcePathStyle(source, endpoint = s3CompatibleEndpoint(source)) {
200
+ if ("force_path_style" in source && typeof source.force_path_style === "boolean") {
201
+ return source.force_path_style;
202
+ }
203
+ if (source.provider === "s3") {
204
+ return undefined;
216
205
  }
217
- throw new Error("R2 signing requires account_id or endpoint_url");
206
+ return Boolean(endpoint);
207
+ }
208
+ function s3CompatibleMetadata(source, endpoint = s3CompatibleEndpoint(source)) {
209
+ return compactMetadata({
210
+ driver: "s3-compatible",
211
+ bucket: source.bucket,
212
+ key: source.key,
213
+ region: s3CompatibleRegion(source),
214
+ endpoint_url: endpoint
215
+ });
216
+ }
217
+ function isHippiusProvider(config) {
218
+ return config.provider === "hippius" && "api_token" in config;
218
219
  }
219
220
  async function hippiusPresign(fetchImpl, baseUrl, token, bucket, key, action, expiresIn) {
220
221
  const url = new URL(`/api/objectstore/buckets/${bucket}/presigned-url/`, baseUrl);
@@ -245,6 +246,12 @@ async function hippiusObjectSize(fetchImpl, baseUrl, token, bucket, key) {
245
246
  function filename(key) {
246
247
  return key.split("/").filter(Boolean).at(-1) ?? key;
247
248
  }
248
- function assertNever(value) {
249
+ function compactMetadata(input) {
250
+ return Object.fromEntries(Object.entries(input).filter(([, value]) => value !== undefined));
251
+ }
252
+ function hasText(value) {
253
+ return typeof value === "string" && value.trim().length > 0;
254
+ }
255
+ function unsupportedProviderConfig(value) {
249
256
  throw new Error(`Unsupported provider config: ${JSON.stringify(value)}`);
250
257
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@beam-network/sdk",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "TypeScript SDK for BEAM transfer creation and management.",
5
5
  "type": "module",
6
6
  "license": "MIT",