@cosmoops/connect-sdk 0.32.0 → 0.34.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.
Files changed (51) hide show
  1. package/CHANGELOG.md +251 -0
  2. package/README.md +17 -5
  3. package/dist/cjs/client/components/access-gate.js +98 -2
  4. package/dist/cjs/client/components/visitor-chat.js +387 -0
  5. package/dist/cjs/client/components/visitor-stream.js +124 -0
  6. package/dist/cjs/client/index.js +16 -3
  7. package/dist/cjs/client/provider.js +29 -2
  8. package/dist/cjs/client/use-checkout.js +3 -1
  9. package/dist/cjs/helpers.js +46 -0
  10. package/dist/cjs/index.js +5 -1
  11. package/dist/cjs/next/index.js +10 -3
  12. package/dist/cjs/server/client.js +8 -1
  13. package/dist/cjs/server/handler.js +142 -0
  14. package/dist/cjs/server/resources/ai.js +50 -0
  15. package/dist/cjs/server/resources/billing.js +5 -0
  16. package/dist/cjs/server/resources/enrichment.js +59 -0
  17. package/dist/cjs/server/resources/entitlements.js +47 -0
  18. package/dist/cjs/server/resources/visitor.js +227 -0
  19. package/dist/esm/client/components/access-gate.js +99 -3
  20. package/dist/esm/client/components/visitor-chat.js +384 -0
  21. package/dist/esm/client/components/visitor-stream.js +120 -0
  22. package/dist/esm/client/index.js +11 -1
  23. package/dist/esm/client/provider.js +28 -2
  24. package/dist/esm/client/use-checkout.js +3 -1
  25. package/dist/esm/helpers.js +43 -0
  26. package/dist/esm/index.js +1 -0
  27. package/dist/esm/next/index.js +10 -3
  28. package/dist/esm/server/client.js +8 -1
  29. package/dist/esm/server/handler.js +142 -0
  30. package/dist/esm/server/resources/ai.js +50 -0
  31. package/dist/esm/server/resources/billing.js +5 -0
  32. package/dist/esm/server/resources/enrichment.js +59 -0
  33. package/dist/esm/server/resources/entitlements.js +47 -0
  34. package/dist/esm/server/resources/visitor.js +222 -0
  35. package/dist/types/client/components/access-gate.d.ts +9 -0
  36. package/dist/types/client/components/visitor-chat.d.ts +120 -0
  37. package/dist/types/client/components/visitor-stream.d.ts +42 -0
  38. package/dist/types/client/index.d.ts +6 -1
  39. package/dist/types/client/provider.d.ts +176 -51
  40. package/dist/types/client/use-checkout.d.ts +18 -1
  41. package/dist/types/helpers.d.ts +36 -1
  42. package/dist/types/index.d.ts +1 -0
  43. package/dist/types/server/client.d.ts +7 -1
  44. package/dist/types/server/handler.d.ts +0 -24
  45. package/dist/types/server/resources/ai.d.ts +48 -1
  46. package/dist/types/server/resources/billing.d.ts +4 -0
  47. package/dist/types/server/resources/enrichment.d.ts +57 -1
  48. package/dist/types/server/resources/entitlements.d.ts +40 -1
  49. package/dist/types/server/resources/visitor.d.ts +200 -0
  50. package/dist/types/types.d.ts +562 -0
  51. package/package.json +3 -2
