@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 +46 -0
- package/dist/index.cjs +167 -0
- package/dist/index.d.cts +199 -0
- package/dist/index.d.ts +199 -0
- package/dist/index.js +141 -0
- package/package.json +38 -0
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
|
+
});
|
package/dist/index.d.cts
ADDED
|
@@ -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.d.ts
ADDED
|
@@ -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
|
+
}
|