@brandfine/client 0.11.0 → 0.14.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.cts CHANGED
@@ -3,6 +3,110 @@ export { e as BrandfineNavItem, f as BrandfineNavItemType, g as BrandfineNavPost
3
3
  export { Cache, CacheOptions, KeyedCache, KeyedCacheOptions, createCache, createKeyedCache } from './cache/index.cjs';
4
4
  export { BrandfineWebhookEvent, BrandfineWebhookHandlerOptions, BrandfineWebhookPayload, createBrandfineWebhookHandler, parseWebhookPayload, verifyWebhookSecret } from './webhook/index.cjs';
5
5
 
6
+ /**
7
+ * Headless Live Chat session — the conversation WITHOUT the widget.
8
+ *
9
+ * `bf.liveChat.createSession()` gives a consumer everything needed to
10
+ * render chat inline in their own design system: transcript state, a
11
+ * send method, and a subscribe API shaped for
12
+ * `useSyncExternalStore` / Svelte stores / Vue refs. Brandfine keeps
13
+ * owning transport, identity, threading and storage; the consumer
14
+ * owns pixels.
15
+ *
16
+ * Storage keys are IDENTICAL to the floating widget's, so a visitor
17
+ * who talks through the widget on one page and an inline panel on
18
+ * another continues the same thread.
19
+ */
20
+ type ChatMessageSender = 'VISITOR' | 'AGENT' | 'SYSTEM';
21
+ type ChatMessage = {
22
+ id: string;
23
+ body: string;
24
+ sender: ChatMessageSender;
25
+ createdAt: string;
26
+ /** Monotonic per-conversation sequence — the realtime resume
27
+ * cursor. Optional: older APIs don't send it. */
28
+ eventId?: number;
29
+ };
30
+ type ConversationStatus = 'OPEN' | 'CLOSED';
31
+ type LiveChatSessionState = 'CONNECTING' | 'OPEN' | 'CLOSED' | 'ERROR';
32
+ /** The `/external/live-chat/config` shape — the RUNTIME config the
33
+ * widget fetches per page load. Distinct from the server-side
34
+ * `LiveChatBootstrap` (which additionally carries the publishable
35
+ * key + script path). */
36
+ type LiveChatRuntimeConfig = {
37
+ enabled: false;
38
+ } | {
39
+ enabled: true;
40
+ greeting: string | null;
41
+ offlineMessage: string | null;
42
+ theme: Record<string, string> | null;
43
+ online?: boolean;
44
+ /** Realtime endpoint (wss://…). Absent = poll (older API, or
45
+ * the transport is off). The session upgrades automatically
46
+ * when present; consumers never touch it. */
47
+ realtimeUrl?: string;
48
+ };
49
+ type LiveChatSessionSnapshot = {
50
+ messages: ChatMessage[];
51
+ status: LiveChatSessionState;
52
+ /** Agents available right now (business-hours based today; agent
53
+ * presence once the realtime transport lands). */
54
+ online: boolean;
55
+ /** Localized via the session's `locale`. */
56
+ greeting: string | null;
57
+ offlineMessage: string | null;
58
+ /** An outbound `send()` is in flight. */
59
+ sending: boolean;
60
+ error: Error | null;
61
+ };
62
+ type StartConversationResult = {
63
+ enabled: false;
64
+ } | {
65
+ enabled: true;
66
+ conversationId: string;
67
+ conversationToken: string;
68
+ greeting: string | null;
69
+ resumed: boolean;
70
+ online?: boolean;
71
+ };
72
+ type LiveChatCreateSessionOptions = {
73
+ /** The server-half bootstrap (same object `install()` takes) —
74
+ * supplies the publishable key (and, when the API advertises it,
75
+ * the realtime endpoint — passed through wholesale). */
76
+ config: {
77
+ enabled: boolean;
78
+ publishableKey?: string;
79
+ realtimeUrl?: string;
80
+ };
81
+ /** Signed identity — SAME shape and rules as `install()`. A bad
82
+ * signature downgrades to anonymous server-side; it is never a
83
+ * second identity path. */
84
+ visitor?: Record<string, unknown>;
85
+ /** BCP-47 tag for greeting/away localization. Defaults to
86
+ * `<html lang>` then browser language, like the widget. */
87
+ locale?: string;
88
+ /** Defaults to `location.href`. */
89
+ pageUrl?: string;
90
+ referrer?: string;
91
+ /** Transcript refresh cadence. Polling is the transport today; a
92
+ * realtime upgrade will keep this as the fallback. */
93
+ pollIntervalMs?: number;
94
+ };
95
+ type LiveChatSession = {
96
+ /** Current transcript (same array identity as the latest snapshot). */
97
+ readonly messages: readonly ChatMessage[];
98
+ readonly state: LiveChatSessionState;
99
+ send(body: string): Promise<ChatMessage>;
100
+ /** Listener fires on every snapshot change. Returns unsubscribe. */
101
+ subscribe(listener: (s: LiveChatSessionSnapshot) => void): () => void;
102
+ getSnapshot(): LiveChatSessionSnapshot;
103
+ /** Stop polling, abort in-flight work, drop listeners. Idempotent. */
104
+ close(): void;
105
+ /** Drop the stored conversation token and start a fresh thread —
106
+ * identity switch on a shared browser. */
107
+ reset(): Promise<void>;
108
+ };
109
+
6
110
  /**
7
111
  * `createBrandfineClient` — the SDK's entry point.
8
112
  *
@@ -164,6 +268,14 @@ type CreateAppointmentRequestInput = {
164
268
  requestedAt: string;
165
269
  /** Optional cookie-derived session id from the consumer site. */
166
270
  visitorSessionId?: string;
271
+ /** Signed host-app identity — the SAME payload shape and signature
272
+ * Live Chat accepts, so one `liveChat.identityToken(externalId)`
273
+ * call signs for both plugins. When present and valid, the booking
274
+ * is attributed to the person (verified name/email take precedence
275
+ * over the free-text fields above, and the booking links to their
276
+ * chat threads via the shared externalId). Invalid or absent →
277
+ * the booking proceeds anonymously — it is never rejected. */
278
+ visitor?: VerifiedVisitor;
167
279
  };
