@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
@@ -292,6 +292,57 @@ export interface PlanFeature {
292
292
  /** Empty string when the catalog carries no description. */
293
293
  description: string;
294
294
  }
295
+ /**
296
+ * How much of one metered resource a plan allows, as the **catalogue** read
297
+ * reports it: the application's own declaration (`key`, `name`, `unit`) joined
298
+ * to this plan's answer.
299
+ *
300
+ * The limits counterpart of `PlanFeature`, and it carries the same asymmetry
301
+ * against `SubscribedPlan.limits` for the same reason - that one is a contract
302
+ * keyed for lookup, this one is a row to render.
303
+ *
304
+ * There is one entry per limit the application declares, including the ones a
305
+ * tier allows none of (`model: "none"`). "Starter: 1 site" only means anything
306
+ * beside "Growth: 5 sites", so a comparison table needs the row even where the
307
+ * answer is nothing; a card showing only what a tier includes filters on
308
+ * `model !== "none"`.
309
+ */
310
+ export interface ResolvedPlanLimit {
311
+ /** What `getLimit` and `SubscribedPlan.limits` are keyed by. */
312
+ key: string;
313
+ /** Human label, e.g. "Sites". */
314
+ name: string;
315
+ /** The noun the number needs, e.g. "sites" - render as `${value} ${unit}`. */
316
+ unit: string;
317
+ /** Empty string when the application's catalog carries no description. */
318
+ description: string;
319
+ model: PlanLimit["model"];
320
+ /** Non-null iff `model` is `"limited"`. */
321
+ value: number | null;
322
+ }
323
+ /**
324
+ * What a plan allows of one metered resource - the **gating** shape, which is
325
+ * what `getLimit` returns and `planLimitAllows` takes.
326
+ *
327
+ * **Three states rather than a nullable number, deliberately.** The obvious
328
+ * encoding - a `number | null` where `null` means unlimited - makes
329
+ * `if (!limit) refuse()` read as correct while refusing every subscriber on
330
+ * the most generous tier, which is the one case nobody tests. Use
331
+ * `planLimitAllows` rather than comparing by hand: `count < value` is off by
332
+ * one when adding more than one row, and `value ?? Infinity` grants unlimited
333
+ * to a plan that allows none.
334
+ */
335
+ export interface PlanLimit {
336
+ /**
337
+ * - `"none"`: this plan does not allow the resource at all. Also what a key
338
+ * the plan carries no answer for reads as - see `SubscribedPlan.limits`.
339
+ * - `"limited"`: a ceiling of `value`.
340
+ * - `"unlimited"`: no ceiling.
341
+ */
342
+ model: "none" | "limited" | "unlimited";
343
+ /** The ceiling. Non-null iff `model` is `"limited"`, and always >= 1 there - zero is spelled `"none"`. */
344
+ value: number | null;
345
+ }
295
346
  export interface Plan {
296
347
  id: string;
297
348
  name: string;
@@ -319,6 +370,8 @@ export interface Plan {
319
370
  trialDays: number | null;
320
371
  /** What this plan unlocks, labelled for display - see `PlanFeature`. */
321
372
  features: PlanFeature[];
373
+ /** How much of each metered resource this plan allows, labelled for display - see `ResolvedPlanLimit`. */
374
+ limits: ResolvedPlanLimit[];
322
375
  }
323
376
  /**
324
377
  * How often a plan bills.
@@ -351,6 +404,27 @@ export interface SubscribedPlan {
351
404
  * comparable, a display name is neither.
352
405
  */
353
406
  features: string[];
407
+ /**
408
+ * How much of each metered resource this workspace may have, keyed by the
409
+ * limit key your application declared - see `PlanLimit`.
410
+ *
411
+ * **Effective, not the tier's raw number**: whatever the add-ons on this
412
+ * subscription raise a limit by is already folded in, so this is the figure
413
+ * to compare your own count against. The tier's base number and the add-ons
414
+ * that raised it are deliberately not broken out here - that is the join
415
+ * Connect does so nobody has to redo it per resource, per partner.
416
+ *
417
+ * **Sparse, and that is the thing to get right**: a key absent here is not
418
+ * "unlimited" and not an error, it is `{ model: "none" }`. Read it through
419
+ * `getLimit`/`planLimitFor` rather than indexing the map, which is what
420
+ * turns a missing key into `undefined` and then, one `?.` later, into an
421
+ * unbounded allowance.
422
+ *
423
+ * Same posture as `features`: Connect cannot intercept calls to your
424
+ * backend, so your backend compares its own count against this number.
425
+ * Connect stores the answer and hands it over.
426
+ */
427
+ limits: Record<string, PlanLimit>;
354
428
  }
