@forjio/suppuo 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,46 @@
1
+ # @forjio/suppuo
2
+
3
+ Typed JS/TS client for the [suppuo.com](https://suppuo.com) helpdesk REST API.
4
+
5
+ ```bash
6
+ npm install @forjio/suppuo
7
+ ```
8
+
9
+ ```ts
10
+ import { SuppuoClient } from "@forjio/suppuo";
11
+
12
+ // Bearer token from `token` or the SUPPUO_TOKEN env var.
13
+ const client = new SuppuoClient({ token: process.env.SUPPUO_TOKEN! });
14
+
15
+ // Agent workspace surface
16
+ const { tickets, counts } = await client.tickets.list({ status: "open" });
17
+ const ticket = await client.tickets.get(tickets[0].id);
18
+ await client.tickets.reply(ticket.id, { body: "On it!", isInternal: false });
19
+ await client.tickets.update(ticket.id, { status: "resolved" });
20
+
21
+ // Canned replies
22
+ await client.cannedReplies.create({ title: "Refund policy", body: "…" });
23
+
24
+ // Public (requester) surface — no token required
25
+ const { accessToken } = await client.public.submitTicket({
26
+ accountId: "acc_…",
27
+ subject: "Order question",
28
+ body: "Where is my order?",
29
+ email: "customer@example.com",
30
+ });
31
+ const view = await client.public.getTicket(accessToken);
32
+ await client.public.replyTicket(accessToken, { body: "Any update?" });
33
+ ```
34
+
35
+ Errors throw `SuppuoError` carrying the API envelope's `error.code`
36
+ (`NOT_FOUND`, `VALIDATION_ERROR`, `AUTH_REQUIRED`, …), the HTTP status,
37
+ and the `meta.requestId`.
38
+
39
+ See [suppuo.com/docs/sdk/js](https://suppuo.com/docs/sdk/js) for the
40
+ full method reference.
41
+
42
+ ## Family
43
+
44
+ Sister to:
45
+ - [`forjio-suppuo`](https://pypi.org/project/forjio-suppuo/) (Python)
46
+ - [`hachimi-cat/suppuo-go`](https://github.com/hachimi-cat/suppuo-go) (Go)
package/dist/index.cjs ADDED
@@ -0,0 +1,167 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ SuppuoClient: () => SuppuoClient,
24
+ SuppuoError: () => SuppuoError
25
+ });
26
+ module.exports = __toCommonJS(index_exports);
27
+ var SuppuoError = class extends Error {
28
+ /** HTTP status (0 for transport-level failures). */
29
+ status;
30
+ /** Envelope `error.code` (UPPER_SNAKE_CASE) or an SDK-side code
31
+ * (`NETWORK_ERROR`, `TIMEOUT`, `INVALID_RESPONSE`). */
32
+ code;
33
+ requestId;
34
+ param;
35
+ constructor(status, code, message, requestId, param) {
36
+ super(message);
37
+ this.name = "SuppuoError";
38
+ this.status = status;
39
+ this.code = code;
40
+ this.requestId = requestId;
41
+ this.param = param;
42
+ }
43
+ };
44
+ var SuppuoClient = class {
45
+ token;
46
+ baseUrl;
47
+ timeoutMs;
48
+ constructor(opts = {}) {
49
+ this.token = opts.token ?? process.env.SUPPUO_TOKEN ?? void 0;
50
+ this.baseUrl = (opts.baseUrl ?? "https://suppuo.com").replace(/\/+$/, "");
51
+ this.timeoutMs = opts.timeoutMs ?? 3e4;
52
+ }
53
+ async request(args) {
54
+ const url = new URL(this.baseUrl + args.path);
55
+ for (const [k, v] of Object.entries(args.query ?? {})) {
56
+ if (v !== void 0) url.searchParams.set(k, String(v));
57
+ }
58
+ const headers = { Accept: "application/json" };
59
+ if (!args.noAuth) {
60
+ if (!this.token) {
61
+ throw new SuppuoError(
62
+ 0,
63
+ "AUTH_REQUIRED",
64
+ "No token configured. Pass `token` or set SUPPUO_TOKEN."
65
+ );
66
+ }
67
+ headers["Authorization"] = `Bearer ${this.token}`;
68
+ }
69
+ if (args.body !== void 0) headers["Content-Type"] = "application/json";
70
+ let res;
71
+ try {
72
+ res = await fetch(url, {
73
+ method: args.method,
74
+ headers,
75
+ body: args.body !== void 0 ? JSON.stringify(args.body) : void 0,
76
+ signal: AbortSignal.timeout(this.timeoutMs)
77
+ });
78
+ } catch (e) {
79
+ if (e instanceof Error && e.name === "TimeoutError") {
80
+ throw new SuppuoError(0, "TIMEOUT", `request timed out after ${this.timeoutMs}ms`);
81
+ }
82
+ throw new SuppuoError(0, "NETWORK_ERROR", e instanceof Error ? e.message : String(e));
83
+ }
84
+ let envelope;
85
+ try {
86
+ envelope = await res.json();
87
+ } catch {
88
+ throw new SuppuoError(res.status, "INVALID_RESPONSE", `non-JSON response (HTTP ${res.status})`);
89
+ }
90
+ if (!res.ok || envelope.error) {
91
+ const err = envelope.error;
92
+ throw new SuppuoError(
93
+ res.status,
94
+ err?.code ?? "UNKNOWN",
95
+ err?.message ?? `HTTP ${res.status}`,
96
+ envelope.meta?.requestId,
97
+ err?.param
98
+ );
99
+ }
100
+ return envelope.data;
101
+ }
102
+ // ─── Tickets (agent workspace surface, Bearer auth) ───────────────
103
+ tickets = {
104
+ /** GET /api/v1/tickets — newest-activity-first, with per-status counts. */
105
+ list: (params) => this.request({
106
+ method: "GET",
107
+ path: "/api/v1/tickets",
108
+ query: { status: params?.status, limit: params?.limit }
109
+ }),
110
+ /** GET /api/v1/tickets/:id — full ticket incl. message thread. */
111
+ get: (id) => this.request({ method: "GET", path: `/api/v1/tickets/${encodeURIComponent(id)}` }),
112
+ /** POST /api/v1/tickets — agent-logged ticket (e.g. arrived via WhatsApp). */
113
+ create: (input) => this.request({ method: "POST", path: "/api/v1/tickets", body: input }),
114
+ /** POST /api/v1/tickets/:id/messages — agent reply (or internal note). */
115
+ reply: (id, input) => this.request({
116
+ method: "POST",
117
+ path: `/api/v1/tickets/${encodeURIComponent(id)}/messages`,
118
+ body: input
119
+ }),
120
+ /** PATCH /api/v1/tickets/:id — status / priority / assignee. */
121
+ update: (id, patch) => this.request({
122
+ method: "PATCH",
123
+ path: `/api/v1/tickets/${encodeURIComponent(id)}`,
124
+ body: patch
125
+ })
126
+ };
127
+ // ─── Canned replies ────────────────────────────────────────────────
128
+ cannedReplies = {
129
+ /** GET /api/v1/canned-replies */
130
+ list: () => this.request({ method: "GET", path: "/api/v1/canned-replies" }),
131
+ /** POST /api/v1/canned-replies */
132
+ create: (input) => this.request({ method: "POST", path: "/api/v1/canned-replies", body: input }),
133
+ /** PATCH /api/v1/canned-replies/:id */
134
+ update: (id, patch) => this.request({
135
+ method: "PATCH",
136
+ path: `/api/v1/canned-replies/${encodeURIComponent(id)}`,
137
+ body: patch
138
+ }),
139
+ /** DELETE /api/v1/canned-replies/:id */
140
+ delete: (id) => this.request({ method: "DELETE", path: `/api/v1/canned-replies/${encodeURIComponent(id)}` })
141
+ };
142
+ // ─── Public (requester-facing, unauthenticated) ────────────────────
143
+ public = {
144
+ /** POST /api/v1/public/tickets — submit to a workspace's hosted form.
145
+ * The returned `accessToken` is the requester's only credential. */
146
+ submitTicket: (input) => this.request({ method: "POST", path: "/api/v1/public/tickets", body: input, noAuth: true }),
147
+ /** GET /api/v1/public/tickets/:accessToken — tokenized status view
148
+ * (public messages only; internal notes are never exposed). */
149
+ getTicket: (accessToken) => this.request({
150
+ method: "GET",
151
+ path: `/api/v1/public/tickets/${encodeURIComponent(accessToken)}`,
152
+ noAuth: true
153
+ }),
154
+ /** POST /api/v1/public/tickets/:accessToken/messages — requester reply. */
155
+ replyTicket: (accessToken, input) => this.request({
156
+ method: "POST",
157
+ path: `/api/v1/public/tickets/${encodeURIComponent(accessToken)}/messages`,
158
+ body: input,
159
+ noAuth: true
160
+ })
161
+ };
162
+ };
163
+ // Annotate the CommonJS export names for ESM import in node:
164
+ 0 && (module.exports = {
165
+ SuppuoClient,
166
+ SuppuoError
167
+ });
@@ -0,0 +1,199 @@
1
+ /**
2
+ * Suppuo SDK — typed JS/TS client for the suppuo.com REST API.
3
+ * Sister to `forjio-suppuo` (Python) and `hachimi-cat/suppuo-go` (Go).
4
+ *
5
+ * Auth = Bearer JWT (a Huudis-minted access token). Pass `token` or set
6
+ * `SUPPUO_TOKEN`. The `public.*` surface (requester-facing hosted-form
7
+ * endpoints) needs no token at all.
8
+ *
9
+ * Every response rides the Forjio envelope `{ data, error, meta }`;
10
+ * the client unwraps it and throws `SuppuoError` (with the envelope's
11
+ * `error.code`) on failure.
12
+ */
13
+ interface ApiEnvelope<T> {
14
+ data: T | null;
15
+ error: {
16
+ code: string;
17
+ message: string;
18
+ param?: string;
19
+ docUrl?: string;
20
+ } | null;
21
+ meta?: {
22
+ requestId: string;
23
+ timestamp: string;
24
+ cursor?: string | null;
25
+ hasMore?: boolean;
26
+ };
27
+ }
28
+ declare class SuppuoError extends Error {
29
+ /** HTTP status (0 for transport-level failures). */
30
+ readonly status: number;
31
+ /** Envelope `error.code` (UPPER_SNAKE_CASE) or an SDK-side code
32
+ * (`NETWORK_ERROR`, `TIMEOUT`, `INVALID_RESPONSE`). */
33
+ readonly code: string;
34
+ readonly requestId: string | undefined;
35
+ readonly param: string | undefined;
36
+ constructor(status: number, code: string, message: string, requestId?: string, param?: string);
37
+ }
38
+ type TicketStatus = 'open' | 'pending' | 'resolved' | 'closed';
39
+ type TicketPriority = 'low' | 'normal' | 'high' | 'urgent';
40
+ type TicketChannel = 'web' | 'email' | 'whatsapp';
41
+ interface Ticket {
42
+ id: string;
43
+ accountId: string;
44
+ number: number;
45
+ subject: string;
46
+ status: TicketStatus;
47
+ priority: TicketPriority;
48
+ channel: TicketChannel;
49
+ requesterEmail: string | null;
50
+ requesterName: string | null;
51
+ requesterPhone?: string | null;
52
+ assigneeSub: string | null;
53
+ accessToken: string;
54
+ lastMessageAt: string;
55
+ createdAt: string;
56
+ updatedAt: string;
57
+ }
58
+ interface TicketMessage {
59
+ id: string;
60
+ ticketId: string;
61
+ authorType: 'agent' | 'requester';
62
+ authorSub?: string | null;
63
+ authorName: string | null;
64
+ body: string;
65
+ isInternal: boolean;
66
+ createdAt: string;
67
+ }
68
+ interface TicketWithMessages extends Ticket {
69
+ messages: TicketMessage[];
70
+ }
71
+ interface TicketList {
72
+ tickets: Ticket[];
73
+ /** Per-status counts for the whole workspace, e.g. `{ open: 3, pending: 1 }`. */
74
+ counts: Partial<Record<TicketStatus, number>>;
75
+ }
76
+ interface CannedReply {
77
+ id: string;
78
+ accountId: string;
79
+ title: string;
80
+ body: string;
81
+ createdAt: string;
82
+ updatedAt: string;
83
+ }
84
+ interface PublicTicketView {
85
+ number: number;
86
+ subject: string;
87
+ status: TicketStatus;
88
+ createdAt: string;
89
+ messages: Array<{
90
+ id: string;
91
+ authorType: 'agent' | 'requester';
92
+ authorName: string | null;
93
+ body: string;
94
+ createdAt: string;
95
+ }>;
96
+ }
97
+ interface SuppuoClientOptions {
98
+ /** Bearer access token (Huudis-minted JWT). Defaults to `SUPPUO_TOKEN`.
99
+ * Optional — the `public.*` surface works without one. */
100
+ token?: string;
101
+ /** Base URL override. Default `https://suppuo.com`. */
102
+ baseUrl?: string;
103
+ /** Per-request fetch timeout. Default 30s. */
104
+ timeoutMs?: number;
105
+ }
106
+ interface FetchArgs {
107
+ method: 'GET' | 'POST' | 'PATCH' | 'DELETE';
108
+ path: string;
109
+ body?: unknown;
110
+ query?: Record<string, string | number | undefined>;
111
+ /** Public endpoints skip the Authorization header entirely. */
112
+ noAuth?: boolean;
113
+ }
114
+ declare class SuppuoClient {
115
+ private readonly token;
116
+ private readonly baseUrl;
117
+ private readonly timeoutMs;
118
+ constructor(opts?: SuppuoClientOptions);
119
+ request<T>(args: FetchArgs): Promise<T>;
120
+ readonly tickets: {
121
+ /** GET /api/v1/tickets — newest-activity-first, with per-status counts. */
122
+ list: (params?: {
123
+ status?: TicketStatus | "all";
124
+ limit?: number;
125
+ }) => Promise<TicketList>;
126
+ /** GET /api/v1/tickets/:id — full ticket incl. message thread. */
127
+ get: (id: string) => Promise<TicketWithMessages>;
128
+ /** POST /api/v1/tickets — agent-logged ticket (e.g. arrived via WhatsApp). */
129
+ create: (input: {
130
+ subject: string;
131
+ body: string;
132
+ requesterEmail: string;
133
+ requesterName?: string;
134
+ priority?: TicketPriority;
135
+ channel?: TicketChannel;
136
+ }) => Promise<Ticket>;
137
+ /** POST /api/v1/tickets/:id/messages — agent reply (or internal note). */
138
+ reply: (id: string, input: {
139
+ body: string;
140
+ isInternal?: boolean;
141
+ authorName?: string;
142
+ }) => Promise<{
143
+ message: TicketMessage;
144
+ status: TicketStatus;
145
+ }>;
146
+ /** PATCH /api/v1/tickets/:id — status / priority / assignee. */
147
+ update: (id: string, patch: {
148
+ status?: TicketStatus;
149
+ priority?: TicketPriority;
150
+ assigneeSub?: string | null;
151
+ }) => Promise<Ticket>;
152
+ };
153
+ readonly cannedReplies: {
154
+ /** GET /api/v1/canned-replies */
155
+ list: () => Promise<{
156
+ cannedReplies: CannedReply[];
157
+ }>;
158
+ /** POST /api/v1/canned-replies */
159
+ create: (input: {
160
+ title: string;
161
+ body: string;
162
+ }) => Promise<CannedReply>;
163
+ /** PATCH /api/v1/canned-replies/:id */
164
+ update: (id: string, patch: {
165
+ title?: string;
166
+ body?: string;
167
+ }) => Promise<CannedReply>;
168
+ /** DELETE /api/v1/canned-replies/:id */
169
+ delete: (id: string) => Promise<{
170
+ deleted: boolean;
171
+ }>;
172
+ };
173
+ readonly public: {
174
+ /** POST /api/v1/public/tickets — submit to a workspace's hosted form.
175
+ * The returned `accessToken` is the requester's only credential. */
176
+ submitTicket: (input: {
177
+ accountId: string;
178
+ subject: string;
179
+ body: string;
180
+ email: string;
181
+ name?: string;
182
+ }) => Promise<{
183
+ number: number;
184
+ accessToken: string;
185
+ }>;
186
+ /** GET /api/v1/public/tickets/:accessToken — tokenized status view
187
+ * (public messages only; internal notes are never exposed). */
188
+ getTicket: (accessToken: string) => Promise<PublicTicketView>;
189
+ /** POST /api/v1/public/tickets/:accessToken/messages — requester reply. */
190
+ replyTicket: (accessToken: string, input: {
191
+ body: string;
192
+ }) => Promise<{
193
+ id: string;
194
+ status: TicketStatus;
195
+ }>;
196
+ };
197
+ }
198
+
199
+ export { type ApiEnvelope, type CannedReply, type PublicTicketView, SuppuoClient, type SuppuoClientOptions, SuppuoError, type Ticket, type TicketChannel, type TicketList, type TicketMessage, type TicketPriority, type TicketStatus, type TicketWithMessages };
@@ -0,0 +1,199 @@
1
+ /**
2
+ * Suppuo SDK — typed JS/TS client for the suppuo.com REST API.
3
+ * Sister to `forjio-suppuo` (Python) and `hachimi-cat/suppuo-go` (Go).
4
+ *
5
+ * Auth = Bearer JWT (a Huudis-minted access token). Pass `token` or set
6
+ * `SUPPUO_TOKEN`. The `public.*` surface (requester-facing hosted-form
7
+ * endpoints) needs no token at all.
8
+ *
9
+ * Every response rides the Forjio envelope `{ data, error, meta }`;
10
+ * the client unwraps it and throws `SuppuoError` (with the envelope's
11
+ * `error.code`) on failure.
12
+ */
13
+ interface ApiEnvelope<T> {
14
+ data: T | null;
15
+ error: {
16
+ code: string;
17
+ message: string;
18
+ param?: string;
19
+ docUrl?: string;
20
+ } | null;
21
+ meta?: {
22
+ requestId: string;
23
+ timestamp: string;
24
+ cursor?: string | null;
25
+ hasMore?: boolean;
26
+ };
27
+ }
28
+ declare class SuppuoError extends Error {
29
+ /** HTTP status (0 for transport-level failures). */
30
+ readonly status: number;
31
+ /** Envelope `error.code` (UPPER_SNAKE_CASE) or an SDK-side code
32
+ * (`NETWORK_ERROR`, `TIMEOUT`, `INVALID_RESPONSE`). */
33
+ readonly code: string;
34
+ readonly requestId: string | undefined;
35
+ readonly param: string | undefined;
36
+ constructor(status: number, code: string, message: string, requestId?: string, param?: string);
37
+ }
38
+ type TicketStatus = 'open' | 'pending' | 'resolved' | 'closed';
39
+ type TicketPriority = 'low' | 'normal' | 'high' | 'urgent';
40
+ type TicketChannel = 'web' | 'email' | 'whatsapp';
41
+ interface Ticket {
42
+ id: string;
43
+ accountId: string;
44
+ number: number;
45
+ subject: string;
46
+ status: TicketStatus;
47
+ priority: TicketPriority;
48
+ channel: TicketChannel;
49
+ requesterEmail: string | null;
50
+ requesterName: string | null;
51
+ requesterPhone?: string | null;
52
+ assigneeSub: string | null;
53
+ accessToken: string;
54
+ lastMessageAt: string;
55
+ createdAt: string;
56
+ updatedAt: string;
57
+ }
58
+ interface TicketMessage {
59
+ id: string;
60
+ ticketId: string;
61
+ authorType: 'agent' | 'requester';
62
+ authorSub?: string | null;
63
+ authorName: string | null;
64
+ body: string;
65
+ isInternal: boolean;
66
+ createdAt: string;
67
+ }
68
+ interface TicketWithMessages extends Ticket {
69
+ messages: TicketMessage[];
70
+ }
71
+ interface TicketList {
72
+ tickets: Ticket[];
73
+ /** Per-status counts for the whole workspace, e.g. `{ open: 3, pending: 1 }`. */
74
+ counts: Partial<Record<TicketStatus, number>>;
75
+ }
76
+ interface CannedReply {
77
+ id: string;
78
+ accountId: string;
79
+ title: string;
80
+ body: string;
81
+ createdAt: string;
82
+ updatedAt: string;
83
+ }
84
+ interface PublicTicketView {
85
+ number: number;
86
+ subject: string;
87
+ status: TicketStatus;
88
+ createdAt: string;
89
+ messages: Array<{
90
+ id: string;
91
+ authorType: 'agent' | 'requester';
92
+ authorName: string | null;
93
+ body: string;
94
+ createdAt: string;
95
+ }>;
96
+ }
97
+ interface SuppuoClientOptions {
98
+ /** Bearer access token (Huudis-minted JWT). Defaults to `SUPPUO_TOKEN`.
99
+ * Optional — the `public.*` surface works without one. */
100
+ token?: string;
101
+ /** Base URL override. Default `https://suppuo.com`. */
102
+ baseUrl?: string;
103
+ /** Per-request fetch timeout. Default 30s. */
104
+ timeoutMs?: number;
105
+ }
106
+ interface FetchArgs {
107
+ method: 'GET' | 'POST' | 'PATCH' | 'DELETE';
108
+ path: string;
109
+ body?: unknown;
110
+ query?: Record<string, string | number | undefined>;
111
+ /** Public endpoints skip the Authorization header entirely. */
112
+ noAuth?: boolean;
113
+ }
114
+ declare class SuppuoClient {
115
+ private readonly token;
116
+ private readonly baseUrl;
117
+ private readonly timeoutMs;
118
+ constructor(opts?: SuppuoClientOptions);
119
+ request<T>(args: FetchArgs): Promise<T>;
120
+ readonly tickets: {
121
+ /** GET /api/v1/tickets — newest-activity-first, with per-status counts. */
122
+ list: (params?: {
123
+ status?: TicketStatus | "all";
124
+ limit?: number;
125
+ }) => Promise<TicketList>;
126
+ /** GET /api/v1/tickets/:id — full ticket incl. message thread. */
127
+ get: (id: string) => Promise<TicketWithMessages>;
128
+ /** POST /api/v1/tickets — agent-logged ticket (e.g. arrived via WhatsApp). */
129
+ create: (input: {
130
+ subject: string;
131
+ body: string;
132
+ requesterEmail: string;
133
+ requesterName?: string;
134
+ priority?: TicketPriority;
135
+ channel?: TicketChannel;
136
+ }) => Promise<Ticket>;
137
+ /** POST /api/v1/tickets/:id/messages — agent reply (or internal note). */
138
+ reply: (id: string, input: {
139
+ body: string;
140
+ isInternal?: boolean;
141
+ authorName?: string;
142
+ }) => Promise<{
143
+ message: TicketMessage;
144
+ status: TicketStatus;
145
+ }>;
146
+ /** PATCH /api/v1/tickets/:id — status / priority / assignee. */
147
+ update: (id: string, patch: {
148
+ status?: TicketStatus;
149
+ priority?: TicketPriority;
150
+ assigneeSub?: string | null;
151
+ }) => Promise<Ticket>;
152
+ };
153
+ readonly cannedReplies: {
154
+ /** GET /api/v1/canned-replies */
155
+ list: () => Promise<{
156
+ cannedReplies: CannedReply[];
157
+ }>;
158
+ /** POST /api/v1/canned-replies */
159
+ create: (input: {
160
+ title: string;
161
+ body: string;
162
+ }) => Promise<CannedReply>;
163
+ /** PATCH /api/v1/canned-replies/:id */
164
+ update: (id: string, patch: {
165
+ title?: string;
166
+ body?: string;
167
+ }) => Promise<CannedReply>;
168
+ /** DELETE /api/v1/canned-replies/:id */
169
+ delete: (id: string) => Promise<{
170
+ deleted: boolean;
171
+ }>;
172
+ };
173
+ readonly public: {
174
+ /** POST /api/v1/public/tickets — submit to a workspace's hosted form.
175
+ * The returned `accessToken` is the requester's only credential. */
176
+ submitTicket: (input: {
177
+ accountId: string;
178
+ subject: string;
179
+ body: string;
180
+ email: string;
181
+ name?: string;
182
+ }) => Promise<{
183
+ number: number;
184
+ accessToken: string;
185
+ }>;
186
+ /** GET /api/v1/public/tickets/:accessToken — tokenized status view
187
+ * (public messages only; internal notes are never exposed). */
188
+ getTicket: (accessToken: string) => Promise<PublicTicketView>;
189
+ /** POST /api/v1/public/tickets/:accessToken/messages — requester reply. */
190
+ replyTicket: (accessToken: string, input: {
191
+ body: string;
192
+ }) => Promise<{
193
+ id: string;
194
+ status: TicketStatus;
195
+ }>;
196
+ };
197
+ }
198
+
199
+ export { type ApiEnvelope, type CannedReply, type PublicTicketView, SuppuoClient, type SuppuoClientOptions, SuppuoError, type Ticket, type TicketChannel, type TicketList, type TicketMessage, type TicketPriority, type TicketStatus, type TicketWithMessages };
package/dist/index.js ADDED
@@ -0,0 +1,141 @@
1
+ // src/index.ts
2
+ var SuppuoError = class extends Error {
3
+ /** HTTP status (0 for transport-level failures). */
4
+ status;
5
+ /** Envelope `error.code` (UPPER_SNAKE_CASE) or an SDK-side code
6
+ * (`NETWORK_ERROR`, `TIMEOUT`, `INVALID_RESPONSE`). */
7
+ code;
8
+ requestId;
9
+ param;
10
+ constructor(status, code, message, requestId, param) {
11
+ super(message);
12
+ this.name = "SuppuoError";
13
+ this.status = status;
14
+ this.code = code;
15
+ this.requestId = requestId;
16
+ this.param = param;
17
+ }
18
+ };
19
+ var SuppuoClient = class {
20
+ token;
21
+ baseUrl;
22
+ timeoutMs;
23
+ constructor(opts = {}) {
24
+ this.token = opts.token ?? process.env.SUPPUO_TOKEN ?? void 0;
25
+ this.baseUrl = (opts.baseUrl ?? "https://suppuo.com").replace(/\/+$/, "");
26
+ this.timeoutMs = opts.timeoutMs ?? 3e4;
27
+ }
28
+ async request(args) {
29
+ const url = new URL(this.baseUrl + args.path);
30
+ for (const [k, v] of Object.entries(args.query ?? {})) {
31
+ if (v !== void 0) url.searchParams.set(k, String(v));
32
+ }
33
+ const headers = { Accept: "application/json" };
34
+ if (!args.noAuth) {
35
+ if (!this.token) {
36
+ throw new SuppuoError(
37
+ 0,
38
+ "AUTH_REQUIRED",
39
+ "No token configured. Pass `token` or set SUPPUO_TOKEN."
40
+ );
41
+ }
42
+ headers["Authorization"] = `Bearer ${this.token}`;
43
+ }
44
+ if (args.body !== void 0) headers["Content-Type"] = "application/json";
45
+ let res;
46
+ try {
47
+ res = await fetch(url, {
48
+ method: args.method,
49
+ headers,
50
+ body: args.body !== void 0 ? JSON.stringify(args.body) : void 0,
51
+ signal: AbortSignal.timeout(this.timeoutMs)
52
+ });
53
+ } catch (e) {
54
+ if (e instanceof Error && e.name === "TimeoutError") {
55
+ throw new SuppuoError(0, "TIMEOUT", `request timed out after ${this.timeoutMs}ms`);
56
+ }
57
+ throw new SuppuoError(0, "NETWORK_ERROR", e instanceof Error ? e.message : String(e));
58
+ }
59
+ let envelope;
60
+ try {
61
+ envelope = await res.json();
62
+ } catch {
63
+ throw new SuppuoError(res.status, "INVALID_RESPONSE", `non-JSON response (HTTP ${res.status})`);
64
+ }
65
+ if (!res.ok || envelope.error) {
66
+ const err = envelope.error;
67
+ throw new SuppuoError(
68
+ res.status,
69
+ err?.code ?? "UNKNOWN",
70
+ err?.message ?? `HTTP ${res.status}`,
71
+ envelope.meta?.requestId,
72
+ err?.param
73
+ );
74
+ }
75
+ return envelope.data;
76
+ }
77
+ // ─── Tickets (agent workspace surface, Bearer auth) ───────────────
78
+ tickets = {
79
+ /** GET /api/v1/tickets — newest-activity-first, with per-status counts. */
80
+ list: (params) => this.request({
81
+ method: "GET",
82
+ path: "/api/v1/tickets",
83
+ query: { status: params?.status, limit: params?.limit }
84
+ }),
85
+ /** GET /api/v1/tickets/:id — full ticket incl. message thread. */
86
+ get: (id) => this.request({ method: "GET", path: `/api/v1/tickets/${encodeURIComponent(id)}` }),
87
+ /** POST /api/v1/tickets — agent-logged ticket (e.g. arrived via WhatsApp). */
88
+ create: (input) => this.request({ method: "POST", path: "/api/v1/tickets", body: input }),
89
+ /** POST /api/v1/tickets/:id/messages — agent reply (or internal note). */
90
+ reply: (id, input) => this.request({
91
+ method: "POST",
92
+ path: `/api/v1/tickets/${encodeURIComponent(id)}/messages`,
93
+ body: input
94
+ }),
95
+ /** PATCH /api/v1/tickets/:id — status / priority / assignee. */
96
+ update: (id, patch) => this.request({
97
+ method: "PATCH",
98
+ path: `/api/v1/tickets/${encodeURIComponent(id)}`,
99
+ body: patch
100
+ })
101
+ };
102
+ // ─── Canned replies ────────────────────────────────────────────────
103
+ cannedReplies = {
104
+ /** GET /api/v1/canned-replies */
105
+ list: () => this.request({ method: "GET", path: "/api/v1/canned-replies" }),
106
+ /** POST /api/v1/canned-replies */
107
+ create: (input) => this.request({ method: "POST", path: "/api/v1/canned-replies", body: input }),
108
+ /** PATCH /api/v1/canned-replies/:id */
109
+ update: (id, patch) => this.request({
110
+ method: "PATCH",
111
+ path: `/api/v1/canned-replies/${encodeURIComponent(id)}`,
112
+ body: patch
113
+ }),
114
+ /** DELETE /api/v1/canned-replies/:id */
115
+ delete: (id) => this.request({ method: "DELETE", path: `/api/v1/canned-replies/${encodeURIComponent(id)}` })
116
+ };
117
+ // ─── Public (requester-facing, unauthenticated) ────────────────────
118
+ public = {
119
+ /** POST /api/v1/public/tickets — submit to a workspace's hosted form.
120
+ * The returned `accessToken` is the requester's only credential. */
121
+ submitTicket: (input) => this.request({ method: "POST", path: "/api/v1/public/tickets", body: input, noAuth: true }),
122
+ /** GET /api/v1/public/tickets/:accessToken — tokenized status view
123
+ * (public messages only; internal notes are never exposed). */
124
+ getTicket: (accessToken) => this.request({
125
+ method: "GET",
126
+ path: `/api/v1/public/tickets/${encodeURIComponent(accessToken)}`,
127
+ noAuth: true
128
+ }),
129
+ /** POST /api/v1/public/tickets/:accessToken/messages — requester reply. */
130
+ replyTicket: (accessToken, input) => this.request({
131
+ method: "POST",
132
+ path: `/api/v1/public/tickets/${encodeURIComponent(accessToken)}/messages`,
133
+ body: input,
134
+ noAuth: true
135
+ })
136
+ };
137
+ };
138
+ export {
139
+ SuppuoClient,
140
+ SuppuoError
141
+ };
package/package.json ADDED
@@ -0,0 +1,38 @@
1
+ {
2
+ "name": "@forjio/suppuo",
3
+ "version": "0.1.0",
4
+ "description": "Suppuo SDK — typed JS/TS client for the suppuo.com REST API. Sister to the Python + Go SDKs.",
5
+ "license": "UNLICENSED",
6
+ "private": false,
7
+ "author": "Forjio <support@forjio.com>",
8
+ "homepage": "https://suppuo.com/docs/sdk/js",
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "git+https://github.com/hachimi-cat/saas-suppuo.git",
12
+ "directory": "sdks/js"
13
+ },
14
+ "type": "module",
15
+ "main": "./dist/index.cjs",
16
+ "module": "./dist/index.js",
17
+ "types": "./dist/index.d.ts",
18
+ "exports": {
19
+ ".": { "types": "./dist/index.d.ts", "import": "./dist/index.js", "require": "./dist/index.cjs" }
20
+ },
21
+ "files": ["dist", "README.md"],
22
+ "scripts": {
23
+ "build": "tsup src/index.ts --format esm,cjs --dts --clean",
24
+ "test": "vitest run",
25
+ "type-check": "tsc --noEmit",
26
+ "prepublishOnly": "npm run build"
27
+ },
28
+ "engines": {
29
+ "node": ">=20.0.0"
30
+ },
31
+ "keywords": ["suppuo", "forjio", "helpdesk", "tickets", "support", "sdk"],
32
+ "devDependencies": {
33
+ "@types/node": "^20.0.0",
34
+ "tsup": "^8.3.0",
35
+ "typescript": "^5.5.0",
36
+ "vitest": "^2.0.0"
37
+ }
38
+ }