@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.
- package/README.md +114 -2
- package/dist/api/api.d.ts +3 -0
- package/dist/api/api.js +7 -3
- package/dist/api/id.d.ts +75 -0
- package/dist/api/id.js +184 -0
- package/dist/api/index.d.ts +14 -1
- package/dist/api/index.js +15 -5
- package/dist/api/model.d.ts +250 -0
- package/dist/api/space.d.ts +147 -0
- package/dist/api/space.js +218 -0
- package/dist/api/store.d.ts +181 -0
- package/dist/api/store.js +352 -0
- package/dist/api/tenant.d.ts +319 -0
- package/dist/api/tenant.js +429 -0
- package/dist/base64.d.ts +11 -0
- package/dist/base64.js +40 -0
- package/dist/client.d.ts +29 -0
- package/dist/client.js +22 -0
- package/dist/dpop.d.ts +82 -0
- package/dist/dpop.js +156 -0
- package/dist/errors.d.ts +13 -1
- package/dist/errors.js +17 -1
- package/dist/space/model.d.ts +147 -0
- package/dist/store/blake3.d.ts +4 -0
- package/dist/store/blake3.js +130 -0
- package/dist/store/cdc.d.ts +12 -0
- package/dist/store/cdc.js +127 -0
- package/dist/store/chunks.d.ts +23 -0
- package/dist/store/chunks.js +63 -0
- package/dist/store/model.d.ts +202 -0
- package/dist/store/model.js +38 -0
- package/package.json +6 -3
|
@@ -0,0 +1,429 @@
|
|
|
1
|
+
// The workspace (tenant) routes of Aduki Mail an API key may call:
|
|
2
|
+
// `/v1/tenant/*`. Each handler checks a `tenant` scope; a key holds the
|
|
3
|
+
// scopes stored on it while the owner who made it still holds them. Making a
|
|
4
|
+
// key and changing a key's scopes stay with a signed-in owner (`api.keys`).
|
|
5
|
+
// Members, roles, invitations and apps live in Aduki ID, which takes no API
|
|
6
|
+
// key: here a member can be listed and removed from mail, not invited.
|
|
7
|
+
import { ProtocolError } from '../errors.js';
|
|
8
|
+
const seg = (value) => encodeURIComponent(value);
|
|
9
|
+
/** The workspace: profile, members, audits, usage and the mail admin routes. */
|
|
10
|
+
export class Tenant {
|
|
11
|
+
members;
|
|
12
|
+
audits;
|
|
13
|
+
usage;
|
|
14
|
+
domains;
|
|
15
|
+
aliases;
|
|
16
|
+
quotas;
|
|
17
|
+
rules;
|
|
18
|
+
webhooks;
|
|
19
|
+
holidays;
|
|
20
|
+
schedule;
|
|
21
|
+
#api;
|
|
22
|
+
constructor(api) {
|
|
23
|
+
this.#api = api;
|
|
24
|
+
this.members = new TenantMembers(api);
|
|
25
|
+
this.audits = new TenantAudits(api);
|
|
26
|
+
this.usage = new TenantUsage(api);
|
|
27
|
+
this.domains = new TenantDomains(api);
|
|
28
|
+
this.aliases = new TenantAliases(api);
|
|
29
|
+
this.quotas = new TenantQuotas(api);
|
|
30
|
+
this.rules = new TenantRules(api);
|
|
31
|
+
this.webhooks = new TenantWebhooks(api);
|
|
32
|
+
this.holidays = new TenantHolidays(api);
|
|
33
|
+
this.schedule = new TenantSchedule(api);
|
|
34
|
+
}
|
|
35
|
+
/** `GET /v1/tenant`: the workspace's profile (scope `tenants:read`). */
|
|
36
|
+
get() {
|
|
37
|
+
return this.#api.call('GET', 'tenant');
|
|
38
|
+
}
|
|
39
|
+
/** `GET /v1/tenant/view/slug/{slug}` (`tenants:read`; another workspace's slug is `403`). */
|
|
40
|
+
bySlug(slug) {
|
|
41
|
+
return this.#api.call('GET', `tenant/view/slug/${seg(slug)}`);
|
|
42
|
+
}
|
|
43
|
+
/** `GET /v1/tenant/security`: MTA-STS, TLSA, BIMI and the DMARC/TLS reports (`security:read`). */
|
|
44
|
+
security() {
|
|
45
|
+
return this.#api.call('GET', 'tenant/security');
|
|
46
|
+
}
|
|
47
|
+
/** `GET /v1/tenant/mail/services`: the mail services and whether this workspace may use each (`mailservices:read`). */
|
|
48
|
+
services() {
|
|
49
|
+
return this.#api.call('GET', 'tenant/mail/services');
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* The members as Aduki Mail sees them. Listing all needs `tenants:read`, the
|
|
54
|
+
* filtered lists `members:read`; removing `tenants:write`. Inviting, roles
|
|
55
|
+
* and closing an account are Aduki ID's and need a signed-in person.
|
|
56
|
+
*/
|
|
57
|
+
export class TenantMembers {
|
|
58
|
+
#api;
|
|
59
|
+
constructor(api) {
|
|
60
|
+
this.#api = api;
|
|
61
|
+
}
|
|
62
|
+
/** `GET /v1/tenant/members`, `.../active` or `.../owners`. */
|
|
63
|
+
list(query = {}) {
|
|
64
|
+
const { state, ...rest } = query;
|
|
65
|
+
return this.#api.page(state ? `tenant/members/${state}` : 'tenant/members', rest);
|
|
66
|
+
}
|
|
67
|
+
/** `GET /v1/tenant/members/search/{q}`. */
|
|
68
|
+
search(text, query = {}) {
|
|
69
|
+
return this.#api.page(`tenant/members/search/${seg(text)}`, query);
|
|
70
|
+
}
|
|
71
|
+
/** `DELETE /v1/tenant/members/{user}`: removes the member's mail account from the workspace (`404` for a non-member). */
|
|
72
|
+
async remove(user) {
|
|
73
|
+
await this.#api.call('DELETE', `tenant/members/${seg(user)}`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
/** The workspace's audit trail (`audits:read`; one record by hex: `tenants:read`). */
|
|
77
|
+
export class TenantAudits {
|
|
78
|
+
#api;
|
|
79
|
+
constructor(api) {
|
|
80
|
+
this.#api = api;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* `GET /v1/tenant/audits[/user/{user}][/action/{action} | /failed |
|
|
84
|
+
* /successful | /ip/{ip}]`: one filter besides `user` at a time (the
|
|
85
|
+
* server has no route for two).
|
|
86
|
+
*/
|
|
87
|
+
list(query = {}) {
|
|
88
|
+
const { user, action, outcome, ip, ...rest } = query;
|
|
89
|
+
if ([action, outcome, ip].filter((v) => v !== undefined).length > 1) {
|
|
90
|
+
throw new TypeError('Filter audits by one of action, outcome or ip');
|
|
91
|
+
}
|
|
92
|
+
let path = user ? `tenant/audits/user/${seg(user)}` : 'tenant/audits';
|
|
93
|
+
if (action !== undefined)
|
|
94
|
+
path += `/action/${seg(action)}`;
|
|
95
|
+
else if (outcome)
|
|
96
|
+
path += `/${outcome}`;
|
|
97
|
+
else if (ip !== undefined)
|
|
98
|
+
path += `/ip/${seg(ip)}`;
|
|
99
|
+
return this.#api.page(path, rest);
|
|
100
|
+
}
|
|
101
|
+
/** `GET /v1/tenant/view/audit/{hex}`: one record with its `meta` (`tenants:read`). */
|
|
102
|
+
get(hex) {
|
|
103
|
+
return this.#api.call('GET', `tenant/view/audit/${seg(hex)}`);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
/** Metered usage (`usage:read`). */
|
|
107
|
+
export class TenantUsage {
|
|
108
|
+
#api;
|
|
109
|
+
constructor(api) {
|
|
110
|
+
this.#api = api;
|
|
111
|
+
}
|
|
112
|
+
/** `GET /v1/tenant/usage`. */
|
|
113
|
+
list(query = {}) {
|
|
114
|
+
return this.#api.page('tenant/usage', query);
|
|
115
|
+
}
|
|
116
|
+
/** `GET /v1/tenant/usage/summary`: one row per metric, no paging. */
|
|
117
|
+
summary() {
|
|
118
|
+
return this.#api.call('GET', 'tenant/usage/summary');
|
|
119
|
+
}
|
|
120
|
+
/** `GET /v1/tenant/usage/metric/{metric}`. */
|
|
121
|
+
metric(metric, query = {}) {
|
|
122
|
+
return this.#api.page(`tenant/usage/metric/${seg(metric)}`, query);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Mail domains: `domains:read`, `domains:write` to change. The DNS records
|
|
127
|
+
* to publish are on the domain (`dkim`, and `pending` of a DKIM rotation).
|
|
128
|
+
*/
|
|
129
|
+
export class TenantDomains {
|
|
130
|
+
dkim;
|
|
131
|
+
#api;
|
|
132
|
+
constructor(api) {
|
|
133
|
+
this.#api = api;
|
|
134
|
+
this.dkim = new TenantDkim(api);
|
|
135
|
+
}
|
|
136
|
+
/** `POST /v1/tenant/domains`: registers a domain (status `pending`); returns its hex. */
|
|
137
|
+
async add(input) {
|
|
138
|
+
const data = await this.#api.call('POST', 'tenant/domains', { body: input });
|
|
139
|
+
if (typeof data?.hex !== 'string')
|
|
140
|
+
throw new ProtocolError('Domain answer has no hex');
|
|
141
|
+
return data.hex;
|
|
142
|
+
}
|
|
143
|
+
/** `GET /v1/tenant/domains`, `.../active` or `.../pending`. */
|
|
144
|
+
list(query = {}) {
|
|
145
|
+
const { state, ...rest } = query;
|
|
146
|
+
return this.#api.page(state ? `tenant/domains/${state}` : 'tenant/domains', rest);
|
|
147
|
+
}
|
|
148
|
+
/** `GET /v1/tenant/domains/{hex}`: the full record with its DNS snapshots. */
|
|
149
|
+
get(hex) {
|
|
150
|
+
return this.#api.call('GET', `tenant/domains/${seg(hex)}`);
|
|
151
|
+
}
|
|
152
|
+
/** `GET /v1/tenant/domains/lookup/name?name=`: a domain by name (`404` when none). */
|
|
153
|
+
byName(name) {
|
|
154
|
+
return this.#api.call('GET', 'tenant/domains/lookup/name', { query: { name } });
|
|
155
|
+
}
|
|
156
|
+
/** `PATCH /v1/tenant/domains/{hex}/name`. */
|
|
157
|
+
async rename(hex, name) {
|
|
158
|
+
await this.#api.call('PATCH', `tenant/domains/${seg(hex)}/name`, { body: { name } });
|
|
159
|
+
}
|
|
160
|
+
/** `PATCH /v1/tenant/domains/{hex}/kind`: `primary`, `sending`, `receiving` or `alias`. */
|
|
161
|
+
async kind(hex, kind) {
|
|
162
|
+
await this.#api.call('PATCH', `tenant/domains/${seg(hex)}/kind`, { body: { kind } });
|
|
163
|
+
}
|
|
164
|
+
/** `PATCH /v1/tenant/domains/{hex}/selector`: the DKIM selector, before the domain has a key (`409` after). */
|
|
165
|
+
async selector(hex, selector) {
|
|
166
|
+
await this.#api.call('PATCH', `tenant/domains/${seg(hex)}/selector`, { body: { selector } });
|
|
167
|
+
}
|
|
168
|
+
/** `PATCH /v1/tenant/domains/{hex}/auth`: the SPF, DMARC, MTA-STS and BIMI settings, all four. */
|
|
169
|
+
async auth(hex, input) {
|
|
170
|
+
await this.#api.call('PATCH', `tenant/domains/${seg(hex)}/auth`, { body: input });
|
|
171
|
+
}
|
|
172
|
+
/** `PATCH /v1/tenant/domains/{hex}/meta`. */
|
|
173
|
+
async meta(hex, meta) {
|
|
174
|
+
await this.#api.call('PATCH', `tenant/domains/${seg(hex)}/meta`, { body: { meta } });
|
|
175
|
+
}
|
|
176
|
+
/** `DELETE /v1/tenant/domains/{hex}`. */
|
|
177
|
+
async delete(hex) {
|
|
178
|
+
await this.#api.call('DELETE', `tenant/domains/${seg(hex)}`);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* DKIM key rotation without a gap: stage a key the server makes, publish its
|
|
183
|
+
* TXT record, check, confirm. No private key is sent or returned.
|
|
184
|
+
*/
|
|
185
|
+
export class TenantDkim {
|
|
186
|
+
#api;
|
|
187
|
+
constructor(api) {
|
|
188
|
+
this.#api = api;
|
|
189
|
+
}
|
|
190
|
+
/** `GET /v1/tenant/domains/{hex}/dkim/rotation`: the active, staged and retired records (`domains:read`). */
|
|
191
|
+
state(hex) {
|
|
192
|
+
return this.#api.call('GET', `tenant/domains/${seg(hex)}/dkim/rotation`);
|
|
193
|
+
}
|
|
194
|
+
/** `POST .../dkim/rotation`: stages the next key (a retry returns the same one); publish `pending.value` at `pending.name`. */
|
|
195
|
+
stage(hex, input = {}) {
|
|
196
|
+
return this.#api.call('POST', `tenant/domains/${seg(hex)}/dkim/rotation`, { body: input });
|
|
197
|
+
}
|
|
198
|
+
/** `POST .../dkim/rotation/check`: whether DNS carries the staged record; changes nothing (`domains:read`). */
|
|
199
|
+
check(hex) {
|
|
200
|
+
return this.#api.call('POST', `tenant/domains/${seg(hex)}/dkim/rotation/check`);
|
|
201
|
+
}
|
|
202
|
+
/** `POST .../dkim/rotation/confirm`: switches signing to the staged key (`409` while DNS does not carry it). */
|
|
203
|
+
confirm(hex) {
|
|
204
|
+
return this.#api.call('POST', `tenant/domains/${seg(hex)}/dkim/rotation/confirm`);
|
|
205
|
+
}
|
|
206
|
+
/** `DELETE .../dkim/rotation`: drops the staged key (`404` when none). */
|
|
207
|
+
async cancel(hex) {
|
|
208
|
+
await this.#api.call('DELETE', `tenant/domains/${seg(hex)}/dkim/rotation`);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
/** Members' extra addresses on the workspace's verified domains: `aliases:read`, `aliases:write`. */
|
|
212
|
+
export class TenantAliases {
|
|
213
|
+
#api;
|
|
214
|
+
constructor(api) {
|
|
215
|
+
this.#api = api;
|
|
216
|
+
}
|
|
217
|
+
/** `POST /v1/tenant/aliases`: `created` is false when the member already had it. */
|
|
218
|
+
add(user, address) {
|
|
219
|
+
return this.#api.call('POST', 'tenant/aliases', { body: { user, address } });
|
|
220
|
+
}
|
|
221
|
+
/** `GET /v1/tenant/aliases[?user=]`: at most 1000, by address. */
|
|
222
|
+
list(query = {}) {
|
|
223
|
+
return this.#api.call('GET', 'tenant/aliases', { query: { user: query.user } });
|
|
224
|
+
}
|
|
225
|
+
/** `DELETE /v1/tenant/aliases/{address}`. */
|
|
226
|
+
async remove(address) {
|
|
227
|
+
await this.#api.call('DELETE', `tenant/aliases/${seg(address)}`);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
/** The workspace's own usage ceilings per metric: `quotas:read`, `quotas:write`. */
|
|
231
|
+
export class TenantQuotas {
|
|
232
|
+
#api;
|
|
233
|
+
constructor(api) {
|
|
234
|
+
this.#api = api;
|
|
235
|
+
}
|
|
236
|
+
/** `POST /v1/tenant/quotas`. */
|
|
237
|
+
add(input) {
|
|
238
|
+
return this.#api.call('POST', 'tenant/quotas', { body: input });
|
|
239
|
+
}
|
|
240
|
+
/** `GET /v1/tenant/quotas`. */
|
|
241
|
+
list(query = {}) {
|
|
242
|
+
return this.#api.page('tenant/quotas', query);
|
|
243
|
+
}
|
|
244
|
+
/** `GET /v1/tenant/quotas/{metric}`. */
|
|
245
|
+
get(metric) {
|
|
246
|
+
return this.#api.call('GET', `tenant/quotas/${seg(metric)}`);
|
|
247
|
+
}
|
|
248
|
+
/** `PATCH /v1/tenant/quotas/{metric}/ceiling`, `/expires`, `/reason`: the fields given, one call each. */
|
|
249
|
+
async edit(metric, change) {
|
|
250
|
+
for (const field of ['ceiling', 'expires', 'reason']) {
|
|
251
|
+
if (change[field] !== undefined) {
|
|
252
|
+
await this.#api.call('PATCH', `tenant/quotas/${seg(metric)}/${field}`, { body: { [field]: change[field] } });
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
/** `DELETE /v1/tenant/quotas/{metric}`. */
|
|
257
|
+
async delete(metric) {
|
|
258
|
+
await this.#api.call('DELETE', `tenant/quotas/${seg(metric)}`);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
/** Spam rules of the workspace: `rules:read`, `rules:write`. */
|
|
262
|
+
export class TenantRules {
|
|
263
|
+
#api;
|
|
264
|
+
constructor(api) {
|
|
265
|
+
this.#api = api;
|
|
266
|
+
}
|
|
267
|
+
/** `POST /v1/tenant/rules`. */
|
|
268
|
+
add(input) {
|
|
269
|
+
return this.#api.call('POST', 'tenant/rules', { body: input });
|
|
270
|
+
}
|
|
271
|
+
/** `GET /v1/tenant/rules`, `.../active`, or `.../target/{target}`. */
|
|
272
|
+
list(query = {}) {
|
|
273
|
+
const { active, target, ...rest } = query;
|
|
274
|
+
if (active && target !== undefined)
|
|
275
|
+
throw new TypeError('Filter rules by active or by target');
|
|
276
|
+
const path = active ? 'tenant/rules/active' : target !== undefined ? `tenant/rules/target/${seg(target)}` : 'tenant/rules';
|
|
277
|
+
return this.#api.page(path, rest);
|
|
278
|
+
}
|
|
279
|
+
/** `GET /v1/tenant/rules/{hex}/detail`: the rule with its `meta`. */
|
|
280
|
+
get(hex) {
|
|
281
|
+
return this.#api.call('GET', `tenant/rules/${seg(hex)}/detail`);
|
|
282
|
+
}
|
|
283
|
+
/** `GET /v1/tenant/rules/name/{name}`. */
|
|
284
|
+
byName(name) {
|
|
285
|
+
return this.#api.call('GET', `tenant/rules/name/${seg(name)}`);
|
|
286
|
+
}
|
|
287
|
+
/**
|
|
288
|
+
* `PATCH /v1/tenant/rules/{hex}/{field}` for each field given (`regex` is
|
|
289
|
+
* sent after `pattern`, so a new pattern and `regex: true` go together).
|
|
290
|
+
*/
|
|
291
|
+
async edit(hex, change) {
|
|
292
|
+
for (const field of ['name', 'target', 'pattern', 'regex', 'score', 'active']) {
|
|
293
|
+
if (change[field] !== undefined) {
|
|
294
|
+
await this.#api.call('PATCH', `tenant/rules/${seg(hex)}/${field}`, { body: { [field]: change[field] } });
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
/** `DELETE /v1/tenant/rules/{hex}`. */
|
|
299
|
+
async delete(hex) {
|
|
300
|
+
await this.#api.call('DELETE', `tenant/rules/${seg(hex)}`);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
/** Delivery-event webhooks: `webhooks:read`, `webhooks:write`. */
|
|
304
|
+
export class TenantWebhooks {
|
|
305
|
+
#api;
|
|
306
|
+
constructor(api) {
|
|
307
|
+
this.#api = api;
|
|
308
|
+
}
|
|
309
|
+
/** `POST /v1/tenant/webhooks`: returns the hex and the signing secret, shown this once. */
|
|
310
|
+
add(input) {
|
|
311
|
+
return this.#api.call('POST', 'tenant/webhooks', { body: input });
|
|
312
|
+
}
|
|
313
|
+
/** `GET /v1/tenant/webhooks`, `.../active`, or `.../subscribers/{event}`. */
|
|
314
|
+
list(query = {}) {
|
|
315
|
+
const { active, event, ...rest } = query;
|
|
316
|
+
if (active && event !== undefined)
|
|
317
|
+
throw new TypeError('Filter webhooks by active or by event');
|
|
318
|
+
const path = active ? 'tenant/webhooks/active' : event !== undefined ? `tenant/webhooks/subscribers/${seg(event)}` : 'tenant/webhooks';
|
|
319
|
+
return this.#api.page(path, rest);
|
|
320
|
+
}
|
|
321
|
+
/** `GET /v1/tenant/webhooks/{hex}`: the record, never the secret. */
|
|
322
|
+
get(hex) {
|
|
323
|
+
return this.#api.call('GET', `tenant/webhooks/${seg(hex)}`);
|
|
324
|
+
}
|
|
325
|
+
/** `PATCH /v1/tenant/webhooks/{hex}/{field}` for each field given (`url` is checked again: `https`, a public host). */
|
|
326
|
+
async edit(hex, change) {
|
|
327
|
+
for (const field of ['url', 'events', 'domains', 'secret', 'active']) {
|
|
328
|
+
if (change[field] !== undefined) {
|
|
329
|
+
await this.#api.call('PATCH', `tenant/webhooks/${seg(hex)}/${field}`, { body: { [field]: change[field] } });
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
/** `POST /v1/tenant/webhooks/{hex}/secret/rotate`: a new secret made by the server, returned once. */
|
|
334
|
+
async rotate(hex) {
|
|
335
|
+
const data = await this.#api.call('POST', `tenant/webhooks/${seg(hex)}/secret/rotate`);
|
|
336
|
+
if (typeof data?.secret !== 'string')
|
|
337
|
+
throw new ProtocolError('Rotate answer has no secret');
|
|
338
|
+
return data.secret;
|
|
339
|
+
}
|
|
340
|
+
/** `DELETE /v1/tenant/webhooks/{hex}`. */
|
|
341
|
+
async delete(hex) {
|
|
342
|
+
await this.#api.call('DELETE', `tenant/webhooks/${seg(hex)}`);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
/** The workspace's holidays and closures: reading `tenant:read`, writing `tenants:write`. */
|
|
346
|
+
export class TenantHolidays {
|
|
347
|
+
#api;
|
|
348
|
+
constructor(api) {
|
|
349
|
+
this.#api = api;
|
|
350
|
+
}
|
|
351
|
+
/** `GET /v1/tenant/holidays?region&year`: the effective days (`region` defaults to the workspace's). */
|
|
352
|
+
list(query = {}) {
|
|
353
|
+
return this.#api.call('GET', 'tenant/holidays', { query: { region: query.region, year: query.year } });
|
|
354
|
+
}
|
|
355
|
+
/**
|
|
356
|
+
* `POST /v1/tenant/holidays`: a day or range of the workspace's own
|
|
357
|
+
* (`kind` `closure` by default, or `observance`), or `{exclude: true,
|
|
358
|
+
* region, date}` to drop the platform's day on that date.
|
|
359
|
+
*/
|
|
360
|
+
add(input) {
|
|
361
|
+
return this.#api.call('POST', 'tenant/holidays', { body: input });
|
|
362
|
+
}
|
|
363
|
+
/** `DELETE /v1/tenant/holidays/{hex}`: one of the workspace's own rows. */
|
|
364
|
+
async remove(hex) {
|
|
365
|
+
await this.#api.call('DELETE', `tenant/holidays/${seg(hex)}`);
|
|
366
|
+
}
|
|
367
|
+
/** `GET /v1/tenant/holidays/region`: the default region, or null. */
|
|
368
|
+
region() {
|
|
369
|
+
return this.#api.call('GET', 'tenant/holidays/region');
|
|
370
|
+
}
|
|
371
|
+
/** `PUT /v1/tenant/holidays/region` (`422` for a region with no data); `null` clears it (`DELETE`). */
|
|
372
|
+
async setRegion(region) {
|
|
373
|
+
if (region === null)
|
|
374
|
+
await this.#api.call('DELETE', 'tenant/holidays/region');
|
|
375
|
+
else
|
|
376
|
+
await this.#api.call('PUT', 'tenant/holidays/region', { body: region });
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
/** Scheduling templates and resources: reading `tenant:read`, writing `tenants:write`. */
|
|
380
|
+
export class TenantSchedule {
|
|
381
|
+
#api;
|
|
382
|
+
constructor(api) {
|
|
383
|
+
this.#api = api;
|
|
384
|
+
}
|
|
385
|
+
/** `GET /v1/tenant/schedule/templates`. */
|
|
386
|
+
templates() {
|
|
387
|
+
return this.#api.call('GET', 'tenant/schedule/templates');
|
|
388
|
+
}
|
|
389
|
+
/** `GET /v1/tenant/schedule/templates/{hex}`. */
|
|
390
|
+
template(hex) {
|
|
391
|
+
return this.#api.call('GET', `tenant/schedule/templates/${seg(hex)}`);
|
|
392
|
+
}
|
|
393
|
+
/** `POST /v1/tenant/schedule/templates` (rules default to weekdays 09:00-17:00, zone to UTC). */
|
|
394
|
+
addTemplate(input) {
|
|
395
|
+
return this.#api.call('POST', 'tenant/schedule/templates', { body: input });
|
|
396
|
+
}
|
|
397
|
+
/** `PATCH /v1/tenant/schedule/templates/{hex}`: name and zone. */
|
|
398
|
+
editTemplate(hex, change) {
|
|
399
|
+
return this.#api.call('PATCH', `tenant/schedule/templates/${seg(hex)}`, { body: change });
|
|
400
|
+
}
|
|
401
|
+
/** `PUT /v1/tenant/schedule/templates/{hex}/rules`: replaces the weekly rules. */
|
|
402
|
+
setRules(hex, rules) {
|
|
403
|
+
return this.#api.call('PUT', `tenant/schedule/templates/${seg(hex)}/rules`, { body: rules });
|
|
404
|
+
}
|
|
405
|
+
/** `DELETE /v1/tenant/schedule/templates/{hex}` (`409` while followed or used). */
|
|
406
|
+
async deleteTemplate(hex) {
|
|
407
|
+
await this.#api.call('DELETE', `tenant/schedule/templates/${seg(hex)}`);
|
|
408
|
+
}
|
|
409
|
+
/** `GET /v1/tenant/schedule/resources`: rooms and equipment, live ones first. */
|
|
410
|
+
resources() {
|
|
411
|
+
return this.#api.call('GET', 'tenant/schedule/resources');
|
|
412
|
+
}
|
|
413
|
+
/** `GET /v1/tenant/schedule/resources/{hex}`. */
|
|
414
|
+
resource(hex) {
|
|
415
|
+
return this.#api.call('GET', `tenant/schedule/resources/${seg(hex)}`);
|
|
416
|
+
}
|
|
417
|
+
/** `POST /v1/tenant/schedule/resources` (`409` for a name in use). */
|
|
418
|
+
addResource(input) {
|
|
419
|
+
return this.#api.call('POST', 'tenant/schedule/resources', { body: input });
|
|
420
|
+
}
|
|
421
|
+
/** `PATCH /v1/tenant/schedule/resources/{hex}`: `null` clears `capacity` and `schedule`; `retired` stops offering it. */
|
|
422
|
+
editResource(hex, change) {
|
|
423
|
+
return this.#api.call('PATCH', `tenant/schedule/resources/${seg(hex)}`, { body: change });
|
|
424
|
+
}
|
|
425
|
+
/** `DELETE /v1/tenant/schedule/resources/{hex}` (`409` once booked: retire it instead). */
|
|
426
|
+
async deleteResource(hex) {
|
|
427
|
+
await this.#api.call('DELETE', `tenant/schedule/resources/${seg(hex)}`);
|
|
428
|
+
}
|
|
429
|
+
}
|
package/dist/base64.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/** JWT claims as decoded from JSON; the SDK never trusts their shape. */
|
|
2
|
+
export type Claims = Record<string, any>;
|
|
3
|
+
export declare function encode(bytes: ArrayBuffer | Uint8Array): string;
|
|
4
|
+
/** @param text base64url (padding and the standard alphabet are tolerated) */
|
|
5
|
+
export declare function decode(text: string): Uint8Array<ArrayBuffer>;
|
|
6
|
+
/** base64url of the UTF-8 bytes */
|
|
7
|
+
export declare function encodeText(text: string): string;
|
|
8
|
+
/** @param text base64url @returns the UTF-8 string */
|
|
9
|
+
export declare function decodeText(text: string): string;
|
|
10
|
+
/** The payload claims of a JWT, unverified. */
|
|
11
|
+
export declare function claims(jwt: string): Claims;
|
package/dist/base64.js
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// base64url without padding (RFC 4648 section 5), on bytes.
|
|
2
|
+
export function encode(bytes) {
|
|
3
|
+
const view = bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes);
|
|
4
|
+
let text = '';
|
|
5
|
+
for (let i = 0; i < view.length; i += 0x8000) {
|
|
6
|
+
text += String.fromCharCode(...view.subarray(i, i + 0x8000));
|
|
7
|
+
}
|
|
8
|
+
return btoa(text).replaceAll('+', '-').replaceAll('/', '_').replace(/=+$/, '');
|
|
9
|
+
}
|
|
10
|
+
/** @param text base64url (padding and the standard alphabet are tolerated) */
|
|
11
|
+
export function decode(text) {
|
|
12
|
+
const plain = text.replaceAll('-', '+').replaceAll('_', '/');
|
|
13
|
+
const padded = plain + '='.repeat((4 - (plain.length % 4)) % 4);
|
|
14
|
+
const raw = atob(padded);
|
|
15
|
+
const out = new Uint8Array(raw.length);
|
|
16
|
+
for (let i = 0; i < raw.length; i++)
|
|
17
|
+
out[i] = raw.charCodeAt(i);
|
|
18
|
+
return out;
|
|
19
|
+
}
|
|
20
|
+
/** base64url of the UTF-8 bytes */
|
|
21
|
+
export function encodeText(text) {
|
|
22
|
+
return encode(new TextEncoder().encode(text));
|
|
23
|
+
}
|
|
24
|
+
/** @param text base64url @returns the UTF-8 string */
|
|
25
|
+
export function decodeText(text) {
|
|
26
|
+
return new TextDecoder().decode(decode(text));
|
|
27
|
+
}
|
|
28
|
+
/** The payload claims of a JWT, unverified. */
|
|
29
|
+
export function claims(jwt) {
|
|
30
|
+
const part = String(jwt).split('.')[1];
|
|
31
|
+
if (!part)
|
|
32
|
+
return {};
|
|
33
|
+
try {
|
|
34
|
+
const value = JSON.parse(decodeText(part));
|
|
35
|
+
return value && typeof value === 'object' ? value : {};
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return {};
|
|
39
|
+
}
|
|
40
|
+
}
|
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { Dpop } from './dpop.js';
|
|
2
|
+
/** Where the client gets its access tokens: `Tokens` (id/tokens.ts) or `Session.tokens`. */
|
|
3
|
+
export interface TokenSource {
|
|
4
|
+
/** a valid access token for the audience */
|
|
5
|
+
token(audience: string): Promise<string>;
|
|
6
|
+
/** renews the audience token unless another caller already replaced `stale` */
|
|
7
|
+
rotate(audience: string, stale: string | null): Promise<{
|
|
8
|
+
token: string;
|
|
9
|
+
}>;
|
|
10
|
+
}
|
|
11
|
+
export interface ClientOptions {
|
|
12
|
+
tokens: TokenSource;
|
|
13
|
+
/** the transport (default global fetch) */
|
|
14
|
+
fetch?: typeof fetch | undefined;
|
|
15
|
+
/** when given, bound tokens are sent with the DPoP scheme */
|
|
16
|
+
dpop?: Dpop | undefined;
|
|
17
|
+
}
|
|
18
|
+
export interface Client {
|
|
19
|
+
/**
|
|
20
|
+
* Sends the request with the audience's token. On a 401 (`auth.stale`
|
|
21
|
+
* included) the token is renewed once, unless another caller already did,
|
|
22
|
+
* and the request is sent once more; a second 401 is returned as is. The body
|
|
23
|
+
* must be re-sendable (a string, bytes or form data, not a stream).
|
|
24
|
+
*/
|
|
25
|
+
fetch: (audience: string, input: Request | string | URL, init?: RequestInit) => Promise<Response>;
|
|
26
|
+
/** A fetch-shaped function for one audience (used for the rights stream). */
|
|
27
|
+
bind: (audience: string) => typeof fetch;
|
|
28
|
+
}
|
|
29
|
+
export declare function createClient({ tokens, fetch: transport, dpop }: ClientOptions): Client;
|
package/dist/client.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export function createClient({ tokens, fetch: transport, dpop }) {
|
|
2
|
+
const inner = transport ?? ((...args) => globalThis.fetch(...args));
|
|
3
|
+
const out = dpop ? dpop.wrap(inner) : inner;
|
|
4
|
+
const call = async (audience, input, init) => {
|
|
5
|
+
const proto = new Request(input, init);
|
|
6
|
+
const token = await tokens.token(audience);
|
|
7
|
+
const first = await out(withBearer(proto.clone(), token));
|
|
8
|
+
if (first.status !== 401)
|
|
9
|
+
return first;
|
|
10
|
+
await first.arrayBuffer().catch(() => { });
|
|
11
|
+
const fresh = (await tokens.rotate(audience, token)).token;
|
|
12
|
+
return out(withBearer(proto.clone(), fresh));
|
|
13
|
+
};
|
|
14
|
+
return {
|
|
15
|
+
fetch: call,
|
|
16
|
+
bind: (audience) => (input, init) => call(audience, input, init),
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
function withBearer(request, token) {
|
|
20
|
+
request.headers.set('Authorization', `Bearer ${token}`);
|
|
21
|
+
return request;
|
|
22
|
+
}
|
package/dist/dpop.d.ts
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where `Dpop.load` keeps its key: any async key/value store that holds a
|
|
3
|
+
* CryptoKeyPair as is (the core `Storage`, IndexedDB, an in-process map).
|
|
4
|
+
* Declared here so the DPoP code does not depend on the browser storage
|
|
5
|
+
* module.
|
|
6
|
+
*/
|
|
7
|
+
export interface KeyStore {
|
|
8
|
+
get: (key: string) => Promise<any>;
|
|
9
|
+
set: (key: string, value: any) => Promise<void>;
|
|
10
|
+
}
|
|
11
|
+
/** A public key as RFC 7638 hashes it. */
|
|
12
|
+
export interface PublicJwk {
|
|
13
|
+
kty: string;
|
|
14
|
+
crv?: string | undefined;
|
|
15
|
+
x?: string | undefined;
|
|
16
|
+
y?: string | undefined;
|
|
17
|
+
}
|
|
18
|
+
/** The part of a Web Lock manager (`navigator.locks`) that `Dpop.load` uses. */
|
|
19
|
+
export interface Locks {
|
|
20
|
+
request<T>(name: string, run: () => Promise<T>): Promise<T>;
|
|
21
|
+
}
|
|
22
|
+
export declare class Dpop {
|
|
23
|
+
#private;
|
|
24
|
+
/** Use `Dpop.load` or `Dpop.generate`. */
|
|
25
|
+
constructor(pair: CryptoKeyPair, jwk: PublicJwk, thumbprint: string, clock?: () => number);
|
|
26
|
+
/** Generates a new non-extractable key. */
|
|
27
|
+
static generate(options?: {
|
|
28
|
+
clock?: () => number;
|
|
29
|
+
}): Promise<Dpop>;
|
|
30
|
+
/** Wraps an existing key pair (its private key must be a P-256 ECDSA signing key). */
|
|
31
|
+
static from(pair: CryptoKeyPair, options?: {
|
|
32
|
+
clock?: () => number;
|
|
33
|
+
}): Promise<Dpop>;
|
|
34
|
+
/**
|
|
35
|
+
* Loads the key from `storage` (key `dpop.key`), or generates and saves one.
|
|
36
|
+
* Two tabs loading at the same time must end with the same key, so the
|
|
37
|
+
* read-or-create runs under a Web Lock when the browser has one (a key made by
|
|
38
|
+
* a tab that lost the race would otherwise be overwritten and its tokens
|
|
39
|
+
* bound to a key nobody holds any more).
|
|
40
|
+
*/
|
|
41
|
+
static load(storage: KeyStore, options?: {
|
|
42
|
+
clock?: () => number;
|
|
43
|
+
locks?: Locks | null;
|
|
44
|
+
}): Promise<Dpop>;
|
|
45
|
+
/** The key's thumbprint (RFC 7638), as stored by Aduki ID in `cnf.jkt`. */
|
|
46
|
+
get thumbprint(): string;
|
|
47
|
+
/** The public JWK (kty, crv, x, y). */
|
|
48
|
+
get jwk(): PublicJwk;
|
|
49
|
+
/**
|
|
50
|
+
* One proof for `method` on `url` (query and fragment are not signed). Pass
|
|
51
|
+
* `access` to bind the proof to a token with `ath`, and `nonce` when the
|
|
52
|
+
* server asked for one.
|
|
53
|
+
*/
|
|
54
|
+
proof({ method, url, access, nonce }: {
|
|
55
|
+
method: string;
|
|
56
|
+
url: string | URL;
|
|
57
|
+
access?: string | null;
|
|
58
|
+
nonce?: string | null;
|
|
59
|
+
}): Promise<string>;
|
|
60
|
+
/**
|
|
61
|
+
* One proof with `htm` and `htu` exactly as given (no upper-casing, no URL
|
|
62
|
+
* trimming), for proofs outside HTTP: the Chats handshake signs
|
|
63
|
+
* `htm = "ae09"`, `htu = "identity.response"` with the challenge as `nonce`.
|
|
64
|
+
*/
|
|
65
|
+
sign({ htm, htu, access, nonce }: {
|
|
66
|
+
htm: string;
|
|
67
|
+
htu: string;
|
|
68
|
+
access?: string | null;
|
|
69
|
+
nonce?: string | null;
|
|
70
|
+
}): Promise<string>;
|
|
71
|
+
/**
|
|
72
|
+
* Returns `request` with its DPoP header and scheme applied by the rules above.
|
|
73
|
+
* @param nonce defaults to the one remembered for the origin
|
|
74
|
+
*/
|
|
75
|
+
apply(request: Request, nonce?: string | null): Promise<Request>;
|
|
76
|
+
/** True when the access token (a JWT) is bound to this key. */
|
|
77
|
+
bound(token: string): boolean;
|
|
78
|
+
/** A fetch that applies DPoP and retries once on a fresh `DPoP-Nonce`. */
|
|
79
|
+
wrap(inner?: typeof fetch): typeof fetch;
|
|
80
|
+
}
|
|
81
|
+
/** The RFC 7638 JWK thumbprint of an EC or OKP public key. */
|
|
82
|
+
export declare function thumbprint(jwk: PublicJwk): Promise<string>;
|