@cosmoops/connect-sdk 0.32.0 → 0.33.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.
@@ -1,9 +1,9 @@
1
1
  "use client";
2
2
  "use strict";
3
3
  Object.defineProperty(exports, "__esModule", { value: true });
4
- exports.normalizeNav = exports.useConnectPush = exports.useActivePath = exports.ConnectSideNav = exports.ConnectSidebarNav = exports.RAIL_EXPANDED = exports.RAIL_COLLAPSED = exports.sidebarRowClass = exports.useConnectShell = exports.ConnectShellProvider = exports.ConnectSidebarRow = exports.ConnectSidebarFooter = exports.ConnectSidebarBody = exports.ConnectSidebarHeader = exports.ConnectSidebar = exports.AppShell = exports.ConnectAccountSessions = exports.ConnectSignOutButton = exports.ConnectHomeButton = exports.ConnectProfileButton = exports.ConnectInvoicesButton = exports.ConnectManageSubscriptionButton = exports.ConnectUserMenu = exports.ConnectProfileModal = exports.ConnectUserButton = exports.ConnectOneTap = exports.ConnectRegisterButton = exports.ConnectLoginButton = exports.ConnectLogo = exports.ConnectMark = exports.ConnectGrievanceNotice = exports.isConnectError = exports.ConnectError = exports.normalizePlanFeatures = exports.blockingBlockers = exports.CONNECT_APP_URL = exports.useConnectResource = exports.useConnectIntegrations = exports.useConnectSessions = exports.useConnectAddonUsage = exports.useConnectCompanyProfile = exports.useConnectApps = exports.useConnectPlans = exports.useConnectEntitlements = exports.useConnectPlatformAccess = exports.useConnectAudience = exports.useConnectWorkspaceType = exports.useConnectUser = exports.useConnect = exports.ConnectProvider = void 0;
5
- exports.monthlyOf = exports.formatPrice = exports.buildCheckoutUrl = exports.useConnectCheckout = exports.ConnectFilterTabs = exports.ConnectCursorPagination = exports.ConnectConfirmDialog = exports.ConnectLinkButton = exports.ConnectPlansPage = exports.ConnectUsageMeter = exports.ConnectPlatformGate = exports.ConnectWorkspaceTypeGate = exports.useConnectFeature = exports.ConnectFeatureGate = exports.useConnectAccessGate = exports.ConnectAccessGate = exports.ConnectReadinessGate = exports.ConnectReadinessBanner = exports.resolveSubscriptionState = exports.ConnectSeatUsage = exports.ConnectTrialBanner = exports.ConnectSubscriptionCard = exports.ConnectNoSubscriptionCard = exports.ConnectSubscriptionGate = exports.ConnectSubscribeDialog = exports.ConnectPlans = exports.ConnectIntegrationsModal = exports.AI_TONE_DOT = exports.AI_TONE_CHIP = exports.AI_EMPTY_VALUE = exports.aiStatusTone = exports.isNumericAiFormat = exports.formatAiValue = exports.AiSources = exports.AiSteps = exports.AiDetails = exports.AiTimeline = exports.AiMetrics = exports.AiChart = exports.AiTable = exports.ConnectAskAi = exports.ConnectAppsGrid = exports.isLinkableConnectScreen = exports.matchConnectScreen = exports.filterNavForAudience = exports.gateExemptPaths = exports.isNavItemActive = exports.resolveActivePath = exports.normalizePath = exports.flattenNav = void 0;
6
- exports.zonedInstant = exports.dayColumns = exports.layoutBookings = exports.useBookingActions = exports.useBookings = exports.useAvailableSlots = exports.useBookableEventTypes = exports.ConnectBookingDetail = exports.ConnectBookingDialog = exports.ConnectBookingCalendar = exports.useConnectWorkspaceMembers = exports.useConnectWorkspaces = exports.ConnectWorkspaceSwitcher = exports.resolvePlanGroup = exports.groupPlansByTier = exports.isFreePlan = exports.ACTION_LABEL = exports.discountLabel = exports.annualSaving = exports.checkoutSubscriptionId = exports.resolveAction = void 0;
4
+ exports.useConnectPush = exports.useActivePath = exports.ConnectSideNav = exports.ConnectSidebarNav = exports.RAIL_EXPANDED = exports.RAIL_COLLAPSED = exports.sidebarRowClass = exports.useConnectShell = exports.ConnectShellProvider = exports.ConnectSidebarRow = exports.ConnectSidebarFooter = exports.ConnectSidebarBody = exports.ConnectSidebarHeader = exports.ConnectSidebar = exports.AppShell = exports.ConnectAccountSessions = exports.ConnectSignOutButton = exports.ConnectHomeButton = exports.ConnectProfileButton = exports.ConnectInvoicesButton = exports.ConnectManageSubscriptionButton = exports.ConnectUserMenu = exports.ConnectProfileModal = exports.ConnectUserButton = exports.ConnectOneTap = exports.ConnectRegisterButton = exports.ConnectLoginButton = exports.ConnectLogo = exports.ConnectMark = exports.ConnectGrievanceNotice = exports.isConnectError = exports.ConnectError = exports.normalizePlanFeatures = exports.blockingBlockers = exports.CONNECT_APP_URL = exports.useConnectResource = exports.useConnectIntegrations = exports.useConnectSessions = exports.useConnectAddonUsage = exports.useConnectCompanyProfile = exports.useConnectApps = exports.useConnectPlans = exports.useConnectEntitlements = exports.useConnectPlatformAccess = exports.useConnectAudience = exports.useConnectWorkspaceType = exports.useConnectUser = exports.useConnectOptional = exports.useConnect = exports.ConnectProvider = void 0;
5
+ exports.ConnectFilterTabs = exports.ConnectCursorPagination = exports.ConnectConfirmDialog = exports.ConnectLinkButton = exports.ConnectPlansPage = exports.ConnectUsageMeter = exports.ConnectPlatformGate = exports.ConnectWorkspaceTypeGate = exports.useConnectFeature = exports.ConnectFeatureGate = exports.useConnectAccessGate = exports.ConnectAccessGate = exports.ConnectReadinessGate = exports.ConnectReadinessBanner = exports.resolveSubscriptionState = exports.ConnectSeatUsage = exports.ConnectTrialBanner = exports.ConnectSubscriptionCard = exports.ConnectNoSubscriptionCard = exports.ConnectSubscriptionGate = exports.ConnectSubscribeDialog = exports.ConnectPlans = exports.ConnectIntegrationsModal = exports.AI_TONE_DOT = exports.AI_TONE_CHIP = exports.AI_EMPTY_VALUE = exports.aiStatusTone = exports.isNumericAiFormat = exports.formatAiValue = exports.AiSources = exports.AiSteps = exports.AiDetails = exports.AiTimeline = exports.AiMetrics = exports.AiChart = exports.AiTable = exports.ConnectAskAi = exports.ConnectAppsGrid = exports.visitorSlotOfferFrom = exports.readVisitorStream = exports.ConnectVisitorChat = exports.isLinkableConnectScreen = exports.matchConnectScreen = exports.filterNavForAudience = exports.gateExemptPaths = exports.isNavItemActive = exports.resolveActivePath = exports.normalizePath = exports.flattenNav = exports.normalizeNav = void 0;
6
+ exports.zonedInstant = exports.dayColumns = exports.layoutBookings = exports.useBookingActions = exports.useBookings = exports.useAvailableSlots = exports.useBookableEventTypes = exports.ConnectBookingDetail = exports.ConnectBookingDialog = exports.ConnectBookingCalendar = exports.useConnectWorkspaceMembers = exports.useConnectWorkspaces = exports.ConnectWorkspaceSwitcher = exports.resolvePlanGroup = exports.groupPlansByTier = exports.isFreePlan = exports.ACTION_LABEL = exports.discountLabel = exports.annualSaving = exports.checkoutSubscriptionId = exports.resolveAction = exports.monthlyOf = exports.formatPrice = exports.buildCheckoutUrl = exports.useConnectCheckout = void 0;
7
7
  /**
8
8
  * Browser half of `@cosmoops/connect-sdk` - React components and hooks.
9
9
  *
@@ -19,6 +19,9 @@ exports.zonedInstant = exports.dayColumns = exports.layoutBookings = exports.use
19
19
  var provider_js_1 = require("./provider.js");
20
20
  Object.defineProperty(exports, "ConnectProvider", { enumerable: true, get: function () { return provider_js_1.ConnectProvider; } });
21
21
  Object.defineProperty(exports, "useConnect", { enumerable: true, get: function () { return provider_js_1.useConnect; } });
22
+ // For a component that must render on a page with no provider - see its own
23
+ // note. `<ConnectVisitorChat>` is the case it exists for.
24
+ Object.defineProperty(exports, "useConnectOptional", { enumerable: true, get: function () { return provider_js_1.useConnectOptional; } });
22
25
  Object.defineProperty(exports, "useConnectUser", { enumerable: true, get: function () { return provider_js_1.useConnectUser; } });
23
26
  Object.defineProperty(exports, "useConnectWorkspaceType", { enumerable: true, get: function () { return provider_js_1.useConnectWorkspaceType; } });
24
27
  Object.defineProperty(exports, "useConnectAudience", { enumerable: true, get: function () { return provider_js_1.useConnectAudience; } });
@@ -126,6 +129,16 @@ Object.defineProperty(exports, "filterNavForAudience", { enumerable: true, get:
126
129
  var ai_screens_js_1 = require("./components/ai-screens.js");
127
130
  Object.defineProperty(exports, "matchConnectScreen", { enumerable: true, get: function () { return ai_screens_js_1.matchConnectScreen; } });
128
131
  Object.defineProperty(exports, "isLinkableConnectScreen", { enumerable: true, get: function () { return ai_screens_js_1.isLinkableConnectScreen; } });
132
+ /*
133
+ * The public chat bubble. Unlike every other component here it renders for a
134
+ * visitor with **no account**, and works on a page with no `ConnectProvider`
135
+ * at all - which is most marketing pages.
136
+ */
137
+ var visitor_chat_js_1 = require("./components/visitor-chat.js");
138
+ Object.defineProperty(exports, "ConnectVisitorChat", { enumerable: true, get: function () { return visitor_chat_js_1.ConnectVisitorChat; } });
139
+ var visitor_stream_js_1 = require("./components/visitor-stream.js");
140
+ Object.defineProperty(exports, "readVisitorStream", { enumerable: true, get: function () { return visitor_stream_js_1.readVisitorStream; } });
141
+ Object.defineProperty(exports, "visitorSlotOfferFrom", { enumerable: true, get: function () { return visitor_stream_js_1.visitorSlotOfferFrom; } });
129
142
  var apps_grid_js_1 = require("./components/apps-grid.js");
