@chatlychat/sdk-node 0.1.0 → 0.2.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/dist/index.d.ts CHANGED
@@ -1,34 +1,23 @@
1
- export interface ClientOptions {
2
- apiKey: string;
3
- apiUrl?: string;
4
- timeoutMs?: number;
5
- fetchFn?: typeof fetch;
6
- }
7
- export interface PageQuery {
8
- cursor?: string;
9
- limit?: number;
10
- }
11
- export interface Page<T> {
12
- items: ReadonlyArray<T>;
13
- nextCursor: string | null;
14
- }
15
- /** Query for {@link ChatlyClient.events}.list — a page, optionally one contact's. */
16
- export interface EventListQuery extends PageQuery {
17
- /** Restrict the stream to one contact. Omit for the whole workspace. */
18
- contactId?: string;
19
- }
20
- export interface ContactInput {
21
- externalId?: string;
22
- email?: string;
23
- phone?: string;
24
- name?: string;
25
- attributes?: Record<string, unknown>;
26
- }
27
- export interface MessageInput {
28
- conversationId: string;
29
- body: string;
30
- kind?: 'text' | 'rich' | 'note' | 'system_event';
31
- }
1
+ /**
2
+ * `@chatlychat/sdk-node` — the official Node.js SDK for the Chatly API.
3
+ *
4
+ * Authenticate with a workspace API key (`ck_…`) from server-side code. This
5
+ * is NOT the browser SDK: the key is a bearer credential for the whole
6
+ * workspace and must never be shipped to a browser. For in-page work use
7
+ * `@chatlychat/sdk-js`.
8
+ *
9
+ * The route surface is `packages/sdk-spec/surface.json`, which all ten Chatly
10
+ * SDKs implement; the behavioural contract is `packages/sdk-spec/CONTRACT.md`.
11
+ */
12
+ import { billingResource, channelsResource, contactsResource, conversationsResource, eventsResource, kbResource, macrosResource, reportsResource, searchResource, ticketsResource, webhooksResource, workspaceResource } from './resources.js';
13
+ import { Transport, type Query, type TransportOptions } from './transport.js';
14
+ export type ClientOptions = TransportOptions;
15
+ export { ChatlyError, ChatlyApiError, ChatlyAuthenticationError, ChatlyPermissionError, ChatlyNotFoundError, ChatlyConflictError, ChatlyValidationError, ChatlyRateLimitError, ChatlyServerError, ChatlyTransportError, } from './errors.js';
16
+ export { DEFAULT_API_URL, DEFAULT_RETRY, SDK_VERSION, type Query, type RetryPolicy } from './transport.js';
17
+ export { autoPaginate, type Page, type PageQuery } from './pagination.js';
18
+ export { verifyWebhookSignature, WEBHOOK_SIGNATURE_TOLERANCE_SECONDS, type VerifyWebhookSignatureInput, } from './webhooks.js';
19
+ export { chatlyIdentityHash } from './identity.js';
20
+ export type { AgentHandlingInput, ContactInput, EventListQuery, MergeContactInput, MergeTicketsInput, MessageInput, ScheduleMessageInput, UnsubscribeContactInput, TrackEventInput, Unmodelled, } from './resources.js';
32
21
  /**
33
22
  * Reaction payload.
34
23
  *
@@ -52,32 +41,32 @@ export interface MessageReactions {
52
41
  */
53
42
  export type WidgetFeatureConfig = Record<string, unknown>;