@@ -0,0 +1,120 @@
1
+ export interface ConnectVisitorChatProps {
2
+ /**
3
+ * The customer whose website this widget is on.
4
+ *
5
+ * Required, and it is the one field that decides whose published knowledge the
6
+ * assistant answers from and whose diary it books into. Your bridge checks it
7
+ * against your own subscriptions before it reaches Connect.
8
+ */
9
+ workspaceId: string;
10
+ /**
11
+ * What the assistant calls itself, and the copy shown before anybody types.
12
+ *
13
+ * **Yours, not Connect's.** These never reach the model and are rendered from
14
+ * your own store, which is what lets the bubble paint its first frame with no
15
+ * network call at all. Connect holds what it enforces; you hold what you draw.
16
+ */
17
+ assistantName?: string;
18
+ greeting?: string;
19
+ /** Opening questions offered as buttons. Three is what fits above a phone keyboard. */
20
+ suggestions?: readonly string[];
21
+ /** A published event type slug in that workspace, to offer a call. Omit for no booking. */
22
+ bookingEventTypeSlug?: string | null;
23
+ /**
24
+ * Where `createConnectHandler` is mounted. Defaults to `/api/connect`, which
25
+ * is its own default.
26
+ *
27
+ * Passed explicitly here, rather than only inherited from `ConnectProvider`,
28
+ * because this component is built to work on a page that has no provider -
29
+ * see the component's own note.
30
+ */
31
+ basePath?: string;
32
+ /**
33
+ * The page the visitor is on, passed to Connect as context.
34
+ *
35
+ * Left unset it uses `location.pathname`. Pass something more meaningful - a
36
+ * page title, a product slug - where the path is not descriptive.
37
+ */
38
+ page?: string;
39
+ /** The bubble's accessible label, and the panel's heading. */
40
+ title?: string;
41
+ /** Start open. The panel owns its state after that. */
42
+ defaultOpen?: boolean;
43
+ /**
44
+ * Show the bubble even to a signed-in user. Off by default.
45
+ *
46
+ * The default is the useful one: a signed-in member has your product's own
47
+ * support surfaces and, if you mount it, `<ConnectAskAi>` - which is grounded
48
+ * in their actual workspace and can therefore answer things this assistant
49
+ * cannot. Two bubbles in the same corner is also the one layout bug this
50
+ * component could cause, since `<ConnectAskAi>` sits at the same `z-30`
51
+ * bottom-right position.
52
+ *
53
+ * Turn it on for a marketing site that is a separate surface from the
54
+ * application, where "signed in" says nothing about what the visitor is
55
+ * reading.
56
+ */
57
+ showWhenSignedIn?: boolean;
58
+ /**
59
+ * Replaces `fetch`, for a mount with **no `ConnectProvider`** around it.
60
+ *
61
+ * Inside a provider this is not needed and not read: the component goes
62
+ * through `mutateResource`/`streamResource`, which already use whatever
63
+ * `fetch` that provider was given - so Storybook and the docs site work by
64
+ * wrapping it, with nothing passed here.
65
+ */
66
+ fetch?: (input: string, init?: RequestInit) => Promise<Response>;
67
+ }
68
+ /**
69
+ * The public chat bubble: a visitor with no account asks about your product,
70
+ * books a call, or gets started.
71
+ *
72
+ * ## It runs on pages that have no `ConnectProvider`
73
+ *
74
+ * Deliberately, and it is the main thing separating this component from every
75
+ * other one here. A marketing page usually has no session to hold and nothing
76
+ * on it that needs one, so requiring the provider would mean wrapping a
77
+ * landing page in an auth context to put a chat bubble on it. It reads the
78
+ * context through `useConnectOptional` and uses it only for `basePath` and the
79
+ * injected `fetch`; both have props, and both have defaults.
80
+ *
81
+ * ## Nothing happens until somebody opens it
82
+ *
83
+ * The conversation is minted on first open, never on mount. On a busy marketing
84
+ * site mounting would mean one Firestore write and one wasted session row per
85
+ * *page view*, for a bubble most visitors never touch - and the bubble itself
86
+ * needs no server state to render. The greeting arrives with that first call
87
+ * and is stored copy rather than generated, so opening costs one fast round
88
+ * trip and the visitor is reading real words immediately.
89
+ *
90
+ * ## It is built for a phone first
91
+ *
92
+ * Open, it is a full-height sheet on a narrow viewport and an anchored panel
93
+ * from `sm` up - a marketing chat that covers a desktop browser is harder to
94
+ * use, and one that does not cover a phone has nowhere to put a keyboard.
95
+ * `h-dvh` rather than `h-screen`, so the sheet tracks the visual viewport when
96
+ * the keyboard opens; `env(safe-area-inset-bottom)` on the composer, so the
97
+ * send button is not under a home indicator.
98
+ *
99
+ * The composer's input type is **the server's decision**, not a guess from what
100
+ * the assistant said - see `VisitorField`. That is what puts an `@` on the
101
+ * keyboard when an email is wanted and lets iOS offer a verification code
102
+ * straight from the notification.
103
+ *
104
+ * ## The times are drawn from the tool result, never the prose
105
+ *
106
+ * A turn that looked up availability carries the slots as structured data, and
107
+ * the buttons come from that. So a time the visitor taps is one the server
108
+ * offered, which is the property `/visitor/book` relies on and cannot check for
109
+ * itself.
110
+ *
111
+ * ## Replies render as text, not markdown
112
+ *
113
+ * `<ConnectAskAi>` lazy-loads a markdown renderer because it answers questions
114
+ * about a workspace's documents, where tables and code blocks carry meaning.
115
+ * This answers three-sentence questions about a product on a marketing page.
116
+ * Shipping 462 KB of renderer to a landing page for prose that is deliberately
117
+ * kept to a couple of lines is the wrong trade, so line breaks are preserved
118
+ * and nothing else is parsed.
119
+ */
120
+ export declare function ConnectVisitorChat({ workspaceId, assistantName, greeting, suggestions, bookingEventTypeSlug, basePath, page, title, defaultOpen, showWhenSignedIn, fetch: fetchProp, }: ConnectVisitorChatProps): import("react").JSX.Element | null;
@@ -0,0 +1,42 @@
1
+ import type { VisitorSlotOffer, VisitorToolCall, VisitorTurnDone } from "../../types.js";
2
+ /**
3
+ * The visitor turn's wire format, read.
4
+ *
5
+ * **A named-event reader, where `<ConnectAskAi>`'s is data-only.** That one
6
+ * consumes the AI SDK's UI message stream, where every frame is a `data:` line
7
+ * carrying a `type` field. This endpoint answers `event:`/`data:` pairs instead,
8
+ * for the reason the route explains: the UI message protocol exists for
9
+ * `useChat`, which owns the message list client-side, and this surface
10
+ * deliberately does not let the client own the transcript.
11
+ *
12
+ * Kept out of the component and pure so it can be tested without a DOM. The
13
+ * three behaviours worth pinning are all here: a frame split across two network
14
+ * reads, a malformed frame, and a stream that ends without its `done`.
15
+ */
16
+ export interface VisitorStreamHandlers {
17
+ onDelta(text: string): void;
18
+ onDone(done: VisitorTurnDone): void;
19
+ /**
20
+ * A failure the server reported *after* the stream opened.
21
+ *
22
+ * It arrives as an event rather than a status because the status was already
23
+ * sent, and the message is written as a sentence for the same reason - it is
24
+ * shown to the visitor verbatim.
25
+ */
26
+ onError(message: string): void;
27
+ }
28
+ export declare function readVisitorStream(response: Response, handlers: VisitorStreamHandlers): Promise<void>;
29
+ /**
30
+ * The times a turn looked up, lifted out of its tool calls.
31
+ *
32
+ * **This is what the buttons are drawn from, never the assistant's sentence.** A
33
+ * time the visitor taps is then one the server offered, which is what makes it
34
+ * impossible to book an appointment at a time nobody gave - the property
35
+ * `/visitor/book` depends on and cannot check for itself.
36
+ *
37
+ * `null` when the turn looked nothing up, and also when the lookup *refused* -
38
+ * a tool answering `ok: false` is the assistant being told there is nothing
39
+ * available, and it says so in prose. Rendering an empty slot list under that
40
+ * would be a second, emptier way of saying the same thing.
41
+ */
42
+ export declare function visitorSlotOfferFrom(toolCalls: readonly VisitorToolCall[]): VisitorSlotOffer | null;
@@ -10,7 +10,7 @@
10
10
  * whatever theme the host application platform already configured, and one theme
