@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.
@@ -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;
@@ -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
+ }
@@ -0,0 +1,153 @@
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
+ * A 2xx authentication answer that could not be used. `refresh` carries a
106
+ * rotated refresh token the answer did include, so callers never lose a token
107
+ * the server already swapped in.
108
+ */
109
+ export declare class AuthError extends AdukiError {
110
+ refresh: string;
111
+ constructor(message: string, options?: {
112
+ refresh?: string;
113
+ cause?: unknown;
114
+ });
115
+ }
116
+ /** A protocol violation: bad state, issuer mismatch, bad id token, missing field. */
117
+ export declare class ProtocolError extends AdukiError {
118
+ }
119
+ /** A storage backend failed. */
120
+ export declare class StorageError extends AdukiError {
121
+ }
122
+ /** What `refusal` needs to know of a failed answer. */
123
+ export interface Refused {
124
+ /** the HTTP status */
125
+ status: number;
126
+ /** the envelope's kind, or the problem document's type ('' when none) */
127
+ kind?: string | undefined;
128
+ /** the server's message ('' or absent: `HTTP <status>`) */
129
+ message?: string | undefined;
130
+ details?: unknown;
131
+ /** the `Retry-After` header, as sent */
132
+ retry?: string | null | undefined;
133
+ /** names the call in the message (`<what> failed: ...`) */
134
+ what?: string | undefined;
135
+ }
136
+ /**
137
+ * The one mapping from a refused answer to a typed error, used by every
138
+ * service module. A 401 is UnauthorizedError (`auth.dpop`, `auth.nonce`:
139
+ * ProofError; `details.reason` `token.required`: TokenRequiredError;
140
+ * `session.expired`, `session.ended`: SessionEndedError); a 429
141
+ * RateLimitedError; a 403 with `details.reason` `subject` or `jkt.mismatch`
142
+ * IdentityMismatchError; `403 auth.stepup` StepUpError;
143
+ * `plan.limit` and `quota.exceeded` LimitError; `sync.reset` SyncResetError;
144
+ * a 5xx ServerError; anything else NetworkError. Every one but the 401s is a
145
+ * NetworkError with `status`, `kind`, `details` and `retryAfter` (from the
146
+ * `Retry-After` header, else `details.retry_after` or `details.retry`).
147
+ */
148
+ export declare function refusal({ status, kind, message, details, retry, what }: Refused): AdukiError;
149
+ /**
150
+ * The seconds of a `Retry-After` value (delta seconds or an HTTP date), or
151
+ * undefined when absent or unreadable. Never negative.
152
+ */
153
+ export declare function retryAfter(value: string | null | undefined, now?: number): number | undefined;
package/dist/errors.js ADDED
@@ -0,0 +1,190 @@
1
+ // Errors mirror the Kotlin SDK's AdukiException hierarchy (aduki-kt core/errors).
2
+ /** Base class of every error the SDK throws. */
3
+ export class AdukiError extends Error {
4
+ constructor(message, options = {}) {
5
+ super(message, options.cause === undefined ? undefined : { cause: options.cause });
6
+ this.name = new.target.name;
7
+ }
8
+ }
9
+ /**
10
+ * Transport failure or a non-2xx answer that is not a 401. `status` is the
11
+ * HTTP status when there was one (undefined: the server was not reached).
12
+ * `retryAfter` holds the seconds the server asked to wait, when it said.
13
+ * The subclasses below name the answers a client acts on; see `refusal`.
14
+ */
15
+ export class NetworkError extends AdukiError {
16
+ constructor(message, options = {}) {
17
+ super(message, options);
18
+ this.status = options.status;
19
+ this.kind = options.kind ?? '';
20
+ this.details = options.details;
21
+ this.retryAfter = options.retryAfter;
22
+ }
23
+ }
24
+ /** `429`: too many requests (`rate.limited`, `mail.queue_busy`, or the gateway's plain 429). Wait `retryAfter` seconds when given. */
25
+ export class RateLimitedError extends NetworkError {
26
+ }
27
+ /** `403 auth.stepup`: the action needs a fresh second factor; the session stays valid. */
28
+ export class StepUpError extends NetworkError {
29
+ }
30
+ /**
31
+ * A package limit or quota was reached: `403 plan.limit` or `507 quota.exceeded`.
32
+ * `key`, `limit` and `used` come from the details when the server sent them.
33
+ */
34
+ export class LimitError extends NetworkError {
35
+ constructor(message, options = {}) {
36
+ super(message, options);
37
+ const details = options.details && typeof options.details === 'object' ? options.details : {};
38
+ this.key = typeof details.key === 'string' ? details.key : '';
39
+ this.limit = typeof details.limit === 'number' ? details.limit : undefined;
40
+ this.used = typeof details.used === 'number' ? details.used : undefined;
41
+ }
42
+ }
43
+ /** `410 sync.reset`: the sync token is no longer known; sync again without one. */
44
+ export class SyncResetError extends NetworkError {
45
+ }
46
+ /** A `5xx` answer (`server.error`, `auth.unavailable`, a gateway 502, 503 or 504); `retryAfter` when the server said. */
47
+ export class ServerError extends NetworkError {
48
+ }
49
+ /** The kinds after which the session is over and its tokens are useless (aduki-kt `Unauthorized.ENDED`). */
50
+ export const ENDED = Object.freeze(['auth.invalid', 'auth.reused', 'auth.revoked', 'auth.expired']);
51
+ /** The server refused the credentials (401): wrong password, spent refresh token, stale token. */
52
+ export class UnauthorizedError extends AdukiError {
53
+ constructor(message, options = {}) {
54
+ super(message, options);
55
+ this.status = 401;
56
+ this.kind = options.kind ?? '';
57
+ this.details = options.details;
58
+ }
59
+ /**
60
+ * True when the kind says the session is over (`auth.invalid`,
61
+ * `auth.reused`, `auth.revoked`, `auth.expired`); false for other kinds
62
+ * (`auth.factor`, `auth.stale`, `auth.dpop`) and for a 401 without one.
63
+ */
64
+ get ended() {
65
+ return ENDED.includes(this.kind);
66
+ }
67
+ }
68
+ /**
69
+ * `401 auth.dpop` or `401 auth.nonce`: the DPoP proof was refused (a key or
70
+ * clock problem), not the credentials (aduki-kt `AdukiException.Proof`). A
71
+ * subclass of UnauthorizedError, so existing `instanceof` checks still match.
72
+ */
73
+ export class ProofError extends UnauthorizedError {
74
+ }
75
+ /** `details.reason` of a refusal, or ''. */
76
+ function reasonOf(details) {
77
+ const value = details && typeof details === 'object' ? details.reason : undefined;
78
+ return typeof value === 'string' ? value : '';
79
+ }
80
+ /**
81
+ * `401` with `details.reason = "token.required"`: the server needs the Aduki
82
+ * ID token and its proof with the request (the Chats handshake on an Aduki
83
+ * deployment). A client that cannot send them is too old: tell the user to
84
+ * update instead of retrying.
85
+ */
86
+ export class TokenRequiredError extends UnauthorizedError {
87
+ }
88
+ /**
89
+ * `401` with `details.reason = "session.expired"` or `"session.ended"`: the
90
+ * Chats session is over (its time ran out, or Aduki ID ended the sign-in
91
+ * session that authorized it). Repeat the handshake; after `session.ended`
92
+ * that needs a current token (the person may have to sign in again).
93
+ */
94
+ export class SessionEndedError extends UnauthorizedError {
95
+ /** `session.expired` or `session.ended` */
96
+ get reason() {
97
+ return reasonOf(this.details);
98
+ }
99
+ }
100
+ /**
101
+ * `403` with `details.reason = "subject"` (the token is for another account
102
+ * than the device's) or `"jkt.mismatch"` (the device is bound to another
103
+ * Aduki ID key than the token's). Retrying with the same key does not help.
104
+ */
105
+ export class IdentityMismatchError extends NetworkError {
106
+ /** `subject` or `jkt.mismatch` */
107
+ get reason() {
108
+ return reasonOf(this.details);
109
+ }
110
+ }
111
+ /**
112
+ * A 2xx authentication answer that could not be used. `refresh` carries a
113
+ * rotated refresh token the answer did include, so callers never lose a token
114
+ * the server already swapped in.
115
+ */
116
+ export class AuthError extends AdukiError {
117
+ constructor(message, options = {}) {
118
+ super(message, options);
119
+ this.refresh = options.refresh ?? '';
120
+ }
121
+ }
122
+ /** A protocol violation: bad state, issuer mismatch, bad id token, missing field. */
123
+ export class ProtocolError extends AdukiError {
124
+ }
125
+ /** A storage backend failed. */
126
+ export class StorageError extends AdukiError {
127
+ }
128
+ /**
129
+ * The one mapping from a refused answer to a typed error, used by every
130
+ * service module. A 401 is UnauthorizedError (`auth.dpop`, `auth.nonce`:
131
+ * ProofError; `details.reason` `token.required`: TokenRequiredError;
132
+ * `session.expired`, `session.ended`: SessionEndedError); a 429
133
+ * RateLimitedError; a 403 with `details.reason` `subject` or `jkt.mismatch`
134
+ * IdentityMismatchError; `403 auth.stepup` StepUpError;
135
+ * `plan.limit` and `quota.exceeded` LimitError; `sync.reset` SyncResetError;
136
+ * a 5xx ServerError; anything else NetworkError. Every one but the 401s is a
137
+ * NetworkError with `status`, `kind`, `details` and `retryAfter` (from the
138
+ * `Retry-After` header, else `details.retry_after` or `details.retry`).
139
+ */
140
+ export function refusal({ status, kind = '', message, details, retry, what }) {
141
+ const text = message || `HTTP ${status}`;
142
+ const reason = reasonOf(details);
143
+ if (status === 401) {
144
+ const Type = reason === 'token.required' ? TokenRequiredError
145
+ : reason === 'session.expired' || reason === 'session.ended' ? SessionEndedError
146
+ : kind === 'auth.dpop' || kind === 'auth.nonce' ? ProofError : UnauthorizedError;
147
+ return new Type(kind ? `${kind}: ${text}` : text, { kind, details });
148
+ }
149
+ const options = { status, kind, details };
150
+ const wait = retryAfter(retry) ?? waited(details);
151
+ if (wait !== undefined)
152
+ options.retryAfter = wait;
153
+ const full = `${what ?? 'Request'} failed: ${text}`;
154
+ if (status === 429)
155
+ return new RateLimitedError(full, options);
156
+ if (status === 403 && (reason === 'subject' || reason === 'jkt.mismatch'))
157
+ return new IdentityMismatchError(full, options);
158
+ if (kind === 'auth.stepup')
159
+ return new StepUpError(full, options);
160
+ if (kind === 'plan.limit' || kind === 'quota.exceeded')
161
+ return new LimitError(full, options);
162
+ if (kind === 'sync.reset')
163
+ return new SyncResetError(full, options);
164
+ if (status >= 500 && status <= 599)
165
+ return new ServerError(full, options);
166
+ return new NetworkError(full, options);
167
+ }
168
+ /**
169
+ * The seconds of a `Retry-After` value (delta seconds or an HTTP date), or
170
+ * undefined when absent or unreadable. Never negative.
171
+ */
172
+ export function retryAfter(value, now = Date.now()) {
173
+ const text = value?.trim();
174
+ if (!text)
175
+ return undefined;
176
+ if (/^\d+$/.test(text))
177
+ return Number(text);
178
+ const at = Date.parse(text);
179
+ if (Number.isNaN(at))
180
+ return undefined;
181
+ return Math.max(0, Math.ceil((at - now) / 1000));
182
+ }
183
+ /** `details.retry_after` (Aduki Mail) or `details.retry` (Aduki ID) in seconds. */
184
+ function waited(details) {
185
+ if (!details || typeof details !== 'object')
186
+ return undefined;
187
+ const o = details;
188
+ const value = o.retry_after ?? o.retry;
189
+ return typeof value === 'number' && Number.isFinite(value) && value >= 0 ? Math.ceil(value) : undefined;
190
+ }
package/dist/http.d.ts ADDED
@@ -0,0 +1,11 @@
1
+ /** `base` and `path` joined with exactly one slash. */
2
+ export declare const url: (base: string, path: string) => string;
3
+ /** Runs fetch and turns a transport failure into NetworkError (an abort is passed through). */
4
+ export declare function send(fetcher: typeof fetch, input: Request | string | URL, init?: RequestInit): Promise<Response>;
5
+ /** The headers and body of a JSON request. */
6
+ export declare const json: (body: unknown) => {
7
+ headers: {
8
+ 'Content-Type': string;
9
+ };
10
+ body: string;
11
+ };
package/dist/http.js ADDED
@@ -0,0 +1,22 @@
1
+ // Small HTTP helpers shared by every service module (Aduki ID today; mail and
2
+ // the other APIs next): joining a base and a path, a JSON request body, and a
3
+ // fetch that reports a transport failure as NetworkError.
4
+ import { NetworkError } from './errors.js';
5
+ /** `base` and `path` joined with exactly one slash. */
6
+ export const url = (base, path) => `${base.replace(/\/+$/, '')}/${path}`;
7
+ /** Runs fetch and turns a transport failure into NetworkError (an abort is passed through). */
8
+ export async function send(fetcher, input, init) {
9
+ try {
10
+ return await fetcher(input, init);
11
+ }
12
+ catch (cause) {
13
+ if (cause instanceof Error && cause.name === 'AbortError')
14
+ throw cause;
15
+ throw new NetworkError(`Request failed: ${cause instanceof Error ? cause.message : cause}`, { cause });
16
+ }
17
+ }
18
+ /** The headers and body of a JSON request. */
19
+ export const json = (body) => ({
20
+ headers: { 'Content-Type': 'application/json' },
21
+ body: JSON.stringify(body),
22
+ });
package/dist/jmap.d.ts ADDED
@@ -0,0 +1,100 @@
1
+ import { AdukiError } from './errors.js';
2
+ export declare const CORE = "urn:ietf:params:jmap:core";
3
+ export declare const MAIL = "urn:ietf:params:jmap:mail";
4
+ export declare const SUBMISSION = "urn:ietf:params:jmap:submission";
5
+ /** Aduki body reading: `Email/get` takes `aduki:bodyMode` (ADK-MAIL-001 section 1.4, v47). */
6
+ export declare const READING = "urn:aduki:params:jmap:reading";
7
+ /** Core and mail: enough for `Mailbox`, `Email` and `Thread`. */
8
+ export declare const USING: readonly string[];
9
+ /** An authenticated fetch for one audience: `Session` and `Client` both have it. */
10
+ export interface AudienceFetch {
11
+ fetch(audience: string, input: Request | string | URL, init?: RequestInit): Promise<Response>;
12
+ }
13
+ /** The part of the JMAP session resource the SDK uses. */
14
+ export interface JmapSession {
15
+ /** the primary mail account id */
16
+ account: string;
17
+ api: string;
18
+ /** URL template with `{accountId}` */
19
+ upload: string;
20
+ /** URL template with `{accountId}`, `{blobId}`, `{name}`, `{type}` */
21
+ download: string;
22
+ /** URL template with `{types}`, `{closeafter}`, `{ping}` */
23
+ eventSource: string;
24
+ /** every session capability advertised */
25
+ capabilities: string[];
26
+ /** `urn:ietf:params:jmap:core` limits (`maxSizeUpload`, `maxCallsInRequest`, ...) */
27
+ limits: Record<string, unknown>;
28
+ /** the server reads bodies (`aduki:bodyMode`), see READING */
29
+ reading: boolean;
30
+ }
31
+ /** One method call: `[name, arguments, call id]`. */
32
+ export type Invocation = [name: string, args: Record<string, unknown>, id: string];
33
+ /** An uploaded blob. */
34
+ export interface Uploaded {
35
+ blob: string;
36
+ type: string;
37
+ size: number;
38
+ }
39
+ /**
40
+ * A JMAP method error (`["error", {type, description}, id]`) or a per-object
41
+ * refusal of a `/set` call (`notCreated`, `notUpdated`, `notDestroyed`).
42
+ * `type` is the JMAP error type (`invalidArguments`, `forbiddenFrom`,
43
+ * `notFound`, ...); `id` names the object for a per-object refusal.
44
+ */
45
+ export declare class JmapError extends AdukiError {
46
+ type: string;
47
+ description: string;
48
+ properties: string[];
49
+ method: string;
50
+ id: string;
51
+ constructor(method: string, error: Record<string, any>, id?: string);
52
+ }
53
+ export interface JmapOptions {
54
+ /** where the token comes from: a `Session` or a `Client` from `createClient` */
55
+ session: AudienceFetch;
56
+ /** the mail host root without `/v1` (default `https://mail.aduki.pro`) */
57
+ root?: string | undefined;
58
+ /** the token audience (default `mail`) */
59
+ audience?: string | undefined;
60
+ }
61
+ export declare const ROOT = "https://mail.aduki.pro";
62
+ export declare class Jmap {
63
+ #private;
64
+ constructor(options: JmapOptions);
65
+ /** Sends an authenticated request to the mail host (a path or an absolute URL). */
66
+ fetch(input: string, init?: RequestInit): Promise<Response>;
67
+ /** The session (`GET /jmap/session`), fetched once; `refresh` fetches it again. */
68
+ session(refresh?: boolean): Promise<JmapSession>;
69
+ /** The primary mail account id. */
70
+ account(): Promise<string>;
71
+ /**
72
+ * Runs `calls` in one request and returns every response in order
73
+ * (`[name, args, id]`). A method `error` response throws JmapError;
74
+ * per-object refusals (`notCreated`, ...) are left to the caller.
75
+ */
76
+ call(calls: Invocation[], using?: readonly string[]): Promise<Invocation[]>;
77
+ /**
78
+ * Runs one method with `accountId` set and returns its arguments (the
79
+ * first response with the call id; implicit responses are dropped).
80
+ */
81
+ method(name: string, args?: Record<string, unknown>, using?: readonly string[]): Promise<Record<string, any>>;
82
+ /** Uploads `data` as a blob of `type` (RFC 8620 section 6.1). */
83
+ upload(data: Blob | ArrayBuffer | Uint8Array, type?: string): Promise<Uploaded>;
84
+ /**
85
+ * Downloads blob `blob` (RFC 8620 section 6.2). Returns the open response
86
+ * (read it with `blob()`, `arrayBuffer()` or `body`); a failure throws.
87
+ * `name` and `type` label the download (`Content-Disposition`, `Content-Type`).
88
+ */
89
+ download(blob: string, options?: {
90
+ name?: string;
91
+ type?: string;
92
+ }): Promise<Response>;
93
+ }
94
+ /**
95
+ * Throws for a non-2xx answer (`envelope.rejection`): 401 is
96
+ * UnauthorizedError, anything else a NetworkError (or a subclass) with the
97
+ * status and the kind of the envelope or the type of the RFC 7807 problem
98
+ * document JMAP uses for request-level errors.
99
+ */
100
+ export declare function refuse(response: Response, what: string): Promise<never>;