54
43
  export declare class ChatlyClient {
55
- private readonly base;
56
- private readonly baseHeaders;
57
- private readonly timeoutMs;
58
- private readonly fetchFn;
59
- /**
60
- * When set, every write made through this client instance uses this
61
- * exact `Idempotency-Key` instead of minting a fresh UUID per call.
62
- * Created via {@link withIdempotencyKey}; the original client is left
63
- * untouched (the method returns a derived view, not a mutation).
64
- */
65
- private readonly pinnedIdempotencyKey;
44
+ private readonly transport;
45
+ readonly contacts: ReturnType<typeof contactsResource>;
46
+ readonly conversations: ReturnType<typeof conversationsResource>;
47
+ readonly tickets: ReturnType<typeof ticketsResource>;
48
+ readonly macros: ReturnType<typeof macrosResource>;
49
+ readonly kb: ReturnType<typeof kbResource>;
50
+ readonly search: ReturnType<typeof searchResource>;
51
+ readonly events: ReturnType<typeof eventsResource>;
52
+ readonly webhooks: ReturnType<typeof webhooksResource>;
53
+ readonly reports: ReturnType<typeof reportsResource>;
54
+ readonly channels: ReturnType<typeof channelsResource>;
55
+ readonly billing: ReturnType<typeof billingResource>;
56
+ readonly workspace: ReturnType<typeof workspaceResource>;
66
57
  constructor(opts: ClientOptions);
67
- /**
68
- * Private clone constructor — used by {@link withIdempotencyKey} so we
69
- * can return a derived client without mutating the original (and
70
- * without re-running the public ctor's defaulting logic).
71
- */
72
- private static derive;
58
+ constructor(transport: Transport);
73
59
  /**
74
60
  * Pin a specific `Idempotency-Key` for one or more upcoming writes.
75
61
  *
76
- * Returns a derived client (the original is unchanged). Use this when
77
- * you want to retry the *same* logical operation: the API treats two
78
- * writes with the same key + same body as a single accepted operation
79
- * and replays the cached response. Two writes with the same key but a
80
- * different body return HTTP 409.
62
+ * Returns a derived client (the original is unchanged). Use this when you
63
+ * want to retry the *same* logical operation across process boundaries: the
64
+ * API treats two writes with the same key + same body as a single accepted
65
+ * operation and replays the cached response. Two writes with the same key
66
+ * but a different body return HTTP 409.
67
+ *
68
+ * You do not need this for ordinary retries — the SDK already reuses one key
69
+ * across its own attempts of a single call.
81
70
  *
82
71
  * ```ts
83
72
  * const key = crypto.randomUUID();
@@ -106,120 +95,17 @@ export declare class ChatlyClient {
106
95
  workspaceId: string;
107
96
  scopes: readonly string[];
108
97
  }>;
109
- contacts: {
110
- list: (q?: PageQuery) => Promise<Page<unknown>>;
111
- upsert: (body: ContactInput) => Promise<unknown>;
112
- };
113
- conversations: {
114
- list: (q?: PageQuery) => Promise<Page<unknown>>;
115
- /**
116
- * Read a thread before replying to it.
117
- *
118
- * The MCP server has exposed this as `get_messages` since it shipped and
119
- * the SDK did not, so an integrator writing "read the conversation, then
120
- * decide" against the SDK had to hand-roll the request while the same
121
- * capability sat behind a tool call.
122
- */
123
- messages: (conversationId: string, q?: PageQuery) => Promise<Page<unknown>>;
124
- send: (body: MessageInput) => Promise<unknown>;
125
- };
126
98
  /**
127
- * Reactions and channel configuration are deliberately absent.
99
+ * Call any route, including the 85 key-accessible ones with no typed method
100
+ * here. Goes through the same auth, idempotency, retry and error handling.
128
101
  *
129
- * Both were here and neither worked: a reaction records *who* reacted, and
130
- * a key is not a person, so those routes require a signed-in user and
131
- * return 403 to any key. Channel configuration needs `channels:write`,
132
- * which no key-accessible route grants. Shipping methods that always fail
133
- * is worse than not shipping them — the failure arrives at runtime, in
134
- * production, in someone else's integration.
135
- */
136
- /** Full-text search across contacts, conversations, messages, tickets, KB. */
137
- search: {
138
- query: (q: string, opts?: Record<string, unknown>) => Promise<unknown>;
139
- /**
140
- * Help-centre articles only.
141
- *
142
- * The same `/v1/search` route narrowed to `kinds[]=kb`, which is exactly
143
- * what the MCP server's `search_kb` tool does. Worth its own method
144
- * because "find the article that answers this" is the common case and
145
- * getting there otherwise means knowing the `kinds[]` parameter exists.
146
- */
147
- kb: (q: string, opts?: Record<string, unknown>) => Promise<unknown>;
148
- };
149
- events: {
150
- track: (input: {
151
- contactId: string;
152
- name: string;
153
- properties?: Record<string, unknown>;
154
- }) => Promise<unknown>;
155
- /**
156
- * Read back what was tracked — the same `/v1/cdp/events` collection
157
- * {@link track} writes to, cursor-paginated newest-first.
158
- *
159
- * The SDK could write events and never read them, so "did my tracking
160
- * call actually land?" had no answer outside the dashboard. Pass
161
- * `contactId` to scope the stream to one person.
162
- */
163
- list: (q?: EventListQuery) => Promise<Page<unknown>>;
164
- };
165
- private get;
166
- private post;
167
- /** PATCH. Same idempotency contract as `post` — it is a write. */
168
- private patch;
169
- private dispatch;
170
- }
171
- /** Freshness window for `t`, in seconds. Matches what the docs recommend. */
172
- export declare const WEBHOOK_SIGNATURE_TOLERANCE_SECONDS = 300;
173
- export interface VerifyWebhookSignatureInput {
174
- /**
175
- * The EXACT bytes we POSTed. Pass the raw body — re-serialising parsed JSON
176
- * reorders keys and changes whitespace, and the digest will not match.
177
- */
178
- body: string | Uint8Array;
179
- /** The `x-livechat-signature` header value: `t=<unix seconds>,v1=<hex>`. */
180
- signatureHeader: string;
181
- /** The endpoint's `whsec_…` signing secret. */
182
- secret: string;
183
- /**
184
- * How far `t` may be from now before the delivery is treated as a replay.
185
- * Defaults to {@link WEBHOOK_SIGNATURE_TOLERANCE_SECONDS}; pass `Infinity`
186
- * only when re-verifying an archived delivery.
102
+ * ```ts
103
+ * const flags = await chatly.request<Flags>('GET', '/v1/feature-flags');
104
+ * ```
187
105
  */
188
- toleranceSeconds?: number;
189
- /** Escape hatch for tests. Unix seconds. */
190
- nowSeconds?: number;
106
+ request<T>(method: 'GET' | 'POST' | 'PATCH' | 'PUT' | 'DELETE', path: string, options?: {
107
+ query?: Query;
108
+ body?: unknown;
109
+ }): Promise<T>;
191
110
  }