130
143
  Object.defineProperty(exports, "ConnectAppsGrid", { enumerable: true, get: function () { return apps_grid_js_1.ConnectAppsGrid; } });
131
144
  // `AppShell askAi` mounts this; exported for an app that wants the panel
@@ -4,6 +4,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
4
4
  exports.CONNECT_APP_URL = void 0;
5
5
  exports.ConnectProvider = ConnectProvider;
6
6
  exports.useConnect = useConnect;
7
+ exports.useConnectOptional = useConnectOptional;
7
8
  exports.useConnectUser = useConnectUser;
8
9
  exports.useConnectWorkspaceType = useConnectWorkspaceType;
9
10
  exports.useConnectAudience = useConnectAudience;
@@ -351,6 +352,29 @@ function useConnect() {
351
352
  }
352
353
  return value;
353
354
  }
355
+ /**
356
+ * The context when there is one, `null` when there is not.
357
+ *
358
+ * **For a component that must work on a page with no provider**, which is a
359
+ * real case rather than defensive coding: `<ConnectVisitorChat>` goes on a
360
+ * public marketing page, and those pages frequently have no `ConnectProvider`
361
+ * anywhere near them - there is no session to hold and nothing on the page that
362
+ * needs one. `useConnect()` throwing is right for every component that reads a
363
+ * session; it is wrong for one whose whole purpose is the visitor who has none.
364
+ *
365
+ * What inheriting the context buys such a component, when it is inside one, is
366
+ * `basePath` and the injected `fetch` - so the same component renders against
367
+ * Storybook's mock bridge and the docs site's canned fetch without either of
368
+ * them having to pass anything.
369
+ *
370
+ * Do not reach for this to make an ordinary component tolerant of a missing
371
+ * provider. A session-reading component that silently renders a signed-out
372
+ * state because somebody forgot to wrap their app is a bug that looks like a
373
+ * product decision.
374
+ */
375
+ function useConnectOptional() {
376
+ return (0, react_1.useContext)(ConnectContext);
377
+ }
354
378
  /** The signed-in user, or `null`. Sugar over `useConnect().session`. */