11
11
  * change lands identically in all of them.
12
12
  */
13
- export { ConnectProvider, useConnect, useConnectUser, useConnectWorkspaceType, useConnectAudience, useConnectPlatformAccess, useConnectEntitlements, useConnectPlans, useConnectApps, useConnectCompanyProfile, useConnectAddonUsage, useConnectSessions, useConnectIntegrations, useConnectResource, CONNECT_APP_URL, } from "./provider.js";
13
+ export { ConnectProvider, useConnect, useConnectOptional, useConnectUser, useConnectWorkspaceType, useConnectAudience, useConnectPlatformAccess, useConnectEntitlements, useConnectPlans, useConnectApps, useConnectCompanyProfile, useConnectAddonUsage, useConnectSessions, useConnectIntegrations, useConnectResource, CONNECT_APP_URL, } from "./provider.js";
14
14
  export type { ConnectProviderProps, ConnectContextValue, ConnectSession, ConnectOneTapHint, ConnectOneTapFailure, UseConnectResourceOptions, } from "./provider.js";
15
15
  export { blockingBlockers, normalizePlanFeatures } from "../helpers.js";
16
16
  export type { WirePlanFeature } from "../helpers.js";
@@ -52,6 +52,11 @@ export { normalizeNav, flattenNav, normalizePath, resolveActivePath, isNavItemAc
52
52
  export type { ConnectNav, ConnectNavItem, ConnectNavSection, ConnectNavLinkProps, ConnectLinkRenderer, } from "./components/nav.js";
53
53
  export type { ConnectScreen } from "../types.js";
54
54
  export { matchConnectScreen, isLinkableConnectScreen, } from "./components/ai-screens.js";
55
+ export { ConnectVisitorChat } from "./components/visitor-chat.js";
56
+ export type { ConnectVisitorChatProps } from "./components/visitor-chat.js";
57
+ export { readVisitorStream, visitorSlotOfferFrom } from "./components/visitor-stream.js";
58
+ export type { VisitorStreamHandlers } from "./components/visitor-stream.js";
59
+ export type { VisitorAppointment, VisitorBooking, VisitorField, VisitorMessage, VisitorSession, VisitorSlot, VisitorSlotOffer, VisitorToolCall, VisitorTurnDone, } from "../types.js";
55
60
  export { ConnectAppsGrid } from "./components/apps-grid.js";
56
61
  export type { ConnectAppsGridProps } from "./components/apps-grid.js";
57
62
  export { ConnectAskAi } from "./components/ask-ai.js";
@@ -196,7 +196,21 @@ export interface ConnectProviderProps {
196
196
  * not a Connect URL - it is your backend that exchanges the code.
197
197
  */
198
198
  redirectUri?: string;
199
- /** Override for non-production Connect instances. */
199
+ /**
200
+ * Override for non-production Connect instances. Defaults to
201
+ * `https://connect.cosmoops.com`.
202
+ *
203
+ * The twin of `CONNECT_APP_URL`, and *not* derived from it - that variable
204
+ * is read on your server and never reaches the browser. This value is where
205
+ * the user menu's Profile, Billing and Invoices destinations open, where
206
+ * `ConnectAppsGrid` sends an unsubscribed application, where checkout
207
+ * opens, and - if you pass `redirectUri` and so build the authorize URL
208
+ * here rather than in your own `/signin` route - where sign-in goes. Point
209
+ * the server somewhere else and leave this alone and the two halves
210
+ * disagree, so pass both or neither. Your root layout is a Server
211
+ * Component, so `appUrl={process.env.CONNECT_APP_URL}` is enough and needs
212
+ * no browser-visible variable.
213
+ */
200
214
  appUrl?: string;
201
215
  /**
202
216
  * Replace the `fetch` used to reach your bridge. The browser-side twin of
@@ -245,6 +259,27 @@ export interface ConnectProviderProps {
245
259
  */
246
260
  export declare function ConnectProvider({ clientId, basePath, redirectUri, appUrl, fetch: fetchImpl, silentSignIn, children, }: ConnectProviderProps): import("react").JSX.Element;
247
261
  export declare function useConnect(): ConnectContextValue;
262
+ /**
263
+ * The context when there is one, `null` when there is not.
264
+ *
265
+ * **For a component that must work on a page with no provider**, which is a
266
+ * real case rather than defensive coding: `<ConnectVisitorChat>` goes on a
267
+ * public marketing page, and those pages frequently have no `ConnectProvider`
268
+ * anywhere near them - there is no session to hold and nothing on the page that
269
+ * needs one. `useConnect()` throwing is right for every component that reads a
270
+ * session; it is wrong for one whose whole purpose is the visitor who has none.
271
+ *
272
+ * What inheriting the context buys such a component, when it is inside one, is
273
+ * `basePath` and the injected `fetch` - so the same component renders against
274
+ * Storybook's mock bridge and the docs site's canned fetch without either of
275
+ * them having to pass anything.
276
+ *
277
+ * Do not reach for this to make an ordinary component tolerant of a missing
278
+ * provider. A session-reading component that silently renders a signed-out
279
+ * state because somebody forgot to wrap their app is a bug that looks like a
280
+ * product decision.
281
+ */
282
+ export declare function useConnectOptional(): ConnectContextValue | null;
248
283
  /** The signed-in user, or `null`. Sugar over `useConnect().session`. */
249
284
  export declare function useConnectUser(): ConnectUser | null;
250
285
  /**
@@ -306,11 +341,20 @@ export declare function useConnectEntitlements(): {
306
341
  isLoading: boolean;
307
342
  error: string | null;
308
343
  /**
309
- * Forces a fresh read - the provider's cache clears the mutated path's
310
- * entry on a successful `mutateResource` call, but nothing re-triggers
311
- * this hook's own effect on its own (none of its dependencies change from
312
- * a cache write nobody here observes). Call this right after a
313
- * `mutateResource` you made against the same `path` completes.
344
+ * Forces a fresh read - evicts this path's cache entry and re-runs the
345
+ * read.
346
+ *
347
+ * It used only to re-trigger the effect, on the assumption that the caller
348
+ * had just evicted the entry itself through `mutateResource`. That made it
349
+ * useless for the other reason to refetch: **something changed somewhere
350
+ * this tab cannot see.** The provider's cache is a `Map` of resolved
351
+ * promises with no expiry, so a `refetch()` without the eviction re-read
352
+ * the same settled promise and reported the same stale answer forever -
353
+ * which is what left `<ConnectAccessGate>` showing "Choose a plan" in a tab
354
+ * whose workspace had subscribed in a different one.
355
+ *
356
+ * Still safe after a `mutateResource` against the same path: evicting an
357
+ * entry that is already gone is a no-op.
314
358
  */
315
359
  refetch(): void;
316
360
  };
@@ -326,11 +370,20 @@ export declare function useConnectAddonUsage(): {
326
370
  isLoading: boolean;
327
371
  error: string | null;
328
372
  /**
329
- * Forces a fresh read - the provider's cache clears the mutated path's
330
- * entry on a successful `mutateResource` call, but nothing re-triggers
331
- * this hook's own effect on its own (none of its dependencies change from
332
- * a cache write nobody here observes). Call this right after a
333
- * `mutateResource` you made against the same `path` completes.
373
+ * Forces a fresh read - evicts this path's cache entry and re-runs the
374
+ * read.
375
+ *
376
+ * It used only to re-trigger the effect, on the assumption that the caller
377
+ * had just evicted the entry itself through `mutateResource`. That made it
378
+ * useless for the other reason to refetch: **something changed somewhere
379
+ * this tab cannot see.** The provider's cache is a `Map` of resolved
380
+ * promises with no expiry, so a `refetch()` without the eviction re-read
381
+ * the same settled promise and reported the same stale answer forever -
382
+ * which is what left `<ConnectAccessGate>` showing "Choose a plan" in a tab
383
+ * whose workspace had subscribed in a different one.
384
+ *
385
+ * Still safe after a `mutateResource` against the same path: evicting an
386
+ * entry that is already gone is a no-op.
334
387
  */
335
388
  refetch(): void;
336
389
  };
@@ -340,11 +393,20 @@ export declare function useConnectPlans(): {
340
393
  isLoading: boolean;
341
394
  error: string | null;
342
395
  /**
343
- * Forces a fresh read - the provider's cache clears the mutated path's
344
- * entry on a successful `mutateResource` call, but nothing re-triggers
345
- * this hook's own effect on its own (none of its dependencies change from
346
- * a cache write nobody here observes). Call this right after a
347
- * `mutateResource` you made against the same `path` completes.
396
+ * Forces a fresh read - evicts this path's cache entry and re-runs the
397
+ * read.
398
+ *
399
+ * It used only to re-trigger the effect, on the assumption that the caller
400
+ * had just evicted the entry itself through `mutateResource`. That made it
401
+ * useless for the other reason to refetch: **something changed somewhere
402
+ * this tab cannot see.** The provider's cache is a `Map` of resolved
403
+ * promises with no expiry, so a `refetch()` without the eviction re-read
404
+ * the same settled promise and reported the same stale answer forever -
405
+ * which is what left `<ConnectAccessGate>` showing "Choose a plan" in a tab
406
+ * whose workspace had subscribed in a different one.
407
+ *
408
+ * Still safe after a `mutateResource` against the same path: evicting an
409
+ * entry that is already gone is a no-op.
348
410
  */
349
411
  refetch(): void;
350
412
  };
@@ -363,11 +425,20 @@ export declare function useConnectCompanyProfile(): {
363
425
  isLoading: boolean;
364
426
  error: string | null;
365
427
  /**
366
- * Forces a fresh read - the provider's cache clears the mutated path's
367
- * entry on a successful `mutateResource` call, but nothing re-triggers
368
- * this hook's own effect on its own (none of its dependencies change from
369
- * a cache write nobody here observes). Call this right after a
370
- * `mutateResource` you made against the same `path` completes.
428
+ * Forces a fresh read - evicts this path's cache entry and re-runs the
429
+ * read.
430
+ *
431
+ * It used only to re-trigger the effect, on the assumption that the caller
432
+ * had just evicted the entry itself through `mutateResource`. That made it
433
+ * useless for the other reason to refetch: **something changed somewhere
434
+ * this tab cannot see.** The provider's cache is a `Map` of resolved
435
+ * promises with no expiry, so a `refetch()` without the eviction re-read
436
+ * the same settled promise and reported the same stale answer forever -
437
+ * which is what left `<ConnectAccessGate>` showing "Choose a plan" in a tab
438
+ * whose workspace had subscribed in a different one.
439
+ *
440
+ * Still safe after a `mutateResource` against the same path: evicting an
441
+ * entry that is already gone is a no-op.
371
442
  */
372
443
  refetch(): void;
373
444
  };
@@ -379,11 +450,20 @@ export declare function useConnectApps(): {
379
450
  isLoading: boolean;
380
451
  error: string | null;
381
452
  /**
382
- * Forces a fresh read - the provider's cache clears the mutated path's
383
- * entry on a successful `mutateResource` call, but nothing re-triggers
384
- * this hook's own effect on its own (none of its dependencies change from
385
- * a cache write nobody here observes). Call this right after a
386
- * `mutateResource` you made against the same `path` completes.
453
+ * Forces a fresh read - evicts this path's cache entry and re-runs the
454
+ * read.
455
+ *
456
+ * It used only to re-trigger the effect, on the assumption that the caller
457
+ * had just evicted the entry itself through `mutateResource`. That made it
458
+ * useless for the other reason to refetch: **something changed somewhere
459
+ * this tab cannot see.** The provider's cache is a `Map` of resolved
460
+ * promises with no expiry, so a `refetch()` without the eviction re-read
461
+ * the same settled promise and reported the same stale answer forever -
462
+ * which is what left `<ConnectAccessGate>` showing "Choose a plan" in a tab
463
+ * whose workspace had subscribed in a different one.
464
+ *
465
+ * Still safe after a `mutateResource` against the same path: evicting an
466
+ * entry that is already gone is a no-op.
387
467
  */
388
468
  refetch(): void;
389
469
  };
@@ -402,11 +482,20 @@ export declare function useConnectWorkspaces(): {
402
482
  isLoading: boolean;
403
483
  error: string | null;
404
484
  /**
405
- * Forces a fresh read - the provider's cache clears the mutated path's
406
- * entry on a successful `mutateResource` call, but nothing re-triggers
407
- * this hook's own effect on its own (none of its dependencies change from
408
- * a cache write nobody here observes). Call this right after a
409
- * `mutateResource` you made against the same `path` completes.
485
+ * Forces a fresh read - evicts this path's cache entry and re-runs the
486
+ * read.
487
+ *
488
+ * It used only to re-trigger the effect, on the assumption that the caller
489
+ * had just evicted the entry itself through `mutateResource`. That made it
490
+ * useless for the other reason to refetch: **something changed somewhere
491
+ * this tab cannot see.** The provider's cache is a `Map` of resolved
492
+ * promises with no expiry, so a `refetch()` without the eviction re-read
493
+ * the same settled promise and reported the same stale answer forever -
494
+ * which is what left `<ConnectAccessGate>` showing "Choose a plan" in a tab
495
+ * whose workspace had subscribed in a different one.
496
+ *
497
+ * Still safe after a `mutateResource` against the same path: evicting an
498
+ * entry that is already gone is a no-op.
410
499
  */
411
500
  refetch(): void;
412
501
  };
@@ -421,11 +510,20 @@ export declare function useConnectWorkspaceMembers(): {
421
510
  isLoading: boolean;
422
511
  error: string | null;
423
512
  /**
424
- * Forces a fresh read - the provider's cache clears the mutated path's
425
- * entry on a successful `mutateResource` call, but nothing re-triggers
426
- * this hook's own effect on its own (none of its dependencies change from
427
- * a cache write nobody here observes). Call this right after a
428
- * `mutateResource` you made against the same `path` completes.
513
+ * Forces a fresh read - evicts this path's cache entry and re-runs the
514
+ * read.
515
+ *
516
+ * It used only to re-trigger the effect, on the assumption that the caller
517
+ * had just evicted the entry itself through `mutateResource`. That made it
518
+ * useless for the other reason to refetch: **something changed somewhere
519
+ * this tab cannot see.** The provider's cache is a `Map` of resolved
520
+ * promises with no expiry, so a `refetch()` without the eviction re-read
521
+ * the same settled promise and reported the same stale answer forever -
522
+ * which is what left `<ConnectAccessGate>` showing "Choose a plan" in a tab
523
+ * whose workspace had subscribed in a different one.
524
+ *
525
+ * Still safe after a `mutateResource` against the same path: evicting an
526
+ * entry that is already gone is a no-op.
429
527
  */
430
528
  refetch(): void;
431
529
  };
@@ -435,11 +533,20 @@ export declare function useConnectIntegrations(): {
435
533
  isLoading: boolean;
436
534
  error: string | null;
437
535
  /**
438
- * Forces a fresh read - the provider's cache clears the mutated path's
439
- * entry on a successful `mutateResource` call, but nothing re-triggers
440
- * this hook's own effect on its own (none of its dependencies change from
441
- * a cache write nobody here observes). Call this right after a
442
- * `mutateResource` you made against the same `path` completes.
536
+ * Forces a fresh read - evicts this path's cache entry and re-runs the
537
+ * read.
538
+ *
539
+ * It used only to re-trigger the effect, on the assumption that the caller
540
+ * had just evicted the entry itself through `mutateResource`. That made it
541
+ * useless for the other reason to refetch: **something changed somewhere
542
+ * this tab cannot see.** The provider's cache is a `Map` of resolved
543
+ * promises with no expiry, so a `refetch()` without the eviction re-read
544
+ * the same settled promise and reported the same stale answer forever -
545
+ * which is what left `<ConnectAccessGate>` showing "Choose a plan" in a tab
546
+ * whose workspace had subscribed in a different one.
547
+ *
548
+ * Still safe after a `mutateResource` against the same path: evicting an
549
+ * entry that is already gone is a no-op.
443
550
  */
444
551
  refetch(): void;
445
552
  };
@@ -455,11 +562,20 @@ export declare function useConnectSessions(): {
455
562
  isLoading: boolean;
456
563
  error: string | null;
457
564
  /**
458
- * Forces a fresh read - the provider's cache clears the mutated path's
459
- * entry on a successful `mutateResource` call, but nothing re-triggers
460
- * this hook's own effect on its own (none of its dependencies change from
461
- * a cache write nobody here observes). Call this right after a
462
- * `mutateResource` you made against the same `path` completes.
565
+ * Forces a fresh read - evicts this path's cache entry and re-runs the
566
+ * read.
567
+ *
568
+ * It used only to re-trigger the effect, on the assumption that the caller
569
+ * had just evicted the entry itself through `mutateResource`. That made it
570
+ * useless for the other reason to refetch: **something changed somewhere
571
+ * this tab cannot see.** The provider's cache is a `Map` of resolved
572
+ * promises with no expiry, so a `refetch()` without the eviction re-read
573
+ * the same settled promise and reported the same stale answer forever -
574
+ * which is what left `<ConnectAccessGate>` showing "Choose a plan" in a tab
575
+ * whose workspace had subscribed in a different one.
576
+ *
577
+ * Still safe after a `mutateResource` against the same path: evicting an
578
+ * entry that is already gone is a no-op.
463
579
  */
464
580
  refetch(): void;
465
581
  };
@@ -491,11 +607,20 @@ export declare function useConnectResource<T>(path: string, options?: Readonly<U
491
607
  isLoading: boolean;
492
608
  error: string | null;
493
609
  /**
494
- * Forces a fresh read - the provider's cache clears the mutated path's
495
- * entry on a successful `mutateResource` call, but nothing re-triggers
496
- * this hook's own effect on its own (none of its dependencies change from
497
- * a cache write nobody here observes). Call this right after a
498
- * `mutateResource` you made against the same `path` completes.
610
+ * Forces a fresh read - evicts this path's cache entry and re-runs the
611
+ * read.
612
+ *
613
+ * It used only to re-trigger the effect, on the assumption that the caller
614
+ * had just evicted the entry itself through `mutateResource`. That made it
615
+ * useless for the other reason to refetch: **something changed somewhere
616
+ * this tab cannot see.** The provider's cache is a `Map` of resolved
617
+ * promises with no expiry, so a `refetch()` without the eviction re-read
618
+ * the same settled promise and reported the same stale answer forever -
619
+ * which is what left `<ConnectAccessGate>` showing "Choose a plan" in a tab
620
+ * whose workspace had subscribed in a different one.
621
+ *
622
+ * Still safe after a `mutateResource` against the same path: evicting an
623
+ * entry that is already gone is a no-op.
499
624
  */
500
625
  refetch(): void;
501
626
  };
@@ -77,6 +77,23 @@ export interface CheckoutUrlOptions {
77
77
  subscriptionId?: string | null;
78
78
  /** Carried into Connect's checkout, where it is validated against the real cart. */
79
79
  coupon?: string | null;
80
+ /**
81
+ * Where Connect sends the customer once the subscription is live - an
82
+ * absolute URL on your own origin.
83
+ *
84
+ * **Without it they do not come back.** Connect's default destination is
85
+ * the new subscription's own billing page, which is the right answer for
86
+ * someone who started in Connect and the wrong one for someone who started
87
+ * on your pricing page: they pay, land on a billing screen for a product
88
+ * they were in the middle of adopting, and nothing on it points home.
89
+ *
90
+ * Connect validates the origin against the redirect URIs registered for
91
+ * your Connect client and silently ignores anything else, so a value it
92
+ * does not recognise costs the return trip and never redirects your
93
+ * customer somewhere you did not register. Register the origin you intend
94
+ * to use; the path and query are yours.
95
+ */
96
+ next?: string | null;
80
97
  }
81
98
  /**
82
99
  * The Connect URL that cancels a subscription.
@@ -103,4 +120,4 @@ export declare function buildCancelUrl({ appUrl, subscriptionId }: {
103
120
  * prefix that has never served this page. Building the other one is what
104
121
  * sent every first-time subscriber from a partner's pricing page to a 404.
105
122
  */
106
- export declare function buildCheckoutUrl({ appUrl, applicationId, planId, subscriptionId, coupon, }: CheckoutUrlOptions): string;
123
+ export declare function buildCheckoutUrl({ appUrl, applicationId, planId, subscriptionId, coupon, next, }: CheckoutUrlOptions): string;
@@ -1,4 +1,4 @@
1
- import type { PlanFeature, ReadinessBlocker, ReadinessResponse } from "./types.js";
1
+ import type { PlanFeature, PlanLimit, ReadinessBlocker, ReadinessResponse } from "./types.js";
2
2
  /**
3
3
  * The blockers that actually deny access, in the order to show them.
4
4
  *
@@ -31,3 +31,38 @@ export type WirePlanFeature = string | PlanFeature;
31
31
  * hand-rolled a bridge instead of using `createConnectHandler`.
32
32
  */
33
33
  export declare function normalizePlanFeatures(features: readonly WirePlanFeature[] | undefined): PlanFeature[];
34
+ /**
35
+ * What a key the plan carries no answer for reads as - see
36
+ * `SubscribedPlan.limits`, which is sparse.
37
+ *
38
+ * `"none"` rather than `"unlimited"`: a plan that was never asked about a
39
+ * limit has not sold it, and defaulting the other way would hand an unbounded
40
+ * allowance to every tier that predates the question.
41
+ */
42
+ export declare const NO_PLAN_LIMIT: PlanLimit;
43
+ /**
44
+ * One plan's answer for one limit key, with the sparse-map default applied.
45
+ *
46
+ * Indexing `plan.limits` directly is what makes a missing key `undefined`,
47
+ * which then reads as unlimited through the first `?.` somebody writes. This
48
+ * returns a `PlanLimit` either way.
49
+ */
50
+ export declare function planLimitFor(limits: Readonly<Record<string, PlanLimit>> | undefined, key: string): PlanLimit;
51
+ /**
52
+ * Whether `additional` more of the resource fit under this limit, given how
53
+ * many the subscriber already has. The comparison, written once.
54
+ *
55
+ * Every way of writing it at a call site is wrong in a different direction:
56
+ * `count < limit.value` is off by one as soon as you add more than one row,
57
+ * `if (!limit.value)` refuses the unlimited tier, and
58
+ * `limit.value ?? Infinity` grants unlimited to a plan that allows none. This
59
+ * is the same implementation Connect's own server uses to validate a plan.
60
+ *
61
+ * ```ts
62
+ * const limit = await connect.entitlements.getLimit(workspaceId, "sites");
63
+ * if (!planLimitAllows(limit, await countSites(workspaceId))) {
64
+ * throw new Error("Your plan's site limit is reached.");
65
+ * }
66
+ * ```
67
+ */
68
+ export declare function planLimitAllows(limit: PlanLimit, currentCount: number, additional?: number): boolean;
@@ -15,6 +15,7 @@
15
15
  */
16
16
  export { ConnectError, isConnectError } from "./errors.js";
17
17
  export { blockingBlockers, normalizePlanFeatures } from "./helpers.js";
18
+ export { NO_PLAN_LIMIT, planLimitAllows, planLimitFor } from "./helpers.js";
18
19
  export { arrayRemove, arrayUnion, deleteField, documentExists, increment, serverTimestamp, storeIndexFor, storeIndexSignature, unchangedSince, } from "./store.js";
19
20
  export type { StoreIndexDirection, StoreIndexField } from "./store.js";
20
21
  export { describeQueryRejection, storeIndexCovers } from "./store-schema.js";
@@ -21,6 +21,7 @@ import { AnalyticsResource } from "./resources/analytics.js";
21
21
  import { OtpResource } from "./resources/otp.js";
22
22
  import { SessionsResource } from "./resources/sessions.js";
23
23
  import { PlatformResource } from "./resources/platform.js";
24
+ import { VisitorResource } from "./resources/visitor.js";
24
25
  import { PushResource } from "./resources/push.js";
25
26
  export declare const DEFAULT_BASE_URL = "https://connect.cosmoops.com";
26
27
  export interface ConnectClientOptions {
@@ -111,6 +112,11 @@ export declare class ConnectClient {
111
112
  readonly integrations: IntegrationsResource;
112
113
  /** Which workspaces the signed-in user belongs to, switching between them, and the team - see `WorkspacesResource`. */
113
114
  readonly workspaces: WorkspacesResource;
115
+ /**
116
+ * The public chat bubble on your own site - see `VisitorResource`. The one
117
+ * resource here whose caller is not one of your users.
118
+ */
119
+ readonly visitor: VisitorResource;
114
120
  /** The platform's own legal identity and DPDP grievance channel - see `PlatformResource`. */
115
121
  readonly platform: PlatformResource;
116
122
  /**
@@ -132,4 +138,4 @@ export declare class ConnectClient {
132
138
  constructor(options: ConnectClientOptions);
133
139
  }
134
140
  /** Kept in step with `package.json` by `sdk.test.ts`. */
135
- export declare const SDK_VERSION = "0.32.0";
141
+ export declare const SDK_VERSION = "0.34.0";
@@ -40,28 +40,4 @@ export interface ConnectHandlerOptions {
40
40
  */
41
41
  stateCookieName?: string;
42
42
  }
43
- /**
44
- * The browser half of this SDK cannot hold an API key - so it does not.
45
- *
46
- * `@cosmoops/connect-sdk/client` talks only to your own origin, and this
47
- * handler is what answers. It lives in your backend, holds the key, and
48
- * exposes a fixed, named set of reads plus sign-out and session revocation.
49
- * Nothing here proxies arbitrary paths: a generic passthrough would hand the
50
- * browser your whole API surface under your credentials, which is the same
51
- * leak with extra steps.
52
- *
53
- * Framework-agnostic - `Request` in, `Response` out, so it mounts anywhere
54
- * that shape exists.
55
- *
56
- * ```ts
57
- * // app/api/connect/[...route]/route.ts
58
- * const handler = createConnectHandler({ client, session });
59
- * export { handler as GET, handler as POST };
60
- * ```
61
- *
62
- * Every route resolves the workspace from the caller's *own* session, never
63
- * from a query parameter. A browser must not be able to ask about an
64
- * workspace it is not signed into, and a client-supplied `workspaceId`
65
- * is exactly that hole.
66
- */
67
43
  export declare function createConnectHandler(options: ConnectHandlerOptions): (request: Request) => Promise<Response>;