@adukilabs/sdk 0.0.0-stage → 0.1.1
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/LICENSE +201 -0
- package/README.md +72 -2
- package/dist/api/api.d.ts +223 -0
- package/dist/api/api.js +361 -0
- package/dist/api/index.d.ts +10 -0
- package/dist/api/index.js +9 -0
- package/dist/api/key.d.ts +16 -0
- package/dist/api/key.js +16 -0
- package/dist/api/model.d.ts +226 -0
- package/dist/envelope.d.ts +59 -0
- package/dist/envelope.js +114 -0
- package/dist/errors.d.ts +153 -0
- package/dist/errors.js +190 -0
- package/dist/http.d.ts +11 -0
- package/dist/http.js +22 -0
- package/dist/jmap.d.ts +100 -0
- package/dist/jmap.js +180 -0
- package/package.json +42 -4
package/dist/api/api.js
ADDED
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
// The API-key client: programs (servers, scripts, workers) that call Aduki
|
|
2
|
+
// Mail with a tenant API key, `Authorization: Key <key>`. No sign-in, no
|
|
3
|
+
// browser storage, no DPoP: the key is the credential, its stored scopes
|
|
4
|
+
// (narrowed to what the workspace owner who made it may do) are its rights.
|
|
5
|
+
//
|
|
6
|
+
// Aduki Mail (`mail.aduki.pro`) is the one service that accepts API keys:
|
|
7
|
+
// its REST API under `/v1` (the `user` and `tenant` routes; never
|
|
8
|
+
// `platform`) and its JMAP endpoints. Aduki ID, Store and Space take only
|
|
9
|
+
// Aduki ID tokens.
|
|
10
|
+
import { rejection } from '../envelope.js';
|
|
11
|
+
import { ProtocolError } from '../errors.js';
|
|
12
|
+
import { json, send, url } from '../http.js';
|
|
13
|
+
import { Jmap } from '../jmap.js';
|
|
14
|
+
import { createKey } from './key.js';
|
|
15
|
+
/** The mail host (REST under `/v1`, JMAP at the root). */
|
|
16
|
+
export const ROOT = 'https://mail.aduki.pro';
|
|
17
|
+
export class Api {
|
|
18
|
+
/** the JMAP client on the same key (mail: `u.mail:read`, sending: `u.mail:send`) */
|
|
19
|
+
jmap;
|
|
20
|
+
user;
|
|
21
|
+
mail;
|
|
22
|
+
mailboxes;
|
|
23
|
+
contacts;
|
|
24
|
+
calendar;
|
|
25
|
+
keys;
|
|
26
|
+
#key;
|
|
27
|
+
#root;
|
|
28
|
+
#fetch;
|
|
29
|
+
random;
|
|
30
|
+
constructor(options) {
|
|
31
|
+
const key = options?.key?.trim();
|
|
32
|
+
if (!key)
|
|
33
|
+
throw new TypeError('Api needs a key');
|
|
34
|
+
if (/\s/.test(key))
|
|
35
|
+
throw new TypeError('An API key has no whitespace');
|
|
36
|
+
this.#key = key;
|
|
37
|
+
this.#root = (options.root ?? ROOT).replace(/\/+$/, '');
|
|
38
|
+
const transport = options.fetch ?? ((...args) => globalThis.fetch(...args));
|
|
39
|
+
this.#fetch = (input, init) => send(transport, input, init);
|
|
40
|
+
this.random = options.random ?? (() => crypto.randomUUID());
|
|
41
|
+
const keyed = { fetch: (_audience, input, init) => this.#send(input, init) };
|
|
42
|
+
this.jmap = new Jmap({ session: keyed, root: this.#root });
|
|
43
|
+
this.user = new User(this);
|
|
44
|
+
this.mail = new MailApi(this);
|
|
45
|
+
this.mailboxes = new Mailboxes(this);
|
|
46
|
+
this.contacts = new ContactsApi(this);
|
|
47
|
+
this.calendar = new CalendarApi(this);
|
|
48
|
+
this.keys = new Keys(this);
|
|
49
|
+
}
|
|
50
|
+
/** The request with the key added (an absolute URL, or a path on the mail host). */
|
|
51
|
+
#send(input, init) {
|
|
52
|
+
const target = typeof input === 'string' && !/^https?:\/\//.test(input) ? url(this.#root, input.replace(/^\/+/, '')) : input;
|
|
53
|
+
const request = new Request(target, init);
|
|
54
|
+
request.headers.set('Authorization', `Key ${this.#key}`);
|
|
55
|
+
return this.#fetch(request);
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Sends a request to the mail host with the key (`path` relative to the
|
|
59
|
+
* host, as `v1/user`, or an absolute URL) and returns the raw response.
|
|
60
|
+
* A transport failure is a NetworkError; the answer is not checked.
|
|
61
|
+
*/
|
|
62
|
+
fetch(path, init) {
|
|
63
|
+
return this.#send(path, init);
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Any REST route under `/v1` (`path` without it, as `user/profile`):
|
|
67
|
+
* returns the envelope's `data` (null for `204`); a refusal throws the
|
|
68
|
+
* typed error of `errors.refusal` (UnauthorizedError for a bad key,
|
|
69
|
+
* NetworkError with `403 access.denied` for a scope the key lacks,
|
|
70
|
+
* RateLimitedError with `retryAfter`, ...).
|
|
71
|
+
*/
|
|
72
|
+
async call(method, path, options = {}) {
|
|
73
|
+
let target = `v1/${path.replace(/^\/+/, '')}`;
|
|
74
|
+
const query = new URLSearchParams();
|
|
75
|
+
for (const [name, value] of Object.entries(options.query ?? {}))
|
|
76
|
+
if (value !== undefined)
|
|
77
|
+
query.set(name, String(value));
|
|
78
|
+
if ([...query].length)
|
|
79
|
+
target += `?${query}`;
|
|
80
|
+
const headers = { Accept: 'application/json' };
|
|
81
|
+
let body;
|
|
82
|
+
if (options.body !== undefined) {
|
|
83
|
+
const request = json(options.body);
|
|
84
|
+
Object.assign(headers, request.headers);
|
|
85
|
+
body = request.body;
|
|
86
|
+
}
|
|
87
|
+
if (options.idempotency)
|
|
88
|
+
headers['Idempotency-Key'] = options.idempotency;
|
|
89
|
+
const init = { method, headers };
|
|
90
|
+
if (body !== undefined)
|
|
91
|
+
init.body = body;
|
|
92
|
+
if (options.signal)
|
|
93
|
+
init.signal = options.signal;
|
|
94
|
+
const response = await this.#send(target, init);
|
|
95
|
+
const text = await response.text();
|
|
96
|
+
const what = `${method} /${target.split('?')[0]}`;
|
|
97
|
+
if (!response.ok)
|
|
98
|
+
throw rejection(response, text, what);
|
|
99
|
+
if (response.status === 204 || !text.trim())
|
|
100
|
+
return null;
|
|
101
|
+
let parsed;
|
|
102
|
+
try {
|
|
103
|
+
parsed = JSON.parse(text);
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
throw new ProtocolError(`${what} answer is not JSON`);
|
|
107
|
+
}
|
|
108
|
+
return (parsed && typeof parsed === 'object' && 'success' in parsed ? (parsed.data ?? null) : parsed);
|
|
109
|
+
}
|
|
110
|
+
/** `GET` of a list route with paging (see PageQuery). */
|
|
111
|
+
page(path, query = {}) {
|
|
112
|
+
return this.call('GET', path, { query: query });
|
|
113
|
+
}
|
|
114
|
+
/** Every item of a list route, following the cursor (`after`) page by page. */
|
|
115
|
+
async *all(path, query = {}) {
|
|
116
|
+
let after;
|
|
117
|
+
do {
|
|
118
|
+
const page = await this.page(path, { ...query, after });
|
|
119
|
+
yield* page.items ?? [];
|
|
120
|
+
after = page.next || undefined;
|
|
121
|
+
} while (after);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
const seg = (value) => encodeURIComponent(value);
|
|
125
|
+
/** The account the key acts as. Scope `users:read`. */
|
|
126
|
+
export class User {
|
|
127
|
+
#api;
|
|
128
|
+
constructor(api) {
|
|
129
|
+
this.#api = api;
|
|
130
|
+
}
|
|
131
|
+
/** `GET /v1/user`: the account. */
|
|
132
|
+
get() {
|
|
133
|
+
return this.#api.call('GET', 'user');
|
|
134
|
+
}
|
|
135
|
+
/** `GET /v1/user/profile`: name, address, workspace, role, plan, storage. */
|
|
136
|
+
profile() {
|
|
137
|
+
return this.#api.call('GET', 'user/profile');
|
|
138
|
+
}
|
|
139
|
+
/** `GET /v1/user/keys`: the API keys made by this account (never the keys themselves). */
|
|
140
|
+
keys(query = {}) {
|
|
141
|
+
return this.#api.page('user/keys', query);
|
|
142
|
+
}
|
|
143
|
+
/** `GET /v1/user/aliases` (scope `aliases:read`). */
|
|
144
|
+
aliases() {
|
|
145
|
+
return this.#api.call('GET', 'user/aliases');
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
/** Mail over REST. Reading needs `mail:read`, changes `mail:write`, sending `mail:send`. */
|
|
149
|
+
export class MailApi {
|
|
150
|
+
#api;
|
|
151
|
+
constructor(api) {
|
|
152
|
+
this.#api = api;
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* `POST /v1/user/mail/send`: a plain-text message, a copy filed in Sent;
|
|
156
|
+
* returns its hex. `from` defaults to the default identity (an address the
|
|
157
|
+
* account may not send as is `403`). Sent with an `Idempotency-Key`
|
|
158
|
+
* (`idempotency`, else a random one): a retry with the same key never
|
|
159
|
+
* sends twice. A full queue is `RateLimitedError` (`mail.queue_busy`).
|
|
160
|
+
*/
|
|
161
|
+
async send(input, options = {}) {
|
|
162
|
+
const body = { to: input.to, subject: input.subject, text: input.text };
|
|
163
|
+
if (input.cc?.length)
|
|
164
|
+
body.cc = input.cc;
|
|
165
|
+
if (input.from)
|
|
166
|
+
body.from = input.from;
|
|
167
|
+
const data = await this.#api.call('POST', 'user/mail/send', { body, idempotency: options.idempotency ?? this.#api.random() });
|
|
168
|
+
if (typeof data?.hex !== 'string')
|
|
169
|
+
throw new ProtocolError('Send answer has no hex');
|
|
170
|
+
return data.hex;
|
|
171
|
+
}
|
|
172
|
+
/** `GET /v1/user/mail/<folder>`: the messages of a folder, newest first. */
|
|
173
|
+
list(folder, query = {}) {
|
|
174
|
+
return this.#api.page(`user/mail/${folder}`, query);
|
|
175
|
+
}
|
|
176
|
+
/** `GET /v1/user/mail/inbox/unread`. */
|
|
177
|
+
/** `GET /v1/user/mail/{hex}`: one message (scope `mail:read`); a deleted or unknown one is a 404. */
|
|
178
|
+
get(hex) {
|
|
179
|
+
return this.#api.call('GET', `user/mail/${seg(hex)}`);
|
|
180
|
+
}
|
|
181
|
+
unread(query = {}) {
|
|
182
|
+
return this.#api.page('user/mail/inbox/unread', query);
|
|
183
|
+
}
|
|
184
|
+
/** `GET /v1/user/mail/folder/{name}`: a mailbox by name. */
|
|
185
|
+
folder(name, query = {}) {
|
|
186
|
+
return this.#api.page(`user/mail/folder/${seg(name)}`, query);
|
|
187
|
+
}
|
|
188
|
+
/** `GET /v1/user/mail/search/{text}`. */
|
|
189
|
+
search(text, query = {}) {
|
|
190
|
+
return this.#api.page(`user/mail/search/${seg(text)}`, query);
|
|
191
|
+
}
|
|
192
|
+
/** `GET /v1/user/mail/threads`. */
|
|
193
|
+
threads(query = {}) {
|
|
194
|
+
return this.#api.page('user/mail/threads', query);
|
|
195
|
+
}
|
|
196
|
+
/** `GET /v1/user/mail/thread/{thread}`: the messages of a thread. */
|
|
197
|
+
thread(thread, query = {}) {
|
|
198
|
+
return this.#api.page(`user/mail/thread/${seg(thread)}`, query);
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* `GET /v1/user/mail/changes`: what changed in a mailbox since `since` (a
|
|
202
|
+
* modseq, 0 for everything). Pass the `uidvalidity` last seen: a stale one
|
|
203
|
+
* comes back with `reset`.
|
|
204
|
+
*/
|
|
205
|
+
changes(input) {
|
|
206
|
+
return this.#api.call('GET', 'user/mail/changes', {
|
|
207
|
+
query: { mailbox: input.mailbox, since: input.since ?? 0, uidvalidity: input.uidvalidity, limit: input.limit },
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
/** `PATCH /v1/user/mail/{hex}/flags`: adds and removes flags (`\\Seen`, `\\Flagged`, ...). */
|
|
211
|
+
async flags(hex, change) {
|
|
212
|
+
await this.#api.call('PATCH', `user/mail/${seg(hex)}/flags`, { body: { add: change.add ?? [], remove: change.remove ?? [] } });
|
|
213
|
+
}
|
|
214
|
+
/** `PATCH /v1/user/mail/{hex}/mailbox`: moves a message to another mailbox (hex). */
|
|
215
|
+
move(hex, mailbox) {
|
|
216
|
+
return this.#api.call('PATCH', `user/mail/${seg(hex)}/mailbox`, { body: { mailbox } });
|
|
217
|
+
}
|
|
218
|
+
/** `DELETE /v1/user/mail/{hex}`. */
|
|
219
|
+
async delete(hex) {
|
|
220
|
+
await this.#api.call('DELETE', `user/mail/${seg(hex)}`);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
/** Mailboxes over REST: `mailbox:read`. */
|
|
224
|
+
export class Mailboxes {
|
|
225
|
+
#api;
|
|
226
|
+
constructor(api) {
|
|
227
|
+
this.#api = api;
|
|
228
|
+
}
|
|
229
|
+
/** `GET /v1/user/mailbox`. */
|
|
230
|
+
list(query = {}) {
|
|
231
|
+
return this.#api.page('user/mailbox', query);
|
|
232
|
+
}
|
|
233
|
+
/** `GET /v1/user/mailbox/unread`: the mailboxes with unread mail. */
|
|
234
|
+
unread(query = {}) {
|
|
235
|
+
return this.#api.page('user/mailbox/unread', query);
|
|
236
|
+
}
|
|
237
|
+
/** `GET /v1/user/mailbox/search/{text}`. */
|
|
238
|
+
search(text, query = {}) {
|
|
239
|
+
return this.#api.page(`user/mailbox/search/${seg(text)}`, query);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
/** The address book over REST: `contacts:read`, `contacts:write` to delete. */
|
|
243
|
+
export class ContactsApi {
|
|
244
|
+
#api;
|
|
245
|
+
constructor(api) {
|
|
246
|
+
this.#api = api;
|
|
247
|
+
}
|
|
248
|
+
/** `GET /v1/user/contacts`. */
|
|
249
|
+
list(query = {}) {
|
|
250
|
+
return this.#api.page('user/contacts', query);
|
|
251
|
+
}
|
|
252
|
+
/** `GET /v1/user/contacts/{hex}`: one contact (scope `contacts:read`). */
|
|
253
|
+
get(hex) {
|
|
254
|
+
return this.#api.call('GET', `user/contacts/${seg(hex)}`);
|
|
255
|
+
}
|
|
256
|
+
/** `GET /v1/user/contacts/search/{text}`. */
|
|
257
|
+
search(text, query = {}) {
|
|
258
|
+
return this.#api.page(`user/contacts/search/${seg(text)}`, query);
|
|
259
|
+
}
|
|
260
|
+
/** `GET /v1/user/contacts/groups`. */
|
|
261
|
+
groups(query = {}) {
|
|
262
|
+
return this.#api.page('user/contacts/groups', query);
|
|
263
|
+
}
|
|
264
|
+
/** `GET /v1/user/contacts/group/{group}`: the cards of a group. */
|
|
265
|
+
group(group, query = {}) {
|
|
266
|
+
return this.#api.page(`user/contacts/group/${seg(group)}`, query);
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* `GET /v1/user/contacts/sync`: one page of changes since `since` (the
|
|
270
|
+
* `token` of the last answer; none for the whole book). A token the server
|
|
271
|
+
* no longer knows throws SyncResetError: sync again without one.
|
|
272
|
+
*/
|
|
273
|
+
sync(input = {}) {
|
|
274
|
+
return this.#api.call('GET', 'user/contacts/sync', { query: { since: input.since, limit: input.limit } });
|
|
275
|
+
}
|
|
276
|
+
/** `DELETE /v1/user/contacts/{hex}`. */
|
|
277
|
+
async delete(hex) {
|
|
278
|
+
await this.#api.call('DELETE', `user/contacts/${seg(hex)}`);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
/** Calendars and events over REST: `calendars:read`, `calendars:write` to delete. */
|
|
282
|
+
export class CalendarApi {
|
|
283
|
+
#api;
|
|
284
|
+
constructor(api) {
|
|
285
|
+
this.#api = api;
|
|
286
|
+
}
|
|
287
|
+
/** `GET /v1/user/calendars`. */
|
|
288
|
+
/** `GET /v1/user/calendars/{hex}`: one calendar (scope `calendars:read`). */
|
|
289
|
+
calendar(hex) {
|
|
290
|
+
return this.#api.call('GET', `user/calendars/${seg(hex)}`);
|
|
291
|
+
}
|
|
292
|
+
/** `GET /v1/user/events/{hex}`: one event with its `calendar` hex (scope `calendars:read`). */
|
|
293
|
+
event(hex) {
|
|
294
|
+
return this.#api.call('GET', `user/events/${seg(hex)}`);
|
|
295
|
+
}
|
|
296
|
+
calendars(query = {}) {
|
|
297
|
+
return this.#api.page('user/calendars', query);
|
|
298
|
+
}
|
|
299
|
+
/** `GET /v1/user/events`. */
|
|
300
|
+
events(query = {}) {
|
|
301
|
+
return this.#api.page('user/events', query);
|
|
302
|
+
}
|
|
303
|
+
/** `GET /v1/user/events/range/{from}/{to}`: occurrences between two instants (RFC 3339, or Dates). */
|
|
304
|
+
range(from, to, query = {}) {
|
|
305
|
+
const iso = (t) => (t instanceof Date ? t.toISOString() : t);
|
|
306
|
+
return this.#api.page(`user/events/range/${seg(iso(from))}/${seg(iso(to))}`, query);
|
|
307
|
+
}
|
|
308
|
+
/** `GET /v1/user/events/upcoming`. */
|
|
309
|
+
upcoming(query = {}) {
|
|
310
|
+
return this.#api.page('user/events/upcoming', query);
|
|
311
|
+
}
|
|
312
|
+
/** `GET /v1/user/events/search/{text}`. */
|
|
313
|
+
search(text, query = {}) {
|
|
314
|
+
return this.#api.page(`user/events/search/${seg(text)}`, query);
|
|
315
|
+
}
|
|
316
|
+
/** `DELETE /v1/user/events/{hex}`. */
|
|
317
|
+
async deleteEvent(hex) {
|
|
318
|
+
await this.#api.call('DELETE', `user/events/${seg(hex)}`);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
/**
|
|
322
|
+
* The workspace's API keys: `tenant.keys:read`, `tenant.keys:write` to
|
|
323
|
+
* rename, rotate and revoke. Making a key or changing its scopes is for a
|
|
324
|
+
* workspace owner signed in (a key is never an owner: `403`).
|
|
325
|
+
*/
|
|
326
|
+
export class Keys {
|
|
327
|
+
#api;
|
|
328
|
+
constructor(api) {
|
|
329
|
+
this.#api = api;
|
|
330
|
+
}
|
|
331
|
+
/** `GET /v1/tenant/keys`, `.../active` or `.../expired`. */
|
|
332
|
+
list(query = {}) {
|
|
333
|
+
const { state, ...rest } = query;
|
|
334
|
+
return this.#api.page(state ? `tenant/keys/${state}` : 'tenant/keys', rest);
|
|
335
|
+
}
|
|
336
|
+
/** `GET /v1/tenant/keys/{hex}`: name, prefix, scopes, dates (never the hash). */
|
|
337
|
+
get(hex) {
|
|
338
|
+
return this.#api.call('GET', `tenant/keys/${seg(hex)}`);
|
|
339
|
+
}
|
|
340
|
+
/** `PATCH /v1/tenant/keys/{hex}/name`. */
|
|
341
|
+
async rename(hex, name) {
|
|
342
|
+
await this.#api.call('PATCH', `tenant/keys/${seg(hex)}/name`, { body: { name } });
|
|
343
|
+
}
|
|
344
|
+
/**
|
|
345
|
+
* A new secret for key `hex` (`PATCH /v1/tenant/keys/{hex}/hash`): makes a
|
|
346
|
+
* random key, stores its hash and returns it. The old secret stops working
|
|
347
|
+
* at once (also for this client, if it is the key in use: make a new Api
|
|
348
|
+
* with the returned key); the key keeps its hex, name and scopes, and its
|
|
349
|
+
* prefix becomes the new key's (servers before ADK-MAIL-001 v52 ignore
|
|
350
|
+
* `prefix` and keep the old one).
|
|
351
|
+
*/
|
|
352
|
+
async rotate(hex) {
|
|
353
|
+
const made = await createKey();
|
|
354
|
+
await this.#api.call('PATCH', `tenant/keys/${seg(hex)}/hash`, { body: { hash: made.hash, prefix: made.prefix } });
|
|
355
|
+
return made.key;
|
|
356
|
+
}
|
|
357
|
+
/** `DELETE /v1/tenant/keys/{hex}`: deletes the key; it stops working at once. */
|
|
358
|
+
async delete(hex) {
|
|
359
|
+
await this.#api.call('DELETE', `tenant/keys/${seg(hex)}`);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export { Api, CalendarApi, ContactsApi, Keys, MailApi, Mailboxes, ROOT, User } from './api.js';
|
|
2
|
+
export type { ApiOptions, CallOptions, Folder, Query } from './api.js';
|
|
3
|
+
export { createKey, hashKey } from './key.js';
|
|
4
|
+
export type { NewKey } from './key.js';
|
|
5
|
+
export type * from './model.js';
|
|
6
|
+
export { AdukiError, LimitError, NetworkError, ProtocolError, RateLimitedError, ServerError, StepUpError, SyncResetError, UnauthorizedError, refusal, retryAfter, } from '../errors.js';
|
|
7
|
+
export type { NetworkErrorOptions, Refused } from '../errors.js';
|
|
8
|
+
export { kinds } from '../envelope.js';
|
|
9
|
+
export { CORE, Jmap, JmapError, MAIL, SUBMISSION, USING } from '../jmap.js';
|
|
10
|
+
export type { Invocation, JmapSession, Uploaded } from '../jmap.js';
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// sdk/api (published on npm as `@adukilabs/sdk`): the client for programs
|
|
2
|
+
// that use an Aduki API key (`Authorization: Key <key>`) on Aduki Mail's
|
|
3
|
+
// REST API and JMAP. Only the core it needs (errors, envelope, http, jmap)
|
|
4
|
+
// comes with it: no sign-in, session, browser storage or DPoP.
|
|
5
|
+
export { Api, CalendarApi, ContactsApi, Keys, MailApi, Mailboxes, ROOT, User } from './api.js';
|
|
6
|
+
export { createKey, hashKey } from './key.js';
|
|
7
|
+
export { AdukiError, LimitError, NetworkError, ProtocolError, RateLimitedError, ServerError, StepUpError, SyncResetError, UnauthorizedError, refusal, retryAfter, } from '../errors.js';
|
|
8
|
+
export { kinds } from '../envelope.js';
|
|
9
|
+
export { CORE, Jmap, JmapError, MAIL, SUBMISSION, USING } from '../jmap.js';
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/** A new key: the raw value to keep secret, and what the server stores. */
|
|
2
|
+
export interface NewKey {
|
|
3
|
+
/** the raw key, sent as `Authorization: Key <key>`; shown once, never sent to the server when created */
|
|
4
|
+
key: string;
|
|
5
|
+
/** SHA-256 of the key as lowercase hex (what the server stores) */
|
|
6
|
+
hash: string;
|
|
7
|
+
/** the first 16 characters of the key (the server's lookup index, not a secret by itself) */
|
|
8
|
+
prefix: string;
|
|
9
|
+
}
|
|
10
|
+
/** SHA-256 of `key` (its UTF-8 bytes) as lowercase hex, as the server hashes it. */
|
|
11
|
+
export declare function hashKey(key: string): Promise<string>;
|
|
12
|
+
/**
|
|
13
|
+
* A fresh random key (`adk_` and 32 random bytes as hex) with its hash and
|
|
14
|
+
* prefix. `random` fills the bytes (default `crypto.getRandomValues`).
|
|
15
|
+
*/
|
|
16
|
+
export declare function createKey(random?: (bytes: Uint8Array<ArrayBuffer>) => Uint8Array): Promise<NewKey>;
|
package/dist/api/key.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// Making an API key on the client (Aduki Mail keeps only its SHA-256): the
|
|
2
|
+
// raw key is random bytes, shown once; the server gets the hash and a short
|
|
3
|
+
// prefix to find it by. Web platform APIs only (WebCrypto).
|
|
4
|
+
const hex = (bytes) => Array.from(bytes, (b) => b.toString(16).padStart(2, '0')).join('');
|
|
5
|
+
/** SHA-256 of `key` (its UTF-8 bytes) as lowercase hex, as the server hashes it. */
|
|
6
|
+
export async function hashKey(key) {
|
|
7
|
+
return hex(new Uint8Array(await crypto.subtle.digest('SHA-256', new TextEncoder().encode(key))));
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* A fresh random key (`adk_` and 32 random bytes as hex) with its hash and
|
|
11
|
+
* prefix. `random` fills the bytes (default `crypto.getRandomValues`).
|
|
12
|
+
*/
|
|
13
|
+
export async function createKey(random = (b) => crypto.getRandomValues(b)) {
|
|
14
|
+
const key = `adk_${hex(random(new Uint8Array(32)))}`;
|
|
15
|
+
return { key, hash: await hashKey(key), prefix: key.slice(0, 16) };
|
|
16
|
+
}
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
/** A list: page mode (`page`, `pages`) or cursor mode (`next`, when more follow). */
|
|
2
|
+
export interface Page<T> {
|
|
3
|
+
items: T[];
|
|
4
|
+
total: number;
|
|
5
|
+
/** page mode: this page (from 1) */
|
|
6
|
+
page?: number;
|
|
7
|
+
/** page mode: the number of pages */
|
|
8
|
+
pages?: number;
|
|
9
|
+
/** cursor mode: pass as `after` for the next page; absent on the last one */
|
|
10
|
+
next?: string;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Which part of a list: `{page, limit}` (pages from 1) or `{after, limit}`
|
|
14
|
+
* (a cursor, the `next` of the page before). `limit` is 1 to 200 (default 50).
|
|
15
|
+
*/
|
|
16
|
+
export interface PageQuery {
|
|
17
|
+
page?: number;
|
|
18
|
+
after?: string;
|
|
19
|
+
limit?: number;
|
|
20
|
+
}
|
|
21
|
+
/** A message row of the mail lists. */
|
|
22
|
+
export interface MessageRow {
|
|
23
|
+
hex: string;
|
|
24
|
+
uid: number;
|
|
25
|
+
subject?: string | null;
|
|
26
|
+
sender?: string | null;
|
|
27
|
+
size: number;
|
|
28
|
+
flags: (string | null)[];
|
|
29
|
+
thread?: string | null;
|
|
30
|
+
spam?: number | null;
|
|
31
|
+
internaldate: string;
|
|
32
|
+
preview?: string | null;
|
|
33
|
+
from?: unknown;
|
|
34
|
+
to: unknown;
|
|
35
|
+
cc: unknown;
|
|
36
|
+
bcc: unknown;
|
|
37
|
+
reply_to: unknown;
|
|
38
|
+
message_id?: string | null;
|
|
39
|
+
has_attachment: boolean;
|
|
40
|
+
mailbox: unknown;
|
|
41
|
+
[field: string]: unknown;
|
|
42
|
+
}
|
|
43
|
+
/** A thread row (`GET /v1/user/mail/threads`). */
|
|
44
|
+
export interface ThreadRow {
|
|
45
|
+
thread: string;
|
|
46
|
+
subject?: string | null;
|
|
47
|
+
count: number;
|
|
48
|
+
unread: number;
|
|
49
|
+
latest: string;
|
|
50
|
+
mailbox: unknown;
|
|
51
|
+
[field: string]: unknown;
|
|
52
|
+
}
|
|
53
|
+
/** A mailbox row. */
|
|
54
|
+
export interface MailboxRow {
|
|
55
|
+
hex: string;
|
|
56
|
+
name: string;
|
|
57
|
+
delimiter: string;
|
|
58
|
+
flags: unknown;
|
|
59
|
+
uidvalidity: number;
|
|
60
|
+
uidnext: number;
|
|
61
|
+
role?: string | null;
|
|
62
|
+
messages: number;
|
|
63
|
+
unread: number;
|
|
64
|
+
created: string;
|
|
65
|
+
[field: string]: unknown;
|
|
66
|
+
}
|
|
67
|
+
/** What changed in one mailbox since a modseq (`GET /v1/user/mail/changes`). */
|
|
68
|
+
export interface MailChanges {
|
|
69
|
+
mailbox: string;
|
|
70
|
+
uidvalidity: number;
|
|
71
|
+
modseq: number;
|
|
72
|
+
/** the `uidvalidity` given is stale: sync the mailbox again from 0 */
|
|
73
|
+
reset: boolean;
|
|
74
|
+
/** more changes follow: ask again from `modseq` */
|
|
75
|
+
more: boolean;
|
|
76
|
+
created: MessageRow[];
|
|
77
|
+
updated: MessageRow[];
|
|
78
|
+
/** uids removed */
|
|
79
|
+
vanished: number[];
|
|
80
|
+
[field: string]: unknown;
|
|
81
|
+
}
|
|
82
|
+
/** A contact card. */
|
|
83
|
+
export interface Card {
|
|
84
|
+
hex: string;
|
|
85
|
+
uid: string;
|
|
86
|
+
href: string;
|
|
87
|
+
etag: string;
|
|
88
|
+
/** the vCard text */
|
|
89
|
+
vcard: string;
|
|
90
|
+
name?: string | null;
|
|
91
|
+
emails: unknown[];
|
|
92
|
+
phones: unknown[];
|
|
93
|
+
groups: unknown[];
|
|
94
|
+
meta: unknown;
|
|
95
|
+
version: number;
|
|
96
|
+
size: number;
|
|
97
|
+
created: string;
|
|
98
|
+
updated: string;
|
|
99
|
+
photo: unknown;
|
|
100
|
+
[field: string]: unknown;
|
|
101
|
+
}
|
|
102
|
+
/** One page of the address book's change feed (`GET /v1/user/contacts/sync`). */
|
|
103
|
+
export interface ContactChanges {
|
|
104
|
+
items: Card[];
|
|
105
|
+
/** hexes removed */
|
|
106
|
+
removed: unknown[];
|
|
107
|
+
/** pass as `since` next time */
|
|
108
|
+
token: string;
|
|
109
|
+
more: boolean;
|
|
110
|
+
/** the answer is the whole book, not changes */
|
|
111
|
+
full: boolean;
|
|
112
|
+
total?: number;
|
|
113
|
+
[field: string]: unknown;
|
|
114
|
+
}
|
|
115
|
+
/** A calendar. */
|
|
116
|
+
export interface CalendarRow {
|
|
117
|
+
hex: string;
|
|
118
|
+
kind: string;
|
|
119
|
+
href: string;
|
|
120
|
+
name: string;
|
|
121
|
+
description?: string | null;
|
|
122
|
+
color?: string | null;
|
|
123
|
+
timezone: string;
|
|
124
|
+
components: string[];
|
|
125
|
+
transp: string;
|
|
126
|
+
created: string;
|
|
127
|
+
updated: string;
|
|
128
|
+
[field: string]: unknown;
|
|
129
|
+
}
|
|
130
|
+
/** An event (occurrences are expanded in a range). */
|
|
131
|
+
export interface EventRow {
|
|
132
|
+
hex: string;
|
|
133
|
+
uid: string;
|
|
134
|
+
href: string;
|
|
135
|
+
etag: string;
|
|
136
|
+
/** the iCalendar object */
|
|
137
|
+
ical: string;
|
|
138
|
+
kind: string;
|
|
139
|
+
start?: string | null;
|
|
140
|
+
end?: string | null;
|
|
141
|
+
summary?: string | null;
|
|
142
|
+
description?: string | null;
|
|
143
|
+
location?: string | null;
|
|
144
|
+
attendees: unknown[];
|
|
145
|
+
organizer?: unknown;
|
|
146
|
+
status?: string | null;
|
|
147
|
+
sequence: number;
|
|
148
|
+
categories: string[];
|
|
149
|
+
rrule?: string | null;
|
|
150
|
+
recurring: boolean;
|
|
151
|
+
/** the calendar's hex */
|
|
152
|
+
calendar: string;
|
|
153
|
+
created: string;
|
|
154
|
+
updated: string;
|
|
155
|
+
[field: string]: unknown;
|
|
156
|
+
}
|
|
157
|
+
/** An API key as listed (never the key itself, nor its hash). */
|
|
158
|
+
export interface KeyRow {
|
|
159
|
+
hex: string;
|
|
160
|
+
name: string;
|
|
161
|
+
prefix: string;
|
|
162
|
+
active: boolean;
|
|
163
|
+
expires?: string | null;
|
|
164
|
+
last?: string | null;
|
|
165
|
+
created: string;
|
|
166
|
+
tenant: unknown;
|
|
167
|
+
user?: unknown;
|
|
168
|
+
[field: string]: unknown;
|
|
169
|
+
}
|
|
170
|
+
/** An API key in full (`GET /v1/tenant/keys/{hex}`). */
|
|
171
|
+
export interface KeyDetail {
|
|
172
|
+
hex: string;
|
|
173
|
+
name: string;
|
|
174
|
+
prefix: string;
|
|
175
|
+
scopes: (string | null)[] | null;
|
|
176
|
+
meta: unknown;
|
|
177
|
+
last?: string | null;
|
|
178
|
+
expires?: string | null;
|
|
179
|
+
revoked?: string | null;
|
|
180
|
+
created: string;
|
|
181
|
+
updated: string;
|
|
182
|
+
[field: string]: unknown;
|
|
183
|
+
}
|
|
184
|
+
/** The account a key acts as (`GET /v1/user`). */
|
|
185
|
+
export interface Account {
|
|
186
|
+
hex: string;
|
|
187
|
+
email: string;
|
|
188
|
+
name: string;
|
|
189
|
+
tenant: unknown;
|
|
190
|
+
owner: boolean;
|
|
191
|
+
state: string;
|
|
192
|
+
timezone: string;
|
|
193
|
+
locale: string;
|
|
194
|
+
created: string;
|
|
195
|
+
[field: string]: unknown;
|
|
196
|
+
}
|
|
197
|
+
/** The account's profile (`GET /v1/user/profile`). */
|
|
198
|
+
export interface Profile {
|
|
199
|
+
hex: string;
|
|
200
|
+
name: string;
|
|
201
|
+
email: string;
|
|
202
|
+
avatar?: {
|
|
203
|
+
hex: string;
|
|
204
|
+
updated: string;
|
|
205
|
+
} | null;
|
|
206
|
+
kind: string;
|
|
207
|
+
tenant?: {
|
|
208
|
+
hex: string;
|
|
209
|
+
name: string;
|
|
210
|
+
slug: string;
|
|
211
|
+
} | null;
|
|
212
|
+
role: {
|
|
213
|
+
label: string;
|
|
214
|
+
owner: boolean;
|
|
215
|
+
};
|
|
216
|
+
plan?: {
|
|
217
|
+
code: string;
|
|
218
|
+
state: string;
|
|
219
|
+
} | null;
|
|
220
|
+
storage: {
|
|
221
|
+
used: number;
|
|
222
|
+
limit?: number | null;
|
|
223
|
+
};
|
|
224
|
+
editable: boolean;
|
|
225
|
+
[field: string]: unknown;
|
|
226
|
+
}
|