@beam-network/sdk 0.5.15 → 0.5.17-dev.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.
@@ -1,5 +1,6 @@
1
1
  import { AbortMultipartUploadCommand, CompleteMultipartUploadCommand, CreateMultipartUploadCommand, GetObjectCommand, HeadObjectCommand, ListPartsCommand, PutObjectCommand, S3Client, UploadPartCommand } from "@aws-sdk/client-s3";
2
2
  import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
3
+ import { huggingFaceEndpoint, huggingFaceFileMetadata, huggingFaceRepoType, huggingFaceRevision } from "./huggingface.js";
3
4
  export function nowIso() {
4
5
  return new Date().toISOString();
5
6
  }
@@ -47,6 +48,18 @@ export async function prepareProviderSource(source, options = {}) {
47
48
  metadata: { bucket: source.bucket, key: source.key, base_url: baseUrl }
48
49
  };
49
50
  }
51
+ if (isHuggingFaceProvider(source)) {
52
+ const metadata = await huggingFaceFileMetadata(options.fetchImpl ?? globalThis.fetch, source);
53
+ return {
54
+ source_id: source.id ?? `src_${index}`,
55
+ type: "http",
56
+ provider: "huggingface",
57
+ url: metadata.url,
58
+ size: metadata.size,
59
+ filename: filename(source.path),
60
+ metadata: huggingFaceMetadata(source, metadata.etag, metadata.commitHash)
61
+ };
62
+ }
50
63
  return unsupportedProviderConfig(source);
51
64
  }
52
65
  export async function prepareProviderSourceForPlan(source, options = {}) {
@@ -83,6 +96,17 @@ export async function prepareProviderSourceForPlan(source, options = {}) {
83
96
  metadata: { bucket: source.bucket, key: source.key, base_url: baseUrl }
84
97
  };
85
98
  }
99
+ if (isHuggingFaceProvider(source)) {
100
+ const metadata = await huggingFaceFileMetadata(options.fetchImpl ?? globalThis.fetch, source);
101
+ return {
102
+ source_id: source.id ?? `src_${index}`,
103
+ type: "http",
104
+ provider: "huggingface",
105
+ size: metadata.size,
106
+ filename: filename(source.path),
107
+ metadata: huggingFaceMetadata(source, metadata.etag, metadata.commitHash)
108
+ };
109
+ }
86
110
  return unsupportedProviderConfig(source);
87
111
  }
88
112
  export function prepareProviderDestination(destination, options = {}) {
@@ -111,6 +135,14 @@ export function prepareProviderDestination(destination, options = {}) {
111
135
  }
112
136
  };
113
137
  }
138
+ if (isHuggingFaceProvider(destination)) {
139
+ return {
140
+ destination_id: destinationId,
141
+ provider: "huggingface",
142
+ logical_prefix: destination.path,
143
+ metadata: huggingFaceMetadata(destination)
144
+ };
145
+ }
114
146
  return unsupportedProviderConfig(destination);
115
147
  }
116
148
  export async function createMultipartUpload(input) {
@@ -169,13 +201,51 @@ export async function abortMultipartUpload(destination, objectKey, uploadId) {
169
201
  }
170
202
  throw new Error(`multipart abort is not supported for ${destination.provider}`);
171
203
  }
