@adukilabs/sdk 0.0.0-stage → 0.1.2

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.
@@ -0,0 +1,130 @@
1
+ // BLAKE3 (hash mode, 32-byte output) in plain TypeScript: Store names every
2
+ // chunk and every manifest by its BLAKE3, and WebCrypto has no BLAKE3. A
3
+ // straight port of the reference implementation (github.com/BLAKE3-team/BLAKE3,
4
+ // reference_impl.rs); the Store golden corpus checks it end to end.
5
+ const IV = Uint32Array.of(0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19);
6
+ const PERMUTATION = [2, 6, 3, 10, 7, 0, 4, 13, 1, 11, 12, 5, 9, 14, 15, 8];
7
+ const CHUNK_START = 1;
8
+ const CHUNK_END = 2;
9
+ const PARENT = 4;
10
+ const ROOT = 8;
11
+ const BLOCK = 64;
12
+ const CHUNK = 1024;
13
+ // One state and one message schedule reused by every compression (single-threaded).
14
+ const state = new Uint32Array(16);
15
+ const words = new Uint32Array(16);
16
+ const scratch = new Uint32Array(16);
17
+ function g(a, b, c, d, x, y) {
18
+ const s = state;
19
+ s[a] = (s[a] + s[b] + x) | 0;
20
+ let t = s[d] ^ s[a];
21
+ s[d] = (t >>> 16) | (t << 16);
22
+ s[c] = (s[c] + s[d]) | 0;
23
+ t = s[b] ^ s[c];
24
+ s[b] = (t >>> 12) | (t << 20);
25
+ s[a] = (s[a] + s[b] + y) | 0;
26
+ t = s[d] ^ s[a];
27
+ s[d] = (t >>> 8) | (t << 24);
28
+ s[c] = (s[c] + s[d]) | 0;
29
+ t = s[b] ^ s[c];
30
+ s[b] = (t >>> 7) | (t << 25);
31
+ }
32
+ /** Compresses `block` (16 words) into `cv` (8 words) in place; `out` gets the first 8 output words. */
33
+ function compress(cv, block, counter, length, flags, out) {
34
+ state.set(cv.subarray(0, 8), 0);
35
+ state[8] = IV[0];
36
+ state[9] = IV[1];
37
+ state[10] = IV[2];
38
+ state[11] = IV[3];
39
+ state[12] = counter >>> 0;
40
+ state[13] = Math.floor(counter / 0x1_0000_0000) >>> 0;
41
+ state[14] = length;
42
+ state[15] = flags;
43
+ const m = words;
44
+ m.set(block);
45
+ for (let round = 0; round < 7; round++) {
46
+ g(0, 4, 8, 12, m[0], m[1]);
47
+ g(1, 5, 9, 13, m[2], m[3]);
48
+ g(2, 6, 10, 14, m[4], m[5]);
49
+ g(3, 7, 11, 15, m[6], m[7]);
50
+ g(0, 5, 10, 15, m[8], m[9]);
51
+ g(1, 6, 11, 12, m[10], m[11]);
52
+ g(2, 7, 8, 13, m[12], m[13]);
53
+ g(3, 4, 9, 14, m[14], m[15]);
54
+ if (round < 6) {
55
+ for (let i = 0; i < 16; i++)
56
+ scratch[i] = m[PERMUTATION[i]];
57
+ m.set(scratch);
58
+ }
59
+ }
60
+ for (let i = 0; i < 8; i++)
61
+ out[i] = state[i] ^ state[i + 8];
62
+ }
63
+ /** The 16 little-endian words of the 64-byte block of `data` at `offset` (zero padded). */
64
+ function load(data, offset, length, into) {
65
+ if (length === BLOCK && (data.byteOffset + offset) % 4 === 0 && littleEndian) {
66
+ into.set(new Uint32Array(data.buffer, data.byteOffset + offset, 16));
67
+ return;
68
+ }
69
+ into.fill(0);
70
+ for (let i = 0; i < length; i++)
71
+ into[i >> 2] |= data[offset + i] << ((i & 3) * 8);
72
+ }
73
+ const littleEndian = new Uint8Array(Uint32Array.of(1).buffer)[0] === 1;
74
+ /** The output of chunk `index` (`data[offset, offset + length)`, at most 1024 bytes). */
75
+ function chunk(data, offset, length, index) {
76
+ const cv = IV.slice();
77
+ const block = new Uint32Array(16);
78
+ const blocks = Math.max(1, Math.ceil(length / BLOCK));
79
+ for (let b = 0; b < blocks - 1; b++) {
80
+ load(data, offset + b * BLOCK, BLOCK, block);
81
+ compress(cv, block, index, BLOCK, b === 0 ? CHUNK_START : 0, cv);
82
+ }
83
+ const last = length - (blocks - 1) * BLOCK;
84
+ load(data, offset + (blocks - 1) * BLOCK, last, block);
85
+ return { cv, block, counter: index, length: last, flags: (blocks === 1 ? CHUNK_START : 0) | CHUNK_END };
86
+ }
87
+ function chaining(output) {
88
+ const out = new Uint32Array(8);
89
+ compress(output.cv, output.block, output.counter, output.length, output.flags, out);
90
+ return out;
91
+ }
92
+ function parent(left, right) {
93
+ const block = new Uint32Array(16);
94
+ block.set(left, 0);
95
+ block.set(right, 8);
96
+ return { cv: IV.slice(), block, counter: 0, length: BLOCK, flags: PARENT };
97
+ }
98
+ /** The BLAKE3 hash (32 bytes) of `data`. */
99
+ export function blake3(data) {
100
+ const chunks = Math.max(1, Math.ceil(data.length / CHUNK));
101
+ const stack = [];
102
+ for (let i = 0; i < chunks - 1; i++) {
103
+ let cv = chaining(chunk(data, i * CHUNK, CHUNK, i));
104
+ // Merge completed subtrees: one merge per trailing zero bit of the chunk count.
105
+ for (let total = i + 1; (total & 1) === 0; total >>= 1)
106
+ cv = chaining(parent(stack.pop(), cv));
107
+ stack.push(cv);
108
+ }
109
+ let output = chunk(data, (chunks - 1) * CHUNK, data.length - (chunks - 1) * CHUNK, chunks - 1);
110
+ while (stack.length > 0)
111
+ output = parent(stack.pop(), chaining(output));
112
+ const root = new Uint32Array(8);
113
+ compress(output.cv, output.block, 0, output.length, output.flags | ROOT, root);
114
+ const bytes = new Uint8Array(32);
115
+ for (let i = 0; i < 8; i++) {
116
+ const w = root[i];
117
+ bytes[i * 4] = w & 0xff;
118
+ bytes[i * 4 + 1] = (w >>> 8) & 0xff;
119
+ bytes[i * 4 + 2] = (w >>> 16) & 0xff;
120
+ bytes[i * 4 + 3] = (w >>> 24) & 0xff;
121
+ }
122
+ return bytes;
123
+ }
124
+ /** Lowercase hex of `bytes`. */
125
+ export function hex(bytes) {
126
+ let out = '';
127
+ for (const b of bytes)
128
+ out += b.toString(16).padStart(2, '0');
129
+ return out;
130
+ }
@@ -0,0 +1,12 @@
1
+ /** Minimum chunk size of a multi-chunk file (256 KiB). */
2
+ export declare const MIN: number;
3
+ /** Average chunk size (1 MiB). */
4
+ export declare const AVG: number;
5
+ /** Maximum chunk size, and the largest file that stays one chunk (4 MiB). */
6
+ export declare const MAX: number;
7
+ /**
8
+ * The length of the first chunk of `source` (`cut_gear` of fastcdc v2020
9
+ * with the Store parameters): at most `MAX`, all of `source` when it is at
10
+ * most `MIN` long.
11
+ */
12
+ export declare function cut(source: Uint8Array): number;
@@ -0,0 +1,127 @@
1
+ // FastCDC 2020 cut points as Store cuts them (the `chunk` crate of the Store
2
+ // repository over `fastcdc` 5.0.0 `v2020`: normalisation level 1, no seed,
3
+ // the default gear table): a file of at most 4 MiB is one chunk, a longer one
4
+ // is cut with minimum 256 KiB, average 1 MiB and maximum 4 MiB. The same
5
+ // cuts as the server's mean identical content deduplicates. The 64-bit gear
6
+ // hash runs on two 32-bit halves.
7
+ /** Minimum chunk size of a multi-chunk file (256 KiB). */
8
+ export const MIN = 256 * 1024;
9
+ /** Average chunk size (1 MiB). */
10
+ export const AVG = 1024 * 1024;
11
+ /** Maximum chunk size, and the largest file that stays one chunk (4 MiB). */
12
+ export const MAX = 4 * 1024 * 1024;
13
+ // fastcdc v2020 GEAR: 256 u64 values, 16 hex digits each.
14
+ const TABLE = '3b5d3c7d207e37dc784d68ba91123086cd52880f882e7298eacf8e4e19fdcca7c31f385dfbd1632b1d5f27001e25abe683130bde3c9ad991c4b225676e9b7649' +
15
+ 'aa329b29e08eb499b67fcbd21e577d580027baaada2acf6be3ef2d5ac73c22260890f24d6ed312b7a809e036851d7c7ef0a6fe5e0013d81b1d026304452cec14' +
16
+ '03864632648e248fcdaacf3dcd92b9b4f5e012e63c1878568862f9d3821c00b6a82f7338750f6f8a1e583dc6c1cb0b6f7a3145b69743a7f1abb20fee404807eb' +
17
+ 'b14b3cfe07b83a5db9dc27898adb9a0f3703f5e91baa62becf0bb866815f7d983d9867c41ea9dcd31be1fa65442bf22c14300da4c55631d9e698e9cbc6545c99' +
18
+ '4763107ec64e92a5c65821fc65696a2476196c064822f0b7485be841f3525e01f652bc9c85974ff5cad8352face9e3e92a6ed1dceb35e98ec6f483badc11680f' +
19
+ '3cfd8c17e9cf12f189b83c5e2ea56471ae665cfd24e392a9ec33c4e504cb89153fb9b15fc9fe7451d7fd1fd1945f219531ade0853443efd8255efc9863e1e2d2' +
20
+ '10eab6008d5642cf46f04863257ac804a52dc42a789a27d3daaadf9ce77af5656b479cd53d87febb6309e2d3f93db72fc5738ffbaa1ff9d66bd57f3f25af7968' +
21
+ '67605486d90d0a4ae14d0b9663bfbdaeb7bbd8d816eb0414def8a4f16b35a116e7932d85aaaffed608161cbae90cfd48855507beb294f08b91234ea6ffd399b2' +
22
+ 'ad70cf4b2435f302d289a97565bc2d278e558437ffca99de96d2704b7115c0400889bbcdfc660e415e0d4e67dc92128d72a9f8917063ed97438b69d409e016e3' +
23
+ 'df4fed8a5d8a439700f41dcf41d403f74814eb038e52603f9dafbacc58e2d651fe2f458e4be170af4457ec414df6a94006e62f1451123314bd1014d173ba92cc' +
24
+ 'def318e25ed577609fea0de9dfca8525459de1e76c20624baeec189617e2d666126a2c06ab5a83cbb1321532360f613265421503dbb401232d67c287ea089ab3' +
25
+ '6c93bff5a56bd6b64ffb2036cab6d98dce7b785b1be7ad4fedb42ef6189fd163dc905288703988f6365f9c1d2c691884c640583680d99bfe3cd4624c07593ec6' +
26
+ '7f1ea8d85d7c5805014842d480b571490b649bcb5a828688bcd5708ed79b18f0e987c862fbd2f2f0982731671f0cd82cbaf13e8b16d8c0638ea3109cbd951bba' +
27
+ 'd141045bfb385cad2acbc1a0af1f7d30e6444d89df03bfdfa18cc771b8188ff99834429db01c39bb214add07fe086a1f8f07c19b1f6b3ff956a297b1bf4ffe55' +
28
+ '94d558e493c54fc740bfc24c764552cb931a706f8a8520cb32229d322935bd522560d0f5dc4fefaf9dbcc48355969bb60fd81c3985c0b56ae03817e1560f2bda' +
29
+ 'c1bb4f81d892b2d5b0c4864f4e28d2d73ecc49f9d9d6c26351307e99b52ba65e8af2b688da84a752f5d72523b91b20b66d95ff1ff4634806562f21555458339a' +
30
+ 'c0ce47f889336346487823e5089b40d8e4727c7ebc6d95925a8f7277e94970bafca2f406b1c8bb505b1f8a95f1791070d304af9fc90286055440ab7fc930e748' +
31
+ '312d25fbca2ab5a110f4a4b234a4d57590301d55047e74733b6372886c61591e293402b77c444e06451f34a4d3e97dd73158d814d81bc57b034942425b9bda69' +
32
+ 'e2032ff9e532d9bb62ae066b8b2179e59545e10c2f8d71d87ff7483eb2d23fc000945fcebdc98d868764bbbe99b26ca21b1ec62284c0bfc358e0fcc4f0aa362b' +
33
+ '5f4abefa878d458dfd74ac2f9607c519a4e3fb37df8cbfa9bf697e43cac574e586f14a3f68f4cd5324a23d076f1ce522e725cd8048868cc8bf3c729eb2464362' +
34
+ 'd8f6cd57b3cc1ed86329e5242554157762aa688ad5ae1ac00a242566269bf845168b1a4753aca74bf789afefff2e7e3c6c3362093b6fccdb4ce8f50bd28c09b2' +
35
+ '006a2db95ae8aa93975b0d623c3d1a8c18605d3935338c5b5bb6f6136cad3c710f53a20701f8d8a6ab8c5ad2e7e93c6740b5ac5127acaa298c7bf63c2075895f' +
36
+ '78bd9f7e014a805cb2c9e9f4f9c8c032efd6049827eb91f32be459f482c16fbdd92ce0c5745aaa8c0aaa8fb298d965b92b37f92c6c803b158c54a5e94e0f0e78' +
37
+ '95f9b6e90c0a3032e7939faa436c7874d16bfe8f6a8a40c944982b86263fd2fae285fb39f984e583779a8df72d7619d3f2d79a8de8d5dd1ed1037354d66684e2' +
38
+ '004c82a4e668a8e531d40a7668b044e6d70578538bd02c11db45431078c5f482977121bb7f6a51ad73d5ccbd34eff8dde437a07d356e17cd47b2782043c95627' +
39
+ '9fb251413e41d49accd70b60652513d31c95b31e8a1b49b2cae73dfd1bcb4c1b34d98331b1f5b70f784e39f22338d92f18613d4a064df420f1d8dae25f0bcebe' +
40
+ '33f77c15ae855efc3c88b3b912eb109c956a2ec96bafeea51aa005b5e0ad0e875500d70527c4bb8ee36c57196421cc4413c4d286cc36ee395654a23d818b2a81' +
41
+ '77b1dc13d161abdc734f44de5f8d5eb560717e174a6c89a2d47d9649266a211e5b13a4322bb69e90f7669609f8b5fc3c21e6ac55bedcdac99b56b62b61166dea' +
42
+ 'f48f66b939797e9c35f332f9c0e6ae9acc733f6a9a878db03da161e41cc108c2b7d74ae535914d514d493b0b11d36469ce264d1dfba9741aa9d1f2dc7436dc06' +
43
+ '70738016604c2a27231d36e96e93f3d57666881197838d194a2a83090aaad40cf1e761591668b35d7363236497f730a7301080e37379dd4d502dea2971827042' +
44
+ 'c2c5eb858f32625f786afb9edfafbdffdaee0d868490b2a4617366b3268609f6ae0e35a0fe46173ed1a07de93e824f11079b8b115ea4cca893a99274558faebb' +
45
+ 'fb1e6e22e08a03b3ea635fdba3698dd0cf53659328503a5ccde3b31e6fd5d7808e3e4221d3614413ef14d0d86bf1a22ce1d830d3f16c5ddbaabd2b2a451504e1';
46
+ const GEAR_HI = new Uint32Array(256);
47
+ const GEAR_LO = new Uint32Array(256);
48
+ const LS_HI = new Uint32Array(256);
49
+ const LS_LO = new Uint32Array(256);
50
+ for (let i = 0; i < 256; i++) {
51
+ const hi = parseInt(TABLE.slice(i * 16, i * 16 + 8), 16) >>> 0;
52
+ const lo = parseInt(TABLE.slice(i * 16 + 8, i * 16 + 16), 16) >>> 0;
53
+ GEAR_HI[i] = hi;
54
+ GEAR_LO[i] = lo;
55
+ // GEAR_LS is GEAR shifted left one bit (wrapping), as in the crate.
56
+ LS_HI[i] = ((hi << 1) | (lo >>> 31)) >>> 0;
57
+ LS_LO[i] = (lo << 1) >>> 0;
58
+ }
59
+ // The masks for an average of 1 MiB at level 1: MASKS[21] (strict) and
60
+ // MASKS[19] (relaxed), and each shifted left one bit.
61
+ const S_HI = 0x0000d917;
62
+ const S_LO = 0x67537000;
63
+ const L_HI = 0x0000d917;
64
+ const L_LO = 0x07537000;
65
+ const SLS_HI = ((S_HI << 1) | (S_LO >>> 31)) >>> 0;
66
+ const SLS_LO = (S_LO << 1) >>> 0;
67
+ const LLS_HI = ((L_HI << 1) | (L_LO >>> 31)) >>> 0;
68
+ const LLS_LO = (L_LO << 1) >>> 0;
69
+ /**
70
+ * The length of the first chunk of `source` (`cut_gear` of fastcdc v2020
71
+ * with the Store parameters): at most `MAX`, all of `source` when it is at
72
+ * most `MIN` long.
73
+ */
74
+ export function cut(source) {
75
+ let remaining = source.length;
76
+ if (remaining <= MIN)
77
+ return remaining;
78
+ let center = AVG;
79
+ if (remaining > MAX)
80
+ remaining = MAX;
81
+ else if (remaining < center)
82
+ center = remaining;
83
+ const limit1 = center >>> 1;
84
+ const limit2 = remaining >>> 1;
85
+ let index = MIN >>> 1;
86
+ let hi = 0;
87
+ let lo = 0;
88
+ while (index < limit1) {
89
+ const a = index * 2;
90
+ // hash = (hash << 2) + GEAR_LS[src[a]]
91
+ let b = source[a];
92
+ hi = ((hi << 2) | (lo >>> 30)) >>> 0;
93
+ lo = (lo << 2) >>> 0;
94
+ let sum = lo + LS_LO[b];
95
+ lo = sum >>> 0;
96
+ hi = (hi + LS_HI[b] + (sum > 0xffffffff ? 1 : 0)) >>> 0;
97
+ if ((hi & SLS_HI) === 0 && (lo & SLS_LO) === 0)
98
+ return a;
99
+ // hash = hash + GEAR[src[a + 1]]
100
+ b = source[a + 1];
101
+ sum = lo + GEAR_LO[b];
102
+ lo = sum >>> 0;
103
+ hi = (hi + GEAR_HI[b] + (sum > 0xffffffff ? 1 : 0)) >>> 0;
104
+ if ((hi & S_HI) === 0 && (lo & S_LO) === 0)
105
+ return a + 1;
106
+ index++;
107
+ }
108
+ while (index < limit2) {
109
+ const a = index * 2;
110
+ let b = source[a];
111
+ hi = ((hi << 2) | (lo >>> 30)) >>> 0;
112
+ lo = (lo << 2) >>> 0;
113
+ let sum = lo + LS_LO[b];
114
+ lo = sum >>> 0;
115
+ hi = (hi + LS_HI[b] + (sum > 0xffffffff ? 1 : 0)) >>> 0;
116
+ if ((hi & LLS_HI) === 0 && (lo & LLS_LO) === 0)
117
+ return a;
118
+ b = source[a + 1];
119
+ sum = lo + GEAR_LO[b];
120
+ lo = sum >>> 0;
121
+ hi = (hi + GEAR_HI[b] + (sum > 0xffffffff ? 1 : 0)) >>> 0;
122
+ if ((hi & L_HI) === 0 && (lo & L_LO) === 0)
123
+ return a + 1;
124
+ index++;
125
+ }
126
+ return remaining;
127
+ }
@@ -0,0 +1,23 @@
1
+ /** One chunk of a manifest: where it is in the file, its size and its BLAKE3 (64 hex digits). */
2
+ export interface Chunk {
3
+ offset: number;
4
+ size: number;
5
+ hash: string;
6
+ }
7
+ /** A file's chunk list, its size and its manifest hash. */
8
+ export interface Manifest {
9
+ size: number;
10
+ hash: string;
11
+ chunks: Chunk[];
12
+ }
13
+ /** Content the SDK can upload: a Blob (a File too), an ArrayBuffer or bytes. */
14
+ export type Content = Blob | ArrayBuffer | Uint8Array;
15
+ /** `content` as a Blob (bytes are copied once). */
16
+ export declare function blob(content: Content): Blob;
17
+ /** The manifest of `content`; `progress` is called with the bytes read so far. */
18
+ export declare function manifest(content: Content, progress?: (read: number) => void): Promise<Manifest>;
19
+ /** The manifest hash of `chunks`. */
20
+ export declare function seal(chunks: readonly {
21
+ hash: string;
22
+ size: number;
23
+ }[]): string;
@@ -0,0 +1,63 @@
1
+ // The manifest of a file as Store declares it (ADK-STORAGE-001 section 3.0,
2
+ // the Store docs "Uploading"): the chunk list from the FastCDC cut points,
3
+ // each chunk named by the BLAKE3 of its bytes, and the manifest hash, the
4
+ // BLAKE3 over `chunk hash (32 bytes) || chunk size (u32 little endian)` in
5
+ // order. Reads the content a window at a time, so a large Blob is never held
6
+ // whole in memory.
7
+ import { blake3, hex } from './blake3.js';
8
+ import { MAX, cut } from './cdc.js';
9
+ /** `content` as a Blob (bytes are copied once). */
10
+ export function blob(content) {
11
+ if (content instanceof Blob)
12
+ return content;
13
+ return new Blob([content instanceof Uint8Array ? new Uint8Array(content) : content]);
14
+ }
15
+ const READ = 8 * 1024 * 1024;
16
+ /** The manifest of `content`; `progress` is called with the bytes read so far. */
17
+ export async function manifest(content, progress) {
18
+ const source = blob(content);
19
+ const total = source.size;
20
+ const chunks = [];
21
+ if (total === 0)
22
+ return { size: 0, hash: hex(blake3(new Uint8Array(0))), chunks };
23
+ if (total <= MAX) {
24
+ const bytes = new Uint8Array(await source.arrayBuffer());
25
+ chunks.push({ offset: 0, size: total, hash: hex(blake3(bytes)) });
26
+ progress?.(total);
27
+ return { size: total, hash: seal(chunks), chunks };
28
+ }
29
+ // A window of unread bytes; a cut needs MAX bytes ahead (or the end).
30
+ let buffer = new Uint8Array(0);
31
+ let start = 0; // file offset of buffer[0]
32
+ let loaded = 0; // file offset after the buffer
33
+ let pos = 0; // offset in buffer of the next chunk
34
+ while (start + pos < total) {
35
+ if (buffer.length - pos < MAX && loaded < total) {
36
+ const next = new Uint8Array(await source.slice(loaded, Math.min(total, loaded + READ)).arrayBuffer());
37
+ const merged = new Uint8Array(buffer.length - pos + next.length);
38
+ merged.set(buffer.subarray(pos), 0);
39
+ merged.set(next, buffer.length - pos);
40
+ start += pos;
41
+ pos = 0;
42
+ buffer = merged;
43
+ loaded += next.length;
44
+ continue;
45
+ }
46
+ const window = buffer.subarray(pos, Math.min(buffer.length, pos + MAX));
47
+ const length = cut(window);
48
+ chunks.push({ offset: start + pos, size: length, hash: hex(blake3(window.subarray(0, length))) });
49
+ pos += length;
50
+ progress?.(start + pos);
51
+ }
52
+ return { size: total, hash: seal(chunks), chunks };
53
+ }
54
+ /** The manifest hash of `chunks`. */
55
+ export function seal(chunks) {
56
+ const bytes = new Uint8Array(chunks.length * 36);
57
+ chunks.forEach((c, i) => {
58
+ for (let j = 0; j < 32; j++)
59
+ bytes[i * 36 + j] = parseInt(c.hash.slice(j * 2, j * 2 + 2), 16);
60
+ new DataView(bytes.buffer).setUint32(i * 36 + 32, c.size, true);
61
+ });
62
+ return hex(blake3(bytes));
63
+ }
@@ -0,0 +1,202 @@
1
+ /** Client metadata of a version: modification time (milliseconds since the epoch) and the executable bit. */
2
+ export interface Attrs {
3
+ mtime: number;
4
+ exec: boolean;
5
+ }
6
+ /** A folder. `parent` null: at the drive root; `trashed`: when it went to the trash. */
7
+ export interface Folder {
8
+ id: string;
9
+ owner: string | null;
10
+ parent: string | null;
11
+ name: string;
12
+ created: string;
13
+ updated: string;
14
+ trashed: string | null;
15
+ }
16
+ /**
17
+ * A file. `version` is the current version and `size` its size; `state` is
18
+ * `pending` until the first version is ready. `attrs` only when the author
19
+ * of the current version sent them.
20
+ */
21
+ export interface FileEntry {
22
+ id: string;
23
+ owner: string | null;
24
+ folder: string | null;
25
+ name: string;
26
+ mime: string;
27
+ size: number;
28
+ version: string | null;
29
+ state: string;
30
+ attrs?: Attrs;
31
+ created: string;
32
+ updated: string;
33
+ trashed: string | null;
34
+ }
35
+ /** The live (or, with `trashed`, the trashed) children of a folder or drive root. */
36
+ export interface Listing {
37
+ folders: Folder[];
38
+ files: FileEntry[];
39
+ }
40
+ /**
41
+ * A version of a file: `open` (chunks being sent), `pending` (committed,
42
+ * being scanned), `ready`, `superseded` or `quarantined`. `hash` is the
43
+ * manifest hash, `author` an account uuid, `expires` set while uncommitted.
44
+ */
45
+ export interface Version {
46
+ id: string;
47
+ file: string;
48
+ state: string;
49
+ size: number;
50
+ hash: string;
51
+ author: string;
52
+ created: string;
53
+ committed: string | null;
54
+ expires: string | null;
55
+ reserved: number;
56
+ attrs?: Attrs;
57
+ }
58
+ /** A version and the chunk positions the server still lacks (`GET /v1/versions/{id}`). */
59
+ export interface VersionStatus extends Version {
60
+ missing: number[];
61
+ }
62
+ /** The answer to `Begin`: `new` (send `missing`, then commit) or `fast` (already ready, nothing to send). */
63
+ export interface Begun {
64
+ version: string;
65
+ state: 'new' | 'fast';
66
+ missing: number[];
67
+ expires: string | null;
68
+ reserved: number;
69
+ }
70
+ /** A drive's numbers: `cap` null is no cap; `used` counts open reservations; `mail` and `chat` are pooled attachments. */
71
+ export interface Quota {
72
+ owner: string;
73
+ cap: number | null;
74
+ per: number | null;
75
+ used: number;
76
+ mail: number;
77
+ chat: number;
78
+ }
79
+ /** The use of a public link. */
80
+ export interface LinkUse {
81
+ password: boolean;
82
+ cdn: boolean;
83
+ downloads: number;
84
+ bytes: number;
85
+ last: string | null;
86
+ }
87
+ /**
88
+ * A share of a file or folder (`kind`) with an account, the tenant, a team
89
+ * or anyone holding a link (`who`). `token` (links only) is in the answer
90
+ * that creates the link and never again.
91
+ */
92
+ export interface Share {
93
+ id: string;
94
+ target: string | null;
95
+ kind: string;
96
+ who: string;
97
+ grantee: string | null;
98
+ access: string;
99
+ expires: string | null;
100
+ created: string;
101
+ link?: LinkUse;
102
+ token?: string;
103
+ }
104
+ /** A share made with the caller and the item it reaches (one of `folder` and `file`). */
105
+ export interface Received {
106
+ share: Share;
107
+ folder: Folder | null;
108
+ file: FileEntry | null;
109
+ }
110
+ /** A folder as a link visitor sees it. */
111
+ export interface PublicFolder {
112
+ id: string;
113
+ name: string;
114
+ }
115
+ /** A file as a link visitor sees it. */
116
+ export interface PublicFile {
117
+ id: string;
118
+ name: string;
119
+ mime: string;
120
+ size: number;
121
+ updated: string;
122
+ }
123
+ /** What a public link shows: a file, or a folder with one level of its subtree; `cdn.parts` when served from the CDN. */
124
+ export interface LinkPage {
125
+ kind: string;
126
+ expires: string | null;
127
+ folder: PublicFolder | null;
128
+ folders: PublicFolder[];
129
+ file: PublicFile | null;
130
+ files: PublicFile[];
131
+ cdn: {
132
+ parts: {
133
+ url: string;
134
+ size: number;
135
+ }[];
136
+ } | null;
137
+ }
138
+ /** A read ticket: single use, five minutes (`expires` in Unix seconds). */
139
+ export interface Ticket {
140
+ ticket: string;
141
+ expires: number;
142
+ }
143
+ /** A thumbnail, poster, remux or rendition of a version. */
144
+ export interface Derivative {
145
+ id: string;
146
+ kind: string;
147
+ preset: string;
148
+ state: string;
149
+ reason: string | null;
150
+ mime: string | null;
151
+ size: number | null;
152
+ width: number | null;
153
+ height: number | null;
154
+ duration: number | null;
155
+ ready: string | null;
156
+ }
157
+ /** One entry of the change feed. */
158
+ export interface Change {
159
+ id: number;
160
+ owner: string | null;
161
+ target: string | null;
162
+ kind: string | null;
163
+ op: string;
164
+ parent: string | null;
165
+ name: string | null;
166
+ version: string | null;
167
+ hash: string | null;
168
+ size: number | null;
169
+ attrs?: Attrs;
170
+ updated: string;
171
+ }
172
+ /** A page of the change feed: pass `cursor` back for the next one; `more` means ask again at once. */
173
+ export interface ChangePage {
174
+ changes: Change[];
175
+ /** owner id to the last change id applied */
176
+ cursor: Record<string, number>;
177
+ /** the drives the caller sees: `own`, `tenant` or `shared` */
178
+ roots: {
179
+ owner: string;
180
+ kind: string;
181
+ }[];
182
+ /** owners in the cursor the caller can no longer see */
183
+ gone: string[];
184
+ more: boolean;
185
+ }
186
+ /** Search hits (ready, untrashed files the caller may read); `degraded` when the index answered partly. */
187
+ export interface SearchResult {
188
+ hits: FileEntry[];
189
+ degraded: boolean;
190
+ }
191
+ type Raw = Record<string, any>;
192
+ /** `{mtime_ns, exec}` as `{mtime, exec}` (milliseconds); undefined when absent. */
193
+ export declare function attrs(raw: unknown): Attrs | undefined;
194
+ export declare const folder: (raw: Raw) => Folder;
195
+ export declare const file: (raw: Raw) => FileEntry;
196
+ export declare const version: (raw: Raw) => Version;
197
+ export declare const status: (raw: Raw) => VersionStatus;
198
+ export declare const change: (raw: Raw) => Change;
199
+ export declare function listing(raw: Raw): Listing;
200
+ export declare function received(raw: Raw): Received;
201
+ export declare function changes(raw: Raw): ChangePage;
202
+ export {};
@@ -0,0 +1,38 @@
1
+ // The shapes Aduki Store answers with (the Store docs, "HTTP API: metadata",
2
+ // checked against `crates/app/src/api/*.rs`), and the small mappers that read
3
+ // them. Ids are 16-digit hex public ids; times are RFC 3339 strings.
4
+ /** `{mtime_ns, exec}` as `{mtime, exec}` (milliseconds); undefined when absent. */
5
+ export function attrs(raw) {
6
+ if (!raw || typeof raw !== 'object')
7
+ return undefined;
8
+ const r = raw;
9
+ return { mtime: Math.round(Number(r.mtime_ns) / 1e6), exec: Boolean(r.exec) };
10
+ }
11
+ function withAttrs(raw) {
12
+ const { attrs: a, ...rest } = raw;
13
+ const out = rest;
14
+ const mapped = attrs(a);
15
+ if (mapped)
16
+ out.attrs = mapped;
17
+ return out;
18
+ }
19
+ export const folder = (raw) => raw;
20
+ export const file = (raw) => withAttrs(raw);
21
+ export const version = (raw) => withAttrs(raw);
22
+ export const status = (raw) => withAttrs({ ...raw, missing: raw.missing ?? [] });
23
+ export const change = (raw) => withAttrs(raw);
24
+ export function listing(raw) {
25
+ return { folders: (raw.folders ?? []).map(folder), files: (raw.files ?? []).map(file) };
26
+ }
27
+ export function received(raw) {
28
+ return { share: raw.share, folder: raw.folder ? folder(raw.folder) : null, file: raw.file ? file(raw.file) : null };
29
+ }
30
+ export function changes(raw) {
31
+ return {
32
+ changes: (raw.changes ?? []).map(change),
33
+ cursor: raw.cursor ?? {},
34
+ roots: raw.roots ?? [],
35
+ gone: raw.gone ?? [],
36
+ more: Boolean(raw.more),
37
+ };
38
+ }
package/package.json CHANGED
@@ -1,6 +1,47 @@
1
1
  {
2
2
  "name": "@adukilabs/sdk",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.2",
4
+ "description": "Aduki API clients for programs: Aduki Mail, Contacts and Calendar with an API key (REST and JMAP), and the workspace drive of Aduki Store and Aduki Space with an Aduki ID service-client key (DPoP-bound tokens). TypeScript, web platform APIs only, no dependencies; Node 20+, Deno, Bun, workers.",
5
+ "license": "Apache-2.0",
6
+ "type": "module",
7
+ "engines": {
8
+ "node": ">=20"
9
+ },
10
+ "sideEffects": false,
11
+ "main": "./dist/api/index.js",
12
+ "types": "./dist/api/index.d.ts",
13
+ "exports": {
14
+ ".": {
15
+ "types": "./dist/api/index.d.ts",
16
+ "default": "./dist/api/index.js"
17
+ },
18
+ "./api": {
19
+ "types": "./dist/api/index.d.ts",
20
+ "default": "./dist/api/index.js"
21
+ },
22
+ "./package.json": "./package.json"
23
+ },
24
+ "files": [
25
+ "dist",
26
+ "LICENSE",
27
+ "README.md"
28
+ ],
29
+ "publishConfig": {
30
+ "access": "public"
31
+ },
32
+ "repository": {
33
+ "type": "git",
34
+ "url": "git+https://github.com/adukilabs/ts.git"
35
+ },
36
+ "keywords": [
37
+ "aduki",
38
+ "api",
39
+ "mail",
40
+ "jmap",
41
+ "calendar",
42
+ "contacts",
43
+ "store",
44
+ "space",
45
+ "dpop"
46
+ ]
47
+ }