355
379
  function useConnectUser() {
356
380
  return useConnect().session?.user ?? null;
@@ -23,6 +23,7 @@ const analytics_js_1 = require("./resources/analytics.js");
23
23
  const otp_js_1 = require("./resources/otp.js");
24
24
  const sessions_js_1 = require("./resources/sessions.js");
25
25
  const platform_js_1 = require("./resources/platform.js");
26
+ const visitor_js_1 = require("./resources/visitor.js");
26
27
  const push_js_1 = require("./resources/push.js");
27
28
  exports.DEFAULT_BASE_URL = "https://connect.cosmoops.com";
28
29
  const APPLICATION_API_PREFIX = "/api/v1/application";
@@ -115,6 +116,11 @@ class ConnectClient {
115
116
  integrations;
116
117
  /** Which workspaces the signed-in user belongs to, switching between them, and the team - see `WorkspacesResource`. */
117
118
  workspaces;
119
+ /**
120
+ * The public chat bubble on your own site - see `VisitorResource`. The one
121
+ * resource here whose caller is not one of your users.
122
+ */
123
+ visitor;
118
124
  /** The platform's own legal identity and DPDP grievance channel - see `PlatformResource`. */
119
125
  platform;
120
126
  /**
@@ -165,6 +171,7 @@ class ConnectClient {
165
171
  this.sessions = new sessions_js_1.SessionsResource(transport, options.clientId ?? null);
166
172
  this.integrations = new integrations_js_1.IntegrationsResource(transport);
167
173
  this.workspaces = new workspaces_js_1.WorkspacesResource(transport);
174
+ this.visitor = new visitor_js_1.VisitorResource(transport);
168
175
  this.platform = new platform_js_1.PlatformResource(transport);
169
176
  this.push = new push_js_1.PushResource(transport);
170
177
  this.storeRelay = (spec) => (0, data_js_1.storePassthrough)(transport, spec);
@@ -99,6 +99,24 @@ function redirect(location, cookies = []) {
99
99
  * workspace it is not signed into, and a client-supplied `workspaceId`
100
100
  * is exactly that hole.
101
101
  */
102
+ /**
103
+ * The visitor's own address, for Connect's per-visitor limiter.
104
+ *
105
+ * **Left-most `x-forwarded-for` entry**, which is the same precedence Connect's
106
+ * own `/api/book` and `/api/auth/session` use, and the same caveat applies: the
107
+ * left-most entry is the client as the first proxy saw it, and a client can
108
+ * prepend to that header. That makes it unsuitable for authorization and fine
109
+ * for this - it partitions your application's own budget between visitors, and
110
+ * the ceilings that actually cap spending are per key and per application.
111
+ *
112
+ * `null` where nothing forwarded one - a local dev server, or a platform that
113
+ * does not set it. Connect then counts every visitor in one shared bucket,
114
+ * which is a stricter limit rather than an exemption.
115
+ */
116
+ function visitorIpOf(request) {
117
+ const forwarded = request.headers.get("x-forwarded-for")?.split(",")[0]?.trim();
118
+ return forwarded || request.headers.get("x-real-ip") || null;
119
+ }
102
120
  function createConnectHandler(options) {
103
121
  const basePath = (options.basePath ?? "/api/connect").replace(/\/+$/, "");
104
122
  const { client, session } = options;
@@ -330,6 +348,130 @@ function createConnectHandler(options) {
330
348
  return json({ readiness, subscription });
331
349
  },
332
350
  "GET /plans": async () => json(await client.billing.listPlans()),
351
+ /* ---------------------------------------------------------------------- */
352
+ /* The visitor assistant - the only routes here with no session at all */
353
+ /* ---------------------------------------------------------------------- */
354
+ /**
355
+ * `POST /visitor/session`, `/visitor/chat`, `/visitor/book`.
356
+ *
357
+ * Three routes that take **no session**, which makes them the exception to
358
+ * the rule every other route here follows. That is not a hole: the caller
359
+ * is a stranger on a public page, and there is no account for a session to
360
+ * identify. The question "whose data is this" has a different answer here -
361
+ * it is the *application's*, and the application is the one thing this
362
+ * bridge does hold a credential for.
363
+ *
364
+ * So the scoping that `requireWorkspace` provides elsewhere is provided
365
+ * here by Connect's own configuration: the workspace a call books into and
366
+ * the knowledge it answers from come from the application record, and no
367
+ * field in any of these bodies names a tenant. A browser cannot widen that
368
+ * by asking, because there is nothing to ask with.
369
+ *
370
+ * **`GET /company-profile` is the precedent and the limit of it.** That one
371
+ * is unauthenticated because the answer is identical for every caller.
372
+ * These are unauthenticated because the *caller* has no identity - but the
373
+ * answer is still per application, and spending is still per application.
374
+ * Which is why they are the only routes here that pass the visitor's own
375
+ * address along: the limiters on the far side count a visitor, and this
376
+ * bridge is the only thing that knows which one.
377
+ */
378
+ "POST /visitor/session": async (request) => {
379
+ const body = (await request.json().catch(() => null));
380
+ /*
381
+ * **The workspace is the browser's claim here, and that is the one thing
382
+ * this bridge must not simply relay.** Every other route on this bridge
383
+ * resolves the tenant from the caller's session; a visitor has none, so
384
+ * the widget names the customer whose site it is on - and a page can name
385
+ * any of them.
386
+ *
387
+ * Connect checks the workspace against your subscriptions, so the worst
388
+ * case is bounded to *your own* customers rather than the platform's. That
389
+ * is still one customer's budget spent from another's page, so an
390
+ * application serving several customers should resolve the workspace from
391
+ * its own widget record - by the embed token in the snippet, say - rather
392
+ * than trusting this field. The SDK cannot do that for you: it does not
393
+ * know what your widgets are keyed by.
394
+ */
395
+ if (typeof body?.workspaceId !== "string") {
396
+ throw new errors_js_1.ConnectError({
397
+ status: 400,
398
+ code: "validation_error",
399
+ message: "workspaceId is required",
400
+ endpoint: "POST /visitor/session",
401
+ });
402
+ }
403
+ return json(await client.visitor.startSession({
404
+ workspaceId: body.workspaceId,
405
+ page: typeof body.page === "string" ? body.page : null,
406
+ resume: typeof body.resume === "string" ? body.resume : null,
407
+ }, { visitorIp: visitorIpOf(request) }));
408
+ },
409
+ "POST /visitor/chat": async (request) => {
410
+ const body = (await request.json().catch(() => null));
411
+ if (typeof body?.sessionId !== "string" ||
412
+ typeof body?.workspaceId !== "string" ||
413
+ typeof body?.message !== "string") {
414
+ throw new errors_js_1.ConnectError({
415
+ status: 400,
416
+ code: "validation_error",
417
+ message: "sessionId, workspaceId and message are required",
418
+ endpoint: "POST /visitor/chat",
419
+ });
420
+ }
421
+ const upstream = await client.visitor.chat({
422
+ sessionId: body.sessionId,
423
+ workspaceId: body.workspaceId,
424
+ message: body.message,
425
+ ...(typeof body.assistantName === "string"
426
+ ? { assistantName: body.assistantName }
427
+ : {}),
428
+ ...(typeof body.bookingEventTypeSlug === "string"
429
+ ? { bookingEventTypeSlug: body.bookingEventTypeSlug }
430
+ : {}),
431
+ }, { visitorIp: visitorIpOf(request) });
432
+ /*
433
+ * Piped, never buffered - awaiting the body would collect the whole reply
434
+ * server-side and deliver it in one lump, which is the only thing
435
+ * streaming was for. The browser hanging up propagates down this pipe and
436
+ * stops generation upstream, which on this surface also stops the
437
+ * spending.
438
+ */
439
+ return new Response(upstream.body, {
440
+ status: upstream.status,
441
+ headers: {
442
+ "content-type": upstream.headers.get("content-type") ?? "text/event-stream",
443
+ "cache-control": "no-store",
444
+ // Without this a proxy in front of your app may buffer the whole
445
+ // stream to inspect it, which undoes the streaming end to end.
446
+ "x-accel-buffering": "no",
447
+ "x-content-type-options": "nosniff",
448
+ },
449
+ });
450
+ },
451
+ "POST /visitor/book": async (request) => {
452
+ const body = (await request.json().catch(() => null));
453
+ if (typeof body?.sessionId !== "string" ||
454
+ typeof body?.workspaceId !== "string" ||
455
+ typeof body?.eventTypeSlug !== "string" ||
456
+ typeof body?.startAt !== "string") {
457
+ throw new errors_js_1.ConnectError({
458
+ status: 400,
459
+ code: "validation_error",
460
+ message: "sessionId, workspaceId, eventTypeSlug and startAt are required",
461
+ endpoint: "POST /visitor/book",
462
+ });
463
+ }
464
+ return json(await client.visitor.book({
465
+ sessionId: body.sessionId,
466
+ workspaceId: body.workspaceId,
467
+ eventTypeSlug: body.eventTypeSlug,
468
+ startAt: body.startAt,
469
+ name: typeof body.name === "string" ? body.name : "",
470
+ phone: typeof body.phone === "string" ? body.phone : "",
471
+ email: typeof body.email === "string" ? body.email : null,
472
+ notes: typeof body.notes === "string" ? body.notes : null,
473
+ }, { visitorIp: visitorIpOf(request) }));
474
+ },
333
475
  /**
334
476
  * The platform's legal identity and grievance channel.
335
477
  *
@@ -142,5 +142,55 @@ class AiResource {
142
142
  });
143
143
  return result.data;
144
144
  }
145
+ /**
146
+ * How ready the site at a domain is for AI agents.
147
+ *
148
+ * Probes for an MCP server, an OpenAPI description, `llms.txt`, structured
149
+ * data and a markdown twin; evaluates the site's `robots.txt` against the
150
+ * assistants that actually fetch pages; checks whether its content survives
151
+ * without JavaScript and whether a bot wall refuses agents outright. Then
152
+ * grades what it found and says what to fix first.
153
+ *
154
+ * Needs an API key scoped to **`ai:readiness`**, which is separate from every
155
+ * other AI scope because it is the widest outbound surface on the API - it
156
+ * POSTs to candidate MCP endpoints and fetches the home page under two user
157
+ * agents.
158
+ *
159
+ * ```ts
160
+ * const result = await connect.ai.readiness({ domain: "acme.com" });
161
+ * if (result.status === "assessed") {
162
+ * console.log(result.report.grade, result.report.topFix?.fix);
163
+ * }
164
+ * ```
165
+ *
166
+ * Four things to know before wiring it in:
167
+ *
168
+ * - **Set your client timeout to at least 90 seconds.** A cold assessment
169
+ * runs the whole probe before answering and can take up to a minute; many
170
+ * HTTP clients default to 30s and abandon a request that was about to
171
+ * succeed. Cached reads return in milliseconds and are the normal case -
172
+ * reports are held for a week.
173
+ * - **A week, not ninety days**, because a report is a to-do list somebody is
174
+ * working through. Pass `refresh` after shipping a fix to see it land.
175
+ * - **Handle `status: "error"` checks as unknown, not as failures.** They mean
176
+ * Connect could not measure that check, and they are excluded from the
177
+ * score rather than counted against it.
178
+ * - **`skipped` says whose problem it is.** `unreachable` means the site did
179
+ * not answer; `provider_unavailable` means Connect's own assessment service
180
+ * did. Only the second is worth retrying silently, and neither should be
181
+ * shown as a failing grade.
182
+ */
183
+ readiness(input, options = {}) {
184
+ return (0, http_js_1.request)(this.transport, {
185
+ method: "POST",
186
+ path: "/ai/readiness",
187
+ json: {
188
+ domain: input.domain,
189
+ ...(input.refresh ? { refresh: true } : {}),
190
+ },
191
+ headers: (0, acting_as_js_1.actingAs)(input.asUser),
192
+ signal: options.signal,
193
+ });
194
+ }
145
195
  }
146
196
  exports.AiResource = AiResource;
@@ -100,5 +100,64 @@ class EnrichmentResource {
100
100
  signal: options.signal,
101
101
  });
102
102
  }