204
+ /** Provider metadata only. These operations never read or proxy object bytes. */
205
+ export async function listMultipartParts(destination, objectKey, uploadId) {
206
+ if (!isS3CompatibleProvider(destination))
207
+ return unsupportedProviderConfig(destination);
208
+ const parts = [];
209
+ let marker;
210
+ do {
211
+ const page = await createS3CompatibleClient(destination).send(new ListPartsCommand({
212
+ Bucket: destination.bucket, Key: objectKey, UploadId: uploadId, MaxParts: 1000, PartNumberMarker: marker
213
+ }));
214
+ for (const part of page.Parts ?? []) {
215
+ if (!part.PartNumber || !part.ETag || !Number.isSafeInteger(part.Size))
216
+ throw new Error("invalid multipart part metadata");
217
+ parts.push({ partNumber: part.PartNumber, etag: part.ETag, size: part.Size });
218
+ }
219
+ const next = page.IsTruncated ? page.NextPartNumberMarker : undefined;
220
+ if (page.IsTruncated && (!next || next === marker))
221
+ throw new Error("invalid multipart pagination");
222
+ marker = next;
223
+ } while (marker);
224
+ return parts;
225
+ }
226
+ export async function completeMultipartUpload(input) {
227
+ if (!isS3CompatibleProvider(input.destination))
228
+ return unsupportedProviderConfig(input.destination);
229
+ const result = await createS3CompatibleClient(input.destination).send(new CompleteMultipartUploadCommand({
230
+ Bucket: input.destination.bucket, Key: input.objectKey, UploadId: input.uploadId,
231
+ MultipartUpload: { Parts: [...input.parts].sort((a, b) => a.partNumber - b.partNumber)
232
+ .map((part) => ({ PartNumber: part.partNumber, ETag: part.etag })) }
233
+ }));
234
+ return { etag: result.ETag, versionId: result.VersionId };
235
+ }
236
+ export async function inspectDestinationObject(destination, objectKey) {
237
+ if (!isS3CompatibleProvider(destination))
238
+ return unsupportedProviderConfig(destination);
239
+ const result = await createS3CompatibleClient(destination).send(new HeadObjectCommand({ Bucket: destination.bucket, Key: objectKey }));
240
+ return { size: result.ContentLength, etag: result.ETag, versionId: result.VersionId, metadata: result.Metadata ?? {} };
241
+ }
172
242
  export async function signDestinationRoute(input) {
173
243
  const targetObjectKey = input.target.object_key;
174
244
  if (!targetObjectKey) {
175
245
  throw new Error("destination signing target is missing object_key");
176
246
  }
177
247
  const [destUrl, sourceRoute] = await Promise.all([
178
- signDestinationUrl({
248
+ input.destUrl ?? signDestinationUrl({
179
249
  destination: input.destination,
180
250
  objectKey: targetObjectKey,
181
251
  uploadId: input.uploadId,
@@ -234,17 +304,25 @@ export async function signSourceReadRange(input) {
234
304
  url: await getSignedUrl(client, new GetObjectCommand({
235
305
  Bucket: input.source.bucket,
236
306
  Key: input.source.key,
237
- Range: range
307
+ Range: range,
308
+ IfMatch: input.ifMatch,
309
+ VersionId: input.versionId
238
310
  }), { expiresIn: input.expiresIn }),
239
- headers: { Range: range }
311
+ headers: { Range: range, ...(input.ifMatch ? { "If-Match": input.ifMatch } : {}) }
240
312
  };
241
313
  }
314
+ if (input.ifMatch || input.versionId)
315
+ throw new Error("conditional source ranges require S3-compatible storage");
242
316
  if (isHippiusProvider(input.source)) {
243
317
  return {
244
318
  url: await hippiusPresign(input.fetchImpl ?? globalThis.fetch, input.source.base_url ?? "https://api.hippius.com", input.source.api_token, input.source.bucket, input.source.key, "get", input.expiresIn),
245
319
  headers: { Range: range }
246
320
  };
247
321
  }
322
+ if (isHuggingFaceProvider(input.source)) {
323
+ const metadata = await huggingFaceFileMetadata(input.fetchImpl ?? globalThis.fetch, input.source);
324
+ return { url: metadata.url, headers: { Range: range } };
325
+ }
248
326
  return unsupportedProviderConfig(input.source);
249
327
  }
250
328
  export async function signDestinationReadRange(input) {
@@ -266,6 +344,15 @@ export async function signDestinationReadRange(input) {
266
344
  headers: { Range: range }
267
345
  };
268
346
  }
347
+ if (isHuggingFaceProvider(input.destination)) {
348
+ // Read-back resolves the committed file. Before the commit lands the bytes exist only as
349
+ // uncommitted LFS parts, which the Hub does not expose.
350
+ const metadata = await huggingFaceFileMetadata(input.fetchImpl ?? globalThis.fetch, {
351
+ ...input.destination,
352
+ path: input.objectKey
353
+ });
354
+ return { url: metadata.url, headers: { Range: range } };
355
+ }
269
356
  return unsupportedProviderConfig(input.destination);
270
357
  }
271
358
  async function signSourceRoute(input) {
@@ -290,9 +377,13 @@ async function signSourceRoute(input) {
290
377
  headers: { Range: range }
291
378
  };
292
379
  }
380
+ if (isHuggingFaceProvider(input.source)) {
381
+ const metadata = await huggingFaceFileMetadata(input.fetchImpl ?? globalThis.fetch, input.source);
382
+ return { url: metadata.url, headers: { Range: range } };
383
+ }
293
384
  return unsupportedProviderConfig(input.source);
294
385
  }
295
- async function signDestinationUrl(input) {
386
+ export async function signDestinationUrl(input) {
296
387
  if (isS3CompatibleProvider(input.destination)) {
297
388
  const client = createS3CompatibleClient(input.destination);
298
389
  if (input.uploadId && input.partNumber) {
@@ -300,11 +391,14 @@ async function signDestinationUrl(input) {
300
391
  Bucket: input.destination.bucket,
301
392
  Key: input.objectKey,
302
393
  UploadId: input.uploadId,
303
- PartNumber: input.partNumber
394
+ PartNumber: input.partNumber,
395
+ ContentMD5: input.contentMd5
304
396
  }), { expiresIn: input.expiresIn });
305
397
  }
306
- return getSignedUrl(client, new PutObjectCommand({ Bucket: input.destination.bucket, Key: input.objectKey }), { expiresIn: input.expiresIn });
398
+ return getSignedUrl(client, new PutObjectCommand({ Bucket: input.destination.bucket, Key: input.objectKey, ContentMD5: input.contentMd5 }), { expiresIn: input.expiresIn });
307
399
  }
400
+ if (input.contentMd5)
401
+ throw new Error("checksum-bound uploads require S3-compatible storage");
308
402
  if (isHippiusProvider(input.destination)) {
309
403
  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);
310
404
  }
@@ -396,6 +490,22 @@ function s3CompatibleMetadata(source, endpoint = s3CompatibleEndpoint(source)) {
396
490
  function isHippiusProvider(config) {
397
491
  return config.provider === "hippius" && "api_token" in config;
398
492
  }
493
+ export function isHuggingFaceProvider(config) {
494
+ return config.provider === "huggingface" && "repo_id" in config;
495
+ }
496
+ function huggingFaceMetadata(config, etag, commitHash) {
497
+ return compactMetadata({
498
+ driver: "huggingface",
499
+ repo_id: config.repo_id,
500
+ repo_type: huggingFaceRepoType(config),
501
+ revision: huggingFaceRevision(config),
502
+ path: config.path,
503
+ endpoint: huggingFaceEndpoint(config),
504
+ // For an LFS blob the Hub's linked ETag is the object's sha256.
505
+ sha256: etag,
506
+ commit_hash: commitHash
507
+ });
508
+ }
399
509
  async function hippiusPresign(fetchImpl, baseUrl, token, bucket, key, action, expiresIn) {
400
510
  const url = new URL(`/api/objectstore/buckets/${bucket}/presigned-url/`, baseUrl);
401
511
  url.searchParams.set("key", key);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@beam-network/sdk",
3
- "version": "0.5.15",
3
+ "version": "0.5.17-dev.0",
4
4
  "description": "TypeScript SDK for BEAM transfer creation and management.",
5
5
  "type": "module",
6
6
  "license": "MIT",