mlclaw 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.
Files changed (47) hide show
  1. package/.agents/skills/mlclaw/SKILL.md +214 -0
  2. package/.agents/skills/mlclaw/agents/openai.yaml +4 -0
  3. package/.gitattributes +35 -0
  4. package/Dockerfile +45 -0
  5. package/LICENSE +21 -0
  6. package/README.md +206 -0
  7. package/assets/mlclaw.svg +143 -0
  8. package/dist/hf-state-sync.js +9532 -0
  9. package/dist/mlclaw-space-runtime.js +5010 -0
  10. package/dist/mlclaw.mjs +16502 -0
  11. package/entrypoint.sh +87 -0
  12. package/mlclaw.ps1 +108 -0
  13. package/mlclaw.sh +117 -0
  14. package/openclaw.default.json +67 -0
  15. package/package.json +66 -0
  16. package/scripts/configure-huggingface-model.mjs +86 -0
  17. package/scripts/configure-telegram.mjs +55 -0
  18. package/scripts/report-telegram-probe.mjs +18 -0
  19. package/space/README.md +42 -0
  20. package/src/hf-bucket-client/client.ts +217 -0
  21. package/src/hf-state-sync/archive.ts +137 -0
  22. package/src/hf-state-sync/cli.ts +92 -0
  23. package/src/hf-state-sync/hub.ts +81 -0
  24. package/src/hf-state-sync/manifest.ts +67 -0
  25. package/src/hf-state-sync/paths.ts +73 -0
  26. package/src/hf-state-sync/restore.ts +109 -0
  27. package/src/hf-state-sync/snapshot.ts +133 -0
  28. package/src/hf-state-sync/sqlite.ts +57 -0
  29. package/src/hf-state-sync/supervise.ts +256 -0
  30. package/src/vendor/hfjs-xet/error.ts +52 -0
  31. package/src/vendor/hfjs-xet/types/public.ts +207 -0
  32. package/src/vendor/hfjs-xet/utils/ChunkCache.ts +102 -0
  33. package/src/vendor/hfjs-xet/utils/RangeList.ts +182 -0
  34. package/src/vendor/hfjs-xet/utils/SplicedBlob.ts +249 -0
  35. package/src/vendor/hfjs-xet/utils/XetBlob.ts +732 -0
  36. package/src/vendor/hfjs-xet/utils/checkCredentials.ts +21 -0
  37. package/src/vendor/hfjs-xet/utils/combineUint8Arrays.ts +13 -0
  38. package/src/vendor/hfjs-xet/utils/createXorbs.ts +782 -0
  39. package/src/vendor/hfjs-xet/utils/shardParser.ts +152 -0
  40. package/src/vendor/hfjs-xet/utils/sum.ts +9 -0
  41. package/src/vendor/hfjs-xet/utils/uploadShards.ts +443 -0
  42. package/src/vendor/hfjs-xet/utils/xetWriteToken.ts +101 -0
  43. package/src/vendor/hfjs-xet/vendor/lz4js/index.ts +540 -0
  44. package/src/vendor/hfjs-xet/vendor/lz4js/util.ts +57 -0
  45. package/src/vendor/hfjs-xet/vendor/lz4js/xxh32.ts +99 -0
  46. package/src/vendor/hfjs-xet/vendor/type-fest/basic.ts +34 -0
  47. package/tsconfig.json +16 -0