103
+ /**
104
+ * How **visible** the business at a domain is online, scored out of 100.
105
+ *
106
+ * The third sibling of `company()` and `brand()`: that one answers what a
107
+ * business does, the other what it looks like, and this one answers how
108
+ * findable it is - by a search engine, by a customer, and by the assistants
109
+ * that increasingly answer questions about a business before anybody visits
110
+ * its site.
111
+ *
112
+ * Needs an API key scoped to **`enrichment:presence`** - separate from
113
+ * `enrichment:read` and `enrichment:brand` because it is the widest outbound
114
+ * read of the three: roughly twenty fetches across the site's well-known
115
+ * paths, and sometimes a headless browser.
116
+ *
117
+ * ```ts
118
+ * const result = await connect.enrichment.presence({ domain: "acme.com" });
119
+ * if (result.status === "scanned") {
120
+ * for (const next of result.scan.recommendations) {
121
+ * console.log(`${next.priority}. ${next.title} - ${next.fix}`);
122
+ * }
123
+ * }
124
+ * ```
125
+ *
126
+ * Four things to know before wiring it in:
127
+ *
128
+ * - **The score is computed, not generated.** Every point comes from a check
129
+ * run against something actually observed, and every check ships with its
130
+ * weight, what was seen and the URL that settles it - so the total is
131
+ * reconstructable. A model only orders `recommendations` and writes the
132
+ * prose; it cannot move the number. That is the whole reason this is worth
133
+ * showing a customer: ask a model how good a company's web presence is and
134
+ * it answers 72 with total confidence for a site it never read.
135
+ * - **`social` is claimed, never verified.** Those are URLs the site itself
136
+ * printed. Nothing proves the account belongs to that business - render it
137
+ * as "linked from their site". Check `machineReadable` too: `false` means
138
+ * a real profile that search engines have to infer from a footer link,
139
+ * which is a specific, common and easily fixed finding.
140
+ * - **A low score is a success, not a skip.** Every check failing is the most
141
+ * useful report this produces. Only `unreachable` means the site did not
142
+ * answer - and `provider_unavailable` means *Connect* did not, which is
143
+ * never something to tell your user about their own site.
144
+ * - **Set your client timeout to at least 90 seconds.** A cold scan runs the
145
+ * whole probe before answering; many HTTP clients default to 30s and
146
+ * abandon a request that was about to succeed. Cached scans return in
147
+ * milliseconds, and cached is the normal case - results are held for a
148
+ * fortnight, so you pay the cold path roughly once per domain.
149
+ */
150
+ presence(input, options = {}) {
151
+ return (0, http_js_1.request)(this.transport, {
152
+ method: "POST",
153
+ path: "/enrichment/presence",
154
+ json: {
155
+ domain: input.domain,
156
+ ...(input.refresh ? { refresh: true } : {}),
157
+ },
158
+ headers: (0, acting_as_js_1.actingAs)(input.asUser),
159
+ signal: options.signal,
160
+ });
161
+ }
103
162
  }
