@chatlychat/sdk-node 0.0.0-stage → 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/CHANGELOG.md ADDED
@@ -0,0 +1,68 @@
1
+ # @chatlychat/sdk-node
2
+
3
+ ## 0.2.0
4
+
5
+ The typed surface goes from 9 methods to 55, and the SDK gains the behaviour
6
+ `packages/sdk-spec/CONTRACT.md` requires of every Chatly SDK.
7
+
8
+ ### Added
9
+
10
+ - **55 typed methods** across 13 namespaces — contacts, conversations,
11
+ tickets, macros, KB, search, CDP events, webhook deliveries, reports,
12
+ channels, billing and workspace — every one a route an API key can actually
13
+ call. The list is `packages/sdk-spec/surface.json`, shared with the Python,
14
+ Go, Ruby, PHP, Java, .NET and Rust SDKs, and each SDK proves its coverage by
15
+ executing every method and writing `surface.lock.json`.
16
+ - **A typed error hierarchy.** `ChatlyApiError` with `status`, `code`,
17
+ `message`, `requestId`, `body` and `retryAfter`, plus
18
+ `ChatlyAuthenticationError` (401), `ChatlyPermissionError` (403),
19
+ `ChatlyNotFoundError` (404), `ChatlyConflictError` (409),
20
+ `ChatlyValidationError` (400/422), `ChatlyRateLimitError` (429),
21
+ `ChatlyServerError` (5xx) and a separate `ChatlyTransportError` for a
22
+ request that never got a response. Previously every failure was
23
+ `new Error("livechat 403: …")`, so telling "wrong key" from "missing scope"
24
+ meant matching on a string.
25
+ - **Retries.** Three attempts by default on a connection failure, a 429 or a
26
+ 5xx, with exponential backoff and jitter, honouring `Retry-After`. Never on
27
+ any other 4xx. One idempotency key is reused across the attempts of a single
28
+ call, which is the only reason the header is safe to retry under.
29
+ - **Auto-pagination.** `listAll()` on every cursor-paginated list, as an async
30
+ generator. It stops on a null cursor and throws rather than looping forever
31
+ if the server repeats one.
32
+ - **`request(method, path, { query, body })`** — the escape hatch. 86 of the
33
+ 141 key-accessible routes have no typed method; this reaches all of them
34
+ through the same auth, idempotency, retry and error handling.
35
+ - `chatlyIdentityHash` and `verifyWebhookSignature` are unchanged and still
36
+ exported from the package root.
37
+
38
+ ### Fixed
39
+
40
+ - **Three writes sent key names the API discards.** zod strips unknown keys,
41
+ so each returned 200 having done nothing: `contacts.merge` sent
42
+ `{ sourceContactId }` where the schema wants `{ mergedId }`;
43
+ `contacts.addTags` sent `{ tags: [...] }` where the route takes one
44
+ `{ tag }`; `contacts.unsubscribe` sent `{}` where `unsubscribed` is
45
+ required. `tickets.merge` now takes `{ loserId }`.
46
+ - **`search.kb()` was not narrowing anything.** It sent `kinds[]=kb`, and the
47
+ search query schema is a zod object with a key named `kinds`, so the
48
+ bracketed key was stripped and the search ran across contacts,
49
+ conversations, messages and tickets — returning a valid 200 the caller could
50
+ not tell from a filtered one. It now sends a plain repeated `kinds`.
51
+ - **`retryAfter` was always null on a real 429.** The API's throttler names
52
+ its buckets and emits `Retry-After-{bucket}`, never a plain `Retry-After`.
53
+ The SDK reads both now and takes the longest; the API also derives the plain
54
+ header.
55
+ - **`MessageInput.kind` was missing `attachment_only`** — the one kind you
56
+ send when there are no words, only a file.
57
+ - Error bodies are read from the API's real nested envelope,
58
+ `{ error: { code, message } }`, with a flat body as a fallback for proxy
59
+ errors.
60
+ - `withIdempotencyKey()` no longer hand-rebuilds each namespace. The previous
61
+ clone forgot `search`, so a derived client's search methods still dispatched
62
+ through the parent — harmless while both were reads, a live bug the moment
63
+ one was not.
64
+ - The published tarball no longer contains the compiled test files.
65
+
66
+ ## 0.1.0
67
+
68
+ First release.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Chatly Engineering
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,108 @@
1
- # Temporary Holding Version
1
+ # @chatlychat/sdk-node
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Server-side client for the Chatly API. Authenticates with an API key, so it is
4
+ for your backend — not a browser, where the key would be readable.
5
+
6
+ ```bash
7
+ pnpm add @chatlychat/sdk-node
8
+ ```
9
+
10
+ ## Quick start
11
+
12
+ ```ts
13
+ import { ChatlyClient } from '@chatlychat/sdk-node';
14
+
15
+ const chatly = new ChatlyClient({
16
+ apiKey: process.env.CHATLY_API_KEY!, // ck_live_…
17
+ apiUrl: 'https://api.chatlychat.com', // optional
18
+ });
19
+
20
+ const contacts = await chatly.contacts.list({ limit: 20 });
21
+ ```
22
+
23
+ Create a key under **Workspace → Developer → API keys**. The token is shown
24
+ once and stored only as a hash — if you lose it, revoke and mint another.
25
+
26
+ Make `whoami()` your first call. It returns the workspace the key belongs to
27
+ and the scopes it actually holds, so a mis-scoped key fails loudly at
28
+ integration time instead of surfacing as a puzzling 403 from an unrelated
29
+ endpoint in production:
30
+
31
+ ```ts
32
+ const { workspaceId, scopes } = await chatly.whoami();
33
+ ```
34
+
35
+ It is the only api-keys route a key may call: minting or listing keys with a
36
+ key would let a leaked read-only key escalate itself.
37
+
38
+ ## What the client can do
39
+
40
+ Every method below maps to an endpoint that accepts key authentication. Scope
41
+ the key to only what your integration needs; a key can never do more than the
42
+ scopes ticked when it was created.
43
+
44
+ | Call | Scope required |
45
+ | ------------------------------------------------------ | -------------------- |
46
+ | `chatly.whoami()` | `workspace:read` |
47
+ | `chatly.contacts.list({ cursor?, limit? })` | `contacts:read` |
48
+ | `chatly.contacts.upsert({ email, name, … })` | `contacts:write` |
49
+ | `chatly.conversations.list({ cursor?, limit? })` | `conversations:read` |
50
+ | `chatly.conversations.messages(conversationId)` | `conversations:read` |
51
+ | `chatly.conversations.send({ conversationId, body })` | `messages:write` |
52
+ | `chatly.search.query('refund', { … })` | `search:read` |
53
+ | `chatly.search.kb('refund')` | `search:read` |
54
+ | `chatly.events.track({ contactId, name, properties })` | `contacts:write` |
55
+
56
+ Messages you send with a key are recorded as sent by an integration, not by
57
+ the person who created the key — a key outlives whoever issued it, so
58
+ attributing its writes to them would be wrong the moment they leave.
59
+
60
+ ## Not included, and why
61
+
62
+ Reactions and channel/widget configuration are absent on purpose. A reaction
63
+ records _who_ reacted and a key is not a person, so those endpoints require a
64
+ signed-in user. Channel configuration needs a scope no key can hold. Both used
65
+ to be here and both returned 403 to every caller.
66
+
67
+ ## Idempotency
68
+
69
+ Writes accept an idempotency key so a retry after a network failure does not
70
+ duplicate work:
71
+
72
+ ```ts
73
+ await chatly.withIdempotencyKey('order-4471-refund').contacts.upsert({ … });
74
+ ```
75
+
76
+ ## Verifying webhooks
77
+
78
+ ```ts
79
+ import { verifyWebhookSignature } from '@chatlychat/sdk-node';
80
+
81
+ const ok = verifyWebhookSignature({
82
+ body: rawBody, // the EXACT bytes, before JSON.parse
83
+ signatureHeader: req.headers['x-livechat-signature'] as string,
84
+ secret: process.env.CHATLY_WEBHOOK_SECRET!,
85
+ });
86
+ ```
87
+
88
+ Pass the raw body — re-serialising parsed JSON reorders keys and changes
89
+ whitespace, so the digest will not match.
90
+
91
+ The scheme is `t=<unix seconds>,v1=<hex>`, where `v1` is HMAC-SHA256 over
92
+ `` `${t}.${body}` ``. The timestamp is inside the signed string, so a captured
93
+ delivery stops verifying once it falls outside `toleranceSeconds`.
94
+
95
+ > **This example was wrong until 2026-10-12, in three ways** — it said
96
+ > `payload` (the field is `body`), `signature` (it is `signatureHeader`) and
97
+ > `x-chatly-signature` (webhooks use **`x-livechat-signature`**). In
98
+ > TypeScript the old snippet was a compile error; in plain JavaScript it was
99
+ > worse than returning `false`, because the verifier splits
100
+ > `input.signatureHeader` first and an absent one throws a `TypeError` out of
101
+ > your webhook handler.
102
+ >
103
+ > The header name is the trap worth knowing: **`x-chatly-signature` is real,
104
+ > but it belongs to four other surfaces** — external-agent deliveries, custom
105
+ > AI tool calls, customer cards and the workflow `fire_webhook` action — and
106
+ > each of those constructs its base string differently. Outbound event
107
+ > webhooks are the one surface using `x-livechat-signature`. Do not share one
108
+ > verifier across them without checking the construction.
@@ -0,0 +1,113 @@
1
+ /**
2
+ * The error model.
3
+ *
4
+ * This SDK used to throw `new Error(`livechat ${status}: ${text}`)` for every
5
+ * failure, which forced every caller into string matching to tell "the key is
6
+ * wrong" from "the key is missing a scope" from "the contact does not exist".
7
+ * Those three need different handling — re-auth, ask an admin, create it — and
8
+ * a caller that cannot distinguish them retries all three or none.
9
+ *
10
+ * Nothing here extends the monorepo's `AppError`: this package is published to
11
+ * people who must not install our internals, so it stays dependency-free by
12
+ * design.
13
+ */
14
+ /** The parsed body of an API error response, as far as it could be read. */
15
+ export interface ApiErrorShape {
16
+ readonly code?: string;
17
+ readonly message?: string;
18
+ }
19
+ /** Base for everything this SDK throws. `instanceof ChatlyError` catches all of it. */
20
+ export declare class ChatlyError extends Error {
21
+ readonly name: string;
22
+ constructor(message: string, options?: {
23
+ cause?: unknown;
24
+ });
25
+ }
26
+ /**
27
+ * The API answered, and the answer was a failure status.
28
+ *
29
+ * `requestId` is the `x-request-id` header. Quote it in a support request —
30
+ * it is the only way to find the exact call in our traces.
31
+ */
32
+ export declare class ChatlyApiError extends ChatlyError {
33
+ readonly name: string;
34
+ readonly status: number;
35
+ readonly code: string | null;
36
+ readonly requestId: string | null;
37
+ /** The raw response text. Everything `code`/`message` could not represent. */
38
+ readonly body: string;
39
+ /** Seconds the server asked us to wait, from `Retry-After`. Only on 429/503. */
40
+ readonly retryAfter: number | null;
41
+ constructor(input: {
42
+ status: number;
43
+ code: string | null;
44
+ message: string;
45
+ requestId: string | null;
46
+ body: string;
47
+ retryAfter: number | null;
48
+ });
49
+ }
50
+ /** 401 — the key is missing, malformed, revoked or belongs to no workspace. */
51
+ export declare class ChatlyAuthenticationError extends ChatlyApiError {
52
+ readonly name: string;
53
+ }
54
+ /** 403 — the key is valid and lacks the scope this route needs. Check `whoami()`. */
55
+ export declare class ChatlyPermissionError extends ChatlyApiError {
56
+ readonly name: string;
57
+ }
58
+ /** 404 — no such resource in this workspace. */
59
+ export declare class ChatlyNotFoundError extends ChatlyApiError {
60
+ readonly name: string;
61
+ }
62
+ /**
63
+ * 409 — most often an idempotency conflict: this `Idempotency-Key` was already
64
+ * used with a DIFFERENT body. Mint a new key, or send the original body.
65
+ */
66
+ export declare class ChatlyConflictError extends ChatlyApiError {
67
+ readonly name: string;
68
+ }
69
+ /** 422 (and 400) — the request body or query failed validation. */
70
+ export declare class ChatlyValidationError extends ChatlyApiError {
71
+ readonly name: string;
72
+ }
73
+ /** 429 — rate limited. `retryAfter` carries the server's own advice, in seconds. */
74
+ export declare class ChatlyRateLimitError extends ChatlyApiError {
75
+ readonly name: string;
76
+ }
77
+ /** 5xx — our problem. Safe to retry; the SDK already did, up to its limit. */
78
+ export declare class ChatlyServerError extends ChatlyApiError {
79
+ readonly name: string;
80
+ }
81
+ /**
82
+ * The request never produced an HTTP response: DNS, TCP, TLS, timeout, abort.
83
+ *
84
+ * Deliberately NOT a subclass of {@link ChatlyApiError} — there is no status
85
+ * to branch on, and code that does `err.status >= 500` must not silently read
86
+ * `undefined` here.
87
+ */
88
+ export declare class ChatlyTransportError extends ChatlyError {
89
+ readonly name: string;
90
+ constructor(message: string, cause?: unknown);
91
+ }
92
+ /** Seconds, from a `Retry-After` that may be a delta or an HTTP date. */
93
+ export declare function parseRetryAfter(header: string | null, nowMs: number): number | null;
94
+ /** Map a failure response onto the narrowest error class that fits. */
95
+ export declare function errorForResponse(input: {
96
+ status: number;
97
+ body: string;
98
+ requestId: string | null;
99
+ retryAfter: number | null;
100
+ }): ChatlyApiError;
101
+ /**
102
+ * The retry advice, from whichever spelling the server used.
103
+ *
104
+ * `@nestjs/throttler` names its buckets, so Chatly emits
105
+ * `Retry-After-{bucket}` and — until 2026-10-12 — never a plain
106
+ * `Retry-After`. Our own published docs said so. This SDK read only the plain
107
+ * header, so `retryAfter` was null on every real 429. The API now derives the
108
+ * plain header as well, but a deployment that has not taken that change still
109
+ * sends only the suffixed ones, so read both and take the LONGEST: backing
110
+ * off for the shortest bucket retries into a limit that is still closed.
111
+ */
112
+ export declare function retryAfterHeader(headers: Headers): string | null;
113
+ //# sourceMappingURL=errors.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,4EAA4E;AAC5E,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED,uFAAuF;AACvF,qBAAa,WAAY,SAAQ,KAAK;IACpC,SAAyB,IAAI,EAAE,MAAM,CAAiB;gBACnC,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,OAAO,CAAA;KAAE;CAGlE;AAED;;;;;GAKG;AACH,qBAAa,cAAe,SAAQ,WAAW;IAC7C,SAAyB,IAAI,EAAE,MAAM,CAAoB;IACzD,SAAgB,MAAM,EAAE,MAAM,CAAC;IAC/B,SAAgB,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpC,SAAgB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzC,8EAA8E;IAC9E,SAAgB,IAAI,EAAE,MAAM,CAAC;IAC7B,gFAAgF;IAChF,SAAgB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;gBAEvB,KAAK,EAAE;QACxB,MAAM,EAAE,MAAM,CAAC;QACf,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;QACpB,OAAO,EAAE,MAAM,CAAC;QAChB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;QACzB,IAAI,EAAE,MAAM,CAAC;QACb,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;KAC3B;CAQF;AAED,+EAA+E;AAC/E,qBAAa,yBAA0B,SAAQ,cAAc;IAC3D,SAAyB,IAAI,EAAE,MAAM,CAA+B;CACrE;AACD,qFAAqF;AACrF,qBAAa,qBAAsB,SAAQ,cAAc;IACvD,SAAyB,IAAI,EAAE,MAAM,CAA2B;CACjE;AACD,gDAAgD;AAChD,qBAAa,mBAAoB,SAAQ,cAAc;IACrD,SAAyB,IAAI,EAAE,MAAM,CAAyB;CAC/D;AACD;;;GAGG;AACH,qBAAa,mBAAoB,SAAQ,cAAc;IACrD,SAAyB,IAAI,EAAE,MAAM,CAAyB;CAC/D;AACD,mEAAmE;AACnE,qBAAa,qBAAsB,SAAQ,cAAc;IACvD,SAAyB,IAAI,EAAE,MAAM,CAA2B;CACjE;AACD,oFAAoF;AACpF,qBAAa,oBAAqB,SAAQ,cAAc;IACtD,SAAyB,IAAI,EAAE,MAAM,CAA0B;CAChE;AACD,8EAA8E;AAC9E,qBAAa,iBAAkB,SAAQ,cAAc;IACnD,SAAyB,IAAI,EAAE,MAAM,CAAuB;CAC7D;AAED;;;;;;GAMG;AACH,qBAAa,oBAAqB,SAAQ,WAAW;IACnD,SAAyB,IAAI,EAAE,MAAM,CAA0B;gBAC5C,OAAO,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,OAAO;CAGpD;AAED,yEAAyE;AACzE,wBAAgB,eAAe,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAanF;AAED,uEAAuE;AACvE,wBAAgB,gBAAgB,CAAC,KAAK,EAAE;IACtC,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;CAC3B,GAAG,cAAc,CAsCjB;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,OAAO,GAAG,MAAM,GAAG,IAAI,CAgBhE"}
package/dist/errors.js ADDED
@@ -0,0 +1,177 @@
1
+ /**
2
+ * The error model.
3
+ *
4
+ * This SDK used to throw `new Error(`livechat ${status}: ${text}`)` for every
5
+ * failure, which forced every caller into string matching to tell "the key is
6
+ * wrong" from "the key is missing a scope" from "the contact does not exist".
7
+ * Those three need different handling — re-auth, ask an admin, create it — and
8
+ * a caller that cannot distinguish them retries all three or none.
9
+ *
10
+ * Nothing here extends the monorepo's `AppError`: this package is published to
11
+ * people who must not install our internals, so it stays dependency-free by
12
+ * design.
13
+ */
14
+ /** Base for everything this SDK throws. `instanceof ChatlyError` catches all of it. */
15
+ export class ChatlyError extends Error {
16
+ name = 'ChatlyError';
17
+ constructor(message, options) {
18
+ super(message, options);
19
+ }
20
+ }
21
+ /**
22
+ * The API answered, and the answer was a failure status.
23
+ *
24
+ * `requestId` is the `x-request-id` header. Quote it in a support request —
25
+ * it is the only way to find the exact call in our traces.
26
+ */
27
+ export class ChatlyApiError extends ChatlyError {
28
+ name = 'ChatlyApiError';
29
+ status;
30
+ code;
31
+ requestId;
32
+ /** The raw response text. Everything `code`/`message` could not represent. */
33
+ body;
34
+ /** Seconds the server asked us to wait, from `Retry-After`. Only on 429/503. */
35
+ retryAfter;
36
+ constructor(input) {
37
+ super(input.message);
38
+ this.status = input.status;
39
+ this.code = input.code;
40
+ this.requestId = input.requestId;
41
+ this.body = input.body;
42
+ this.retryAfter = input.retryAfter;
43
+ }
44
+ }
45
+ /** 401 — the key is missing, malformed, revoked or belongs to no workspace. */
46
+ export class ChatlyAuthenticationError extends ChatlyApiError {
47
+ name = 'ChatlyAuthenticationError';
48
+ }
49
+ /** 403 — the key is valid and lacks the scope this route needs. Check `whoami()`. */
50
+ export class ChatlyPermissionError extends ChatlyApiError {
51
+ name = 'ChatlyPermissionError';
52
+ }
53
+ /** 404 — no such resource in this workspace. */
54
+ export class ChatlyNotFoundError extends ChatlyApiError {
55
+ name = 'ChatlyNotFoundError';
56
+ }
57
+ /**
58
+ * 409 — most often an idempotency conflict: this `Idempotency-Key` was already
59
+ * used with a DIFFERENT body. Mint a new key, or send the original body.
60
+ */
61
+ export class ChatlyConflictError extends ChatlyApiError {
62
+ name = 'ChatlyConflictError';
63
+ }
64
+ /** 422 (and 400) — the request body or query failed validation. */
65
+ export class ChatlyValidationError extends ChatlyApiError {
66
+ name = 'ChatlyValidationError';
67
+ }
68
+ /** 429 — rate limited. `retryAfter` carries the server's own advice, in seconds. */
69
+ export class ChatlyRateLimitError extends ChatlyApiError {
70
+ name = 'ChatlyRateLimitError';
71
+ }
72
+ /** 5xx — our problem. Safe to retry; the SDK already did, up to its limit. */
73
+ export class ChatlyServerError extends ChatlyApiError {
74
+ name = 'ChatlyServerError';
75
+ }
76
+ /**
77
+ * The request never produced an HTTP response: DNS, TCP, TLS, timeout, abort.
78
+ *
79
+ * Deliberately NOT a subclass of {@link ChatlyApiError} — there is no status
80
+ * to branch on, and code that does `err.status >= 500` must not silently read
81
+ * `undefined` here.
82
+ */
83
+ export class ChatlyTransportError extends ChatlyError {
84
+ name = 'ChatlyTransportError';
85
+ constructor(message, cause) {
86
+ super(message, { cause });
87
+ }
88
+ }
89
+ /** Seconds, from a `Retry-After` that may be a delta or an HTTP date. */
90
+ export function parseRetryAfter(header, nowMs) {
91
+ if (header === null || header.trim() === '') {
92
+ return null;
93
+ }
94
+ const seconds = Number(header.trim());
95
+ if (Number.isFinite(seconds) && seconds >= 0) {
96
+ return seconds;
97
+ }
98
+ const at = Date.parse(header);
99
+ if (Number.isFinite(at)) {
100
+ return Math.max(0, Math.round((at - nowMs) / 1000));
101
+ }
102
+ return null;
103
+ }
104
+ /** Map a failure response onto the narrowest error class that fits. */
105
+ export function errorForResponse(input) {
106
+ let parsed = {};
107
+ try {
108
+ const json = JSON.parse(input.body);
109
+ if (typeof json === 'object' && json !== null) {
110
+ // The API's envelope is `{ code, message }`, sometimes nested under `error`.
111
+ const outer = json;
112
+ const inner = typeof outer['error'] === 'object' && outer['error'] !== null
113
+ ? outer['error']
114
+ : outer;
115
+ parsed = {
116
+ code: typeof inner['code'] === 'string' ? inner['code'] : undefined,
117
+ message: typeof inner['message'] === 'string' ? inner['message'] : undefined,
118
+ };
119
+ }
120
+ }
121
+ catch {
122
+ // Not JSON — an HTML error page from a proxy, or an empty body. `body` keeps it.
123
+ }
124
+ const message = parsed.message ?? `Chatly API responded ${String(input.status)}`;
125
+ const args = {
126
+ status: input.status,
127
+ code: parsed.code ?? null,
128
+ message,
129
+ requestId: input.requestId,
130
+ body: input.body,
131
+ retryAfter: input.retryAfter,
132
+ };
133
+ if (input.status === 401)
134
+ return new ChatlyAuthenticationError(args);
135
+ if (input.status === 403)
136
+ return new ChatlyPermissionError(args);
137
+ if (input.status === 404)
138
+ return new ChatlyNotFoundError(args);
139
+ if (input.status === 409)
140
+ return new ChatlyConflictError(args);
141
+ if (input.status === 400 || input.status === 422)
142
+ return new ChatlyValidationError(args);
143
+ if (input.status === 429)
144
+ return new ChatlyRateLimitError(args);
145
+ if (input.status >= 500)
146
+ return new ChatlyServerError(args);
147
+ return new ChatlyApiError(args);
148
+ }
149
+ /**
150
+ * The retry advice, from whichever spelling the server used.
151
+ *
152
+ * `@nestjs/throttler` names its buckets, so Chatly emits
153
+ * `Retry-After-{bucket}` and — until 2026-10-12 — never a plain
154
+ * `Retry-After`. Our own published docs said so. This SDK read only the plain
155
+ * header, so `retryAfter` was null on every real 429. The API now derives the
156
+ * plain header as well, but a deployment that has not taken that change still
157
+ * sends only the suffixed ones, so read both and take the LONGEST: backing
158
+ * off for the shortest bucket retries into a limit that is still closed.
159
+ */
160
+ export function retryAfterHeader(headers) {
161
+ const plain = headers.get('retry-after');
162
+ if (plain !== null && plain.trim() !== '') {
163
+ return plain;
164
+ }
165
+ let longest = null;
166
+ headers.forEach((value, name) => {
167
+ if (!name.toLowerCase().startsWith('retry-after-')) {
168
+ return;
169
+ }
170
+ const seconds = Number(value);
171
+ if (Number.isFinite(seconds) && (longest === null || seconds > longest)) {
172
+ longest = seconds;
173
+ }
174
+ });
175
+ return longest === null ? null : String(longest);
176
+ }
177
+ //# sourceMappingURL=errors.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.js","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Compute the `userHash` the widget needs for identity verification.
3
+ *
4
+ * When a workspace turns identity verification on, the widget must prove the
5
+ * `externalId` it claims was issued by the customer's backend rather than
6
+ * typed into a browser console — otherwise anyone can open a chat as anyone
7
+ * else's account. This is the backend half of that handshake:
8
+ *
9
+ * const userHash = chatlyIdentityHash(process.env.CHATLY_IDV_SECRET, user.id);
10
+ * // then render:
11
+ * // Chatly('identify', { externalId: user.id, userHash, email: user.email })
12
+ *
13
+ * It lived in the API's own source as a comment-only snippet, where the
14
+ * people who need it — customers integrating their backend — could never
15
+ * import it.
16
+ *
17
+ * The secret is the workspace's identity-verification secret. It must never
18
+ * reach the browser; only the digest does.
19
+ */
20
+ export declare function chatlyIdentityHash(secret: string, externalId: string): string;
21
+ //# sourceMappingURL=identity.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"identity.d.ts","sourceRoot":"","sources":["../src/identity.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,MAAM,CAQ7E"}
@@ -0,0 +1,30 @@
1
+ import { createHmac } from 'node:crypto';
2
+ /**
3
+ * Compute the `userHash` the widget needs for identity verification.
4
+ *
5
+ * When a workspace turns identity verification on, the widget must prove the
6
+ * `externalId` it claims was issued by the customer's backend rather than
7
+ * typed into a browser console — otherwise anyone can open a chat as anyone
8
+ * else's account. This is the backend half of that handshake:
9
+ *
10
+ * const userHash = chatlyIdentityHash(process.env.CHATLY_IDV_SECRET, user.id);
11
+ * // then render:
12
+ * // Chatly('identify', { externalId: user.id, userHash, email: user.email })
13
+ *
14
+ * It lived in the API's own source as a comment-only snippet, where the
15
+ * people who need it — customers integrating their backend — could never
16
+ * import it.
17
+ *
18
+ * The secret is the workspace's identity-verification secret. It must never
19
+ * reach the browser; only the digest does.
20
+ */
21
+ export function chatlyIdentityHash(secret, externalId) {
22
+ if (secret.length === 0) {
23
+ throw new Error('chatlyIdentityHash: secret is empty');
24
+ }
25
+ if (externalId.length === 0) {
26
+ throw new Error('chatlyIdentityHash: externalId is empty');
27
+ }
28
+ return createHmac('sha256', secret).update(externalId).digest('hex');
29
+ }
30
+ //# sourceMappingURL=identity.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"identity.js","sourceRoot":"","sources":["../src/identity.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAEzC;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,UAAU,kBAAkB,CAAC,MAAc,EAAE,UAAkB;IACnE,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACxB,MAAM,IAAI,KAAK,CAAC,qCAAqC,CAAC,CAAC;IACzD,CAAC;IACD,IAAI,UAAU,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC5B,MAAM,IAAI,KAAK,CAAC,yCAAyC,CAAC,CAAC;IAC7D,CAAC;IACD,OAAO,UAAU,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;AACvE,CAAC"}