@adukilabs/sdk 0.1.1 → 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,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,7 +1,7 @@
1
1
  {
2
2
  "name": "@adukilabs/sdk",
3
- "version": "0.1.1",
4
- "description": "Aduki API client for programs that use API keys (Authorization: Key): Aduki Mail, Contacts and Calendar over REST and JMAP. TypeScript, web platform APIs only, no dependencies; Node 20+, Deno, Bun, workers.",
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
5
  "license": "Apache-2.0",
6
6
  "type": "module",
7
7
  "engines": {
@@ -39,6 +39,9 @@
39
39
  "mail",
40
40
  "jmap",
41
41
  "calendar",
42
- "contacts"
42
+ "contacts",
43
+ "store",
44
+ "space",
45
+ "dpop"
43
46
  ]
44
47
  }