168
280
  type CreatedAppointmentRequest = {
169
281
  id: string;
@@ -171,11 +283,40 @@ type CreatedAppointmentRequest = {
171
283
  requestedAt: string;
172
284
  durationMinutes: number;
173
285
  status: 'PENDING';
286
+ /** True iff `visitor.identityToken` HMAC-verified — your signal
287
+ * that the booking landed attributed rather than anonymous. */
288
+ identityVerified: boolean;
174
289
  /** Visitor's self-cancel token. Embed it in confirmation
175
290
  * emails / on-page UI so the visitor can cancel without an
176
291
  * account. One-time use; revoked once any party acts. */
177
292
  cancellationToken: string | null;
178
293
  };
294
+ type AppointmentStatus = 'PENDING' | 'CONFIRMED' | 'REJECTED' | 'CANCELLED';
295
+ /** One appointment as the identity-scoped read-back returns it —
296
+ * the visitor-safe projection ("where does my request stand?"). */
297
+ type Appointment = {
298
+ id: string;
299
+ status: AppointmentStatus;
300
+ /** UTC ISO 8601 of the requested slot start (reflects the current
301
+ * slot after a reschedule). */
302
+ requestedAt: string;
303
+ /** The confirmed slot — non-null only once CONFIRMED. */
304
+ scheduledAt: string | null;
305
+ durationMinutes: number;
306
+ /** Workspace's IANA timezone for local rendering. */
307
+ timezone: string;
308
+ /** Customer's note — populated only on REJECTED. */
309
+ declineReason: string | null;
310
+ rescheduleCount: number;
311
+ respondedAt: string | null;
312
+ createdAt: string;
313
+ };
314
+ /** Proof of identity for read-back calls: the same externalId +
315
+ * `liveChat.identityToken(externalId)` pair used when booking. */
316
+ type AppointmentIdentity = {
317
+ externalId: string;
318
+ identityToken: string;
319
+ };
179
320
  type AppointmentsApi = {
180
321
  /**
181
322
  * Available slots for the workspace's booking window.
@@ -191,12 +332,37 @@ type AppointmentsApi = {
191
332
  * the slot is still bookable; if it isn't, throws
192
333
  * `BrandfineApiError` with status 404 / 409.
193
334
  *
194
- * The visitor's browser does not have any other appointment
195
- * actions in v1post-submission status changes (approve /
196
- * decline / reschedule) happen via email, driven by the
197
- * customer in the CMS.
335
+ * Pass `visitor` (signed with `liveChat.identityToken()`) to book
336
+ * as a known person that unlocks `list`/`get` read-back below.
337
+ * Anonymous bookings remain fully supported; their status flow
338
+ * stays email-driven via the cancellation-token link.
198
339
  */
199
340
  createRequest: (input: CreateAppointmentRequestInput) => Promise<CreatedAppointmentRequest>;
341
+ /**
342
+ * Every appointment belonging to the verified person, newest
343
+ * first. Requires a valid identity signature — throws
344
+ * `BrandfineApiError` 401 on a bad one (reads need proof; there
345
+ * is no anonymous downgrade for reading history). Server-side
346
+ * only, like `identityToken()` itself.
347
+ */
348
+ list: (identity: AppointmentIdentity) => Promise<Appointment[]>;
349
+ /**
350
+ * One appointment by id, identity-scoped. 404s when the id does
351
+ * not exist OR belongs to someone else — indistinguishable by
352
+ * design.
353
+ */
354
+ get: (id: string, identity: AppointmentIdentity) => Promise<Appointment>;
355
+ /**
356
+ * Spend the one-time `cancellationToken` from `createRequest` to
357
+ * cancel a still-PENDING request. The token IS the credential —
358
+ * no id or API key needed. Throws `BrandfineApiError` 400 when
359
+ * the request is no longer PENDING, 404 when the token is
360
+ * unknown/already spent.
361
+ */
362
+ cancel: (cancellationToken: string) => Promise<{
363
+ id: string;
364
+ status: 'CANCELLED';
365
+ }>;
200
366
  };
201
367
  type AnalyticsConfig = {
202
368
  enabled: false;
@@ -368,6 +534,12 @@ type LiveChatBootstrap = {
368
534
  theme: Record<string, string> | null;
369
535
  /** Widget bundle path relative to the API base URL. */
370
536
  scriptPath: string;
537
+ /** Realtime endpoint (wss://…). OPTIONAL by design — this is
538
+ * what makes the upgrade non-breaking: an older SDK ignores
539
+ * it, a newer SDK against an older API sees it absent and
540
+ * polls. Consumers pass `config` through wholesale, so it
541
+ * reaches the browser with no change on their side. */
542
+ realtimeUrl?: string;
371
543
  };
372
544
  type LiveChatInstallResult = {
373
545
  installed: false;
@@ -383,6 +555,13 @@ type LiveChatInstallResult = {
383
555
  * the Brandfine API verifies the signature. An invalid or missing
384
556
  * token silently downgrades the conversation to anonymous.
385
557
  */
558
+ /**
559
+ * A signed host-app identity payload. Named per-plugin below for
560
+ * discoverability, but it is ONE shape signed ONE way: a token from
561
+ * `liveChat.identityToken(externalId)` is accepted by Live Chat AND
562
+ * Appointments (`createRequest.visitor`, `list`/`get`).
563
+ */
564
+ type VerifiedVisitor = LiveChatVisitor;
386
565
  type LiveChatVisitor = {
387
566
  /** Your app's stable id for this person (user id, lead reference…).
388
567
  * Conversations sharing an externalId are the same person across
@@ -415,6 +594,14 @@ type LiveChatInstallOptions = {
415
594
  * onto the widget host, it performs no crypto.
416
595
  */
417
596
  visitor?: LiveChatVisitor;
597
+ /**
598
+ * Locale for the widget's visitor-facing strings (greeting, away
599
+ * message), e.g. your i18n router's active locale. Optional — the
600
+ * widget falls back to the page's `<html lang>` and then the
601
+ * browser language. Resolved server-side against the workspace's
602
+ * configured translations; unknown locales get the default text.
603
+ */
604
+ locale?: string;
418
605
  };
419
606
  type LiveChatApi = {
420
607
  /**
@@ -439,18 +626,61 @@ type LiveChatApi = {
439
626
  install: (opts: LiveChatInstallOptions) => Promise<LiveChatInstallResult>;
440
627
  /**
441
628
  * Computes the visitor identity token:
442
- * hex(HMAC_SHA256(identitySecret, externalId)). SERVER-ONLY — it
443
- * throws in a browser context and throws when no secret is
444
- * provided (explicitly or via BRANDFINE_LIVE_CHAT_IDENTITY_SECRET),
445
- * rather than ever emitting an unsigned/mis-signed payload.
629
+ * hex(HMAC_SHA256(signingSecret, externalId)). SERVER-ONLY — it
630
+ * throws in a browser context rather than ever computing next to
631
+ * the DOM.
446
632
  *
447
- * Get the secret from the CMS: Plugins Live Chat Manage
448
- * settings Integrate Identity secret. Keep it in server env;
449
- * shipping it to a browser lets anyone impersonate any visitor.
633
+ * Signing secret, in order: `{ secret }` option
634
+ * `BRANDFINE_LIVE_CHAT_IDENTITY_SECRET` env**derived from the
635
+ * client's API key** (HMAC with a fixed domain-separation
636
+ * constant; the Brandfine API derives the same value from its
637
+ * stored copy). The derived path needs ZERO extra configuration —
638
+ * an explicit secret is only for signers that shouldn't hold the
639
+ * broad key, or legacy workspaces without a revealable key
640
+ * (generate one in the CMS: Plugins → Live Chat → Integrate).
450
641
  */
451
642
  identityToken: (externalId: string, opts?: {
452
643
  secret?: string;
453
644
  }) => Promise<string>;
645
+ /**
646
+ * Headless session — the conversation without the widget, for
647
+ * building your own inline chat UI. Browser-only. Takes the same
648
+ * `config` and signed `visitor` as `install()` (identical identity
649
+ * rules and thread continuity — it IS the same conversation the
650
+ * widget would join, sharing its storage keys). Transport today is
651
+ * polling (`pollIntervalMs`, default 5000); a realtime upgrade
652
+ * will keep this API and the poll as fallback.
653
+ */
654
+ createSession: (opts: LiveChatCreateSessionOptions) => Promise<LiveChatSession>;
655
+ /**
656
+ * Raw, stateless wire methods for full control. NOTE on auth: the
657
+ * config + start endpoints authenticate with this client's
658
+ * `apiKey` header — in a browser, construct the client with the
659
+ * PUBLISHABLE key (`createBrandfineClient({ apiKey: config.publishableKey })`),
660
+ * never the broad key. Message endpoints authenticate with the
661
+ * conversation token alone — treat it as a bearer credential:
662
+ * don't log it, don't put it in URLs you share.
663
+ */
664
+ runtimeConfig: (locale?: string) => Promise<LiveChatRuntimeConfig>;
665
+ startConversation: (input: {
666
+ visitorSessionId: string;
667
+ conversationToken?: string;
668
+ pageUrl?: string;
669
+ referrer?: string;
670
+ locale?: string;
671
+ visitor?: Record<string, unknown>;
672
+ }) => Promise<StartConversationResult>;
673
+ sendMessage: (conversationToken: string, input: {
674
+ body: string;
675
+ clientId?: string;
676
+ }) => Promise<ChatMessage>;
677
+ history: (conversationToken: string, opts?: {
678
+ after?: string;
679
+ afterEvent?: number;
680
+ }) => Promise<{
681
+ messages: ChatMessage[];
682
+ status: ConversationStatus;
683
+ }>;
454
684
  };
455
685
  declare function createBrandfineClient(config: BrandfineClientConfig): BrandfineClient;
456
686
 
@@ -468,4 +698,4 @@ declare function createBrandfineClient(config: BrandfineClientConfig): Brandfine
468
698
  */
469
699
  declare const SDK_VERSION: "0.0.0";
470
700
 
471
- export { type AnalyticsConfig, type AnalyticsInstallResult, type AnalyticsOverview, type AnalyticsOverviewRange, BrandfineApiError, BrandfineCategory, type BrandfineClient, type BrandfineClientConfig, BrandfineNavigation, BrandfinePost, BrandfineWorkspace, type CreateSubmissionInput, type InstallOptions, ListCategoriesOptions, ListPostsOptions, type LiveChatBootstrap, type LiveChatInstallOptions, type LiveChatInstallResult, type LiveChatVisitor, SDK_VERSION, type Submission, createBrandfineClient };
701
+ export { type AnalyticsConfig, type AnalyticsInstallResult, type AnalyticsOverview, type AnalyticsOverviewRange, type Appointment, type AppointmentAvailability, type AppointmentIdentity, type AppointmentSlot, type AppointmentStatus, BrandfineApiError, BrandfineCategory, type BrandfineClient, type BrandfineClientConfig, BrandfineNavigation, BrandfinePost, BrandfineWorkspace, type ChatMessage, type ChatMessageSender, type ConversationStatus, type CreateAppointmentRequestInput, type CreateSubmissionInput, type CreatedAppointmentRequest, type InstallOptions, ListCategoriesOptions, ListPostsOptions, type LiveChatBootstrap, type LiveChatCreateSessionOptions, type LiveChatInstallOptions, type LiveChatInstallResult, type LiveChatRuntimeConfig, type LiveChatSession, type LiveChatSessionSnapshot, type LiveChatSessionState, type LiveChatVisitor, SDK_VERSION, type StartConversationResult, type Submission, type VerifiedVisitor, createBrandfineClient };
package/dist/index.d.ts CHANGED
@@ -3,6 +3,110 @@ export { e as BrandfineNavItem, f as BrandfineNavItemType, g as BrandfineNavPost
3
3
  export { Cache, CacheOptions, KeyedCache, KeyedCacheOptions, createCache, createKeyedCache } from './cache/index.js';
4
4
  export { BrandfineWebhookEvent, BrandfineWebhookHandlerOptions, BrandfineWebhookPayload, createBrandfineWebhookHandler, parseWebhookPayload, verifyWebhookSecret } from './webhook/index.js';
5
5
 
6
+ /**
7
+ * Headless Live Chat session — the conversation WITHOUT the widget.
8
+ *
9
+ * `bf.liveChat.createSession()` gives a consumer everything needed to
10
+ * render chat inline in their own design system: transcript state, a
11
+ * send method, and a subscribe API shaped for
12
+ * `useSyncExternalStore` / Svelte stores / Vue refs. Brandfine keeps
13
+ * owning transport, identity, threading and storage; the consumer
14
+ * owns pixels.
15
+ *
16
+ * Storage keys are IDENTICAL to the floating widget's, so a visitor
17
+ * who talks through the widget on one page and an inline panel on
18
+ * another continues the same thread.
19
+ */
20
+ type ChatMessageSender = 'VISITOR' | 'AGENT' | 'SYSTEM';
21
+ type ChatMessage = {
22
+ id: string;
23
+ body: string;
24
+ sender: ChatMessageSender;
25
+ createdAt: string;
26
+ /** Monotonic per-conversation sequence — the realtime resume
27
+ * cursor. Optional: older APIs don't send it. */
28
+ eventId?: number;
29
+ };
30
+ type ConversationStatus = 'OPEN' | 'CLOSED';
31
+ type LiveChatSessionState = 'CONNECTING' | 'OPEN' | 'CLOSED' | 'ERROR';
32
+ /** The `/external/live-chat/config` shape — the RUNTIME config the
33
+ * widget fetches per page load. Distinct from the server-side
34
+ * `LiveChatBootstrap` (which additionally carries the publishable
35
+ * key + script path). */
36
+ type LiveChatRuntimeConfig = {
37
+ enabled: false;
38
+ } | {
39
+ enabled: true;
40
+ greeting: string | null;
41
+ offlineMessage: string | null;
42
+ theme: Record<string, string> | null;
43
+ online?: boolean;
44
+ /** Realtime endpoint (wss://…). Absent = poll (older API, or
45
+ * the transport is off). The session upgrades automatically
46
+ * when present; consumers never touch it. */
47
+ realtimeUrl?: string;
48
+ };
49
+ type LiveChatSessionSnapshot = {
50
+ messages: ChatMessage[];
51
+ status: LiveChatSessionState;
52
+ /** Agents available right now (business-hours based today; agent
53
+ * presence once the realtime transport lands). */
54
+ online: boolean;
55
+ /** Localized via the session's `locale`. */
56
+ greeting: string | null;
57
+ offlineMessage: string | null;
58
+ /** An outbound `send()` is in flight. */
59
+ sending: boolean;
60
+ error: Error | null;
61
+ };
62
+ type StartConversationResult = {
63
+ enabled: false;
64
+ } | {
65
+ enabled: true;
66
+ conversationId: string;
67
+ conversationToken: string;
68
+ greeting: string | null;
69
+ resumed: boolean;
70
+ online?: boolean;
71
+ };
72
+ type LiveChatCreateSessionOptions = {
73
+ /** The server-half bootstrap (same object `install()` takes) —
74
+ * supplies the publishable key (and, when the API advertises it,
75
+ * the realtime endpoint — passed through wholesale). */
76
+ config: {
77
+ enabled: boolean;
78
+ publishableKey?: string;
79
+ realtimeUrl?: string;
80
+ };
81
+ /** Signed identity — SAME shape and rules as `install()`. A bad
82
+ * signature downgrades to anonymous server-side; it is never a
83
+ * second identity path. */
84
+ visitor?: Record<string, unknown>;
85
+ /** BCP-47 tag for greeting/away localization. Defaults to
86
+ * `<html lang>` then browser language, like the widget. */
87
+ locale?: string;
88
+ /** Defaults to `location.href`. */
89
+ pageUrl?: string;
90
+ referrer?: string;
91
+ /** Transcript refresh cadence. Polling is the transport today; a
92
+ * realtime upgrade will keep this as the fallback. */
93
+ pollIntervalMs?: number;
94
+ };
95
+ type LiveChatSession = {
96
+ /** Current transcript (same array identity as the latest snapshot). */
97
+ readonly messages: readonly ChatMessage[];
98
+ readonly state: LiveChatSessionState;
99
+ send(body: string): Promise<ChatMessage>;
100
+ /** Listener fires on every snapshot change. Returns unsubscribe. */
101
+ subscribe(listener: (s: LiveChatSessionSnapshot) => void): () => void;
102
+ getSnapshot(): LiveChatSessionSnapshot;
103
+ /** Stop polling, abort in-flight work, drop listeners. Idempotent. */
104
+ close(): void;
105
+ /** Drop the stored conversation token and start a fresh thread —
106
+ * identity switch on a shared browser. */
107
+ reset(): Promise<void>;
108
+ };
109
+
6
110
  /**
7
111
  * `createBrandfineClient` — the SDK's entry point.
8
112
  *
@@ -164,6 +268,14 @@ type CreateAppointmentRequestInput = {
164
268
  requestedAt: string;
165
269
  /** Optional cookie-derived session id from the consumer site. */
166
270
  visitorSessionId?: string;
271
+ /** Signed host-app identity — the SAME payload shape and signature
272
+ * Live Chat accepts, so one `liveChat.identityToken(externalId)`
273
+ * call signs for both plugins. When present and valid, the booking
274
+ * is attributed to the person (verified name/email take precedence
275
+ * over the free-text fields above, and the booking links to their
276
+ * chat threads via the shared externalId). Invalid or absent →
277
+ * the booking proceeds anonymously — it is never rejected. */
278
+ visitor?: VerifiedVisitor;
167
279
  };
168
280
  type CreatedAppointmentRequest = {
169
281
  id: string;
@@ -171,11 +283,40 @@ type CreatedAppointmentRequest = {
171
283
  requestedAt: string;
172
284
  durationMinutes: number;
173
285
  status: 'PENDING';
286
+ /** True iff `visitor.identityToken` HMAC-verified — your signal
287
+ * that the booking landed attributed rather than anonymous. */
288
+ identityVerified: boolean;
174
289
  /** Visitor's self-cancel token. Embed it in confirmation
175
290
  * emails / on-page UI so the visitor can cancel without an
176
291
  * account. One-time use; revoked once any party acts. */
177
292
  cancellationToken: string | null;
178
293
  };
294
+ type AppointmentStatus = 'PENDING' | 'CONFIRMED' | 'REJECTED' | 'CANCELLED';
295
+ /** One appointment as the identity-scoped read-back returns it —
296
+ * the visitor-safe projection ("where does my request stand?"). */
297
+ type Appointment = {
298
+ id: string;
299
+ status: AppointmentStatus;
300
+ /** UTC ISO 8601 of the requested slot start (reflects the current
301
+ * slot after a reschedule). */
302
+ requestedAt: string;
303
+ /** The confirmed slot — non-null only once CONFIRMED. */
304
+ scheduledAt: string | null;
305
+ durationMinutes: number;
306
+ /** Workspace's IANA timezone for local rendering. */
307
+ timezone: string;
308
+ /** Customer's note — populated only on REJECTED. */
309
+ declineReason: string | null;
310
+ rescheduleCount: number;
311
+ respondedAt: string | null;
312
+ createdAt: string;
313
+ };
314
+ /** Proof of identity for read-back calls: the same externalId +
315
+ * `liveChat.identityToken(externalId)` pair used when booking. */
316
+ type AppointmentIdentity = {
317
+ externalId: string;
318
+ identityToken: string;
319
+ };
179
320
  type AppointmentsApi = {
180
321
  /**
181
322
  * Available slots for the workspace's booking window.
@@ -191,12 +332,37 @@ type AppointmentsApi = {
191
332
  * the slot is still bookable; if it isn't, throws
192
333
  * `BrandfineApiError` with status 404 / 409.
193
334
  *
194
- * The visitor's browser does not have any other appointment
195
- * actions in v1post-submission status changes (approve /
196
- * decline / reschedule) happen via email, driven by the
197
- * customer in the CMS.
335
+ * Pass `visitor` (signed with `liveChat.identityToken()`) to book
336
+ * as a known person that unlocks `list`/`get` read-back below.
337
+ * Anonymous bookings remain fully supported; their status flow
338
+ * stays email-driven via the cancellation-token link.
198
339
  */
199
340
  createRequest: (input: CreateAppointmentRequestInput) => Promise<CreatedAppointmentRequest>;
341
+ /**
342
+ * Every appointment belonging to the verified person, newest
343
+ * first. Requires a valid identity signature — throws
344
+ * `BrandfineApiError` 401 on a bad one (reads need proof; there
345
+ * is no anonymous downgrade for reading history). Server-side
346
+ * only, like `identityToken()` itself.
347
+ */
348
+ list: (identity: AppointmentIdentity) => Promise<Appointment[]>;
349
+ /**
350
+ * One appointment by id, identity-scoped. 404s when the id does
351
+ * not exist OR belongs to someone else — indistinguishable by
352
+ * design.
353
+ */
354
+ get: (id: string, identity: AppointmentIdentity) => Promise<Appointment>;
355
+ /**
356
+ * Spend the one-time `cancellationToken` from `createRequest` to
357
+ * cancel a still-PENDING request. The token IS the credential —
358
+ * no id or API key needed. Throws `BrandfineApiError` 400 when
359
+ * the request is no longer PENDING, 404 when the token is
360
+ * unknown/already spent.
361
+ */
362
+ cancel: (cancellationToken: string) => Promise<{
363
+ id: string;
364
+ status: 'CANCELLED';
365
+ }>;
200
366
  };
201
367
  type AnalyticsConfig = {
202
368
  enabled: false;
@@ -368,6 +534,12 @@ type LiveChatBootstrap = {
368
534
  theme: Record<string, string> | null;
369
535
  /** Widget bundle path relative to the API base URL. */
370
536
  scriptPath: string;
537
+ /** Realtime endpoint (wss://…). OPTIONAL by design — this is
538
+ * what makes the upgrade non-breaking: an older SDK ignores
539
+ * it, a newer SDK against an older API sees it absent and
540
+ * polls. Consumers pass `config` through wholesale, so it
541
+ * reaches the browser with no change on their side. */
542
+ realtimeUrl?: string;
371
543
  };
372
544
  type LiveChatInstallResult = {
373
545
  installed: false;
@@ -383,6 +555,13 @@ type LiveChatInstallResult = {
383
555
  * the Brandfine API verifies the signature. An invalid or missing
384
556
  * token silently downgrades the conversation to anonymous.
385
557
  */
558
+ /**
559
+ * A signed host-app identity payload. Named per-plugin below for
560
+ * discoverability, but it is ONE shape signed ONE way: a token from
561
+ * `liveChat.identityToken(externalId)` is accepted by Live Chat AND
562
+ * Appointments (`createRequest.visitor`, `list`/`get`).
563
+ */
564
+ type VerifiedVisitor = LiveChatVisitor;
386
565
  type LiveChatVisitor = {
387
566
  /** Your app's stable id for this person (user id, lead reference…).
388
567
  * Conversations sharing an externalId are the same person across
@@ -415,6 +594,14 @@ type LiveChatInstallOptions = {
415
594
  * onto the widget host, it performs no crypto.
416
595
  */
417
596
  visitor?: LiveChatVisitor;
597
+ /**
598
+ * Locale for the widget's visitor-facing strings (greeting, away
599
+ * message), e.g. your i18n router's active locale. Optional — the
600
+ * widget falls back to the page's `<html lang>` and then the
601
+ * browser language. Resolved server-side against the workspace's
602
+ * configured translations; unknown locales get the default text.
603
+ */
604
+ locale?: string;
418
605
  };
419
606
  type LiveChatApi = {
420
607
  /**
@@ -439,18 +626,61 @@ type LiveChatApi = {
439
626
  install: (opts: LiveChatInstallOptions) => Promise<LiveChatInstallResult>;
440
627
  /**
441
628
  * Computes the visitor identity token:
442
- * hex(HMAC_SHA256(identitySecret, externalId)). SERVER-ONLY — it
443
- * throws in a browser context and throws when no secret is
444
- * provided (explicitly or via BRANDFINE_LIVE_CHAT_IDENTITY_SECRET),
445
- * rather than ever emitting an unsigned/mis-signed payload.
629
+ * hex(HMAC_SHA256(signingSecret, externalId)). SERVER-ONLY — it
630
+ * throws in a browser context rather than ever computing next to
631
+ * the DOM.
446
632
  *
447
- * Get the secret from the CMS: Plugins Live Chat Manage
448
- * settings Integrate Identity secret. Keep it in server env;
449
- * shipping it to a browser lets anyone impersonate any visitor.
633
+ * Signing secret, in order: `{ secret }` option
634
+ * `BRANDFINE_LIVE_CHAT_IDENTITY_SECRET` env**derived from the
635
+ * client's API key** (HMAC with a fixed domain-separation
636
+ * constant; the Brandfine API derives the same value from its
637
+ * stored copy). The derived path needs ZERO extra configuration —
638
+ * an explicit secret is only for signers that shouldn't hold the
639
+ * broad key, or legacy workspaces without a revealable key
640
+ * (generate one in the CMS: Plugins → Live Chat → Integrate).
450
641
  */
451
642
  identityToken: (externalId: string, opts?: {
452
643
  secret?: string;
453
644
  }) => Promise<string>;
645
+ /**
646
+ * Headless session — the conversation without the widget, for
647
+ * building your own inline chat UI. Browser-only. Takes the same
648
+ * `config` and signed `visitor` as `install()` (identical identity
649
+ * rules and thread continuity — it IS the same conversation the
650
+ * widget would join, sharing its storage keys). Transport today is
651
+ * polling (`pollIntervalMs`, default 5000); a realtime upgrade
652
+ * will keep this API and the poll as fallback.
653
+ */
654
+ createSession: (opts: LiveChatCreateSessionOptions) => Promise<LiveChatSession>;
655
+ /**
656
+ * Raw, stateless wire methods for full control. NOTE on auth: the
657
+ * config + start endpoints authenticate with this client's
658
+ * `apiKey` header — in a browser, construct the client with the
659
+ * PUBLISHABLE key (`createBrandfineClient({ apiKey: config.publishableKey })`),
660
+ * never the broad key. Message endpoints authenticate with the
661
+ * conversation token alone — treat it as a bearer credential:
662
+ * don't log it, don't put it in URLs you share.
663
+ */
664
+ runtimeConfig: (locale?: string) => Promise<LiveChatRuntimeConfig>;
665
+ startConversation: (input: {
666
+ visitorSessionId: string;
667
+ conversationToken?: string;
668
+ pageUrl?: string;
669
+ referrer?: string;
670
+ locale?: string;
671
+ visitor?: Record<string, unknown>;
672
+ }) => Promise<StartConversationResult>;
673
+ sendMessage: (conversationToken: string, input: {
674
+ body: string;
675
+ clientId?: string;
676
+ }) => Promise<ChatMessage>;
677
+ history: (conversationToken: string, opts?: {
678
+ after?: string;
679
+ afterEvent?: number;
680
+ }) => Promise<{
681
+ messages: ChatMessage[];
682
+ status: ConversationStatus;
683
+ }>;
454
684
  };
455
685
  declare function createBrandfineClient(config: BrandfineClientConfig): BrandfineClient;
456
686
 
@@ -468,4 +698,4 @@ declare function createBrandfineClient(config: BrandfineClientConfig): Brandfine
468
698
  */
469
699
  declare const SDK_VERSION: "0.0.0";
470
700
 
471
- export { type AnalyticsConfig, type AnalyticsInstallResult, type AnalyticsOverview, type AnalyticsOverviewRange, BrandfineApiError, BrandfineCategory, type BrandfineClient, type BrandfineClientConfig, BrandfineNavigation, BrandfinePost, BrandfineWorkspace, type CreateSubmissionInput, type InstallOptions, ListCategoriesOptions, ListPostsOptions, type LiveChatBootstrap, type LiveChatInstallOptions, type LiveChatInstallResult, type LiveChatVisitor, SDK_VERSION, type Submission, createBrandfineClient };
701
+ export { type AnalyticsConfig, type AnalyticsInstallResult, type AnalyticsOverview, type AnalyticsOverviewRange, type Appointment, type AppointmentAvailability, type AppointmentIdentity, type AppointmentSlot, type AppointmentStatus, BrandfineApiError, BrandfineCategory, type BrandfineClient, type BrandfineClientConfig, BrandfineNavigation, BrandfinePost, BrandfineWorkspace, type ChatMessage, type ChatMessageSender, type ConversationStatus, type CreateAppointmentRequestInput, type CreateSubmissionInput, type CreatedAppointmentRequest, type InstallOptions, ListCategoriesOptions, ListPostsOptions, type LiveChatBootstrap, type LiveChatCreateSessionOptions, type LiveChatInstallOptions, type LiveChatInstallResult, type LiveChatRuntimeConfig, type LiveChatSession, type LiveChatSessionSnapshot, type LiveChatSessionState, type LiveChatVisitor, SDK_VERSION, type StartConversationResult, type Submission, type VerifiedVisitor, createBrandfineClient };