@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.
- package/LICENSE +201 -0
- package/README.md +184 -2
- package/dist/api/api.d.ts +226 -0
- package/dist/api/api.js +365 -0
- package/dist/api/id.d.ts +75 -0
- package/dist/api/id.js +184 -0
- package/dist/api/index.d.ts +23 -0
- package/dist/api/index.js +19 -0
- package/dist/api/key.d.ts +16 -0
- package/dist/api/key.js +16 -0
- package/dist/api/model.d.ts +476 -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/envelope.d.ts +59 -0
- package/dist/envelope.js +114 -0
- package/dist/errors.d.ts +165 -0
- package/dist/errors.js +206 -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/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 +45 -4
package/dist/dpop.js
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
// DPoP (RFC 9449; ADK-AUTH-001 section 10, aduki-kt Dpop.kt) with ES256 and a
|
|
2
|
+
// non-extractable WebCrypto P-256 key. The private key never leaves the
|
|
3
|
+
// key store: it can only sign. It is persisted as a CryptoKey in IndexedDB
|
|
4
|
+
// (or any storage that structured-clones keys), or kept in memory only.
|
|
5
|
+
//
|
|
6
|
+
// - A request with no Authorization (sign-in, refresh) gets a proof without `ath`.
|
|
7
|
+
// - A request whose token is bound to this key (`cnf.jkt` equals the thumbprint)
|
|
8
|
+
// is sent as `Authorization: DPoP <token>` with a proof carrying `ath`.
|
|
9
|
+
// - Any other credential is left untouched.
|
|
10
|
+
// - A DPoP-Nonce from the server is remembered per origin; a 401 or 400
|
|
11
|
+
// carrying a nonce is retried once with it.
|
|
12
|
+
import { claims, encode, encodeText } from './base64.js';
|
|
13
|
+
import { ProtocolError } from './errors.js';
|
|
14
|
+
const ALG = { name: 'ECDSA', namedCurve: 'P-256' };
|
|
15
|
+
const KEY = 'dpop.key';
|
|
16
|
+
export class Dpop {
|
|
17
|
+
#pair;
|
|
18
|
+
#jwk;
|
|
19
|
+
#thumbprint;
|
|
20
|
+
#clock;
|
|
21
|
+
#nonces = new Map();
|
|
22
|
+
/** Use `Dpop.load` or `Dpop.generate`. */
|
|
23
|
+
constructor(pair, jwk, thumbprint, clock = Date.now) {
|
|
24
|
+
this.#pair = pair;
|
|
25
|
+
this.#jwk = jwk;
|
|
26
|
+
this.#thumbprint = thumbprint;
|
|
27
|
+
this.#clock = clock;
|
|
28
|
+
}
|
|
29
|
+
/** Generates a new non-extractable key. */
|
|
30
|
+
static async generate(options = {}) {
|
|
31
|
+
const pair = await globalThis.crypto.subtle.generateKey(ALG, false, ['sign', 'verify']);
|
|
32
|
+
return Dpop.from(pair, options);
|
|
33
|
+
}
|
|
34
|
+
/** Wraps an existing key pair (its private key must be a P-256 ECDSA signing key). */
|
|
35
|
+
static async from(pair, options = {}) {
|
|
36
|
+
const exported = await globalThis.crypto.subtle.exportKey('jwk', pair.publicKey);
|
|
37
|
+
const jwk = { kty: 'EC', crv: 'P-256', x: exported.x, y: exported.y };
|
|
38
|
+
return new Dpop(pair, jwk, await thumbprint(jwk), options.clock);
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Loads the key from `storage` (key `dpop.key`), or generates and saves one.
|
|
42
|
+
* Two tabs loading at the same time must end with the same key, so the
|
|
43
|
+
* read-or-create runs under a Web Lock when the browser has one (a key made by
|
|
44
|
+
* a tab that lost the race would otherwise be overwritten and its tokens
|
|
45
|
+
* bound to a key nobody holds any more).
|
|
46
|
+
*/
|
|
47
|
+
static async load(storage, options = {}) {
|
|
48
|
+
const locks = options.locks === undefined ? globalThis.navigator?.locks : options.locks;
|
|
49
|
+
const once = async () => {
|
|
50
|
+
const saved = await storage.get(KEY);
|
|
51
|
+
if (saved?.privateKey && saved?.publicKey)
|
|
52
|
+
return Dpop.from(saved, options);
|
|
53
|
+
const made = await Dpop.generate(options);
|
|
54
|
+
await storage.set(KEY, made.#pair);
|
|
55
|
+
return made;
|
|
56
|
+
};
|
|
57
|
+
return locks ? locks.request('aduki-dpop-key', once) : once();
|
|
58
|
+
}
|
|
59
|
+
/** The key's thumbprint (RFC 7638), as stored by Aduki ID in `cnf.jkt`. */
|
|
60
|
+
get thumbprint() {
|
|
61
|
+
return this.#thumbprint;
|
|
62
|
+
}
|
|
63
|
+
/** The public JWK (kty, crv, x, y). */
|
|
64
|
+
get jwk() {
|
|
65
|
+
return { ...this.#jwk };
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* One proof for `method` on `url` (query and fragment are not signed). Pass
|
|
69
|
+
* `access` to bind the proof to a token with `ath`, and `nonce` when the
|
|
70
|
+
* server asked for one.
|
|
71
|
+
*/
|
|
72
|
+
proof({ method, url, access = null, nonce = null }) {
|
|
73
|
+
return this.sign({ htm: method.toUpperCase(), htu: String(url).split('#')[0].split('?')[0], access, nonce });
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* One proof with `htm` and `htu` exactly as given (no upper-casing, no URL
|
|
77
|
+
* trimming), for proofs outside HTTP: the Chats handshake signs
|
|
78
|
+
* `htm = "ae09"`, `htu = "identity.response"` with the challenge as `nonce`.
|
|
79
|
+
*/
|
|
80
|
+
async sign({ htm, htu, access = null, nonce = null }) {
|
|
81
|
+
const header = { typ: 'dpop+jwt', alg: 'ES256', jwk: this.#jwk };
|
|
82
|
+
const body = {
|
|
83
|
+
htm,
|
|
84
|
+
htu,
|
|
85
|
+
iat: Math.floor(this.#clock() / 1000),
|
|
86
|
+
jti: encode(globalThis.crypto.getRandomValues(new Uint8Array(16))),
|
|
87
|
+
};
|
|
88
|
+
if (access)
|
|
89
|
+
body.ath = await athash(access);
|
|
90
|
+
if (nonce)
|
|
91
|
+
body.nonce = nonce;
|
|
92
|
+
const input = `${encodeText(JSON.stringify(header))}.${encodeText(JSON.stringify(body))}`;
|
|
93
|
+
// WebCrypto ECDSA returns the raw r||s form that JWS ES256 uses.
|
|
94
|
+
const signature = await globalThis.crypto.subtle.sign({ name: 'ECDSA', hash: 'SHA-256' }, this.#pair.privateKey, new TextEncoder().encode(input));
|
|
95
|
+
return `${input}.${encode(signature)}`;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Returns `request` with its DPoP header and scheme applied by the rules above.
|
|
99
|
+
* @param nonce defaults to the one remembered for the origin
|
|
100
|
+
*/
|
|
101
|
+
async apply(request, nonce = this.#nonces.get(new URL(request.url).origin) ?? null) {
|
|
102
|
+
const sent = request.headers.get('Authorization') ?? '';
|
|
103
|
+
const [scheme, token] = sent.split(' ');
|
|
104
|
+
const headers = new Headers(request.headers);
|
|
105
|
+
if (sent.trim() === '') {
|
|
106
|
+
headers.set('DPoP', await this.proof({ method: request.method, url: request.url, nonce }));
|
|
107
|
+
}
|
|
108
|
+
else if ((scheme === 'Bearer' || scheme === 'DPoP') && token && this.bound(token)) {
|
|
109
|
+
headers.set('Authorization', `DPoP ${token}`);
|
|
110
|
+
headers.set('DPoP', await this.proof({ method: request.method, url: request.url, access: token, nonce }));
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
return request;
|
|
114
|
+
}
|
|
115
|
+
return new Request(request, { headers });
|
|
116
|
+
}
|
|
117
|
+
/** True when the access token (a JWT) is bound to this key. */
|
|
118
|
+
bound(token) {
|
|
119
|
+
return claims(token).cnf?.jkt === this.#thumbprint;
|
|
120
|
+
}
|
|
121
|
+
/** A fetch that applies DPoP and retries once on a fresh `DPoP-Nonce`. */
|
|
122
|
+
wrap(inner = (...args) => globalThis.fetch(...args)) {
|
|
123
|
+
return async (input, init) => {
|
|
124
|
+
const request = new Request(input, init);
|
|
125
|
+
const spare = request.clone();
|
|
126
|
+
const response = await inner(await this.apply(request));
|
|
127
|
+
const origin = new URL(request.url).origin;
|
|
128
|
+
const nonce = response.headers.get('DPoP-Nonce');
|
|
129
|
+
if (nonce)
|
|
130
|
+
this.#nonces.set(origin, nonce);
|
|
131
|
+
if (!nonce || (response.status !== 401 && response.status !== 400))
|
|
132
|
+
return response;
|
|
133
|
+
await response.arrayBuffer().catch(() => { });
|
|
134
|
+
const retry = await inner(await this.apply(spare, nonce));
|
|
135
|
+
const next = retry.headers.get('DPoP-Nonce');
|
|
136
|
+
if (next)
|
|
137
|
+
this.#nonces.set(origin, next);
|
|
138
|
+
return retry;
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
/** base64url SHA-256 of the access token (`ath`) */
|
|
143
|
+
async function athash(token) {
|
|
144
|
+
return encode(await globalThis.crypto.subtle.digest('SHA-256', new TextEncoder().encode(token)));
|
|
145
|
+
}
|
|
146
|
+
/** The RFC 7638 JWK thumbprint of an EC or OKP public key. */
|
|
147
|
+
export async function thumbprint(jwk) {
|
|
148
|
+
let canonical;
|
|
149
|
+
if (jwk.kty === 'EC')
|
|
150
|
+
canonical = { crv: jwk.crv, kty: 'EC', x: jwk.x, y: jwk.y };
|
|
151
|
+
else if (jwk.kty === 'OKP')
|
|
152
|
+
canonical = { crv: jwk.crv, kty: 'OKP', x: jwk.x };
|
|
153
|
+
else
|
|
154
|
+
throw new ProtocolError(`Unsupported key type ${jwk.kty}`);
|
|
155
|
+
return encode(await globalThis.crypto.subtle.digest('SHA-256', new TextEncoder().encode(JSON.stringify(canonical))));
|
|
156
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { AdukiError } from './errors.js';
|
|
2
|
+
/** Stable error kinds clients switch on (ADK-NET-001 section 4). */
|
|
3
|
+
export declare const kinds: Readonly<{
|
|
4
|
+
missing: "auth.missing";
|
|
5
|
+
invalid: "auth.invalid";
|
|
6
|
+
stale: "auth.stale";
|
|
7
|
+
stepup: "auth.stepup";
|
|
8
|
+
denied: "access.denied";
|
|
9
|
+
input: "input.invalid";
|
|
10
|
+
absent: "entity.missing";
|
|
11
|
+
conflict: "entity.conflict";
|
|
12
|
+
limited: "rate.limited";
|
|
13
|
+
reset: "sync.reset";
|
|
14
|
+
quota: "quota.exceeded";
|
|
15
|
+
plan: "plan.limit";
|
|
16
|
+
unavailable: "auth.unavailable";
|
|
17
|
+
server: "server.error";
|
|
18
|
+
}>;
|
|
19
|
+
/** A parsed envelope: an object with a `success` field (the rest is the server's). */
|
|
20
|
+
export type Envelope = Record<string, any>;
|
|
21
|
+
/** `{status, kind, message, details}` of a failure envelope. */
|
|
22
|
+
export interface Failure {
|
|
23
|
+
status: number;
|
|
24
|
+
kind: string;
|
|
25
|
+
message: string;
|
|
26
|
+
details: unknown;
|
|
27
|
+
}
|
|
28
|
+
/** The parsed envelope, or null when the body is not one. */
|
|
29
|
+
export declare function parse(body: string): Envelope | null;
|
|
30
|
+
/**
|
|
31
|
+
* The `data` of a success envelope, or the parsed body itself when it is not an
|
|
32
|
+
* envelope (servers from before it pass through unchanged).
|
|
33
|
+
*/
|
|
34
|
+
export declare function data(body: string): any;
|
|
35
|
+
/** `{status, kind, message, details}` from a failure envelope, or null. */
|
|
36
|
+
export declare function failure(body: string): Failure | null;
|
|
37
|
+
/**
|
|
38
|
+
* The typed error of a failed answer whose body was `body` (see
|
|
39
|
+
* `errors.refusal`): the envelope's kind, message and details, the
|
|
40
|
+
* `Retry-After` header, and with `problem` the `type` and `detail` of an
|
|
41
|
+
* RFC 7807 problem document (JMAP request-level errors) when the body is no
|
|
42
|
+
* envelope. A body that is neither (the gateway's plain 429 or 503) leaves
|
|
43
|
+
* the status alone to speak.
|
|
44
|
+
* @param what names the call in error messages
|
|
45
|
+
*/
|
|
46
|
+
export declare function rejection(response: Response, body: string, what: string, options?: {
|
|
47
|
+
problem?: boolean;
|
|
48
|
+
}): AdukiError;
|
|
49
|
+
/**
|
|
50
|
+
* Reads a fetch Response as an envelope. A 2xx answer returns its `data`.
|
|
51
|
+
* A failure throws the typed error of `rejection`: UnauthorizedError for a
|
|
52
|
+
* 401 (`kind: message`), otherwise a NetworkError (or one of its subclasses:
|
|
53
|
+
* RateLimitedError, StepUpError, LimitError, SyncResetError, ServerError)
|
|
54
|
+
* with the HTTP status, the kind, the details and `retryAfter`.
|
|
55
|
+
* @param what names the call in error messages
|
|
56
|
+
*/
|
|
57
|
+
export declare function read(response: Response, what: string): Promise<any>;
|
|
58
|
+
/** True for the `auth.stale` kind (rights changed, renew the token). */
|
|
59
|
+
export declare function isStale(error: unknown): boolean;
|
package/dist/envelope.js
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
// The Aduki REST envelope (ADK-NET-001 section 3):
|
|
2
|
+
// {success: true, data} or {success: false, error: {status, kind, message, details}}.
|
|
3
|
+
import { NetworkError, UnauthorizedError, refusal } from './errors.js';
|
|
4
|
+
/** Stable error kinds clients switch on (ADK-NET-001 section 4). */
|
|
5
|
+
export const kinds = Object.freeze({
|
|
6
|
+
missing: 'auth.missing',
|
|
7
|
+
invalid: 'auth.invalid',
|
|
8
|
+
stale: 'auth.stale',
|
|
9
|
+
stepup: 'auth.stepup',
|
|
10
|
+
denied: 'access.denied',
|
|
11
|
+
input: 'input.invalid',
|
|
12
|
+
absent: 'entity.missing',
|
|
13
|
+
conflict: 'entity.conflict',
|
|
14
|
+
limited: 'rate.limited',
|
|
15
|
+
reset: 'sync.reset',
|
|
16
|
+
quota: 'quota.exceeded',
|
|
17
|
+
plan: 'plan.limit',
|
|
18
|
+
unavailable: 'auth.unavailable',
|
|
19
|
+
server: 'server.error',
|
|
20
|
+
});
|
|
21
|
+
/** The parsed envelope, or null when the body is not one. */
|
|
22
|
+
export function parse(body) {
|
|
23
|
+
const text = String(body ?? '').trimStart();
|
|
24
|
+
if (!text.startsWith('{'))
|
|
25
|
+
return null;
|
|
26
|
+
try {
|
|
27
|
+
const value = JSON.parse(text);
|
|
28
|
+
return value && typeof value === 'object' && 'success' in value ? value : null;
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* The `data` of a success envelope, or the parsed body itself when it is not an
|
|
36
|
+
* envelope (servers from before it pass through unchanged).
|
|
37
|
+
*/
|
|
38
|
+
export function data(body) {
|
|
39
|
+
const envelope = parse(body);
|
|
40
|
+
if (!envelope || envelope.success !== true) {
|
|
41
|
+
try {
|
|
42
|
+
return JSON.parse(body);
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
return body;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return envelope.data ?? null;
|
|
49
|
+
}
|
|
50
|
+
/** `{status, kind, message, details}` from a failure envelope, or null. */
|
|
51
|
+
export function failure(body) {
|
|
52
|
+
const error = parse(body)?.error;
|
|
53
|
+
if (!error || typeof error !== 'object')
|
|
54
|
+
return null;
|
|
55
|
+
return {
|
|
56
|
+
status: Number(error.status) || 0,
|
|
57
|
+
kind: String(error.kind ?? ''),
|
|
58
|
+
message: String(error.message ?? ''),
|
|
59
|
+
details: error.details,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* The typed error of a failed answer whose body was `body` (see
|
|
64
|
+
* `errors.refusal`): the envelope's kind, message and details, the
|
|
65
|
+
* `Retry-After` header, and with `problem` the `type` and `detail` of an
|
|
66
|
+
* RFC 7807 problem document (JMAP request-level errors) when the body is no
|
|
67
|
+
* envelope. A body that is neither (the gateway's plain 429 or 503) leaves
|
|
68
|
+
* the status alone to speak.
|
|
69
|
+
* @param what names the call in error messages
|
|
70
|
+
*/
|
|
71
|
+
export function rejection(response, body, what, options = {}) {
|
|
72
|
+
const error = failure(body);
|
|
73
|
+
let kind = error?.kind ?? '';
|
|
74
|
+
let message = error?.message ?? '';
|
|
75
|
+
let details = error?.details;
|
|
76
|
+
if (!error && options.problem) {
|
|
77
|
+
try {
|
|
78
|
+
const problem = JSON.parse(body);
|
|
79
|
+
if (problem && typeof problem === 'object') {
|
|
80
|
+
kind = String(problem.type ?? '');
|
|
81
|
+
message = String(problem.detail ?? problem.title ?? '');
|
|
82
|
+
details = problem;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
// not JSON: the status says it
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return refusal({ status: response.status, kind, message, details, retry: response.headers.get('Retry-After'), what });
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Reads a fetch Response as an envelope. A 2xx answer returns its `data`.
|
|
93
|
+
* A failure throws the typed error of `rejection`: UnauthorizedError for a
|
|
94
|
+
* 401 (`kind: message`), otherwise a NetworkError (or one of its subclasses:
|
|
95
|
+
* RateLimitedError, StepUpError, LimitError, SyncResetError, ServerError)
|
|
96
|
+
* with the HTTP status, the kind, the details and `retryAfter`.
|
|
97
|
+
* @param what names the call in error messages
|
|
98
|
+
*/
|
|
99
|
+
export async function read(response, what) {
|
|
100
|
+
const body = await response.text();
|
|
101
|
+
if (!response.ok)
|
|
102
|
+
throw rejection(response, body, what);
|
|
103
|
+
const envelope = parse(body);
|
|
104
|
+
if (!envelope)
|
|
105
|
+
throw new NetworkError(`Empty response from ${what}`, { status: response.status });
|
|
106
|
+
if (envelope.data === undefined || envelope.data === null) {
|
|
107
|
+
throw new NetworkError(`Empty response from ${what}`, { status: response.status });
|
|
108
|
+
}
|
|
109
|
+
return envelope.data;
|
|
110
|
+
}
|
|
111
|
+
/** True for the `auth.stale` kind (rights changed, renew the token). */
|
|
112
|
+
export function isStale(error) {
|
|
113
|
+
return error instanceof UnauthorizedError && error.kind === kinds.stale;
|
|
114
|
+
}
|
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/** Base class of every error the SDK throws. */
|
|
2
|
+
export declare class AdukiError extends Error {
|
|
3
|
+
constructor(message: string, options?: {
|
|
4
|
+
cause?: unknown;
|
|
5
|
+
});
|
|
6
|
+
}
|
|
7
|
+
/** The options of NetworkError and its subclasses. */
|
|
8
|
+
export interface NetworkErrorOptions {
|
|
9
|
+
status?: number | undefined;
|
|
10
|
+
kind?: string | undefined;
|
|
11
|
+
details?: unknown;
|
|
12
|
+
/** seconds to wait before trying again (`Retry-After`, or the envelope's `details`) */
|
|
13
|
+
retryAfter?: number | undefined;
|
|
14
|
+
cause?: unknown;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Transport failure or a non-2xx answer that is not a 401. `status` is the
|
|
18
|
+
* HTTP status when there was one (undefined: the server was not reached).
|
|
19
|
+
* `retryAfter` holds the seconds the server asked to wait, when it said.
|
|
20
|
+
* The subclasses below name the answers a client acts on; see `refusal`.
|
|
21
|
+
*/
|
|
22
|
+
export declare class NetworkError extends AdukiError {
|
|
23
|
+
status: number | undefined;
|
|
24
|
+
kind: string;
|
|
25
|
+
details: unknown;
|
|
26
|
+
retryAfter: number | undefined;
|
|
27
|
+
constructor(message: string, options?: NetworkErrorOptions);
|
|
28
|
+
}
|
|
29
|
+
/** `429`: too many requests (`rate.limited`, `mail.queue_busy`, or the gateway's plain 429). Wait `retryAfter` seconds when given. */
|
|
30
|
+
export declare class RateLimitedError extends NetworkError {
|
|
31
|
+
}
|
|
32
|
+
/** `403 auth.stepup`: the action needs a fresh second factor; the session stays valid. */
|
|
33
|
+
export declare class StepUpError extends NetworkError {
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* A package limit or quota was reached: `403 plan.limit` or `507 quota.exceeded`.
|
|
37
|
+
* `key`, `limit` and `used` come from the details when the server sent them.
|
|
38
|
+
*/
|
|
39
|
+
export declare class LimitError extends NetworkError {
|
|
40
|
+
key: string;
|
|
41
|
+
limit: number | undefined;
|
|
42
|
+
used: number | undefined;
|
|
43
|
+
constructor(message: string, options?: NetworkErrorOptions);
|
|
44
|
+
}
|
|
45
|
+
/** `410 sync.reset`: the sync token is no longer known; sync again without one. */
|
|
46
|
+
export declare class SyncResetError extends NetworkError {
|
|
47
|
+
}
|
|
48
|
+
/** A `5xx` answer (`server.error`, `auth.unavailable`, a gateway 502, 503 or 504); `retryAfter` when the server said. */
|
|
49
|
+
export declare class ServerError extends NetworkError {
|
|
50
|
+
}
|
|
51
|
+
/** The kinds after which the session is over and its tokens are useless (aduki-kt `Unauthorized.ENDED`). */
|
|
52
|
+
export declare const ENDED: readonly string[];
|
|
53
|
+
/** The server refused the credentials (401): wrong password, spent refresh token, stale token. */
|
|
54
|
+
export declare class UnauthorizedError extends AdukiError {
|
|
55
|
+
status: number;
|
|
56
|
+
kind: string;
|
|
57
|
+
details: unknown;
|
|
58
|
+
constructor(message: string, options?: {
|
|
59
|
+
kind?: string;
|
|
60
|
+
details?: unknown;
|
|
61
|
+
cause?: unknown;
|
|
62
|
+
});
|
|
63
|
+
/**
|
|
64
|
+
* True when the kind says the session is over (`auth.invalid`,
|
|
65
|
+
* `auth.reused`, `auth.revoked`, `auth.expired`); false for other kinds
|
|
66
|
+
* (`auth.factor`, `auth.stale`, `auth.dpop`) and for a 401 without one.
|
|
67
|
+
*/
|
|
68
|
+
get ended(): boolean;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* `401 auth.dpop` or `401 auth.nonce`: the DPoP proof was refused (a key or
|
|
72
|
+
* clock problem), not the credentials (aduki-kt `AdukiException.Proof`). A
|
|
73
|
+
* subclass of UnauthorizedError, so existing `instanceof` checks still match.
|
|
74
|
+
*/
|
|
75
|
+
export declare class ProofError extends UnauthorizedError {
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* `401` with `details.reason = "token.required"`: the server needs the Aduki
|
|
79
|
+
* ID token and its proof with the request (the Chats handshake on an Aduki
|
|
80
|
+
* deployment). A client that cannot send them is too old: tell the user to
|
|
81
|
+
* update instead of retrying.
|
|
82
|
+
*/
|
|
83
|
+
export declare class TokenRequiredError extends UnauthorizedError {
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* `401` with `details.reason = "session.expired"` or `"session.ended"`: the
|
|
87
|
+
* Chats session is over (its time ran out, or Aduki ID ended the sign-in
|
|
88
|
+
* session that authorized it). Repeat the handshake; after `session.ended`
|
|
89
|
+
* that needs a current token (the person may have to sign in again).
|
|
90
|
+
*/
|
|
91
|
+
export declare class SessionEndedError extends UnauthorizedError {
|
|
92
|
+
/** `session.expired` or `session.ended` */
|
|
93
|
+
get reason(): string;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* `403` with `details.reason = "subject"` (the token is for another account
|
|
97
|
+
* than the device's) or `"jkt.mismatch"` (the device is bound to another
|
|
98
|
+
* Aduki ID key than the token's). Retrying with the same key does not help.
|
|
99
|
+
*/
|
|
100
|
+
export declare class IdentityMismatchError extends NetworkError {
|
|
101
|
+
/** `subject` or `jkt.mismatch` */
|
|
102
|
+
get reason(): string;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* `403 access.denied` with `details.reason = "client"`: the route is not for
|
|
106
|
+
* a program. A workspace's service client (machine access) is refused where
|
|
107
|
+
* only a person may act: in Store shares, links, devices, reports, search,
|
|
108
|
+
* tickets and the change feed; in Space the strict and money routes. Another
|
|
109
|
+
* token does not help: the route needs a person signed in.
|
|
110
|
+
*/
|
|
111
|
+
export declare class ClientRefusedError extends NetworkError {
|
|
112
|
+
/** always `client` */
|
|
113
|
+
get reason(): string;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* A 2xx authentication answer that could not be used. `refresh` carries a
|
|
117
|
+
* rotated refresh token the answer did include, so callers never lose a token
|
|
118
|
+
* the server already swapped in.
|
|
119
|
+
*/
|
|
120
|
+
export declare class AuthError extends AdukiError {
|
|
121
|
+
refresh: string;
|
|
122
|
+
constructor(message: string, options?: {
|
|
123
|
+
refresh?: string;
|
|
124
|
+
cause?: unknown;
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
/** A protocol violation: bad state, issuer mismatch, bad id token, missing field. */
|
|
128
|
+
export declare class ProtocolError extends AdukiError {
|
|
129
|
+
}
|
|
130
|
+
/** A storage backend failed. */
|
|
131
|
+
export declare class StorageError extends AdukiError {
|
|
132
|
+
}
|
|
133
|
+
/** What `refusal` needs to know of a failed answer. */
|
|
134
|
+
export interface Refused {
|
|
135
|
+
/** the HTTP status */
|
|
136
|
+
status: number;
|
|
137
|
+
/** the envelope's kind, or the problem document's type ('' when none) */
|
|
138
|
+
kind?: string | undefined;
|
|
139
|
+
/** the server's message ('' or absent: `HTTP <status>`) */
|
|
140
|
+
message?: string | undefined;
|
|
141
|
+
details?: unknown;
|
|
142
|
+
/** the `Retry-After` header, as sent */
|
|
143
|
+
retry?: string | null | undefined;
|
|
144
|
+
/** names the call in the message (`<what> failed: ...`) */
|
|
145
|
+
what?: string | undefined;
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* The one mapping from a refused answer to a typed error, used by every
|
|
149
|
+
* service module. A 401 is UnauthorizedError (`auth.dpop`, `auth.nonce`:
|
|
150
|
+
* ProofError; `details.reason` `token.required`: TokenRequiredError;
|
|
151
|
+
* `session.expired`, `session.ended`: SessionEndedError); a 429
|
|
152
|
+
* RateLimitedError; a 403 with `details.reason` `subject` or `jkt.mismatch`
|
|
153
|
+
* IdentityMismatchError; a 403 with `details.reason` `client`
|
|
154
|
+
* ClientRefusedError; `403 auth.stepup` StepUpError;
|
|
155
|
+
* `plan.limit` and `quota.exceeded` LimitError; `sync.reset` SyncResetError;
|
|
156
|
+
* a 5xx ServerError; anything else NetworkError. Every one but the 401s is a
|
|
157
|
+
* NetworkError with `status`, `kind`, `details` and `retryAfter` (from the
|
|
158
|
+
* `Retry-After` header, else `details.retry_after` or `details.retry`).
|
|
159
|
+
*/
|
|
160
|
+
export declare function refusal({ status, kind, message, details, retry, what }: Refused): AdukiError;
|
|
161
|
+
/**
|
|
162
|
+
* The seconds of a `Retry-After` value (delta seconds or an HTTP date), or
|
|
163
|
+
* undefined when absent or unreadable. Never negative.
|
|
164
|
+
*/
|
|
165
|
+
export declare function retryAfter(value: string | null | undefined, now?: number): number | undefined;
|