192
- /**
193
- * Verify the signature header on a webhook delivery from the platform.
194
- *
195
- * `v1` is HMAC-SHA256 over `` `${t}.${body}` `` — the timestamp is bound INTO
196
- * the digest, which is what makes the replay window enforceable at all.
197
- *
198
- * This used to HMAC the body ALONE, so it rejected every genuine delivery the
199
- * platform sent: the producer has always signed `${t}.${body}` and our own
200
- * published documentation has always described that construction. Anyone
201
- * following either the SDK or the docs got a hard "invalid signature" on 100%
202
- * of real traffic. The wire format is unchanged — only this verifier was wrong.
203
- */
204
- export declare function verifyWebhookSignature(input: VerifyWebhookSignatureInput): boolean;
205
- /**
206
- * Compute the `userHash` the widget needs for identity verification.
207
- *
208
- * When a workspace turns identity verification on, the widget must prove the
209
- * `externalId` it claims was issued by the customer's backend rather than
210
- * typed into a browser console -- otherwise anyone can open a chat as anyone
211
- * else's account. This is the backend half of that handshake:
212
- *
213
- * const userHash = chatlyIdentityHash(process.env.CHATLY_IDV_SECRET, user.id);
214
- * // then render:
215
- * // Chatly('identify', { externalId: user.id, userHash, email: user.email })
216
- *
217
- * It lived in the API's own source as a comment-only snippet, where the
218
- * people who need it -- customers integrating their backend -- could never
219
- * import it.
220
- *
221
- * The secret is the workspace's identity-verification secret. It must never
222
- * reach the browser; only the digest does.
223
- */
224
- export declare function chatlyIdentityHash(secret: string, externalId: string): string;
225
111
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAOA,MAAM,WAAW,aAAa;IAC5B,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,OAAO,KAAK,CAAC;CACxB;AAED,MAAM,WAAW,SAAS;IACxB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,IAAI,CAAC,CAAC;IACrB,KAAK,EAAE,aAAa,CAAC,CAAC,CAAC,CAAC;IACxB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;CAC3B;AAED,qFAAqF;AACrF,MAAM,WAAW,cAAe,SAAQ,SAAS;IAC/C,wEAAwE;IACxE,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,YAAY;IAC3B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACtC;AAED,MAAM,WAAW,YAAY;IAC3B,cAAc,EAAE,MAAM,CAAC;IACvB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,MAAM,GAAG,cAAc,CAAC;CAClD;AAED;;;;;;;GAOG;AACH,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,OAAO,EAAE,aAAa,CAAC;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,OAAO,CAAA;KAAE,CAAC,CAAC;CACrF;AAED;;;;GAIG;AACH,MAAM,MAAM,mBAAmB,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AAE1D,qBAAa,YAAY;IACvB,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAS;IAC9B,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAU;IACtC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;IACnC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAe;IACvC;;;;;OAKG;IACH,OAAO,CAAC,QAAQ,CAAC,oBAAoB,CAAgB;gBAElC,IAAI,EAAE,aAAa;IA0BtC;;;;OAIG;IACH,OAAO,CAAC,MAAM,CAAC,MAAM;IA4CrB;;;;;;;;;;;;;;;;;OAiBG;IACI,kBAAkB,CAAC,GAAG,EAAE,MAAM,GAAG,YAAY;IAOpD;;;;;;;;;;;;OAYG;IACU,MAAM,IAAI,OAAO,CAAC;QAAE,WAAW,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAA;KAAE,CAAC;IAI3E,QAAQ;mBACH,SAAS;uBACJ,YAAY;MAC3B;IAEK,aAAa;mBACR,SAAS;QACnB;;;;;;;WAOG;mCACwB,MAAM,MAAK,SAAS;qBAElC,YAAY;MAEzB;IAEF;;;;;;;;;OASG;IAEH,8EAA8E;IACvE,MAAM;mBACA,MAAM,SAAQ,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;QAEhD;;;;;;;WAOG;gBACK,MAAM,SAAQ,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;MAE7C;IAEK,MAAM;uBACI;YAAE,SAAS,EAAE,MAAM,CAAC;YAAC,IAAI,EAAE,MAAM,CAAC;YAAC,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;SAAE;QAExF;;;;;;;WAOG;mBACO,cAAc;MACxB;YAEY,GAAG;YAUH,IAAI;IAclB,kEAAkE;YACpD,KAAK;YASL,QAAQ;CA+BvB;AAED,6EAA6E;AAC7E,eAAO,MAAM,mCAAmC,MAAM,CAAC;AAEvD,MAAM,WAAW,2BAA2B;IAC1C;;;OAGG;IACH,IAAI,EAAE,MAAM,GAAG,UAAU,CAAC;IAC1B,4EAA4E;IAC5E,eAAe,EAAE,MAAM,CAAC;IACxB,gDAAgD;IAChD,MAAM,EAAE,MAAM,CAAC;IACf;;;;OAIG;IACH,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,4CAA4C;IAC5C,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,2BAA2B,GAAG,OAAO,CAiClF;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,MAAM,CAQ7E"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,OAAO,EACL,eAAe,EACf,gBAAgB,EAChB,gBAAgB,EAChB,qBAAqB,EACrB,cAAc,EACd,UAAU,EACV,cAAc,EACd,eAAe,EACf,cAAc,EACd,eAAe,EACf,gBAAgB,EAChB,iBAAiB,EAClB,MAAM,gBAAgB,CAAC;AACxB,OAAO,EAAE,SAAS,EAAE,KAAK,KAAK,EAAE,KAAK,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAE9E,MAAM,MAAM,aAAa,GAAG,gBAAgB,CAAC;AAE7C,OAAO,EACL,WAAW,EACX,cAAc,EACd,yBAAyB,EACzB,qBAAqB,EACrB,mBAAmB,EACnB,mBAAmB,EACnB,qBAAqB,EACrB,oBAAoB,EACpB,iBAAiB,EACjB,oBAAoB,GACrB,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,eAAe,EAAE,aAAa,EAAE,WAAW,EAAE,KAAK,KAAK,EAAE,KAAK,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAC3G,OAAO,EAAE,YAAY,EAAE,KAAK,IAAI,EAAE,KAAK,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAC1E,OAAO,EACL,sBAAsB,EACtB,mCAAmC,EACnC,KAAK,2BAA2B,GACjC,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,kBAAkB,EAAE,MAAM,eAAe,CAAC;AACnD,YAAY,EACV,kBAAkB,EAClB,YAAY,EACZ,cAAc,EACd,iBAAiB,EACjB,iBAAiB,EACjB,YAAY,EACZ,oBAAoB,EACpB,uBAAuB,EACvB,eAAe,EACf,UAAU,GACX,MAAM,gBAAgB,CAAC;AAExB;;;;;;;GAOG;AACH,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,OAAO,EAAE,aAAa,CAAC;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,OAAO,CAAA;KAAE,CAAC,CAAC;CACrF;AAED;;;;GAIG;AACH,MAAM,MAAM,mBAAmB,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AAE1D,qBAAa,YAAY;IACvB,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAY;IAEtC,SAAgB,QAAQ,EAAE,UAAU,CAAC,OAAO,gBAAgB,CAAC,CAAC;IAC9D,SAAgB,aAAa,EAAE,UAAU,CAAC,OAAO,qBAAqB,CAAC,CAAC;IACxE,SAAgB,OAAO,EAAE,UAAU,CAAC,OAAO,eAAe,CAAC,CAAC;IAC5D,SAAgB,MAAM,EAAE,UAAU,CAAC,OAAO,cAAc,CAAC,CAAC;IAC1D,SAAgB,EAAE,EAAE,UAAU,CAAC,OAAO,UAAU,CAAC,CAAC;IAClD,SAAgB,MAAM,EAAE,UAAU,CAAC,OAAO,cAAc,CAAC,CAAC;IAC1D,SAAgB,MAAM,EAAE,UAAU,CAAC,OAAO,cAAc,CAAC,CAAC;IAC1D,SAAgB,QAAQ,EAAE,UAAU,CAAC,OAAO,gBAAgB,CAAC,CAAC;IAC9D,SAAgB,OAAO,EAAE,UAAU,CAAC,OAAO,eAAe,CAAC,CAAC;IAC5D,SAAgB,QAAQ,EAAE,UAAU,CAAC,OAAO,gBAAgB,CAAC,CAAC;IAC9D,SAAgB,OAAO,EAAE,UAAU,CAAC,OAAO,eAAe,CAAC,CAAC;IAC5D,SAAgB,SAAS,EAAE,UAAU,CAAC,OAAO,iBAAiB,CAAC,CAAC;gBAE7C,IAAI,EAAE,aAAa;gBACnB,SAAS,EAAE,SAAS;IA8BvC;;;;;;;;;;;;;;;;;;;;OAoBG;IACI,kBAAkB,CAAC,GAAG,EAAE,MAAM,GAAG,YAAY;IAOpD;;;;;;;;;;;;OAYG;IACU,MAAM,IAAI,OAAO,CAAC;QAAE,WAAW,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAA;KAAE,CAAC;IAMlF;;;;;;;OAOG;IACU,OAAO,CAAC,CAAC,EACpB,MAAM,EAAE,KAAK,GAAG,MAAM,GAAG,OAAO,GAAG,KAAK,GAAG,QAAQ,EACnD,IAAI,EAAE,MAAM,EACZ,OAAO,GAAE;QAAE,KAAK,CAAC,EAAE,KAAK,CAAC;QAAC,IAAI,CAAC,EAAE,OAAO,CAAA;KAAO,GAC9C,OAAO,CAAC,CAAC,CAAC;CAGd"}
package/dist/index.js CHANGED
@@ -1,96 +1,74 @@
1
- import { createHmac, randomUUID, timingSafeEqual } from 'node:crypto';
2
- /** Node 14.17+ has `crypto.randomUUID`; the SDK targets Node 20+ so it's safe. */
3
- function nodeRandomUuid() {
4
- return randomUUID();
5
- }
1
+ /**
2
+ * `@chatlychat/sdk-node` — the official Node.js SDK for the Chatly API.
3
+ *
4
+ * Authenticate with a workspace API key (`ck_…`) from server-side code. This
5
+ * is NOT the browser SDK: the key is a bearer credential for the whole
6
+ * workspace and must never be shipped to a browser. For in-page work use
7
+ * `@chatlychat/sdk-js`.
8
+ *
9
+ * The route surface is `packages/sdk-spec/surface.json`, which all ten Chatly
10
+ * SDKs implement; the behavioural contract is `packages/sdk-spec/CONTRACT.md`.
11
+ */
12
+ import { billingResource, channelsResource, contactsResource, conversationsResource, eventsResource, kbResource, macrosResource, reportsResource, searchResource, ticketsResource, webhooksResource, workspaceResource, } from './resources.js';
13
+ import { Transport } from './transport.js';
14
+ export { ChatlyError, ChatlyApiError, ChatlyAuthenticationError, ChatlyPermissionError, ChatlyNotFoundError, ChatlyConflictError, ChatlyValidationError, ChatlyRateLimitError, ChatlyServerError, ChatlyTransportError, } from './errors.js';
15
+ export { DEFAULT_API_URL, DEFAULT_RETRY, SDK_VERSION } from './transport.js';
16
+ export { autoPaginate } from './pagination.js';
17
+ export { verifyWebhookSignature, WEBHOOK_SIGNATURE_TOLERANCE_SECONDS, } from './webhooks.js';
18
+ export { chatlyIdentityHash } from './identity.js';
6
19
  export class ChatlyClient {
7
- base;
8
- baseHeaders;
9
- timeoutMs;
10
- fetchFn;
11
- /**
12
- * When set, every write made through this client instance uses this
13
- * exact `Idempotency-Key` instead of minting a fresh UUID per call.
14
- * Created via {@link withIdempotencyKey}; the original client is left
15
- * untouched (the method returns a derived view, not a mutation).
16
- */
17
- pinnedIdempotencyKey;
18
- constructor(opts) {
20
+ transport;
21
+ contacts;
22
+ conversations;
23
+ tickets;
24
+ macros;
25
+ kb;
26
+ search;
27
+ events;
28
+ webhooks;
29
+ reports;
30
+ channels;
31
+ billing;
32
+ workspace;
33
+ constructor(optsOrTransport) {
19
34
  /*
20
- * Production, not a dev hostname.
21
- *
22
- * This was `https://api.livechat.local`, which resolves nowhere — so a
23
- * developer who followed the published docs and omitted `apiUrl` got an
24
- * SDK whose every call failed, and the docs page carried a warning
25
- * telling each reader to override it. Reported by a customer who
26
- * integrated from the docs alone, not caught here, because every test in
27
- * this package passes an explicit `apiUrl`.
35
+ * Every namespace is built from THIS instance's transport.
28
36
  *
29
- * A self-hosted install still overrides it; that is the only caller that
30
- * ever should.
37
+ * The previous design hand-wrote a `derive` clone that re-bound each
38
+ * namespace one by one, and forgot `search` — so a client returned by
39
+ * `withIdempotencyKey` had search methods still closed over the PARENT's
40
+ * dispatch. Harmless while both were reads against the same base URL, and
41
+ * a live bug the moment anything on `search` became a write. Building
42
+ * namespaces in the constructor from one transport makes the class of bug
43
+ * unreachable rather than tested for.
31
44
  */
32
- this.base = opts.apiUrl ?? 'https://api.chatlychat.com';
33
- this.baseHeaders = new Headers({
34
- authorization: `Bearer ${opts.apiKey}`,
35
- 'content-type': 'application/json',
36
- accept: 'application/json',
37
- 'user-agent': '@chatlychat/sdk-node/0.1.0',
38
- });
39
- this.timeoutMs = opts.timeoutMs ?? 15_000;
40
- this.fetchFn = opts.fetchFn ?? globalThis.fetch;
41
- this.pinnedIdempotencyKey = null;
42
- }
43
- /**
44
- * Private clone constructor — used by {@link withIdempotencyKey} so we
45
- * can return a derived client without mutating the original (and
46
- * without re-running the public ctor's defaulting logic).
47
- */
48
- static derive(parent, pinnedIdempotencyKey) {
49
- const child = Object.create(ChatlyClient.prototype);
50
- Object.assign(child, {
51
- base: parent.base,
52
- baseHeaders: parent.baseHeaders,
53
- timeoutMs: parent.timeoutMs,
54
- fetchFn: parent.fetchFn,
55
- pinnedIdempotencyKey,
56
- });
57
- // The route bags are field-initialized below the class body via the
58
- // arrow-property pattern; re-bind them so they target the derived
59
- // instance and call the child's `post`/`get`.
60
- child.contacts = {
61
- list: (q = {}) => child.get('/v1/contacts', { ...q }),
62
- upsert: (body) => child.post('/v1/contacts', body),
63
- };
64
- child.conversations = {
65
- list: (q = {}) => child.get('/v1/conversations', { ...q }),
66
- messages: (conversationId, q = {}) => child.get(`/v1/conversations/${conversationId}/messages`, { ...q }),
67
- send: (body) => child.post(`/v1/conversations/${body.conversationId}/messages`, body),
68
- };
69
- child.events = {
70
- track: (input) => child.post('/v1/cdp/events', input),
71
- list: (q = {}) => child.get('/v1/cdp/events', { ...q }),
72
- };
73
- // `search` was never rebound here, so a derived client's search methods
74
- // still called the PARENT's `get`. Harmless today because both are reads
75
- // against the same base URL, and a live bug the moment anything on
76
- // `search` becomes a write or the derived client differs by more than a
77
- // pinned key. `derivedSurfaceMatches` in the spec fails if any namespace
78
- // is forgotten again — TypeScript only caught `messages` because that
79
- // namespace was already rebound.
80
- child.search = {
81
- query: (q, opts = {}) => child.get('/v1/search', { q, ...opts }),
82
- kb: (q, opts = {}) => child.get('/v1/search', { q, 'kinds[]': 'kb', ...opts }),
83
- };
84
- return child;
45
+ this.transport =
46
+ optsOrTransport instanceof Transport ? optsOrTransport : new Transport(optsOrTransport);
47
+ const t = this.transport;
48
+ this.contacts = contactsResource(t);
49
+ this.conversations = conversationsResource(t);
50
+ this.tickets = ticketsResource(t);
51
+ this.macros = macrosResource(t);
52
+ this.kb = kbResource(t);
53
+ this.search = searchResource(t);
54
+ this.events = eventsResource(t);
55
+ this.webhooks = webhooksResource(t);
56
+ this.reports = reportsResource(t);
57
+ this.channels = channelsResource(t);
58
+ this.billing = billingResource(t);
59
+ this.workspace = workspaceResource(t);
85
60
  }
86
61
  /**
87
62
  * Pin a specific `Idempotency-Key` for one or more upcoming writes.
88
63
  *
89
- * Returns a derived client (the original is unchanged). Use this when
90
- * you want to retry the *same* logical operation: the API treats two
91
- * writes with the same key + same body as a single accepted operation
92
- * and replays the cached response. Two writes with the same key but a
93
- * different body return HTTP 409.
64
+ * Returns a derived client (the original is unchanged). Use this when you
65
+ * want to retry the *same* logical operation across process boundaries: the
66
+ * API treats two writes with the same key + same body as a single accepted
67
+ * operation and replays the cached response. Two writes with the same key
68
+ * but a different body return HTTP 409.
69
+ *
70
+ * You do not need this for ordinary retries — the SDK already reuses one key
71
+ * across its own attempts of a single call.
94
72
  *
95
73
  * ```ts
96
74
  * const key = crypto.randomUUID();
@@ -105,7 +83,7 @@ export class ChatlyClient {
105
83
  if (typeof key !== 'string' || key.length === 0) {
106
84
  throw new TypeError('withIdempotencyKey: key must be a non-empty string');
107
85
  }
108
- return ChatlyClient.derive(this, key);
86
+ return new ChatlyClient(this.transport.withPinnedKey(key));
109
87
  }
110
88
  /**
111
89
  * Describe the credential this client is using: which workspace it belongs
@@ -121,195 +99,18 @@ export class ChatlyClient {
121
99
  * there was no way for an integrator to discover it.
122
100
  */
123
101
  async whoami() {
124
- return this.get('/v1/api-keys/whoami', {});
102
+ return this.transport.get('/v1/api-keys/whoami');
125
103
  }
126
- contacts = {
127
- list: (q = {}) => this.get('/v1/contacts', { ...q }),
128
- upsert: (body) => this.post('/v1/contacts', body),
129
- };
130
- conversations = {
131
- list: (q = {}) => this.get('/v1/conversations', { ...q }),
132
- /**
133
- * Read a thread before replying to it.
134
- *
135
- * The MCP server has exposed this as `get_messages` since it shipped and
136
- * the SDK did not, so an integrator writing "read the conversation, then
137
- * decide" against the SDK had to hand-roll the request while the same
138
- * capability sat behind a tool call.
139
- */
140
- messages: (conversationId, q = {}) => this.get(`/v1/conversations/${conversationId}/messages`, { ...q }),
141
- send: (body) => this.post(`/v1/conversations/${body.conversationId}/messages`, body),
142
- };
143
104
  /**
144
- * Reactions and channel configuration are deliberately absent.
105
+ * Call any route, including the 85 key-accessible ones with no typed method
106
+ * here. Goes through the same auth, idempotency, retry and error handling.
145
107
  *
146
- * Both were here and neither worked: a reaction records *who* reacted, and
147
- * a key is not a person, so those routes require a signed-in user and
148
- * return 403 to any key. Channel configuration needs `channels:write`,
149
- * which no key-accessible route grants. Shipping methods that always fail
150
- * is worse than not shipping them — the failure arrives at runtime, in
151
- * production, in someone else's integration.
108
+ * ```ts
109
+ * const flags = await chatly.request<Flags>('GET', '/v1/feature-flags');
110
+ * ```
152
111
  */
153
- /** Full-text search across contacts, conversations, messages, tickets, KB. */
154
- search = {
155
- query: (q, opts = {}) => this.get('/v1/search', { q, ...opts }),
156
- /**
157
- * Help-centre articles only.
158
- *
159
- * The same `/v1/search` route narrowed to `kinds[]=kb`, which is exactly
160
- * what the MCP server's `search_kb` tool does. Worth its own method
161
- * because "find the article that answers this" is the common case and
162
- * getting there otherwise means knowing the `kinds[]` parameter exists.
163
- */
164
- kb: (q, opts = {}) => this.get('/v1/search', { q, 'kinds[]': 'kb', ...opts }),
165
- };
166
- events = {
167
- track: (input) => this.post('/v1/cdp/events', input),
168
- /**
169
- * Read back what was tracked — the same `/v1/cdp/events` collection
170
- * {@link track} writes to, cursor-paginated newest-first.
171
- *
172
- * The SDK could write events and never read them, so "did my tracking
173
- * call actually land?" had no answer outside the dashboard. Pass
174
- * `contactId` to scope the stream to one person.
175
- */
176
- list: (q = {}) => this.get('/v1/cdp/events', { ...q }),
177
- };
178
- async get(path, q) {
179
- const url = new URL(`${this.base}${path}`);
180
- for (const [k, v] of Object.entries(q)) {
181
- if (v !== undefined) {
182
- url.searchParams.set(k, String(v));
183
- }
184
- }
185
- return this.dispatch(url, { method: 'GET' });
186
- }
187
- async post(path, body) {
188
- // Production API requires Idempotency-Key on every write. The
189
- // `pinnedIdempotencyKey` path is the supported way to retry the
190
- // SAME logical operation (set via `withIdempotencyKey(key)`); when
191
- // unset we mint a fresh UUID per call so a fetch-level network
192
- // retry inside this SDK still won't double-write upstream.
193
- const idempotencyKey = this.pinnedIdempotencyKey ?? nodeRandomUuid();
194
- return this.dispatch(new URL(`${this.base}${path}`), {
195
- method: 'POST',
196
- body: JSON.stringify(body),
197
- headers: { 'idempotency-key': idempotencyKey },
198
- });
199
- }
200
- /** PATCH. Same idempotency contract as `post` — it is a write. */
201
- async patch(path, body) {
202
- const idempotencyKey = this.pinnedIdempotencyKey ?? nodeRandomUuid();
203
- return this.dispatch(new URL(`${this.base}${path}`), {
204
- method: 'PATCH',
205
- body: JSON.stringify(body),
206
- headers: { 'idempotency-key': idempotencyKey },
207
- });
208
- }
209
- async dispatch(url, init) {
210
- const ctrl = new AbortController();
211
- const timer = setTimeout(() => ctrl.abort(), this.timeoutMs);
212
- try {
213
- // Merge base auth/content headers with any per-call overrides
214
- // (e.g. `idempotency-key` from `post`). Previously this dropped
215
- // per-call headers because it reassigned `headers` wholesale —
216
- // which silently broke the idempotency contract on every write.
217
- const headers = new Headers(this.baseHeaders);
218
- if (init.headers !== undefined) {
219
- new Headers(init.headers).forEach((value, name) => {
220
- headers.set(name, value);
221
- });
222
- }
223
- const res = await this.fetchFn(url.toString(), {
224
- ...init,
225
- signal: ctrl.signal,
226
- headers,
227
- });
228
- if (!res.ok) {
229
- const text = await res.text();
230
- throw new Error(`livechat ${res.status}: ${text}`);
231
- }
232
- if (res.status === 204) {
233
- return undefined;
234
- }
235
- return (await res.json());
236
- }
237
- finally {
238
- clearTimeout(timer);
239
- }
240
- }
241
- }
242
- /** Freshness window for `t`, in seconds. Matches what the docs recommend. */
243
- export const WEBHOOK_SIGNATURE_TOLERANCE_SECONDS = 300;
244
- /**
245
- * Verify the signature header on a webhook delivery from the platform.
246
- *
247
- * `v1` is HMAC-SHA256 over `` `${t}.${body}` `` — the timestamp is bound INTO
248
- * the digest, which is what makes the replay window enforceable at all.
249
- *
250
- * This used to HMAC the body ALONE, so it rejected every genuine delivery the
251
- * platform sent: the producer has always signed `${t}.${body}` and our own
252
- * published documentation has always described that construction. Anyone
253
- * following either the SDK or the docs got a hard "invalid signature" on 100%
254
- * of real traffic. The wire format is unchanged — only this verifier was wrong.
255
- */
256
- export function verifyWebhookSignature(input) {
257
- const parts = new Map();
258
- for (const part of input.signatureHeader.split(',')) {
259
- const eq = part.indexOf('=');
260
- if (eq > 0) {
261
- parts.set(part.slice(0, eq).trim(), part.slice(eq + 1).trim());
262
- }
263
- }
264
- const rawTs = parts.get('t');
265
- const tag = parts.get('v1');
266
- if (rawTs === undefined || tag === undefined) {
267
- return false;
268
- }
269
- const issuedAt = Number.parseInt(rawTs, 10);
270
- if (!Number.isFinite(issuedAt)) {
271
- return false;
272
- }
273
- const tolerance = input.toleranceSeconds ?? WEBHOOK_SIGNATURE_TOLERANCE_SECONDS;
274
- const now = input.nowSeconds ?? Math.floor(Date.now() / 1000);
275
- if (Math.abs(now - issuedAt) > tolerance) {
276
- return false;
277
- }
278
- const hmac = createHmac('sha256', input.secret);
279
- hmac.update(`${rawTs}.`);
280
- hmac.update(typeof input.body === 'string' ? Buffer.from(input.body, 'utf8') : input.body);
281
- const a = Buffer.from(tag, 'hex');
282
- const b = Buffer.from(hmac.digest('hex'), 'hex');
283
- // Length check first: timingSafeEqual throws on a length mismatch, and a
284
- // thrown verifier is an accepted webhook in a handler that forgot try/catch.
285
- return a.length === b.length && timingSafeEqual(a, b);
286
- }
287
- /**
288
- * Compute the `userHash` the widget needs for identity verification.
289
- *
290
- * When a workspace turns identity verification on, the widget must prove the
291
- * `externalId` it claims was issued by the customer's backend rather than
292
- * typed into a browser console -- otherwise anyone can open a chat as anyone
293
- * else's account. This is the backend half of that handshake:
294
- *
295
- * const userHash = chatlyIdentityHash(process.env.CHATLY_IDV_SECRET, user.id);
296
- * // then render:
297
- * // Chatly('identify', { externalId: user.id, userHash, email: user.email })
298
- *
299
- * It lived in the API's own source as a comment-only snippet, where the
300
- * people who need it -- customers integrating their backend -- could never
301
- * import it.
302
- *
303
- * The secret is the workspace's identity-verification secret. It must never
304
- * reach the browser; only the digest does.
305
- */
306
- export function chatlyIdentityHash(secret, externalId) {
307
- if (secret.length === 0) {
308
- throw new Error('chatlyIdentityHash: secret is empty');
309
- }
310
- if (externalId.length === 0) {
311
- throw new Error('chatlyIdentityHash: externalId is empty');
112
+ async request(method, path, options = {}) {
113
+ return this.transport.send(method, path, options.query ?? {}, options.body);
312
114
  }
313
- return createHmac('sha256', secret).update(externalId).digest('hex');
314
115
  }
315
116
  //# sourceMappingURL=index.js.map