@@ -0,0 +1,152 @@
1
+ // @ts-nocheck -- vendored upstream code, kept verbatim; our strict tsconfig does not apply.
2
+ // Vendored from huggingface/huggingface.js@f8fdf6be (packages/hub/src/utils/shardParser.ts), MIT License.
3
+ // Delete this directory when bucket support is upstreamed to @huggingface/hub.
4
+ import { SHARD_FOOTER_VERSION, SHARD_HEADER_VERSION, SHARD_MAGIC_TAG } from "./uploadShards";
5
+
6
+ const HASH_LENGTH = 32;
7
+ const XORB_HASH_BOOKEND = "ff".repeat(HASH_LENGTH);
8
+
9
+ // Read 4 uint64 in little endian and convert to hex
10
+ function readHashFromArray(array: Uint8Array, offset: number): string {
11
+ let hash = "";
12
+ for (let i = 0; i < HASH_LENGTH; i += 8) {
13
+ hash += `${array[offset + i + 7].toString(16).padStart(2, "0")}${array[offset + i + 6]
14
+ .toString(16)
15
+ .padStart(2, "0")}${array[offset + i + 5].toString(16).padStart(2, "0")}${array[offset + i + 4]
16
+ .toString(16)
17
+ .padStart(2, "0")}${array[offset + i + 3].toString(16).padStart(2, "0")}${array[offset + i + 2]
18
+ .toString(16)
19
+ .padStart(2, "0")}${array[offset + i + 1].toString(16).padStart(2, "0")}${array[offset + i]
20
+ .toString(16)
21
+ .padStart(2, "0")}`;
22
+ }
23
+ return hash;
24
+ }
25
+
26
+ export interface ShardData {
27
+ hmacKey: string;
28
+ xorbs: Array<{
29
+ hash: string;
30
+ chunks: Array<{
31
+ hash: string;
32
+ startOffset: number;
33
+ unpackedLength: number;
34
+ }>;
35
+ }>;
36
+ }
37
+
38
+ export async function parseShardData(shardBlob: Blob): Promise<ShardData> {
39
+ const shard = new Uint8Array(await shardBlob.arrayBuffer());
40
+ const shardView = new DataView(shard.buffer);
41
+
42
+ const magicTag = shard.slice(0, SHARD_MAGIC_TAG.length);
43
+ if (!magicTag.every((byte, i) => byte === SHARD_MAGIC_TAG[i])) {
44
+ throw new Error("Invalid shard magic tag");
45
+ }
46
+
47
+ const version = shardView.getBigUint64(SHARD_MAGIC_TAG.length, true);
48
+ if (version !== SHARD_HEADER_VERSION) {
49
+ throw new Error(`Invalid shard version: ${version}`);
50
+ }
51
+
52
+ const footerSize = Number(shardView.getBigUint64(SHARD_MAGIC_TAG.length + 8, true));
53
+
54
+ // Read footer to get section offsets
55
+ const footerStart = shard.length - footerSize;
56
+ const footerVersion = shardView.getBigUint64(footerStart, true);
57
+ if (footerVersion !== SHARD_FOOTER_VERSION) {
58
+ throw new Error(`Invalid shard footer version: ${footerVersion}`);
59
+ }
60
+
61
+ // version: u64, // Footer version (must be 1)
62
+ // file_info_offset: u64, // Offset to file info section
63
+ // cas_info_offset: u64, // Offset to CAS info section
64
+ // file_lookup_offset: u64, // Offset to file lookup table
65
+ // file_lookup_num_entry: u64, // Number of file lookup entries
66
+ // cas_lookup_offset: u64, // Offset to CAS lookup table
67
+ // cas_lookup_num_entry: u64, // Number of CAS lookup entries
68
+ // chunk_lookup_offset: u64, // Offset to chunk lookup table
69
+ // chunk_lookup_num_entry: u64, // Number of chunk lookup entries
70
+ // chunk_hash_hmac_key: [u64; 4], // HMAC key for chunk hashes (32 bytes)
71
+ // shard_creation_timestamp: u64, // Creation time (seconds since epoch)
72
+ // shard_key_expiry: u64, // Expiry time (seconds since epoch)
73
+ // _buffer: [u64; 6], // Reserved space (48 bytes)
74
+ // stored_bytes_on_disk: u64, // Total bytes stored on disk
75
+ // materialized_bytes: u64, // Total materialized bytes
76
+ // stored_bytes: u64, // Total stored bytes
77
+ // footer_offset: u64,
78
+
79
+ // const fileInfoStart = Number(shardView.getBigUint64(footerStart + 8, true));
80
+ const xorbInfoStart = Number(shardView.getBigUint64(footerStart + 16, true));
81
+ const fileLookupStart = Number(shardView.getBigUint64(footerStart + 24, true));
82
+ // const numFileLookups = Number(shardView.getBigUint64(footerStart + 32, true));
83
+ // const xorbLookupStart = Number(shardView.getBigUint64(footerStart + 40, true));
84
+ // const numXorbLookups = Number(shardView.getBigUint64(footerStart + 48, true));
85
+ // const chunkLookupStart = Number(shardView.getBigUint64(footerStart + 56, true));
86
+ // const numChunkLookups = Number(shardView.getBigUint64(footerStart + 64, true));
87
+ const hmacKey = readHashFromArray(shard, footerStart + 72);
88
+ // const shardCreationTimestamp = Number(shardView.getBigUint64(footerStart + 104, true));
89
+ // const shardKeyExpiry = Number(shardView.getBigUint64(footerStart + 112, true));
90
+ // const storedBytesOnDisk = Number(shardView.getBigUint64(footerStart + 168, true));
91
+ // const materializedBytes = Number(shardView.getBigUint64(footerStart + 176, true));
92
+ // const storedBytes = Number(shardView.getBigUint64(footerStart + 184, true));
93
+ // const footerOffset = Number(shardView.getBigUint64(footerStart + 192, true));
94
+
95
+ // Parse XORB Info Section
96
+ const xorbs: ShardData["xorbs"] = [];
97
+ let offset = xorbInfoStart;
98
+
99
+ while (offset < fileLookupStart) {
100
+ // Read xorb entry
101
+ const xorbHash = readHashFromArray(shard, offset);
102
+ offset += HASH_LENGTH;
103
+
104
+ if (xorbHash === XORB_HASH_BOOKEND) {
105
+ break;
106
+ }
107
+
108
+ // const flags = shardView.getUint32(offset, true);
109
+ offset += 4;
110
+
111
+ const chunkCount = shardView.getUint32(offset, true);
112
+ offset += 4;
113
+
114
+ // const numBytesInXorb = shardView.getUint32(offset, true);
115
+ offset += 4;
116
+
117
+ // const numBytesUnpacked = shardView.getUint32(offset, true);
118
+ offset += 4;
119
+
120
+ // Read chunks for this xorb
121
+ const chunks: ShardData["xorbs"][number]["chunks"] = [];
122
+ for (let i = 0; i < chunkCount; i++) {
123
+ const chunkHash = readHashFromArray(shard, offset);
124
+ offset += HASH_LENGTH;
125
+
126
+ const startOffset = shardView.getUint32(offset, true);
127
+ offset += 4;
128
+
129
+ const length = shardView.getUint32(offset, true);
130
+ offset += 4;
131
+
132
+ // Skip reserved 8 bytes
133
+ offset += 8;
134
+
135
+ chunks.push({
136
+ hash: chunkHash,
137
+ startOffset,
138
+ unpackedLength: length,
139
+ });
140
+ }
141
+
142
+ xorbs.push({
143
+ hash: xorbHash,
144
+ chunks,
145
+ });
146
+ }
147
+
148
+ return {
149
+ hmacKey,
150
+ xorbs,
151
+ };
152
+ }
@@ -0,0 +1,9 @@
1
+ // @ts-nocheck -- vendored upstream code, kept verbatim; our strict tsconfig does not apply.
2
+ // Vendored from huggingface/huggingface.js@f8fdf6be (packages/hub/src/utils/sum.ts), MIT License.
3
+ // Delete this directory when bucket support is upstreamed to @huggingface/hub.
4
+ /**
5
+ * Sum of elements in array
6
+ */
7
+ export function sum(arr: number[]): number {
8
+ return arr.reduce((a, b) => a + b, 0);
9
+ }
@@ -0,0 +1,443 @@
1
+ // @ts-nocheck -- vendored upstream code, kept verbatim; our strict tsconfig does not apply.
2
+ // Vendored from huggingface/huggingface.js@f8fdf6be (packages/hub/src/utils/uploadShards.ts), MIT License.
3
+ // Delete this directory when bucket support is upstreamed to @huggingface/hub.
4
+ import { createApiError } from "../error";
5
+ import type { RepoId } from "../types/public";
6
+ import { createXorbs } from "./createXorbs";
7
+ import { sum } from "./sum";
8
+ import { xetWriteToken } from "./xetWriteToken";
9
+
10
+ const SHARD_MAX_SIZE = 64 * 1024 * 1024;
11
+ const SHARD_HEADER_SIZE = 48;
12
+ const SHARD_FOOTER_SIZE = 200;
13
+ const HASH_LENGTH = 32;
14
+ const XORB_FOOTER_LENGTH = 48;
15
+ const FILE_FOOTER_LENGTH = 48;
16
+ export const SHARD_HEADER_VERSION = 2n;
17
+ export const SHARD_FOOTER_VERSION = 1n;
18
+
19
+ const MDB_FILE_FLAG_WITH_VERIFICATION = 0x80000000; // Cannot define as 1 << 31 because it becomes a negative number
20
+ const MDB_FILE_FLAG_WITH_METADATA_EXT = 0x40000000;
21
+
22
+ export const SHARD_MAGIC_TAG = new Uint8Array([
23
+ "H".charCodeAt(0),
24
+ "F".charCodeAt(0),
25
+ "R".charCodeAt(0),
26
+ "e".charCodeAt(0),
27
+ "p".charCodeAt(0),
28
+ "o".charCodeAt(0),
29
+ "M".charCodeAt(0),
30
+ "e".charCodeAt(0),
31
+ "t".charCodeAt(0),
32
+ "a".charCodeAt(0),
33
+ "D".charCodeAt(0),
34
+ "a".charCodeAt(0),
35
+ "t".charCodeAt(0),
36
+ "a".charCodeAt(0),
37
+ 0,
38
+ 85,
39
+ 105,
40
+ 103,
41
+ 69,
42
+ 106,
43
+ 123,
44
+ 129,
45
+ 87,
46
+ 131,
47
+ 165,
48
+ 189,
49
+ 217,
50
+ 92,
51
+ 205,
52
+ 209,
53
+ 74,
54
+ 169,
55
+ ]);
56
+
57
+ export interface XetTokenParams {
58
+ sessionId?: string;
59
+ casUrl?: string;
60
+ accessToken?: string;
61
+ expiresAt?: Date;
62
+ refreshWriteTokenUrl: string;
63
+ }
64
+
65
+ interface UploadShardsParams {
66
+ accessToken: string | undefined;
67
+ hubUrl: string;
68
+ xetParams: XetTokenParams;
69
+ fetch?: typeof fetch;
70
+ repo: RepoId;
71
+ rev: string;
72
+ isPullRequest?: boolean;
73
+ yieldCallback?: (event: { event: "fileProgress"; path: string; progress: number }) => void;
74
+ }
75
+
76
+ /**
77
+ * Outputs the file sha256 after their xorbs/shards have been uploaded.
78
+ */
79
+ export async function* uploadShards(
80
+ source: AsyncGenerator<{ content: Blob; path: string; sha256?: string }>,
81
+ params: UploadShardsParams,
82
+ ): AsyncGenerator<
83
+ | {
84
+ event: "file";
85
+ path: string;
86
+ xetHash: string;
87
+ sha256: string | undefined;
88
+ dedupRatio: number;
89
+ }
90
+ | { event: "fileProgress"; path: string; progress: number }
91
+ > {
92
+ const xorbHashes: Array<string> = [];
93
+ const seenFileXetHashes = new Set<string>();
94
+
95
+ const fileInfoSection = new Uint8Array(Math.floor(SHARD_MAX_SIZE - SHARD_HEADER_SIZE - SHARD_FOOTER_SIZE) * 0.25);
96
+ const xorbInfoSection = new Uint8Array(Math.floor(SHARD_MAX_SIZE - SHARD_HEADER_SIZE - SHARD_FOOTER_SIZE) * 0.75);
97
+
98
+ const xorbView = new DataView(xorbInfoSection.buffer);
99
+ let xorbViewOffset = 0;
100
+ const fileInfoView = new DataView(fileInfoSection.buffer);
101
+ let fileViewOffset = 0;
102
+ let xorbTotalSize = 0n;
103
+ let fileTotalSize = 0n;
104
+ let xorbTotalUnpackedSize = 0n;
105
+
106
+ for await (const output of createXorbs(source, params)) {
107
+ switch (output.event) {
108
+ case "xorb": {
109
+ xorbHashes.push(output.hash);
110
+
111
+ // Calculate space needed for this xorb entry
112
+ const xorbEntrySize = HASH_LENGTH + 4 + 4 + 4 + 4; // hash + flags + count + unpacked + packed
113
+ const chunksSize = output.chunks.length * (HASH_LENGTH + 4 + 4 + 8); // per chunk: hash + length + offset + reserved
114
+ const totalXorbSize = xorbEntrySize + chunksSize;
115
+
116
+ // Check if adding this xorb would exceed buffer capacity
117
+ if (xorbViewOffset + totalXorbSize > xorbInfoSection.length) {
118
+ // Upload current shard and reset buffers
119
+ if (xorbViewOffset > 0 || fileViewOffset > 0) {
120
+ await uploadShard(createShard(), params);
121
+ }
122
+ }
123
+
124
+ // todo: handle when going out of bounds
125
+ writeHashToArray(output.hash, xorbInfoSection, xorbViewOffset);
126
+ xorbViewOffset += HASH_LENGTH;
127
+ xorbView.setUint32(xorbViewOffset, 0, true); // flags
128
+ xorbViewOffset += 4;
129
+ xorbView.setUint32(xorbViewOffset, output.chunks.length, true);
130
+ xorbViewOffset += 4;
131
+ const xorbUnpackedSize = sum(output.chunks.map((x) => x.length));
132
+ xorbView.setUint32(xorbViewOffset, xorbUnpackedSize, true);
133
+ xorbTotalUnpackedSize += BigInt(xorbUnpackedSize);
134
+ xorbTotalSize += BigInt(output.xorb.byteLength);
135
+ xorbViewOffset += 4;
136
+ xorbView.setUint32(xorbViewOffset, output.xorb.byteLength, true);
137
+ xorbViewOffset += 4;
138
+
139
+ let chunkBytes = 0;
140
+ for (const chunk of output.chunks) {
141
+ writeHashToArray(chunk.hash, xorbInfoSection, xorbViewOffset);
142
+ xorbViewOffset += HASH_LENGTH;
143
+ // start offset
144
+ xorbView.setUint32(xorbViewOffset, chunkBytes, true);
145
+ xorbViewOffset += 4;
146
+ // chunk length
147
+ xorbView.setUint32(xorbViewOffset, chunk.length, true);
148
+ xorbViewOffset += 4;
149
+ xorbView.setBigUint64(xorbViewOffset, 0n, true); // reserved
150
+ xorbViewOffset += 8;
151
+ chunkBytes += chunk.length;
152
+ }
153
+
154
+ for (const file of output.files) {
155
+ yield {
156
+ event: "fileProgress",
157
+ path: file.path,
158
+ progress: file.lastSentProgress,
159
+ };
160
+ }
161
+
162
+ await uploadXorb(output, params);
163
+ //^ Todo: queue it and do not await it
164
+
165
+ for (const file of output.files) {
166
+ yield { event: "fileProgress", path: file.path, progress: file.progress };
167
+ }
168
+ break;
169
+ }
170
+ case "file": {
171
+ yield {
172
+ event: "file",
173
+ path: output.path,
174
+ xetHash: output.hash,
175
+ sha256: output.sha256,
176
+ dedupRatio: output.dedupRatio,
177
+ }; // Maybe wait until shard is uploaded before yielding.
178
+
179
+ if (seenFileXetHashes.has(output.hash)) {
180
+ break;
181
+ }
182
+ seenFileXetHashes.add(output.hash);
183
+
184
+ // Calculate space needed for this file entry
185
+ const fileHeaderSize = HASH_LENGTH + 4 + 4 + 8; // hash + flags + rep length + reserved
186
+ const representationSize = output.representation.length * (HASH_LENGTH + 4 + 4 + 4 + 4); // per rep: xorb hash + flags + length + offset + endOffset
187
+ const verificationSize = output.representation.length * (HASH_LENGTH + 16); // per rep: range hash + reserved
188
+ const fileSha256 = output.sha256;
189
+ const hasMetadataExt = fileSha256 !== undefined;
190
+ const metadataSize = hasMetadataExt ? HASH_LENGTH + 16 : 0; // sha256 + reserved
191
+ const totalFileSize = fileHeaderSize + representationSize + verificationSize + metadataSize;
192
+
193
+ // Check if adding this file would exceed buffer capacity
194
+ if (fileViewOffset + totalFileSize > fileInfoSection.length) {
195
+ // Upload current shard and reset buffers
196
+ if (xorbViewOffset > 0 || fileViewOffset > 0) {
197
+ await uploadShard(createShard(), params);
198
+ }
199
+ }
200
+
201
+ writeHashToArray(output.hash, fileInfoSection, fileViewOffset);
202
+ fileViewOffset += HASH_LENGTH;
203
+ // Cannot use | binary operator since it works with int32 not uint32 and one of the flags is 1 << 31
204
+ fileInfoView.setUint32(
205
+ fileViewOffset,
206
+ MDB_FILE_FLAG_WITH_VERIFICATION + (hasMetadataExt ? MDB_FILE_FLAG_WITH_METADATA_EXT : 0),
207
+ true,
208
+ );
209
+ fileViewOffset += 4;
210
+ fileInfoView.setUint32(fileViewOffset, output.representation.length, true);
211
+ fileViewOffset += 4;
212
+ fileInfoView.setBigUint64(fileViewOffset, 0n, true); // reserved
213
+ fileViewOffset += 8;
214
+
215
+ for (const repItem of output.representation) {
216
+ writeHashToArray(
217
+ typeof repItem.xorbId === "number" ? xorbHashes[repItem.xorbId] : repItem.xorbId,
218
+ fileInfoSection,
219
+ fileViewOffset,
220
+ );
221
+ fileViewOffset += HASH_LENGTH;
222
+ fileInfoView.setUint32(fileViewOffset, 0, true); // Xorb flags
223
+ fileViewOffset += 4;
224
+ fileInfoView.setUint32(fileViewOffset, repItem.length, true);
225
+ fileViewOffset += 4;
226
+ fileInfoView.setUint32(fileViewOffset, repItem.indexStart, true);
227
+ fileViewOffset += 4;
228
+ fileInfoView.setUint32(fileViewOffset, repItem.indexEnd, true);
229
+ fileViewOffset += 4;
230
+ }
231
+
232
+ // File verification data
233
+ for (const repItem of output.representation) {
234
+ writeHashToArray(repItem.rangeHash, fileInfoSection, fileViewOffset);
235
+ fileViewOffset += HASH_LENGTH;
236
+ // reserved in file verification data
237
+ for (let i = 0; i < 16; i++) {
238
+ fileInfoSection[fileViewOffset + i] = 0;
239
+ }
240
+ fileViewOffset += 16;
241
+ }
242
+
243
+ if (hasMetadataExt) {
244
+ // File metadata ext
245
+ writeHashToArray(fileSha256, fileInfoSection, fileViewOffset);
246
+ fileViewOffset += HASH_LENGTH;
247
+
248
+ // reserved in file metadata ext
249
+ for (let i = 0; i < 16; i++) {
250
+ fileInfoSection[fileViewOffset + i] = 0;
251
+ }
252
+ fileViewOffset += 16;
253
+ }
254
+
255
+ break;
256
+ }
257
+ }
258
+ }
259
+
260
+ function createShard(): Uint8Array {
261
+ const shard = new Uint8Array(
262
+ SHARD_HEADER_SIZE + SHARD_FOOTER_SIZE + xorbViewOffset + XORB_FOOTER_LENGTH + fileViewOffset + FILE_FOOTER_LENGTH,
263
+ );
264
+
265
+ const shardView = new DataView(shard.buffer);
266
+ let shardOffset = 0;
267
+
268
+ // Header
269
+ shard.set(SHARD_MAGIC_TAG, shardOffset);
270
+ shardOffset += SHARD_MAGIC_TAG.length;
271
+
272
+ shardView.setBigUint64(shardOffset, SHARD_HEADER_VERSION, true);
273
+ shardOffset += 8;
274
+
275
+ shardView.setBigUint64(shardOffset, BigInt(SHARD_FOOTER_SIZE), true);
276
+ shardOffset += 8;
277
+
278
+ // File Info Section
279
+ shard.set(fileInfoSection.slice(0, fileViewOffset), shardOffset);
280
+ shardOffset += fileViewOffset;
281
+
282
+ // File info bookend
283
+ for (let i = 0; i < 32; i++) {
284
+ shard[shardOffset + i] = 0xff;
285
+ }
286
+ shardOffset += 32;
287
+ for (let i = 0; i < 16; i++) {
288
+ shard[shardOffset + i] = 0;
289
+ }
290
+ shardOffset += 16;
291
+
292
+ // XORB Info Section
293
+ const xorbInfoOffset = shardOffset;
294
+ shard.set(xorbInfoSection.slice(0, xorbViewOffset), shardOffset);
295
+ shardOffset += xorbViewOffset;
296
+
297
+ // Xorb info bookend
298
+ for (let i = 0; i < 32; i++) {
299
+ shard[shardOffset + i] = 0xff;
300
+ }
301
+ shardOffset += 32;
302
+ for (let i = 0; i < 16; i++) {
303
+ shard[shardOffset + i] = 0;
304
+ }
305
+ shardOffset += 16;
306
+
307
+ // Footer
308
+ const footerOffset = shardOffset;
309
+
310
+ // version: u64, // Footer version (must be 1)
311
+ // file_info_offset: u64, // Offset to file info section
312
+ // cas_info_offset: u64, // Offset to CAS info section
313
+ // reserved 48 bytes
314
+ // chunk_hash_hmac_key: [u64; 4], // HMAC key for chunk hashes (32 bytes)
315
+ // shard_creation_timestamp: u64, // Creation time (seconds since epoch)
316
+ // shard_key_expiry: u64, // Expiry time (seconds since epoch)
317
+ // _buffer: [u64; 6], // Reserved space (48 bytes)
318
+ // stored_bytes_on_disk: u64, // Total bytes stored on disk
319
+ // materialized_bytes: u64, // Total materialized bytes
320
+ // stored_bytes: u64, // Total stored bytes
321
+ // footer_offset: u64,
322
+
323
+ shardView.setBigUint64(shardOffset, SHARD_FOOTER_VERSION, true);
324
+ shardOffset += 8;
325
+ shardView.setBigUint64(shardOffset, BigInt(SHARD_HEADER_SIZE), true); // beginning of fileinfo section
326
+ shardOffset += 8;
327
+ shardView.setBigUint64(shardOffset, BigInt(xorbInfoOffset), true); // beginning of xorbinfo section
328
+ shardOffset += 8;
329
+
330
+ for (let i = 0; i < 48; i++) {
331
+ shardView.setUint8(shardOffset + i, 0);
332
+ }
333
+ shardOffset += 48;
334
+
335
+ // Chunk HMAC
336
+ for (let i = 0; i < 32; i++) {
337
+ shardView.setUint8(shardOffset + i, 0);
338
+ }
339
+ shardOffset += 32;
340
+
341
+ shardView.setBigUint64(shardOffset, BigInt(Math.floor(Date.now() / 1000)), true);
342
+ shardOffset += 8;
343
+
344
+ // Shard key expiration
345
+ shardView.setBigUint64(shardOffset, 0n, true);
346
+ shardOffset += 8;
347
+
348
+ // Reserved space (48 bytes)
349
+ for (let i = 0; i < 48; i++) {
350
+ shardView.setUint8(shardOffset + i, 0);
351
+ }
352
+ shardOffset += 48;
353
+
354
+ shardView.setBigUint64(shardOffset, xorbTotalSize, true);
355
+ shardOffset += 8;
356
+
357
+ shardView.setBigUint64(shardOffset, fileTotalSize, true);
358
+ shardOffset += 8;
359
+
360
+ shardView.setBigUint64(shardOffset, xorbTotalUnpackedSize, true);
361
+ shardOffset += 8;
362
+
363
+ shardView.setBigUint64(shardOffset, BigInt(footerOffset), true);
364
+
365
+ xorbViewOffset = 0;
366
+ fileViewOffset = 0;
367
+ xorbTotalSize = 0n;
368
+ xorbTotalUnpackedSize = 0n;
369
+ fileTotalSize = 0n;
370
+
371
+ return shard;
372
+ }
373
+
374
+ // If un-uploaded data remains, upload it
375
+ if (xorbViewOffset || fileViewOffset) {
376
+ await uploadShard(createShard(), params);
377
+ }
378
+ }
379
+
380
+ // Todo: switch from hex to non-hex when WASM switches. For now consider hash is hex
381
+ function writeHashToArray(hash: string, array: Uint8Array, offset: number) {
382
+ for (let i = 0; i < hash.length; i += 16) {
383
+ // Write a uint64 in little endian
384
+ array[offset + i / 2] = parseInt(hash.substring(i + 2 * 7, i + 2 * 8), 16);
385
+ array[offset + i / 2 + 1] = parseInt(hash.substring(i + 2 * 6, i + 2 * 7), 16);
386
+ array[offset + i / 2 + 2] = parseInt(hash.substring(i + 2 * 5, i + 2 * 6), 16);
387
+ array[offset + i / 2 + 3] = parseInt(hash.substring(i + 2 * 4, i + 2 * 5), 16);
388
+ array[offset + i / 2 + 4] = parseInt(hash.substring(i + 2 * 3, i + 2 * 4), 16);
389
+ array[offset + i / 2 + 5] = parseInt(hash.substring(i + 2 * 2, i + 2 * 3), 16);
390
+ array[offset + i / 2 + 6] = parseInt(hash.substring(i + 2 * 1, i + 2 * 2), 16);
391
+ array[offset + i / 2 + 7] = parseInt(hash.substring(i + 2 * 0, i + 2 * 1), 16);
392
+ }
393
+ }
394
+
395
+ async function uploadXorb(
396
+ xorb: { hash: string; xorb: Uint8Array; files: Array<{ path: string; progress: number; lastSentProgress: number }> },
397
+ params: UploadShardsParams,
398
+ ) {
399
+ const token = await xetWriteToken(params);
400
+
401
+ const resp = await (params.fetch ?? fetch)(`${token.casUrl}/v1/xorbs/default/${xorb.hash}`, {
402
+ method: "POST",
403
+ body: xorb.xorb,
404
+ headers: {
405
+ Authorization: `Bearer ${token.accessToken}`,
406
+ ...(params.xetParams.sessionId ? { "X-Xet-Session-Id": params.xetParams.sessionId } : {}),
407
+ },
408
+ ...{
409
+ progressHint: {
410
+ progressCallback: (progress: number) => {
411
+ for (const file of xorb.files) {
412
+ params.yieldCallback?.({
413
+ event: "fileProgress",
414
+ path: file.path,
415
+ progress: file.lastSentProgress + (file.progress - file.lastSentProgress) * progress,
416
+ });
417
+ }
418
+ },
419
+ },
420
+ },
421
+ });
422
+
423
+ if (!resp.ok) {
424
+ throw await createApiError(resp);
425
+ }
426
+ }
427
+
428
+ async function uploadShard(shard: Uint8Array, params: UploadShardsParams) {
429
+ const token = await xetWriteToken(params);
430
+
431
+ const resp = await (params.fetch ?? fetch)(`${token.casUrl}/v1/shards`, {
432
+ method: "POST",
433
+ body: shard,
434
+ headers: {
435
+ Authorization: `Bearer ${token.accessToken}`,
436
+ ...(params.xetParams.sessionId ? { "X-Xet-Session-Id": params.xetParams.sessionId } : {}),
437
+ },
438
+ });
439
+
440
+ if (!resp.ok) {
441
+ throw await createApiError(resp);
442
+ }
443
+ }
@@ -0,0 +1,101 @@
1
+ // @ts-nocheck -- vendored upstream code, kept verbatim; our strict tsconfig does not apply.
2
+ // Vendored from huggingface/huggingface.js@f8fdf6be (packages/hub/src/utils/xetWriteToken.ts), MIT License.
3
+ // Delete this directory when bucket support is upstreamed to @huggingface/hub.
4
+ import { createApiError } from "../error";
5
+ import type { XetTokenParams } from "./uploadShards";
6
+
7
+ export interface XetWriteTokenParams {
8
+ accessToken: string | undefined;
9
+ fetch?: typeof fetch;
10
+ xetParams: XetTokenParams;
11
+ }
12
+
13
+ const JWT_SAFETY_PERIOD = 60_000;
14
+ const JWT_CACHE_SIZE = 1_000;
15
+
16
+ const jwtPromises: Map<string, Promise<{ accessToken: string; casUrl: string }>> = new Map();
17
+ /**
18
+ * Cache to store JWTs, to avoid making many auth requests when downloading multiple files from the same repo
19
+ */
20
+ const jwts: Map<
21
+ string,
22
+ {
23
+ accessToken: string;
24
+ expiresAt: Date;
25
+ casUrl: string;
26
+ }
27
+ > = new Map();
28
+
29
+ export async function xetWriteToken(params: XetWriteTokenParams): Promise<{ accessToken: string; casUrl: string }> {
30
+ if (
31
+ params.xetParams.expiresAt &&
32
+ params.xetParams.casUrl &&
33
+ params.xetParams.accessToken &&
34
+ params.xetParams.expiresAt > new Date(Date.now() + JWT_SAFETY_PERIOD)
35
+ ) {
36
+ return { accessToken: params.xetParams.accessToken, casUrl: params.xetParams.casUrl };
37
+ }
38
+ const key = params.xetParams.refreshWriteTokenUrl;
39
+
40
+ const jwt = jwts.get(key);
41
+
42
+ if (jwt && jwt.expiresAt > new Date(Date.now() + JWT_SAFETY_PERIOD)) {
43
+ return { accessToken: jwt.accessToken, casUrl: jwt.casUrl };
44
+ }
45
+
46
+ // If we already have a promise for this repo, return it
47
+ const existingPromise = jwtPromises.get(key);
48
+ if (existingPromise) {
49
+ return existingPromise;
50
+ }
51
+
52
+ const promise = (async () => {
53
+ const resp = await (params.fetch ?? fetch)(params.xetParams.refreshWriteTokenUrl, {
54
+ headers: {
55
+ ...(params.accessToken
56
+ ? {
57
+ Authorization: `Bearer ${params.accessToken}`,
58
+ }
59
+ : {}),
60
+ ...(params.xetParams.sessionId ? { "X-Xet-Session-Id": params.xetParams.sessionId } : {}),
61
+ },
62
+ });
63
+
64
+ if (!resp.ok) {
65
+ throw await createApiError(resp);
66
+ }
67
+
68
+ const json: { accessToken: string; casUrl: string; exp: number } = await resp.json();
69
+ const jwt = {
70
+ accessToken: json.accessToken,
71
+ expiresAt: new Date(json.exp * 1000),
72
+ casUrl: json.casUrl,
73
+ };
74
+
75
+ jwtPromises.delete(key);
76
+
77
+ for (const [key, value] of jwts.entries()) {
78
+ if (value.expiresAt < new Date(Date.now() + JWT_SAFETY_PERIOD)) {
79
+ jwts.delete(key);
80
+ } else {
81
+ break;
82
+ }
83
+ }
84
+ if (jwts.size >= JWT_CACHE_SIZE) {
85
+ const keyToDelete = jwts.keys().next().value;
86
+ if (keyToDelete) {
87
+ jwts.delete(keyToDelete);
88
+ }
89
+ }
90
+ jwts.set(key, jwt);
91
+
92
+ return {
93
+ accessToken: json.accessToken,
94
+ casUrl: json.casUrl,
95
+ };
96
+ })();
97
+
98
+ jwtPromises.set(key, promise);
99
+
100
+ return promise;
101
+ }