104
163
  exports.EnrichmentResource = EnrichmentResource;
@@ -0,0 +1,227 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.VisitorKnowledgeResource = exports.VisitorResource = void 0;
4
+ const http_js_1 = require("../http.js");
5
+ /**
6
+ * The header a bridge forwards the visitor's own address on.
7
+ *
8
+ * It exists because `x-forwarded-for` cannot do this job here: by the time your
9
+ * backend calls Connect, that header describes the hop from *your server*, and
10
+ * the address Connect's per-visitor limiter needs is the one on the hop before
11
+ * it - which only your bridge has seen.
12
+ *
13
+ * **What it is and is not trusted for.** It partitions your own application's
14
+ * budget fairly between visitors; it does not grant anything. A caller sending a
15
+ * fresh value per request defeats only the per-visitor limiter and is still
16
+ * bounded by the per-key burst limit and the per-application daily ceiling -
17
+ * which are the two that actually cap spending, and neither is client-supplied.
18
+ * Omit it and every visitor shares one bucket, which is a stricter limit rather
19
+ * than an exemption.
20
+ */
21
+ const VISITOR_IP_HEADER = "x-connect-visitor-ip";
22
+ function visitorHeaders(options) {
23
+ return options?.visitorIp ? { [VISITOR_IP_HEADER]: options.visitorIp } : {};
24
+ }
25
+ /**
26
+ * The public chat bubble on your own marketing site.
27
+ *
28
+ * The one resource here whose caller is **not** one of your users. There is no
29
+ * `asUser`, no `workspaceId` and no session anywhere in it: the counterpart is a
30
+ * stranger who has no account, and everything the assistant may say or do is
31
+ * configuration a platform admin set on your application record.
32
+ *
33
+ * That is why none of these methods takes an id naming anything. The workspace a
34
+ * call books into, the event type it books, the knowledge it answers from and the
35
+ * daily budget it spends are all read from your application - so nothing a
36
+ * visitor types, and nothing an injection attempt in your own chat window
37
+ * achieves, reaches a different tenant.
38
+ *
39
+ * **Never call these from a browser.** They take your API key, like every other
40
+ * resource on this client. `createConnectHandler` already exposes them on your
41
+ * own origin for `<ConnectVisitorChat>` to use, which is how the credential stays
42
+ * server-side.
43
+ */
44
+ class VisitorResource {
45
+ transport;
46
+ /**
47
+ * The knowledge base behind the assistant - see `VisitorKnowledgeResource`.
48
+ *
49
+ * Nested rather than a top-level resource, because it is meaningless on its
50
+ * own: it exists to decide what *this* assistant may say.
51
+ */
52
+ knowledge;
53
+ constructor(transport) {
54
+ this.transport = transport;
55
+ this.knowledge = new VisitorKnowledgeResource(transport);
56
+ }
57
+ /**
58
+ * Open a conversation, or reopen one.
59
+ *
60
+ * Generates nothing - the greeting and the suggested questions come off the
61
+ * application record - so this is the one call in the flow that is always fast,
62
+ * which is what lets a bubble paint real copy instead of a spinner.
63
+ *
64
+ * Pass `resume` with a `sessionId` you kept and the transcript comes back with
65
+ * it. A `resume` that is expired, unknown or another application's silently
66
+ * starts a fresh conversation rather than failing: the token is the only thing
67
+ * being offered and it is being rejected, so there is nothing for the visitor
68
+ * to do about an error.
69
+ */
70
+ startSession(input, options = {}) {
71
+ return (0, http_js_1.request)(this.transport, {
72
+ method: "POST",
73
+ path: "/visitor/session",
74
+ json: {
75
+ workspaceId: input.workspaceId,
76
+ page: input.page ?? null,
77
+ resume: input.resume ?? null,
78
+ },
79
+ headers: visitorHeaders(options),
80
+ signal: options.signal,
81
+ });
82
+ }
83
+ /**
84
+ * One turn, streamed.
85
+ *
86
+ * Returns the raw `Response` rather than a parsed body, the same way
87
+ * `ai.chat` does: this answers server-sent events, and buffering them here
88
+ * would throw away the only reason to stream. The events are `delta`
89
+ * (`{text}`), `done` (`VisitorTurnDone`) and `error` (`{message}`).
90
+ *
91
+ * **The transcript is not a parameter**, which is the security property this
92
+ * endpoint is built around: the caller is ultimately a browser on a public
93
+ * page, and a client that can supply its own transcript can supply one in
94
+ * which anything has already been established. Only the conversation id and
95
+ * the new message travel.
96
+ */
97
+ chat(input, options = {}) {
98
+ return (0, http_js_1.request)(this.transport, {
99
+ method: "POST",
100
+ path: "/visitor/chat",
101
+ json: input,
102
+ headers: visitorHeaders(options),
103
+ raw: true,
104
+ signal: options.signal,
105
+ });
106
+ }
107
+ /**
108
+ * Book the call a visitor chose.
109
+ *
110
+ * `startAt` must be a time the turn's own slot lookup returned - an arbitrary
111
+ * instant is refused. That, plus a name and phone collected in real fields,
112
+ * is what makes it impossible to book an appointment at a time nobody offered
113
+ * or with a phone number a model parsed out of prose.
114
+ *
115
+ * A conversation books at most one appointment; a second call answers `409`.
116
+ */
117
+ book(input, options = {}) {
118
+ return (0, http_js_1.request)(this.transport, {
119
+ method: "POST",
120
+ path: "/visitor/book",
121
+ headers: visitorHeaders(options),
122
+ signal: options.signal,
123
+ json: {
124
+ ...input,
125
+ startAt: input.startAt instanceof Date ? input.startAt.toISOString() : input.startAt,
126
+ email: input.email ?? null,
127
+ notes: input.notes ?? null,
128
+ },
129
+ });
130
+ }
131
+ }
132
+ exports.VisitorResource = VisitorResource;
133
+ /**
134
+ * A subscribing workspace's **public** knowledge base - what their website chat
135
+ * may tell strangers.
136
+ *
137
+ * ## It is deliberately not their Context Index
138
+ *
139
+ * `ai.chat` answers from a workspace's Drive-fed corpus, for a signed-in member
140
+ * of that workspace. This is a separate store that holds only what somebody
141
+ * explicitly published, and the separation is the reason a publishable widget
142
+ * credential is safe to publish: everything the widget can reach was already
143
+ * meant for the public.
144
+ *
145
+ * Do not try to fill it by uploading to Drive. Nothing reaches this store as a
146
+ * side effect of anything else - that is the invariant, not an inconvenience.
147
+ *
148
+ * ## Publishing is its own call
149
+ *
150
+ * `save` always writes a draft. `publish` is what makes text readable by
151
+ * strangers, so no edit can do it by accident, and `unpublish` can fail - which
152
+ * means the item is **still live**, not that the call was pointless.
153
+ */
154
+ class VisitorKnowledgeResource {
155
+ transport;
156
+ constructor(transport) {
157
+ this.transport = transport;
158
+ }
159
+ /** Everything, drafts included - the list a customer audits. */
160
+ list(workspaceId) {
161
+ return (0, http_js_1.request)(this.transport, {
162
+ method: "GET",
163
+ path: "/visitor/knowledge",
164
+ query: { workspaceId },
165
+ });
166
+ }
167
+ /**
168
+ * Create or edit a draft.
169
+ *
170
+ * A body over the cap is **refused rather than truncated** - split a long page
171
+ * into sections, which is also what makes retrieval work.
172
+ */
173
+ async save(input) {
174
+ const result = await (0, http_js_1.request)(this.transport, {
175
+ method: "POST",
176
+ path: "/visitor/knowledge",
177
+ json: {
178
+ ...input,
179
+ itemId: input.itemId ?? null,
180
+ sourceRef: input.sourceRef ?? null,
181
+ },
182
+ });
183
+ return result.item;
184
+ }
185
+ /**
186
+ * Make one item readable by strangers.
187
+ *
188
+ * May be slow: the publish that outgrows the prompt provisions a retrieval
189
+ * corpus and indexes everything published so far. An indexing failure still
190
+ * publishes and comes back with `indexState: "failed"` and `indexError` set,
191
+ * so a provider hiccup never makes your button look broken.
192
+ */
193
+ async publish(input) {
194
+ const result = await (0, http_js_1.request)(this.transport, {
195
+ method: "POST",
196
+ path: "/visitor/knowledge/publish",
197
+ json: input,
198
+ });
199
+ return result.item;
200
+ }
201
+ /**
202
+ * Take one item out of what strangers can read.
203
+ *
204
+ * **A failure means it is still live.** The retrievable copy is removed before
205
+ * the record is flipped, so a provider that refuses the delete aborts rather
206
+ * than leaving an item your screen shows as withdrawn that the widget goes on
207
+ * quoting. Surface the error and let them retry; do not grey the row out
208
+ * optimistically.
209
+ */
210
+ async unpublish(input) {
211
+ const result = await (0, http_js_1.request)(this.transport, {
212
+ method: "POST",
213
+ path: "/visitor/knowledge/unpublish",
214
+ json: input,
215
+ });
216
+ return result.item;
217
+ }
218
+ /** Remove it entirely. A published item is unpublished first. */
219
+ async delete(input) {
220
+ await (0, http_js_1.request)(this.transport, {
221
+ method: "POST",
222
+ path: "/visitor/knowledge/delete",
223
+ json: input,
224
+ });
225
+ }
226
+ }
227
+ exports.VisitorKnowledgeResource = VisitorKnowledgeResource;