@loftbox/sdk 0.1.0

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 ADDED
@@ -0,0 +1,95 @@
1
+ # LoftBox TypeScript SDK
2
+
3
+ AI 에이전트를 위한 이메일 인프라 SDK. 의존성 없음(Node 18+ 내장 `fetch` 사용).
4
+
5
+ ## 설치
6
+
7
+ ```bash
8
+ npm install @loftbox/sdk
9
+ ```
10
+
11
+ ## 빠른 시작
12
+
13
+ ```typescript
14
+ import { LoftBox } from '@loftbox/sdk';
15
+
16
+ const client = new LoftBox({ apiKey: 'lb_live_xxx' });
17
+
18
+ // 에이전트 + 메일박스
19
+ const agent = await client.agents.create({ name: 'Support Bot', slug: 'support-bot' });
20
+ const mailbox = await client.mailboxes.create(agent.id, { localPart: 'support' });
21
+
22
+ // 발송 (멱등 키로 중복 방지)
23
+ const msg = await client.messages.send({
24
+ mailboxId: mailbox.id,
25
+ to: ['recipient@example.com'],
26
+ subject: 'Hello',
27
+ bodyText: 'World',
28
+ idempotencyKey: 'welcome-42',
29
+ });
30
+
31
+ // 수신 폴링 → ack
32
+ const inbox = await client.mailboxes.listInbox(mailbox.id);
33
+ await client.mailboxes.ackInbox(
34
+ mailbox.id,
35
+ inbox.data.map((m) => m.id),
36
+ );
37
+ ```
38
+
39
+ ## 기능
40
+
41
+ - **발송** `messages.send(...)` — 텍스트/HTML/Markdown, 첨부, cc, 답장 헤더
42
+ - **예약 발송** `send({ ..., sendAt: '2030-01-01T09:00:00Z' })`
43
+ - **멱등 발송** `send({ ..., idempotencyKey })`
44
+ - **수신** `mailboxes.listInbox()` + `ackInbox()`, `message.extracted_text`(인용 제거 본문)
45
+ - **라벨** `messages.addLabels()`, `removeLabel()`, `list({ label })`
46
+ - **전문검색** `messages.list({ q })`, `threads.list({ q })`
47
+ - **스레드** `threads.list()`, `listMessages()`
48
+ - **승인** `messages.approve(id, reason)`, `reject(...)`
49
+ - **웹훅** `webhooks.create(agentId, url, eventTypes)`
50
+ - **도메인 / suppression** `domains.*`, `suppressions.*`
51
+
52
+ ## 오류 처리
53
+
54
+ ```typescript
55
+ import { RateLimitError, NotFoundError } from '@loftbox/sdk';
56
+
57
+ try {
58
+ await client.messages.send({
59
+ /* ... */
60
+ });
61
+ } catch (e) {
62
+ if (e instanceof RateLimitError) {
63
+ console.log(`${e.retryAfterSecs}s 후 재시도`);
64
+ } else if (e instanceof NotFoundError) {
65
+ console.log(e.statusCode, e.message);
66
+ }
67
+ }
68
+ ```
69
+
70
+ ## 페이지네이션
71
+
72
+ 목록 메서드는 `{ data, next_cursor }` 를 반환합니다:
73
+
74
+ ```typescript
75
+ let page = await client.messages.list({ mailboxId: mailbox.id, limit: 50 });
76
+ while (true) {
77
+ for (const m of page.data) {
78
+ /* ... */
79
+ }
80
+ if (!page.next_cursor) break;
81
+ page = await client.messages.list({
82
+ mailboxId: mailbox.id,
83
+ limit: 50,
84
+ cursor: page.next_cursor,
85
+ });
86
+ }
87
+ ```
88
+
89
+ ## 예제
90
+
91
+ `examples/quickstart.ts` 참고.
92
+
93
+ ## 라이선스
94
+
95
+ MIT
@@ -0,0 +1,103 @@
1
+ import type { Agent, Attachment, Domain, DomainStatus, ListMessagesParams, LoftBoxConfig, Mailbox, Message, Page, SendMessageParams, Suppression, Thread, Webhook } from "./types.js";
2
+ type Query = Record<string, string | number | undefined | null>;
3
+ /** LoftBox API 클라이언트 (fetch 기반, 의존성 없음). */
4
+ export declare class LoftBox {
5
+ readonly auth: AuthResource;
6
+ readonly agents: AgentsResource;
7
+ readonly mailboxes: MailboxesResource;
8
+ readonly messages: MessagesResource;
9
+ readonly threads: ThreadsResource;
10
+ readonly webhooks: WebhooksResource;
11
+ readonly domains: DomainsResource;
12
+ readonly suppressions: SuppressionsResource;
13
+ readonly attachments: AttachmentsResource;
14
+ private readonly apiKey;
15
+ private readonly baseUrl;
16
+ private readonly timeoutMs;
17
+ private readonly fetchImpl;
18
+ constructor(config: LoftBoxConfig);
19
+ /** @internal */
20
+ request<T>(method: string, path: string, opts?: {
21
+ json?: unknown;
22
+ query?: Query;
23
+ headers?: Record<string, string>;
24
+ }): Promise<T>;
25
+ }
26
+ declare class Resource {
27
+ protected readonly c: LoftBox;
28
+ constructor(c: LoftBox);
29
+ }
30
+ declare class AuthResource extends Resource {
31
+ signup(email: string, organizationName: string, slug?: string): Promise<Record<string, unknown>>;
32
+ verifySignup(email: string, verificationToken: string): Promise<Record<string, unknown>>;
33
+ }
34
+ declare class AgentsResource extends Resource {
35
+ create(params: {
36
+ name: string;
37
+ slug: string;
38
+ description?: string;
39
+ purpose?: string;
40
+ externalId?: string;
41
+ ownerLabel?: string;
42
+ metadata?: Record<string, unknown>;
43
+ }): Promise<Agent>;
44
+ get(agentId: string): Promise<Agent>;
45
+ list(params?: {
46
+ limit?: number;
47
+ cursor?: string;
48
+ }): Promise<Page<Agent>>;
49
+ }
50
+ declare class MailboxesResource extends Resource {
51
+ create(agentId: string, params: {
52
+ localPart: string;
53
+ domainId?: string;
54
+ displayName?: string;
55
+ webhookUrl?: string;
56
+ retentionDays?: number;
57
+ }): Promise<Mailbox>;
58
+ listByAgent(agentId: string): Promise<Page<Mailbox>>;
59
+ listInbox(mailboxId: string, params?: {
60
+ limit?: number;
61
+ cursor?: string;
62
+ }): Promise<Page<Message>>;
63
+ ackInbox(mailboxId: string, messageIds: string[]): Promise<unknown>;
64
+ }
65
+ declare class MessagesResource extends Resource {
66
+ send(params: SendMessageParams): Promise<Message>;
67
+ get(messageId: string): Promise<Message>;
68
+ list(params?: ListMessagesParams): Promise<Page<Message>>;
69
+ addLabels(messageId: string, labels: string[]): Promise<Message>;
70
+ removeLabel(messageId: string, label: string): Promise<Message>;
71
+ approve(messageId: string, reason: string): Promise<Message>;
72
+ reject(messageId: string, reason: string): Promise<Message>;
73
+ }
74
+ declare class ThreadsResource extends Resource {
75
+ list(params?: {
76
+ mailboxId?: string;
77
+ q?: string;
78
+ limit?: number;
79
+ cursor?: string;
80
+ }): Promise<Page<Thread>>;
81
+ listMessages(threadId: string): Promise<Page<Message>>;
82
+ }
83
+ declare class WebhooksResource extends Resource {
84
+ create(agentId: string, url: string, eventTypes: string[]): Promise<Webhook>;
85
+ }
86
+ declare class DomainsResource extends Resource {
87
+ create(domain: string): Promise<Domain>;
88
+ list(): Promise<Page<Domain>>;
89
+ status(domainId: string): Promise<DomainStatus>;
90
+ }
91
+ declare class SuppressionsResource extends Resource {
92
+ list(params?: {
93
+ limit?: number;
94
+ before?: string;
95
+ }): Promise<Page<Suppression>>;
96
+ create(address: string): Promise<Suppression>;
97
+ remove(suppressionId: string): Promise<void>;
98
+ }
99
+ declare class AttachmentsResource extends Resource {
100
+ listForMessage(messageId: string): Promise<Page<Attachment>>;
101
+ presignedUrl(attachmentId: string): Promise<Record<string, unknown>>;
102
+ }
103
+ export {};
package/dist/client.js ADDED
@@ -0,0 +1,310 @@
1
+ import { LoftBoxError, errorForStatus } from "./errors.js";
2
+ const DEFAULT_BASE_URL = "https://api.loftbox.net";
3
+ const DEFAULT_TIMEOUT_MS = 30_000;
4
+ const USER_AGENT = "loftbox-ts/0.1.0";
5
+ /** LoftBox API 클라이언트 (fetch 기반, 의존성 없음). */
6
+ export class LoftBox {
7
+ auth;
8
+ agents;
9
+ mailboxes;
10
+ messages;
11
+ threads;
12
+ webhooks;
13
+ domains;
14
+ suppressions;
15
+ attachments;
16
+ apiKey;
17
+ baseUrl;
18
+ timeoutMs;
19
+ fetchImpl;
20
+ constructor(config) {
21
+ if (!config?.apiKey)
22
+ throw new Error("apiKey 는 필수입니다");
23
+ this.apiKey = config.apiKey;
24
+ this.baseUrl = (config.baseUrl ?? DEFAULT_BASE_URL).replace(/\/$/, "");
25
+ this.timeoutMs = config.timeoutMs ?? DEFAULT_TIMEOUT_MS;
26
+ const f = config.fetch ?? globalThis.fetch;
27
+ if (!f)
28
+ throw new Error("fetch 를 사용할 수 없습니다 (Node 18+ 또는 config.fetch 제공)");
29
+ this.fetchImpl = f;
30
+ this.auth = new AuthResource(this);
31
+ this.agents = new AgentsResource(this);
32
+ this.mailboxes = new MailboxesResource(this);
33
+ this.messages = new MessagesResource(this);
34
+ this.threads = new ThreadsResource(this);
35
+ this.webhooks = new WebhooksResource(this);
36
+ this.domains = new DomainsResource(this);
37
+ this.suppressions = new SuppressionsResource(this);
38
+ this.attachments = new AttachmentsResource(this);
39
+ }
40
+ /** @internal */
41
+ async request(method, path, opts = {}) {
42
+ const url = new URL(this.baseUrl + path);
43
+ if (opts.query) {
44
+ for (const [k, v] of Object.entries(opts.query)) {
45
+ if (v !== undefined && v !== null)
46
+ url.searchParams.set(k, String(v));
47
+ }
48
+ }
49
+ const headers = {
50
+ Authorization: `Bearer ${this.apiKey}`,
51
+ Accept: "application/json",
52
+ "User-Agent": USER_AGENT,
53
+ ...opts.headers,
54
+ };
55
+ if (opts.json !== undefined)
56
+ headers["Content-Type"] = "application/json";
57
+ // 타임아웃은 fetch + 본문 읽기 전체를 덮는다 — 헤더만 보내고 body 를 멈추는
58
+ // 서버/프록시에도 timeoutMs 가 적용되도록(codex 리뷰 Major).
59
+ const controller = new AbortController();
60
+ const timer = setTimeout(() => controller.abort(), this.timeoutMs);
61
+ let resp;
62
+ let text;
63
+ try {
64
+ resp = await this.fetchImpl(url.toString(), {
65
+ method,
66
+ headers,
67
+ body: opts.json !== undefined ? JSON.stringify(opts.json) : undefined,
68
+ signal: controller.signal,
69
+ });
70
+ text = await resp.text();
71
+ }
72
+ catch (e) {
73
+ throw new LoftBoxError(`요청 실패: ${e.message}`);
74
+ }
75
+ finally {
76
+ clearTimeout(timer);
77
+ }
78
+ const requestId = resp.headers.get("x-request-id") ?? undefined;
79
+ let body = undefined;
80
+ if (text) {
81
+ try {
82
+ body = JSON.parse(text);
83
+ }
84
+ catch {
85
+ body = text;
86
+ }
87
+ }
88
+ if (!resp.ok) {
89
+ let message = `HTTP ${resp.status}`;
90
+ let retryAfterSecs;
91
+ if (body && typeof body === "object") {
92
+ const b = body;
93
+ const err = b.error;
94
+ if (err && typeof err === "object") {
95
+ const eo = err;
96
+ if (typeof eo.message === "string")
97
+ message = eo.message;
98
+ if (typeof eo.retry_after === "number")
99
+ retryAfterSecs = eo.retry_after;
100
+ }
101
+ else if (typeof b.message === "string") {
102
+ message = b.message;
103
+ }
104
+ else if (typeof err === "string") {
105
+ message = err;
106
+ }
107
+ else if (typeof b.detail === "string") {
108
+ message = b.detail;
109
+ }
110
+ }
111
+ else if (typeof body === "string" && body) {
112
+ message = body;
113
+ }
114
+ const headerRa = resp.headers.get("retry-after");
115
+ if (headerRa && /^\d+$/.test(headerRa))
116
+ retryAfterSecs = Number(headerRa);
117
+ throw errorForStatus(resp.status, message, {
118
+ body,
119
+ requestId,
120
+ retryAfterSecs,
121
+ });
122
+ }
123
+ return body;
124
+ }
125
+ }
126
+ function page(raw) {
127
+ if (Array.isArray(raw))
128
+ return { data: raw, next_cursor: null };
129
+ const src = (raw ?? {});
130
+ return { data: src.data ?? [], next_cursor: src.next_cursor ?? null };
131
+ }
132
+ class Resource {
133
+ c;
134
+ constructor(c) {
135
+ this.c = c;
136
+ }
137
+ }
138
+ class AuthResource extends Resource {
139
+ signup(email, organizationName, slug) {
140
+ return this.c.request("POST", "/v1/auth/signup", {
141
+ json: { email, organization_name: organizationName, slug: slug ?? null },
142
+ });
143
+ }
144
+ verifySignup(email, verificationToken) {
145
+ return this.c.request("POST", "/v1/auth/signup/verify", {
146
+ json: { email, verification_token: verificationToken },
147
+ });
148
+ }
149
+ }
150
+ class AgentsResource extends Resource {
151
+ create(params) {
152
+ return this.c.request("POST", "/v1/agents", {
153
+ json: {
154
+ name: params.name,
155
+ slug: params.slug,
156
+ description: params.description ?? null,
157
+ purpose: params.purpose ?? null,
158
+ external_id: params.externalId ?? null,
159
+ owner_label: params.ownerLabel ?? null,
160
+ metadata: params.metadata ?? null,
161
+ },
162
+ });
163
+ }
164
+ get(agentId) {
165
+ return this.c.request("GET", `/v1/agents/${encodeURIComponent(agentId)}`);
166
+ }
167
+ async list(params = {}) {
168
+ return page(await this.c.request("GET", "/v1/agents", {
169
+ query: { limit: params.limit, cursor: params.cursor },
170
+ }));
171
+ }
172
+ }
173
+ class MailboxesResource extends Resource {
174
+ create(agentId, params) {
175
+ return this.c.request("POST", `/v1/agents/${encodeURIComponent(agentId)}/mailboxes`, {
176
+ json: {
177
+ local_part: params.localPart,
178
+ domain_id: params.domainId ?? null,
179
+ display_name: params.displayName ?? null,
180
+ webhook_url: params.webhookUrl ?? null,
181
+ retention_days: params.retentionDays ?? null,
182
+ },
183
+ });
184
+ }
185
+ async listByAgent(agentId) {
186
+ return page(await this.c.request("GET", `/v1/agents/${encodeURIComponent(agentId)}/mailboxes`));
187
+ }
188
+ async listInbox(mailboxId, params = {}) {
189
+ return page(await this.c.request("GET", `/v1/mailboxes/${encodeURIComponent(mailboxId)}/inbox`, {
190
+ query: { limit: params.limit, cursor: params.cursor },
191
+ }));
192
+ }
193
+ ackInbox(mailboxId, messageIds) {
194
+ return this.c.request("POST", `/v1/mailboxes/${encodeURIComponent(mailboxId)}/inbox/ack`, {
195
+ json: { message_ids: messageIds },
196
+ });
197
+ }
198
+ }
199
+ class MessagesResource extends Resource {
200
+ send(params) {
201
+ const headers = params.idempotencyKey
202
+ ? { "Idempotency-Key": params.idempotencyKey }
203
+ : undefined;
204
+ return this.c.request("POST", "/v1/messages", {
205
+ json: {
206
+ mailbox_id: params.mailboxId,
207
+ to: params.to,
208
+ subject: params.subject,
209
+ body_text: params.bodyText ?? null,
210
+ body_html: params.bodyHtml ?? null,
211
+ body_markdown: params.bodyMarkdown ?? null,
212
+ cc: params.cc ?? [],
213
+ in_reply_to: params.inReplyTo ?? null,
214
+ references: params.references ?? [],
215
+ metadata: params.metadata ?? null,
216
+ attachments: params.attachments ?? [],
217
+ send_at: params.sendAt ?? null,
218
+ },
219
+ headers,
220
+ });
221
+ }
222
+ get(messageId) {
223
+ return this.c.request("GET", `/v1/messages/${encodeURIComponent(messageId)}`);
224
+ }
225
+ async list(params = {}) {
226
+ return page(await this.c.request("GET", "/v1/messages", {
227
+ query: {
228
+ mailbox_id: params.mailboxId,
229
+ direction: params.direction,
230
+ status: params.status,
231
+ label: params.label,
232
+ q: params.q,
233
+ limit: params.limit,
234
+ cursor: params.cursor,
235
+ },
236
+ }));
237
+ }
238
+ addLabels(messageId, labels) {
239
+ return this.c.request("POST", `/v1/messages/${encodeURIComponent(messageId)}/labels`, {
240
+ json: { labels },
241
+ });
242
+ }
243
+ removeLabel(messageId, label) {
244
+ return this.c.request("DELETE", `/v1/messages/${encodeURIComponent(messageId)}/labels/${encodeURIComponent(label)}`);
245
+ }
246
+ approve(messageId, reason) {
247
+ return this.c.request("POST", `/v1/messages/${encodeURIComponent(messageId)}/approve`, {
248
+ json: { reason },
249
+ });
250
+ }
251
+ reject(messageId, reason) {
252
+ return this.c.request("POST", `/v1/messages/${encodeURIComponent(messageId)}/reject`, {
253
+ json: { reason },
254
+ });
255
+ }
256
+ }
257
+ class ThreadsResource extends Resource {
258
+ async list(params = {}) {
259
+ return page(await this.c.request("GET", "/v1/threads", {
260
+ query: {
261
+ mailbox_id: params.mailboxId,
262
+ q: params.q,
263
+ limit: params.limit,
264
+ cursor: params.cursor,
265
+ },
266
+ }));
267
+ }
268
+ async listMessages(threadId) {
269
+ return page(await this.c.request("GET", `/v1/threads/${encodeURIComponent(threadId)}/messages`));
270
+ }
271
+ }
272
+ class WebhooksResource extends Resource {
273
+ create(agentId, url, eventTypes) {
274
+ return this.c.request("POST", `/v1/agents/${encodeURIComponent(agentId)}/webhooks`, {
275
+ json: { url, event_types: eventTypes },
276
+ });
277
+ }
278
+ }
279
+ class DomainsResource extends Resource {
280
+ create(domain) {
281
+ return this.c.request("POST", "/v1/domains", { json: { domain } });
282
+ }
283
+ async list() {
284
+ return page(await this.c.request("GET", "/v1/domains"));
285
+ }
286
+ status(domainId) {
287
+ return this.c.request("GET", `/v1/domains/${encodeURIComponent(domainId)}/status`);
288
+ }
289
+ }
290
+ class SuppressionsResource extends Resource {
291
+ async list(params = {}) {
292
+ return page(await this.c.request("GET", "/v1/suppressions", {
293
+ query: { limit: params.limit, before: params.before },
294
+ }));
295
+ }
296
+ create(address) {
297
+ return this.c.request("POST", "/v1/suppressions", { json: { address } });
298
+ }
299
+ async remove(suppressionId) {
300
+ await this.c.request("DELETE", `/v1/suppressions/${encodeURIComponent(suppressionId)}`);
301
+ }
302
+ }
303
+ class AttachmentsResource extends Resource {
304
+ async listForMessage(messageId) {
305
+ return page(await this.c.request("GET", `/v1/messages/${encodeURIComponent(messageId)}/attachments`));
306
+ }
307
+ presignedUrl(attachmentId) {
308
+ return this.c.request("GET", `/v1/attachments/${encodeURIComponent(attachmentId)}/url`);
309
+ }
310
+ }
@@ -0,0 +1,46 @@
1
+ /** LoftBox SDK 예외. */
2
+ export declare class LoftBoxError extends Error {
3
+ readonly statusCode?: number;
4
+ readonly body?: unknown;
5
+ readonly requestId?: string;
6
+ constructor(message: string, opts?: {
7
+ statusCode?: number;
8
+ body?: unknown;
9
+ requestId?: string;
10
+ });
11
+ }
12
+ /** 401 — API 키 없음/유효하지 않음. */
13
+ export declare class AuthenticationError extends LoftBoxError {
14
+ constructor(...args: ConstructorParameters<typeof LoftBoxError>);
15
+ }
16
+ /** 403 — scope 부족. */
17
+ export declare class PermissionError extends LoftBoxError {
18
+ constructor(...args: ConstructorParameters<typeof LoftBoxError>);
19
+ }
20
+ /** 404 — 리소스 없음. */
21
+ export declare class NotFoundError extends LoftBoxError {
22
+ constructor(...args: ConstructorParameters<typeof LoftBoxError>);
23
+ }
24
+ /** 409 — 멱등/상태 충돌. */
25
+ export declare class ConflictError extends LoftBoxError {
26
+ constructor(...args: ConstructorParameters<typeof LoftBoxError>);
27
+ }
28
+ /** 400 / 422 — 요청 검증 실패. */
29
+ export declare class ValidationError extends LoftBoxError {
30
+ constructor(...args: ConstructorParameters<typeof LoftBoxError>);
31
+ }
32
+ /** 429 — rate limit 초과. retryAfterSecs 만큼 대기 후 재시도. */
33
+ export declare class RateLimitError extends LoftBoxError {
34
+ readonly retryAfterSecs?: number;
35
+ constructor(message: string, opts?: {
36
+ statusCode?: number;
37
+ body?: unknown;
38
+ requestId?: string;
39
+ retryAfterSecs?: number;
40
+ });
41
+ }
42
+ export declare function errorForStatus(statusCode: number, message: string, opts?: {
43
+ body?: unknown;
44
+ requestId?: string;
45
+ retryAfterSecs?: number;
46
+ }): LoftBoxError;
package/dist/errors.js ADDED
@@ -0,0 +1,82 @@
1
+ /** LoftBox SDK 예외. */
2
+ export class LoftBoxError extends Error {
3
+ statusCode;
4
+ body;
5
+ requestId;
6
+ constructor(message, opts = {}) {
7
+ super(message);
8
+ // 하위 타깃 트랜스파일에서도 instanceof 가 동작하도록 prototype 복원.
9
+ Object.setPrototypeOf(this, new.target.prototype);
10
+ this.name = "LoftBoxError";
11
+ this.statusCode = opts.statusCode;
12
+ this.body = opts.body;
13
+ this.requestId = opts.requestId;
14
+ }
15
+ }
16
+ /** 401 — API 키 없음/유효하지 않음. */
17
+ export class AuthenticationError extends LoftBoxError {
18
+ constructor(...args) {
19
+ super(...args);
20
+ this.name = "AuthenticationError";
21
+ }
22
+ }
23
+ /** 403 — scope 부족. */
24
+ export class PermissionError extends LoftBoxError {
25
+ constructor(...args) {
26
+ super(...args);
27
+ this.name = "PermissionError";
28
+ }
29
+ }
30
+ /** 404 — 리소스 없음. */
31
+ export class NotFoundError extends LoftBoxError {
32
+ constructor(...args) {
33
+ super(...args);
34
+ this.name = "NotFoundError";
35
+ }
36
+ }
37
+ /** 409 — 멱등/상태 충돌. */
38
+ export class ConflictError extends LoftBoxError {
39
+ constructor(...args) {
40
+ super(...args);
41
+ this.name = "ConflictError";
42
+ }
43
+ }
44
+ /** 400 / 422 — 요청 검증 실패. */
45
+ export class ValidationError extends LoftBoxError {
46
+ constructor(...args) {
47
+ super(...args);
48
+ this.name = "ValidationError";
49
+ }
50
+ }
51
+ /** 429 — rate limit 초과. retryAfterSecs 만큼 대기 후 재시도. */
52
+ export class RateLimitError extends LoftBoxError {
53
+ retryAfterSecs;
54
+ constructor(message, opts = {}) {
55
+ super(message, opts);
56
+ this.name = "RateLimitError";
57
+ this.retryAfterSecs = opts.retryAfterSecs;
58
+ }
59
+ }
60
+ export function errorForStatus(statusCode, message, opts = {}) {
61
+ const base = { statusCode, body: opts.body, requestId: opts.requestId };
62
+ switch (statusCode) {
63
+ case 400:
64
+ case 422:
65
+ return new ValidationError(message, base);
66
+ case 401:
67
+ return new AuthenticationError(message, base);
68
+ case 403:
69
+ return new PermissionError(message, base);
70
+ case 404:
71
+ return new NotFoundError(message, base);
72
+ case 409:
73
+ return new ConflictError(message, base);
74
+ case 429:
75
+ return new RateLimitError(message, {
76
+ ...base,
77
+ retryAfterSecs: opts.retryAfterSecs,
78
+ });
79
+ default:
80
+ return new LoftBoxError(message, base);
81
+ }
82
+ }
@@ -0,0 +1,3 @@
1
+ export { LoftBox } from "./client.js";
2
+ export { LoftBoxError, AuthenticationError, PermissionError, NotFoundError, ConflictError, ValidationError, RateLimitError, } from "./errors.js";
3
+ export type { LoftBoxConfig, Agent, Attachment, Domain, DomainStatus, Mailbox, Message, Page, Suppression, Thread, Webhook, SendMessageParams, ListMessagesParams, } from "./types.js";
package/dist/index.js ADDED
@@ -0,0 +1,2 @@
1
+ export { LoftBox } from "./client.js";
2
+ export { LoftBoxError, AuthenticationError, PermissionError, NotFoundError, ConflictError, ValidationError, RateLimitError, } from "./errors.js";
@@ -0,0 +1,122 @@
1
+ /** LoftBox SDK 타입.
2
+ *
3
+ * 응답 모델은 서버가 필드를 추가해도 깨지지 않도록 인덱스 시그니처를 둔다.
4
+ * 필드명은 API wire(snake_case)를 그대로 따른다.
5
+ */
6
+ export interface LoftBoxConfig {
7
+ apiKey: string;
8
+ baseUrl?: string;
9
+ /** 요청 타임아웃(ms). 기본 30000. */
10
+ timeoutMs?: number;
11
+ /** 테스트/프록시용 커스텀 fetch. 기본 globalThis.fetch. */
12
+ fetch?: typeof fetch;
13
+ }
14
+ interface Extensible {
15
+ [key: string]: unknown;
16
+ }
17
+ export interface Agent extends Extensible {
18
+ id: string;
19
+ slug?: string;
20
+ name: string;
21
+ description?: string | null;
22
+ created_at?: string | null;
23
+ }
24
+ export interface Mailbox extends Extensible {
25
+ id: string;
26
+ agent_id?: string;
27
+ address: string;
28
+ display_name?: string | null;
29
+ active?: boolean;
30
+ created_at?: string | null;
31
+ }
32
+ export interface Attachment extends Extensible {
33
+ id: string;
34
+ filename?: string;
35
+ content_type?: string;
36
+ size_bytes?: number;
37
+ }
38
+ export interface Message extends Extensible {
39
+ id: string;
40
+ public_id?: string | null;
41
+ mailbox_id?: string;
42
+ thread_id?: string | null;
43
+ direction?: string;
44
+ status?: string;
45
+ subject?: string | null;
46
+ body_text?: string | null;
47
+ body_html?: string | null;
48
+ body_markdown?: string | null;
49
+ /** #229 인용 제거된 수신 답장 본문. */
50
+ extracted_text?: string | null;
51
+ /** #236 라벨. */
52
+ labels?: string[];
53
+ /** #241 예약발송 시각(RFC3339). */
54
+ scheduled_at?: string | null;
55
+ sent_at?: string | null;
56
+ received_at?: string | null;
57
+ created_at?: string | null;
58
+ }
59
+ export interface Thread extends Extensible {
60
+ id: string;
61
+ mailbox_id?: string;
62
+ subject?: string | null;
63
+ last_message_at?: string | null;
64
+ }
65
+ export interface Webhook extends Extensible {
66
+ id: string;
67
+ url: string;
68
+ event_types?: string[];
69
+ /** 생성 응답에서 1회만 반환되는 서명 시크릿 — 즉시 저장, 로그 금지. */
70
+ secret?: string | null;
71
+ }
72
+ export interface Domain extends Extensible {
73
+ id: string;
74
+ domain?: string;
75
+ status?: string;
76
+ }
77
+ export interface DomainStatus extends Extensible {
78
+ domain?: string;
79
+ status?: string;
80
+ inbound?: unknown;
81
+ outbound?: unknown;
82
+ next_actions?: unknown;
83
+ }
84
+ export interface Suppression extends Extensible {
85
+ id: string;
86
+ address: string;
87
+ reason?: string | null;
88
+ created_at?: string | null;
89
+ }
90
+ /** cursor 페이지네이션 응답. */
91
+ export interface Page<T> {
92
+ data: T[];
93
+ next_cursor: string | null;
94
+ }
95
+ /** messages.send 옵션. */
96
+ export interface SendMessageParams {
97
+ mailboxId: string;
98
+ to: string[];
99
+ subject: string;
100
+ bodyText?: string;
101
+ bodyHtml?: string;
102
+ bodyMarkdown?: string;
103
+ cc?: string[];
104
+ inReplyTo?: string;
105
+ references?: string[];
106
+ metadata?: Record<string, unknown>;
107
+ attachments?: Array<Record<string, unknown>>;
108
+ /** RFC3339 미래 시각 — 예약발송. */
109
+ sendAt?: string;
110
+ /** 중복 발송 방지 멱등 키. */
111
+ idempotencyKey?: string;
112
+ }
113
+ export interface ListMessagesParams {
114
+ mailboxId?: string;
115
+ direction?: string;
116
+ status?: string;
117
+ label?: string;
118
+ q?: string;
119
+ limit?: number;
120
+ cursor?: string;
121
+ }
122
+ export {};
package/dist/types.js ADDED
@@ -0,0 +1,6 @@
1
+ /** LoftBox SDK 타입.
2
+ *
3
+ * 응답 모델은 서버가 필드를 추가해도 깨지지 않도록 인덱스 시그니처를 둔다.
4
+ * 필드명은 API wire(snake_case)를 그대로 따른다.
5
+ */
6
+ export {};
package/package.json ADDED
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "@loftbox/sdk",
3
+ "version": "0.1.0",
4
+ "description": "LoftBox TypeScript SDK - Email infrastructure for AI agents",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "https://github.com/TheMagicTower/loftbox-sdk-ts.git"
9
+ },
10
+ "homepage": "https://loftbox.net",
11
+ "keywords": ["email", "ai-agents", "smtp", "inbox", "loftbox"],
12
+ "type": "module",
13
+ "main": "dist/index.js",
14
+ "types": "dist/index.d.ts",
15
+ "exports": {
16
+ ".": {
17
+ "types": "./dist/index.d.ts",
18
+ "import": "./dist/index.js"
19
+ }
20
+ },
21
+ "files": ["dist", "README.md"],
22
+ "engines": {
23
+ "node": ">=18"
24
+ },
25
+ "scripts": {
26
+ "build": "tsc",
27
+ "typecheck": "tsc --noEmit",
28
+ "test": "node --import tsx --test tests/*.test.ts",
29
+ "format:check": "prettier --check \"src/**/*.ts\" \"tests/**/*.ts\"",
30
+ "format": "prettier --write \"src/**/*.ts\" \"tests/**/*.ts\"",
31
+ "prepublishOnly": "npm run build"
32
+ },
33
+ "dependencies": {},
34
+ "devDependencies": {
35
+ "@types/node": "^20",
36
+ "prettier": "^3",
37
+ "tsx": "^4",
38
+ "typescript": "^5"
39
+ }
40
+ }