355
429
  export interface AppliedDiscount {
356
430
  source: "coupon" | "admin";
@@ -534,6 +608,160 @@ export interface WriteLogResponse {
534
608
  attributed: boolean;
535
609
  }
536
610
  /** One turn of a RAG chat. Matches the AI SDK's `UIMessage` loosely enough to pass one straight through. */
611
+ /** Where a knowledge item's text came from. */
612
+ export type VisitorKnowledgeSource = "website" | "authored" | "upload";
613
+ /**
614
+ * How a published item reaches the model.
615
+ *
616
+ * `inline` is the **normal and better** state for a small knowledge base, not a
617
+ * degraded one: the model is shown every published item rather than a top-k
618
+ * slice, with no chunk boundaries and no retrieval round trip. The other three
619
+ * only occur once a knowledge base has outgrown the prompt.
620
+ */
621
+ export type VisitorKnowledgeIndexState = "inline" | "indexing" | "indexed" | "failed";
622
+ /**
623
+ * One thing a workspace's website chat is allowed to tell strangers.
624
+ *
625
+ * **Not the workspace's Context Index.** That corpus is fed by every Drive
626
+ * upload; this store holds only what somebody explicitly published, which is
627
+ * what makes it safe to answer an anonymous visitor from. Nothing arrives here
628
+ * as a side effect of anything else.
629
+ */
630
+ export interface VisitorKnowledgeItem {
631
+ id: string;
632
+ title: string;
633
+ body: string;
634
+ source: VisitorKnowledgeSource;
635
+ /** The crawled URL, or the uploaded file's name. `null` for authored text. */
636
+ sourceRef: string | null;
637
+ status: "draft" | "published";
638
+ indexState: VisitorKnowledgeIndexState;
639
+ /** Why the last index attempt failed, for your own screen to show. */
640
+ indexError: string | null;
641
+ /** ISO-8601, or `null` for a draft. */
642
+ publishedAt: string | null;
643
+ createdAt: string;
644
+ updatedAt: string;
645
+ }
646
+ /**
647
+ * The knowledge base, and how its published half reaches the model.
648
+ *
649
+ * `mode` is a property of the *set*, which is why it is here and not on each
650
+ * item - and why a screen should read it from here rather than inferring it.
651
+ */
652
+ export interface VisitorKnowledgeList {
653
+ /** Drafts included: this is the list a customer audits. */
654
+ items: VisitorKnowledgeItem[];
655
+ mode: "none" | "inline" | "retrieval";
656
+ }
657
+ /**
658
+ * What the widget should ask for next, decided by Connect.
659
+ *
660
+ * `"message"` is the ordinary composer. The others mean render that field's own
661
+ * input instead - and the reason this is a server decision rather than something
662
+ * the widget infers from the assistant's sentence is the phone: an email asked
663
+ * for in prose gets typed into a general text box with autocorrect on, where
664
+ * `type="email" inputmode="email" autocomplete="email"` brings up a keyboard
665
+ * with an `@` on it. A six-digit code wants `autocomplete="one-time-code"`,
666
+ * which is what lets iOS offer the code straight from the notification.
667
+ */
668
+ export type VisitorField = "message" | "email" | "code" | "name";
669
+ /** One stored message in a visitor conversation. `at` is ISO-8601. */
670
+ export interface VisitorMessage {
671
+ role: "visitor" | "assistant";
672
+ text: string;
673
+ at: string;
674
+ }
675
+ /**
676
+ * An appointment this conversation booked, as a visitor is allowed to see it.
677
+ *
678
+ * Deliberately not the booking: no host ids, no workspace ids, no internal
679
+ * reference - the same narrowing Connect's own public booking page makes.
680
+ */
681
+ export interface VisitorBooking {
682
+ startAt: string;
683
+ endAt: string;
684
+ /** The workspace's zone, not the visitor's - the zone the confirmation uses. */
685
+ timezone: string;
686
+ }
687
+ /**
688
+ * A conversation, as `visitor.startSession` opens or reopens it.
689
+ *
690
+ * **It carries no greeting and no suggestions.** Those are your own widget
691
+ * settings, rendered from your own store - so your bubble paints its first frame
692
+ * with no call to Connect at all, and this exists only to mint or recover the
693
+ * conversation Connect holds.
694
+ */
695
+ export interface VisitorSession {
696
+ /**
697
+ * Hold this and send it with every turn.
698
+ *
699
+ * Treat it as a credential: it is the only thing identifying the conversation,
700
+ * and anyone holding it can continue it. Keep it in `sessionStorage` - it
701
+ * should survive a reload, which is what lets somebody follow a link out of
702
+ * the chat and come back, and it should not travel to another tab or outlive
703
+ * the visit.
704
+ */
705
+ sessionId: string;
706
+ /** The transcript, oldest first. Empty on a new conversation. */
707
+ messages: VisitorMessage[];
708
+ field: VisitorField;
709
+ turnsRemaining: number;
710
+ booking: VisitorAppointment | null;
711
+ }
712
+ /**
713
+ * The appointment `visitor.book` placed, and the one a reopened conversation
714
+ * reports.
715
+ *
716
+ * `VisitorBooking` plus `label`, and the extra field is the point: the words are
717
+ * produced by the same formatter the confirmation email uses, so the chat and
718
+ * the email cannot read differently about the same appointment. Render it rather
719
+ * than formatting `startAt` yourself - the visitor's browser is in their zone and
720
+ * the appointment is in the host's, and `Intl`'s month names differ between
721
+ * engines besides.
722
+ */
723
+ export interface VisitorAppointment extends VisitorBooking {
724
+ label: string;
725
+ }
726
+ /** One time the assistant may offer, as its slot lookup returned it. */
727
+ export interface VisitorSlot {
728
+ /** ISO-8601. Pass this back verbatim to book it. */
729
+ startAt: string;
730
+ /** Already written out in the workspace's own timezone - render this, don't reformat. */
731
+ label: string;
732
+ }
733
+ /**
734
+ * The times a turn looked up, lifted out of the tool result.
735
+ *
736
+ * **Draw the buttons from this, never from the assistant's sentence.** A time the
737
+ * visitor taps is then one the server offered, which is what makes
738
+ * `visitor.book` unable to take a time nobody was given.
739
+ */
740
+ export interface VisitorSlotOffer {
741
+ eventType: string;
742
+ durationMinutes: number;
743
+ timezone: string;
744
+ slots: VisitorSlot[];
745
+ }
746
+ /** What a tool did this turn. `result` is the tool's own payload - switch on `name`. */
747
+ export interface VisitorToolCall {
748
+ name: string;
749
+ result: unknown;
750
+ }
751
+ /** The `done` event at the end of a streamed turn. */
752
+ export interface VisitorTurnDone {
753
+ toolCalls: VisitorToolCall[];
754
+ field: VisitorField;
755
+ turnsRemaining: number;
756
+ /**
757
+ * Whether the reply was grounded in published knowledge.
758
+ *
759
+ * `false` means nothing published covered the question, so the assistant said
760
+ * so and offered a call. Worth recording: a run of them is a knowledge base
761
+ * with a gap in it, which is the most useful signal this endpoint produces.
762
+ */
763
+ grounded: boolean;
764
+ }
537
765
  export interface ChatMessage {
538
766
  role: "user" | "assistant" | "system";
539
767
  [field: string]: unknown;
@@ -872,6 +1100,67 @@ export interface EmitEventsResponse {
872
1100
  * registered a handler for.
873
1101
  */
874
1102
  export type ConnectWebhookEventType = "workspace.deletion_scheduled" | "workspace.deletion_cancelled" | "workspace.deleted" | "subscription.cancelled" | "call.completed" | "call.missed" | "booking.confirmed" | "booking.cancelled" | "booking.rescheduled" | "contact.created" | "contact.updated";
1103
+ /** What becomes of one class of data when the workspace is purged. */
1104
+ export type ConnectDataDisposition = "erase" | "anonymize" | "retain";
1105
+ /** Who answers for your copy of the customer's data. See the deletion-policy guide. */
1106
+ export type ConnectFiduciaryRole = "connect_owned" | "processor" | "independent";
1107
+ /** One kind of data your application holds, and what your policy said happens to it. */
1108
+ export interface ConnectDataClassNotice {
1109
+ /** Stable key, as declared in Console. Safe to switch on. */
1110
+ key: string;
1111
+ /** What the customer was shown - e.g. "Call records". */
1112
+ label: string;
1113
+ disposition: ConnectDataDisposition;
1114
+ /**
1115
+ * How many days this class outlives the workspace **in your own archive**,
1116
+ * or `null` when it goes with the purge.
1117
+ *
1118
+ * The one number in either deletion event you have to act on, and the
1119
+ * reason `policy` is on the wire at all: `purgeAfter` is Connect's clock,
1120
+ * this is yours. Read it rather than hardcoding the figure you typed into
1121
+ * Console - that copy drifts the first time somebody edits the policy, and
1122
+ * this one is the version the customer was actually promised.
1123
+ *
1124
+ * It is Connect's clamped value, so it is never longer than the platform
1125
+ * ceiling even if your stored policy says otherwise.
1126
+ */
1127
+ retainDays: number | null;
1128
+ /** The clause or rule you named as requiring it, shown to the customer verbatim. `null` for a class that is erased. */
1129
+ basis: string | null;
1130
+ }
1131
+ /** An identifier allocated to the workspace that must be held before reissue - a third clock, neither the grace window nor a retention floor. */
1132
+ export interface ConnectResourceReleaseNotice {
1133
+ key: string;
1134
+ label: string;
1135
+ /** Days to hold it before handing it to somebody else. */
1136
+ quarantineDays: number;
1137
+ }
1138
+ /**
1139
+ * Your application's own data policy, as it stood when this deletion was
1140
+ * confirmed - the declaration the customer read before pressing the button.
1141
+ *
1142
+ * Carried on both deletion events so your handler can be driven by the
1143
+ * declaration rather than by a second copy of it in your code. Two things
1144
+ * about it are worth relying on:
1145
+ *
1146
+ * **It is frozen.** An operator editing the policy while a workspace is
1147
+ * already pending deletion does not change what that deletion runs under;
1148
+ * `version` is which one it is, so a deletion certificate can name it.
1149
+ *
1150
+ * **It is yours alone.** A deletion fans out to every application the
1151
+ * workspace subscribed to, and each one receives only its own policy.
1152
+ */
1153
+ export interface ConnectDeletionPolicyNotice {
1154
+ /** The policy version in force when the deletion was confirmed. */
1155
+ version: number;
1156
+ fiduciaryRole: ConnectFiduciaryRole;
1157
+ /** Whether your policy promised the customer an export before the window closed. */
1158
+ exportBeforePurge: boolean;
1159
+ policyUrl: string | null;
1160
+ /** Empty when your application has declared no policy - which the customer was shown as "this application hasn't told us", not as "nothing is kept". */
1161
+ dataClasses: ConnectDataClassNotice[];
1162
+ resourceReleases: ConnectResourceReleaseNotice[];
1163
+ }
875
1164
  /**
876
1165
  * A workspace's deletion has been confirmed and the grace window is running.
877
1166
  *
@@ -887,6 +1176,13 @@ export interface WorkspaceDeletionScheduledEvent {
887
1176
  purgeAfter: string;
888
1177
  requestedBy: "owner" | "platform_admin";
889
1178
  graceDays: number;
1179
+ /**
1180
+ * Your declared policy for this deletion - including how long each data
1181
+ * class may outlive the purge. `null` for a deletion confirmed before
1182
+ * policies existed, which is the only case where you fall back to your own
1183
+ * records.
1184
+ */
1185
+ policy: ConnectDeletionPolicyNotice | null;
890
1186
  }
891
1187
  /**
892
1188
  * The deletion was withdrawn and the workspace is live again.
@@ -904,6 +1200,13 @@ export interface WorkspaceDeletedEvent {
904
1200
  workspaceId: string;
905
1201
  /** ISO-8601. */
906
1202
  purgedAt: string;
1203
+ /**
1204
+ * The same frozen policy `workspace.deletion_scheduled` carried, repeated
1205
+ * here because this is the event you act on: whether a class is destroyed
1206
+ * now or moved to a retained store for another `retainDays` is this
1207
+ * object's answer, not your code's.
1208
+ */
1209
+ policy: ConnectDeletionPolicyNotice | null;
907
1210
  }
908
1211
  /** A subscription ended. Fires on its own, not only as part of a deletion. */
909
1212
  export interface SubscriptionCancelledEvent {
@@ -1909,6 +2212,265 @@ export type BrandExtractionResult = {
1909
2212
  status: "skipped";
1910
2213
  reason: BrandSkipReason;
1911
2214
  };
2215
+ /** The five questions a readiness report answers. */
2216
+ export type ReadinessDimension =
2217
+ /** Can an agent *act* here? MCP, OpenAPI, agent cards. */
2218
+ "agent_interfaces"
2219
+ /** Can an agent get the content at all? JavaScript dependency, bot walls, semantic structure. */
2220
+ | "retrievability"
2221
+ /** Can an agent *read* it? llms.txt, markdown twins, structured data. */
2222
+ | "machine_readable"
2223
+ /** Is an agent *allowed*, and does the site say so deliberately? */
2224
+ | "crawl_consent"
2225
+ /** Is the site safe and identifiable to transact with? */
2226
+ | "trust";
2227
+ /**
2228
+ * How a check came out.
2229
+ *
2230
+ * `error` is the one to handle deliberately: it means Connect **could not
2231
+ * measure** this - a timeout, a refused connection - and it is excluded from
2232
+ * the score rather than counted as a failure. Render it as unknown, never as a
2233
+ * problem. `absent` ("you do not have this") and `fail` ("you have it and it is
2234
+ * broken") are likewise distinct and want different words.
2235
+ */
2236
+ export type ReadinessCheckStatus = "pass" | "partial" | "fail" | "absent" | "error";
2237
+ /**
2238
+ * Whether a check can cost points or only earn them.
2239
+ *
2240
+ * A `bonus` check raises the score when it passes and leaves it alone when it
2241
+ * does not - so a site is never marked down for missing a convention almost
2242
+ * nobody has adopted yet. Worth surfacing differently in a UI: a failed `core`
2243
+ * check is a gap, an absent `bonus` one is an opportunity.
2244
+ */
2245
+ export type ReadinessCheckKind = "core" | "bonus";
2246
+ /** One measured fact, with the evidence that settles it. */
2247
+ export interface ReadinessCheck {
2248
+ /** Stable dotted id, e.g. `mcp.server`. Safe to key on; findings cite it. */
2249
+ id: string;
2250
+ dimension: ReadinessDimension;
2251
+ kind: ReadinessCheckKind;
2252
+ title: string;
2253
+ status: ReadinessCheckStatus;
2254
+ /** Relative importance inside its dimension. Only ratios are meaningful. */
2255
+ weight: number;
2256
+ /** What was actually observed, in one line - specific enough to show the user verbatim. */
2257
+ observed: string;
2258
+ /** The URL that proves it, where there is one. */
2259
+ evidence: string | null;
2260
+ }
2261
+ export interface ReadinessDimensionScore {
2262
+ dimension: ReadinessDimension;
2263
+ /** 0-100. */
2264
+ score: number;
2265
+ /** This dimension's share of the overall score. */
2266
+ weight: number;
2267
+ /** Core checks that could not be measured. Show this rather than implying full coverage. */
2268
+ unmeasured: number;
2269
+ }
2270
+ export type ReadinessGrade = "A" | "B" | "C" | "D" | "F";
2271
+ /** One thing to do about it, anchored in the checks that motivated it. */
2272
+ export interface ReadinessFinding {
2273
+ severity: "critical" | "warning" | "note";
2274
+ /** The checks this is about. Every id here appears in `checks`. */
2275
+ checkIds: string[];
2276
+ title: string;
2277
+ detail: string;
2278
+ fix: string;
2279
+ effort: "low" | "medium" | "high";
2280
+ }
2281
+ /**
2282
+ * Whether an assistant could answer a buyer's question from the site's own pages.
2283
+ *
2284
+ * The one judged rather than measured part of the report - a model read the
2285
+ * pages and decided. Everything else in `checks` was parsed from real responses.
2286
+ */
2287
+ export interface ReadinessAnswerability {
2288
+ offering: boolean;
2289
+ pricing: boolean;
2290
+ howToBuy: boolean;
2291
+ contact: boolean;
2292
+ summary: string;
2293
+ gaps: string[];
2294
+ }
2295
+ /**
2296
+ * How ready a site is for AI agents.
2297
+ *
2298
+ * The important property, and the one that makes this worth showing a customer:
2299
+ * **the score was computed from the checks, and the checks were measured.**
2300
+ * Whether an MCP server exists is a completed JSON-RPC handshake, not a file
2301
+ * that happened to answer 200; whether assistants are blocked is that site's
2302
+ * `robots.txt` evaluated by its real rules. A model contributed only
2303
+ * `answerability` and the prose in `findings`, and it cannot alter a status or
2304
+ * the score.
2305
+ */
2306
+ export interface ReadinessReport {
2307
+ /** Normalised - lowercase, no scheme, no `www.`. */
2308
+ domain: string;
2309
+ /** 0-100, weighted across dimensions. */
2310
+ score: number;
2311
+ grade: ReadinessGrade;
2312
+ dimensions: ReadinessDimensionScore[];
2313
+ checks: ReadinessCheck[];
2314
+ /** Ordered by consequence, most serious first. */
2315
+ findings: ReadinessFinding[];
2316
+ answerability: ReadinessAnswerability | null;
2317
+ /** The one thing to do first. Drawn from `findings`, so it never disagrees with them. */
2318
+ topFix: {
2319
+ checkId: string;
2320
+ title: string;
2321
+ fix: string;
2322
+ } | null;
2323
+ /** ISO-8601. Age the report against this. */
2324
+ assessedAt: string;
2325
+ }
2326
+ /**
2327
+ * Why there is nothing to report. A normal 200, not an error.
2328
+ *
2329
+ * The distinction that matters for what you show and whether you retry:
2330
+ * `unreachable` is about the **site** - it did not answer - and may succeed
2331
+ * later. `provider_unavailable` is about **Connect** - our assessment service
2332
+ * is down or unconfigured - and is worth retrying without telling the user
2333
+ * anything about their own site. `no_domain` and `blocked_domain` never succeed
2334
+ * on retry.
2335
+ */
2336
+ export type ReadinessSkipReason = "no_domain" | "blocked_domain" | "consumer_domain" | "unreachable" | "provider_unavailable";
2337
+ export type ReadinessAssessmentResult = {
2338
+ status: "assessed";
2339
+ report: ReadinessReport;
2340
+ /** Whether Connect served this from its platform-wide cache. The report is the same either way. */
2341
+ cached: boolean;
2342
+ } | {
2343
+ status: "skipped";
2344
+ reason: ReadinessSkipReason;
2345
+ };
2346
+ /** The five things a presence is scored on. */
2347
+ export type PresenceCategory = "findability" | "identity" | "social" | "contactability" | "trust";
2348
+ /**
2349
+ * How a check came out.
2350
+ *
2351
+ * `partial` is a real third state rather than a rounded failure: a site with
2352
+ * `og:title` but no `og:image` has done most of the work, and rendering that as
2353
+ * a failure is both wrong and discouraging.
2354
+ */
2355
+ export type PresenceCriterionStatus = "pass" | "partial" | "fail";
2356
+ /** Where a score sits, coarsely. Advisory - use it to pick wording, not to make a decision. */
2357
+ export type PresenceBand = "absent" | "emerging" | "established" | "strong";
2358
+ /** The networks a scan recognises. `other` is a profile that matched no known pattern, kept rather than dropped. */
2359
+ export type PresenceNetwork = "linkedin" | "instagram" | "facebook" | "youtube" | "google_business" | "crunchbase" | "x" | "tiktok" | "github" | "other";
2360
+ /**
2361
+ * One check, its verdict and the evidence behind it.
2362
+ *
2363
+ * `weight` and `earned` are both published so the total can be reconstructed:
2364
+ * a `partial` that cost two points and one that cost six look identical
2365
+ * without them, and a score nobody can reconstruct is a score nobody trusts.
2366
+ */
2367
+ export interface PresenceCriterion {
2368
+ /** Stable across scans, so two reports of one domain can be diffed. */
2369
+ key: string;
2370
+ category: PresenceCategory;
2371
+ label: string;
2372
+ status: PresenceCriterionStatus;
2373
+ /** The check's full weight in the rubric. */
2374
+ weight: number;
2375
+ /** The share of `weight` actually earned. */
2376
+ earned: number;
2377
+ /** What was seen, in one line. */
2378
+ observed: string;
2379
+ /** The URL that settles it, when there is one. */
2380
+ citation: string | null;
2381
+ /** What to change. `null` when the check passed. */
2382
+ fix: string | null;
2383
+ }
2384
+ export interface PresenceCategoryScore {
2385
+ category: PresenceCategory;
2386
+ /** 0-100 within the category, so a weak one is legible beside a strong one. */
2387
+ score: number;
2388
+ weight: number;
2389
+ earned: number;
2390
+ }
2391
+ /**
2392
+ * A profile the subject's own site linked.
2393
+ *
2394
+ * **Claimed, never verified.** Nothing in a scan proves the account belongs to
2395
+ * that business - that needs a credential for each network. Render it as
2396
+ * "linked from their site", never as a confirmed account.
2397
+ */
2398
+ export interface PresenceSocialProfile {
2399
+ network: PresenceNetwork;
2400
+ url: string;
2401
+ handle: string | null;
2402
+ /**
2403
+ * Whether the site declared it in `schema.org` `sameAs` markup rather than
2404
+ * only as a link.
2405
+ *
2406
+ * The one to read closely. `false` means a real presence with none of the
2407
+ * discoverability - a search engine has to infer the connection from a
2408
+ * footer icon - and it is among the most common findings a scan produces.
2409
+ */
2410
+ machineReadable: boolean;
2411
+ }
2412
+ /** One prioritised thing to do. `fix` is Connect's own reviewed text; `why` is written for this business. */
2413
+ export interface PresenceRecommendation {
2414
+ /** Names the check this came from, so advice is always traceable to evidence. */
2415
+ criterionKey: string;
2416
+ category: PresenceCategory;
2417
+ /** 1 is what to do first. Always 1..n with no gaps. */
2418
+ priority: number;
2419
+ title: string;
2420
+ /** What it costs this business specifically. Empty when the narrative could not be generated. */
2421
+ why: string;
2422
+ fix: string;
2423
+ }
2424
+ /**
2425
+ * How visible a business is online.
2426
+ *
2427
+ * The important property, and the one that makes this worth showing a
2428
+ * customer: **the score was computed from the checks, and the checks were
2429
+ * measured.** Every point comes from something a fetcher observed on the site.
2430
+ * A model contributed only the ordering of `recommendations` and the prose in
2431
+ * `why` and `summary`; it cannot alter a status, a weight or the score.
2432
+ */
2433
+ export interface PresenceScan {
2434
+ /** Normalised - lowercase, no scheme, no `www.`. */
2435
+ domain: string;
2436
+ /** 0-100. */
2437
+ score: number;
2438
+ band: PresenceBand;
2439
+ categories: PresenceCategoryScore[];
2440
+ /** Every check, passed and failed. The audit trail behind the number. */
2441
+ criteria: PresenceCriterion[];
2442
+ /** Every profile found, including networks the rubric does not weight individually. */
2443
+ social: PresenceSocialProfile[];
2444
+ /** Two or three sentences on the overall state. Empty when it could not be generated - the scan is complete without it. */
2445
+ summary: string;
2446
+ /** What to do, in order. Render this before `criteria`. */
2447
+ recommendations: PresenceRecommendation[];
2448
+ /** ISO-8601. Age the scan against this. */
2449
+ scannedAt: string;
2450
+ }
2451
+ /**
2452
+ * Why there is nothing to report. A normal 200, not an error.
2453
+ *
2454
+ * The distinction that matters for what you show and whether you retry:
2455
+ * `unreachable` is about the **site** - the home page did not answer - and may
2456
+ * succeed later. `provider_unavailable` is about **Connect** - our scanner is
2457
+ * down or unconfigured - and is worth retrying without telling the user
2458
+ * anything about their own site. `no_domain`, `consumer_domain` and
2459
+ * `blocked_domain` never succeed on retry.
2460
+ *
2461
+ * Note what is **not** here: a low score. Every check failing is a successful
2462
+ * scan and the most useful report this produces.
2463
+ */
2464
+ export type PresenceSkipReason = "no_domain" | "consumer_domain" | "blocked_domain" | "unreachable" | "provider_unavailable";
2465
+ export type PresenceScanResult = {
2466
+ status: "scanned";
2467
+ scan: PresenceScan;
2468
+ /** Whether Connect served this from its platform-wide cache. The scan is the same either way. */
2469
+ cached: boolean;
2470
+ } | {
2471
+ status: "skipped";
2472
+ reason: PresenceSkipReason;
2473
+ };
1912
2474
  /**
1913
2475
  * What kind of conversation a channel carries. `voice` is the only kind
1914
2476
  * Connect stocks inventory for today - `whatsapp` and `meeting` are modelled
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cosmoops/connect-sdk",
3
- "version": "0.32.0",
3
+ "version": "0.34.0",
4
4
  "description": "Official server-side SDK for the CosmoOps Connect platform API - Login with Connect, entitlements, billing and metering.",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "type": "commonjs",
@@ -83,7 +83,8 @@
83
83
  "test": "bun test",
84
84
  "docs:md": "typedoc --options typedoc.markdown.json",
85
85
  "storybook": "storybook dev -p 6106",
86
- "build:storybook": "storybook build -o storybook-static"
86
+ "build:storybook": "storybook build -o storybook-static",
87
+ "release:stage": "bun run scripts/stage-release.ts"
87
88
  },
88
89
  "devDependencies": {
89
90
  "@connect/ai": "workspace:*",