@cosmoops/connect-sdk 0.20.0 → 0.23.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 (48) hide show
  1. package/CHANGELOG.md +281 -0
  2. package/README.md +2 -0
  3. package/dist/cjs/client/components/identity.js +24 -0
  4. package/dist/cjs/client/components/one-tap-state.js +96 -0
  5. package/dist/cjs/client/components/one-tap.js +106 -0
  6. package/dist/cjs/client/components/plan-pricing.js +28 -0
  7. package/dist/cjs/client/components/plans.js +8 -3
  8. package/dist/cjs/client/components/user-menu.js +2 -6
  9. package/dist/cjs/client/index.js +11 -3
  10. package/dist/cjs/client/provider.js +102 -7
  11. package/dist/cjs/server/client.js +12 -1
  12. package/dist/cjs/server/handler.js +126 -2
  13. package/dist/cjs/server/resources/calls.js +57 -0
  14. package/dist/cjs/server/resources/channels.js +115 -0
  15. package/dist/cjs/server/resources/enrichment.js +56 -0
  16. package/dist/cjs/server/resources/identity.js +40 -0
  17. package/dist/cjs/server/resources/workspaces.js +73 -2
  18. package/dist/esm/client/components/identity.js +21 -0
  19. package/dist/esm/client/components/one-tap-state.js +90 -0
  20. package/dist/esm/client/components/one-tap.js +103 -0
  21. package/dist/esm/client/components/plan-pricing.js +27 -0
  22. package/dist/esm/client/components/plans.js +9 -4
  23. package/dist/esm/client/components/user-menu.js +1 -5
  24. package/dist/esm/client/index.js +8 -1
  25. package/dist/esm/client/provider.js +102 -7
  26. package/dist/esm/server/client.js +12 -1
  27. package/dist/esm/server/handler.js +127 -3
  28. package/dist/esm/server/resources/calls.js +53 -0
  29. package/dist/esm/server/resources/channels.js +111 -0
  30. package/dist/esm/server/resources/enrichment.js +52 -0
  31. package/dist/esm/server/resources/identity.js +39 -0
  32. package/dist/esm/server/resources/workspaces.js +73 -2
  33. package/dist/types/client/components/identity.d.ts +17 -0
  34. package/dist/types/client/components/one-tap-state.d.ts +39 -0
  35. package/dist/types/client/components/one-tap.d.ts +50 -0
  36. package/dist/types/client/components/plan-pricing.d.ts +23 -0
  37. package/dist/types/client/index.d.ts +4 -2
  38. package/dist/types/client/provider.d.ts +101 -3
  39. package/dist/types/client/use-checkout.d.ts +9 -0
  40. package/dist/types/server/client.d.ts +9 -1
  41. package/dist/types/server/index.d.ts +3 -0
  42. package/dist/types/server/resources/calls.d.ts +45 -0
  43. package/dist/types/server/resources/channels.d.ts +91 -0
  44. package/dist/types/server/resources/enrichment.d.ts +44 -0
  45. package/dist/types/server/resources/identity.d.ts +35 -0
  46. package/dist/types/server/resources/workspaces.d.ts +63 -3
  47. package/dist/types/types.d.ts +344 -1
  48. package/package.json +1 -1
@@ -1,9 +1,9 @@
1
1
  "use client";
2
2
  "use strict";
3
3
  Object.defineProperty(exports, "__esModule", { value: true });
4
- exports.normalizePath = exports.flattenNav = exports.normalizeNav = 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.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.annualSaving = exports.resolveAction = 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 = 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 = void 0;
4
+ exports.flattenNav = exports.normalizeNav = 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.resolveAction = 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 = 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 = void 0;
7
7
  /**
8
8
  * Browser half of `@cosmoops/connect-sdk` - React components and hooks.
9
9
  *
@@ -50,6 +50,8 @@ Object.defineProperty(exports, "ConnectLogo", { enumerable: true, get: function
50
50
  var auth_buttons_js_1 = require("./components/auth-buttons.js");
51
51
  Object.defineProperty(exports, "ConnectLoginButton", { enumerable: true, get: function () { return auth_buttons_js_1.ConnectLoginButton; } });
52
52
  Object.defineProperty(exports, "ConnectRegisterButton", { enumerable: true, get: function () { return auth_buttons_js_1.ConnectRegisterButton; } });
53
+ var one_tap_js_1 = require("./components/one-tap.js");
54
+ Object.defineProperty(exports, "ConnectOneTap", { enumerable: true, get: function () { return one_tap_js_1.ConnectOneTap; } });
53
55
  var user_menu_js_1 = require("./components/user-menu.js");
54
56
  Object.defineProperty(exports, "ConnectUserButton", { enumerable: true, get: function () { return user_menu_js_1.ConnectUserButton; } });
55
57
  Object.defineProperty(exports, "ConnectProfileModal", { enumerable: true, get: function () { return user_menu_js_1.ConnectProfileModal; } });
@@ -208,6 +210,12 @@ var plan_pricing_js_1 = require("./components/plan-pricing.js");
208
210
  Object.defineProperty(exports, "formatPrice", { enumerable: true, get: function () { return plan_pricing_js_1.formatPrice; } });
209
211
  Object.defineProperty(exports, "monthlyOf", { enumerable: true, get: function () { return plan_pricing_js_1.monthlyOf; } });
210
212
  Object.defineProperty(exports, "resolveAction", { enumerable: true, get: function () { return plan_pricing_js_1.resolveAction; } });
213
+ // The other half of `resolveAction`, and exported with it deliberately: a
214
+ // partner writing its own pricing grid has to turn that action into a
215
+ // checkout URL, and reading the subscription id straight off the
216
+ // entitlements response instead is what sent a "Subscribe" button to
217
+ // Connect's change-plan screen. See its doc comment.
218
+ Object.defineProperty(exports, "checkoutSubscriptionId", { enumerable: true, get: function () { return plan_pricing_js_1.checkoutSubscriptionId; } });
211
219
  Object.defineProperty(exports, "annualSaving", { enumerable: true, get: function () { return plan_pricing_js_1.annualSaving; } });
212
220
  Object.defineProperty(exports, "discountLabel", { enumerable: true, get: function () { return plan_pricing_js_1.discountLabel; } });
213
221
  Object.defineProperty(exports, "ACTION_LABEL", { enumerable: true, get: function () { return plan_pricing_js_1.ACTION_LABEL; } });
@@ -20,6 +20,7 @@ exports.useConnectSessions = useConnectSessions;
20
20
  exports.useConnectResource = useConnectResource;
21
21
  const jsx_runtime_1 = require("react/jsx-runtime");
22
22
  const react_1 = require("react");
23
+ const one_tap_state_js_1 = require("./components/one-tap-state.js");
23
24
  exports.CONNECT_APP_URL = "https://connect.cosmoops.com";
24
25
  const ConnectContext = (0, react_1.createContext)(null);
25
26
  /** Marks that this tab has already spent its one silent attempt - see `ConnectProviderProps.silentSignIn`. */
@@ -204,8 +205,91 @@ function ConnectProvider({ clientId = "", basePath = "/api/connect", redirectUri
204
205
  credentials: "same-origin",
205
206
  }).catch(() => { });
206
207
  cache.current.clear();
208
+ // Someone who just signed out must not be offered the same account back on
209
+ // the next render - see `suppressOneTapAfterSignOut`.
210
+ (0, one_tap_state_js_1.suppressOneTapAfterSignOut)();
207
211
  setResult({ session: { user: null, subscription: null, role: null, permissions: [], staff: false, audience: null }, error: null });
208
212
  }, [basePath, fetchImpl]);
213
+ /**
214
+ * Where Connect answers One Tap, taken from the session read rather than
215
+ * assembled here - the bridge knows the `client_id` and the browser need not.
216
+ */
217
+ const oneTapEndpoints = result?.session.oneTap ?? null;
218
+ const readOneTapHint = (0, react_1.useCallback)(async () => {
219
+ if (!oneTapEndpoints)
220
+ return null;
221
+ try {
222
+ // `credentials: "include"` because this is the one request in the SDK
223
+ // that leaves the application's own origin. It carries the visitor's
224
+ // Connect cookie only when the two are same-site; everywhere else it is
225
+ // simply sent without one and comes back `signed_out`.
226
+ const response = await (fetchImpl ?? globalThis.fetch)(oneTapEndpoints.hintUrl, {
227
+ credentials: "include",
228
+ headers: { accept: "application/json" },
229
+ });
230
+ if (!response.ok)
231
+ return null;
232
+ return (await response.json());
233
+ }
234
+ catch {
235
+ // Offline, blocked, or Connect is down. None of those is evidence about
236
+ // whether this visitor has a session, so the honest answer is silence.
237
+ return null;
238
+ }
239
+ }, [fetchImpl, oneTapEndpoints]);
240
+ const oneTapSignIn = (0, react_1.useCallback)(async () => {
241
+ if (!oneTapEndpoints)
242
+ return { ok: false, reason: "unavailable" };
243
+ try {
244
+ // 1. The bridge mints the PKCE pair and keeps the verifier in an
245
+ // `HttpOnly` cookie on this origin.
246
+ const start = await mutateResource("/one-tap/start");
247
+ // 2. Connect mints a code against that challenge. The custom header is
248
+ // what forces the preflight, so an origin this client has not
249
+ // registered is refused before the request is ever sent.
250
+ const authorize = await (fetchImpl ?? globalThis.fetch)(oneTapEndpoints.authorizeUrl, {
251
+ method: "POST",
252
+ credentials: "include",
253
+ headers: {
254
+ accept: "application/json",
255
+ "content-type": "application/json",
256
+ "x-connect-one-tap": "1",
257
+ },
258
+ body: JSON.stringify({
259
+ redirect_uri: start.redirectUri,
260
+ code_challenge: start.codeChallenge,
261
+ }),
262
+ });
263
+ const body = (await authorize.json().catch(() => null));
264
+ if (!authorize.ok || !body?.code) {
265
+ // Connect names the reason, and each one has a different next step -
266
+ // `consent_required` wants the interactive screen, `rate_limited` wants
267
+ // to be left alone. Anything unrecognised is reported as a plain
268
+ // failure rather than guessed at.
269
+ const reason = body?.error;
270
+ return {
271
+ ok: false,
272
+ reason: reason === "login_required" ||
273
+ reason === "consent_required" ||
274
+ reason === "interaction_required" ||
275
+ reason === "rate_limited"
276
+ ? reason
277
+ : "failed",
278
+ };
279
+ }
280
+ // 3. The bridge exchanges the code with the verifier the page never saw,
281
+ // and stores the session.
282
+ await mutateResource("/one-tap/complete", { code: body.code });
283
+ // The prompt was accepted, so whatever backoff earlier dismissals had
284
+ // accumulated is spent - it described reluctance that no longer holds.
285
+ (0, one_tap_state_js_1.clearOneTapSuppression)();
286
+ await refresh();
287
+ return { ok: true };
288
+ }
289
+ catch {
290
+ return { ok: false, reason: "failed" };
291
+ }
292
+ }, [fetchImpl, mutateResource, oneTapEndpoints, refresh]);
209
293
  const value = (0, react_1.useMemo)(() => ({
210
294
  clientId,
211
295
  appUrl,
@@ -219,6 +303,8 @@ function ConnectProvider({ clientId = "", basePath = "/api/connect", redirectUri
219
303
  mutateResource,
220
304
  invalidateResource,
221
305
  streamResource,
306
+ readOneTapHint,
307
+ oneTapSignIn,
222
308
  }), [
223
309
  clientId,
224
310
  appUrl,
@@ -232,6 +318,8 @@ function ConnectProvider({ clientId = "", basePath = "/api/connect", redirectUri
232
318
  mutateResource,
233
319
  invalidateResource,
234
320
  streamResource,
321
+ readOneTapHint,
322
+ oneTapSignIn,
235
323
  ]);
236
324
  return (0, jsx_runtime_1.jsx)(ConnectContext.Provider, { value: value, children: children });
237
325
  }
@@ -320,7 +408,7 @@ function useConnectPlans() {
320
408
  * `CompanyProfile`. `<ConnectGrievanceNotice>` renders it for you.
321
409
  */
322
410
  function useConnectCompanyProfile() {
323
- return useConnectResource("/company-profile");
411
+ return useConnectResource("/company-profile", { requiresSession: false });
324
412
  }
325
413
  /** The apps this workspace can open. */
326
414
  function useConnectApps() {
@@ -361,10 +449,15 @@ function useConnectSessions() {
361
449
  /**
362
450
  * One bridge read, shared through the provider's cache.
363
451
  *
364
- * Only fetches once a user is known: every resource route needs a session, so
365
- * firing before one exists just buys a guaranteed 401.
452
+ * Waits for a session by default: every resource route but one needs it, so
453
+ * firing before one exists just buys a guaranteed 401. `requiresSession:
454
+ * false` opts out, and the reason it exists is the grievance notice - a
455
+ * statutory contact is at its most useful on a public page a signed-out
456
+ * visitor is reading, so a hook that answered `null` until someone logged in
457
+ * would render nothing at exactly the moment that matters.
366
458
  */
367
- function useConnectResource(path) {
459
+ function useConnectResource(path, options) {
460
+ const requiresSession = options?.requiresSession ?? true;
368
461
  const { fetchResource, session, isLoading: sessionLoading } = useConnect();
369
462
  // Stamped with the `path` it belongs to, so switching paths reports loading
370
463
  // without a synchronous `setIsLoading(true)` - and, more usefully, so a
@@ -375,7 +468,7 @@ function useConnectResource(path) {
375
468
  // Signed out is handled by derivation below rather than by writing state
376
469
  // here - a synchronous setState inside an effect costs an extra render pass
377
470
  // and is what `react-hooks/set-state-in-effect` exists to catch.
378
- if (sessionLoading || !session?.user)
471
+ if (requiresSession && (sessionLoading || !session?.user))
379
472
  return;
380
473
  let active = true;
381
474
  fetchResource(path)
@@ -394,8 +487,10 @@ function useConnectResource(path) {
394
487
  // `version` is otherwise unread - it exists purely to force this effect
395
488
  // to re-run on `refetch()`, after the caller has already evicted this
396
489
  // path's cache entry via `mutateResource`.
397
- }, [fetchResource, path, session?.user, sessionLoading, version]);
398
- const signedOut = !sessionLoading && !session?.user;
490
+ }, [fetchResource, path, requiresSession, session?.user, sessionLoading, version]);
491
+ // A public read is never settled by the absence of a session - it is
492
+ // genuinely pending until the fetch lands.
493
+ const signedOut = requiresSession && !sessionLoading && !session?.user;
399
494
  const loaded = entry?.path === path ? entry : null;
400
495
  return {
401
496
  data: loaded?.data ?? null,
@@ -4,10 +4,13 @@ exports.SDK_VERSION = exports.ConnectClient = exports.DEFAULT_BASE_URL = void 0;
4
4
  const access_js_1 = require("./resources/access.js");
5
5
  const apps_js_1 = require("./resources/apps.js");
6
6
  const ai_js_1 = require("./resources/ai.js");
7
+ const enrichment_js_1 = require("./resources/enrichment.js");
7
8
  const data_js_1 = require("./resources/data.js");
8
9
  const workspaces_js_1 = require("./resources/workspaces.js");
9
10
  const scheduling_js_1 = require("./resources/scheduling.js");
10
11
  const contacts_js_1 = require("./resources/contacts.js");
12
+ const channels_js_1 = require("./resources/channels.js");
13
+ const calls_js_1 = require("./resources/calls.js");
11
14
  const kv_js_1 = require("./resources/kv.js");
12
15
  const billing_js_1 = require("./resources/billing.js");
13
16
  const drive_js_1 = require("./resources/drive.js");
@@ -85,8 +88,13 @@ class ConnectClient {
85
88
  scheduling;
86
89
  /** Who a workspace already knows how to reach, by phone or by id. */
87
90
  contacts;
91
+ /** The phone numbers Connect has assigned to your application, and where each one routes. */
92
+ channels;
93
+ /** The calls that arrived on those channels, with their transcripts. */
94
+ calls;
88
95
  /** RAG chat and structured extraction. */
89
96
  ai;
97
+ enrichment;
90
98
  /** Your application's own document store - see `DataResource`. */
91
99
  data;
92
100
  /** Your application's key/value store - see `KvResource`. */
@@ -135,7 +143,10 @@ class ConnectClient {
135
143
  this.drive = new drive_js_1.DriveResource(transport);
136
144
  this.scheduling = new scheduling_js_1.SchedulingResource(transport);
137
145
  this.contacts = new contacts_js_1.ContactsResource(transport);
146
+ this.channels = new channels_js_1.ChannelsResource(transport);
147
+ this.calls = new calls_js_1.CallsResource(transport);
138
148
  this.ai = new ai_js_1.AiResource(transport);
149
+ this.enrichment = new enrichment_js_1.EnrichmentResource(transport);
139
150
  this.data = new data_js_1.DataResource(transport);
140
151
  this.kv = new kv_js_1.KvResource(transport);
141
152
  this.otp = new otp_js_1.OtpResource(transport);
@@ -150,4 +161,4 @@ class ConnectClient {
150
161
  }
151
162
  exports.ConnectClient = ConnectClient;
152
163
  /** Kept in step with `package.json` by `sdk.test.ts`. */
153
- exports.SDK_VERSION = "0.20.0";
164
+ exports.SDK_VERSION = "0.23.0";
@@ -105,6 +105,7 @@ function createConnectHandler(options) {
105
105
  const defaultRedirect = options.defaultRedirect ?? "/";
106
106
  const stateCookieName = options.stateCookieName ?? "connect_oauth_state";
107
107
  const silentCookieName = `${stateCookieName}_silent`;
108
+ const pkceCookieName = `${stateCookieName}_pkce`;
108
109
  const callbackPath = `${basePath}/callback`;
109
110
  /**
110
111
  * Short-lived, httpOnly, and `SameSite=Lax` - `Strict` would not be sent on
@@ -151,6 +152,35 @@ function createConnectHandler(options) {
151
152
  .filter(Boolean)
152
153
  .join("; ");
153
154
  }
155
+ /**
156
+ * Holds the One Tap PKCE **verifier** for the few seconds between the tap and
157
+ * the exchange.
158
+ *
159
+ * This is the cookie the whole no-navigation flow rests on. Connect hands the
160
+ * authorization code to the *page*, so the code alone must not be enough to
161
+ * mint a session - the verifier is what completes it, and it never leaves
162
+ * this origin's cookie jar. `SameSite=Lax` is sufficient and correct: unlike
163
+ * the state cookie there is no cross-site redirect that has to carry it, and
164
+ * `Lax` additionally means a cross-site `POST` to `/one-tap/complete` arrives
165
+ * without it and simply fails.
166
+ *
167
+ * Five minutes, not the state cookie's fifteen: it is minted on the tap and
168
+ * spent on the next request, so a long window is only a longer chance for a
169
+ * stale one to be lying around.
170
+ */
171
+ function pkceCookie(value, secure) {
172
+ const cleared = value === "";
173
+ return [
174
+ `${pkceCookieName}=${encodeURIComponent(value)}`,
175
+ `Path=${basePath}`,
176
+ "HttpOnly",
177
+ "SameSite=Lax",
178
+ secure ? "Secure" : "",
179
+ cleared ? "Max-Age=0" : "Max-Age=300",
180
+ ]
181
+ .filter(Boolean)
182
+ .join("; ");
183
+ }
154
184
  async function requireToken(request) {
155
185
  const token = await session.read(request);
156
186
  if (!token) {
@@ -258,6 +288,14 @@ function createConnectHandler(options) {
258
288
  },
259
289
  });
260
290
  }
291
+ /**
292
+ * Where the browser asks Connect about the visitor, and where it asks for a
293
+ * code once they tap - or `null` when this deployment cannot run One Tap at
294
+ * all (no `clientId`, or a session store that cannot `write`, in which case
295
+ * `/one-tap/complete` does not exist and offering the prompt would strand
296
+ * anyone who used it).
297
+ */
298
+ const oneTapEndpoints = session.write ? client.identity.oneTapEndpoints() : null;
261
299
  const routes = {
262
300
  "GET /session": async (request) => {
263
301
  const token = await session.read(request);
@@ -267,9 +305,21 @@ function createConnectHandler(options) {
267
305
  // the client never has to tell "signed out" from "signed in with no
268
306
  // role" by which keys happen to be present.
269
307
  if (!token) {
270
- return json({ user: null, subscription: null, role: null, permissions: [], staff: false });
308
+ return json({
309
+ user: null,
310
+ subscription: null,
311
+ role: null,
312
+ permissions: [],
313
+ staff: false,
314
+ // Carried on the read the provider already makes on mount, so One Tap
315
+ // costs no extra round trip to discover where to ask. Present on both
316
+ // branches even though only the signed-out one can use it: a response
317
+ // whose shape depends on the answer is a response consumers start
318
+ // testing for key presence instead of value.
319
+ oneTap: oneTapEndpoints,
320
+ });
271
321
  }
272
- return json(await client.identity.getUser(token));
322
+ return json({ ...(await client.identity.getUser(token)), oneTap: oneTapEndpoints });
273
323
  },
274
324
  "GET /entitlements": async (request) => {
275
325
  const { workspaceId } = await requireWorkspace(request);
@@ -704,6 +754,74 @@ function createConnectHandler(options) {
704
754
  await session.write(request, response, result.sessionToken);
705
755
  return response;
706
756
  },
757
+ /**
758
+ * One Tap, step one: mint the PKCE pair, keep the verifier here, hand the
759
+ * challenge to the page.
760
+ *
761
+ * Called on the *tap*, not on mount, so the five-minute verifier is minted
762
+ * seconds before it is spent rather than sitting in a cookie for as long as
763
+ * someone leaves the prompt on screen.
764
+ *
765
+ * A `POST` despite reading nothing: it sets a cookie, and a `GET` that
766
+ * changes state is both wrong and prefetchable - a link prefetcher or a
767
+ * scanner would silently rotate the verifier out from under an open prompt.
768
+ */
769
+ "POST /one-tap/start": async (request) => {
770
+ const url = new URL(request.url);
771
+ const { verifier, challenge } = await (0, identity_js_1.createPkcePair)();
772
+ const response = json({
773
+ codeChallenge: challenge,
774
+ // The already-registered callback URI. One Tap never navigates to it -
775
+ // it is the value the code is *bound* to, and it must be byte-identical
776
+ // at `/one-tap/complete`, so both ends derive it the same way the
777
+ // redirect flow does rather than accepting it from the page.
778
+ redirectUri: `${url.origin}${callbackPath}`,
779
+ });
780
+ response.headers.append("set-cookie", pkceCookie(verifier, url.protocol === "https:"));
781
+ return response;
782
+ },
783
+ /**
784
+ * One Tap, step two: trade the page's code for a session, using the
785
+ * verifier the page never saw.
786
+ *
787
+ * This is `GET /callback`'s body without the navigation - same exchange,
788
+ * same verify-before-store ordering, same session store. The verifier is
789
+ * what makes the difference: a code lifted out of the browser cannot be
790
+ * redeemed anywhere else, because the exchange is refused without it.
791
+ */
792
+ "POST /one-tap/complete": async (request) => {
793
+ const url = new URL(request.url);
794
+ const secure = url.protocol === "https:";
795
+ const clearPkce = pkceCookie("", secure);
796
+ const verifier = readCookie(request, pkceCookieName);
797
+ if (!verifier) {
798
+ // The prompt sat unanswered past the cookie's life, or a cross-site
799
+ // `POST` arrived without it. Either way there is nothing to complete,
800
+ // and the client falls back to the interactive flow.
801
+ const expired = json({ error: "expired" }, 400);
802
+ expired.headers.append("set-cookie", clearPkce);
803
+ return expired;
804
+ }
805
+ const body = (await request.json().catch(() => null));
806
+ const code = typeof body?.code === "string" ? body.code : null;
807
+ if (!code) {
808
+ const invalid = json({ error: "invalid_request" }, 400);
809
+ invalid.headers.append("set-cookie", clearPkce);
810
+ return invalid;
811
+ }
812
+ const result = await client.identity.exchangeCode({
813
+ code,
814
+ redirectUri: `${url.origin}${callbackPath}`,
815
+ codeVerifier: verifier,
816
+ });
817
+ // Verified before the session is stored, exactly as the callback does -
818
+ // an unverifiable token must never become a signed-in user.
819
+ await client.identity.verifyIdToken(result.idToken);
820
+ const response = json({ ok: true });
821
+ response.headers.append("set-cookie", clearPkce);
822
+ await session.write(request, response, result.sessionToken);
823
+ return response;
824
+ },
707
825
  "POST /logout": async (request) => {
708
826
  const token = await session.read(request);
709
827
  const response = json({ ok: true });
@@ -722,6 +840,12 @@ function createConnectHandler(options) {
722
840
  if (!session.write) {
723
841
  delete routes["GET /signin"];
724
842
  delete routes["GET /callback"];
843
+ // Same reason, and `oneTapEndpoints` is already `null` above so the client
844
+ // never offers the prompt in the first place - this is the second half of
845
+ // that, so a hand-written call cannot reach a route that would throw on
846
+ // `session.write!`.
847
+ delete routes["POST /one-tap/start"];
848
+ delete routes["POST /one-tap/complete"];
725
849
  // A switch rotates the session token. Without somewhere to persist the new
726
850
  // one, the old one would be revoked and the user silently signed out - so
727
851
  // the route does not exist rather than working once and breaking.
@@ -0,0 +1,57 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.CallsResource = void 0;
4
+ const http_js_1 = require("../http.js");
5
+ /**
6
+ * Every call that arrived on a channel assigned to your application.
7
+ *
8
+ * The application is resolved from your API key and is never a parameter, so
9
+ * there is no request that reads another partner's calls.
10
+ *
11
+ * `channelId` is the only filter, and that is a budget decision rather than an
12
+ * oversight: each filter costs a Firestore composite index against a project
13
+ * already near its ceiling. Filter the rest client-side, or read the
14
+ * aggregates from `analytics`.
15
+ */
16
+ class CallsResource {
17
+ transport;
18
+ constructor(transport) {
19
+ this.transport = transport;
20
+ }
21
+ /**
22
+ * Your calls, newest first, across all of your subscribing workspaces.
23
+ *
24
+ * **Page until `hasMore` is `false`; do not stop on an empty `items`.** A
25
+ * page can come back empty with more behind it.
26
+ */
27
+ list(input = {}) {
28
+ return (0, http_js_1.request)(this.transport, {
29
+ method: "GET",
30
+ path: "/calls",
31
+ query: {
32
+ channelId: input.channelId,
33
+ cursor: input.cursor,
34
+ limit: input.limit === undefined ? undefined : String(input.limit),
35
+ },
36
+ });
37
+ }
38
+ /**
39
+ * One call and every transcript turn, ordered by `sequence` - which is the
40
+ * sort to trust, since two turns can share a millisecond.
41
+ *
42
+ * `recordingFileId` and `transcriptFileId` are Drive file ids, readable with
43
+ * a `drive:read` key. Both are `null` until the carrier posts the audio,
44
+ * which is the normal state for a call that has only just ended - branch on
45
+ * `recordingStatus` rather than treating `null` as a failure.
46
+ *
47
+ * Throws `ConnectError` with `code: "not_found"` for a call that does not
48
+ * exist *or* belongs to another application.
49
+ */
50
+ get(callId) {
51
+ return (0, http_js_1.request)(this.transport, {
52
+ method: "GET",
53
+ path: `/calls/${encodeURIComponent(callId)}`,
54
+ });
55
+ }
56
+ }
57
+ exports.CallsResource = CallsResource;
@@ -0,0 +1,115 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ChannelsResource = void 0;
4
+ const http_js_1 = require("../http.js");
5
+ /**
6
+ * The phone numbers Connect has assigned to your application.
7
+ *
8
+ * **Inventory is platform-owned.** A number is a regulated asset bought under
9
+ * Connect's carrier account and reassignable between workspaces, so your
10
+ * application never provisions, prices or releases one - it claims from a pool
11
+ * it cannot see, and reads back what it was given. That is why `claim` takes a
12
+ * workspace and a kind rather than a number, and why nothing here exposes a
13
+ * carrier, a provider reference or a monthly rate.
14
+ *
15
+ * The application is resolved from your API key and is never a parameter, so
16
+ * no request shape here can reach another partner's channels. A channel
17
+ * belonging to someone else answers `404`, not `403` - confirming it exists
18
+ * would make these reads an oracle for enumerating their channel ids.
19
+ */
20
+ class ChannelsResource {
21
+ transport;
22
+ constructor(transport) {
23
+ this.transport = transport;
24
+ }
25
+ /**
26
+ * Every channel assigned to your application, across all of your subscribing
27
+ * workspaces.
28
+ *
29
+ * Page until `hasMore` is `false`.
30
+ */
31
+ list(input = {}) {
32
+ return (0, http_js_1.request)(this.transport, {
33
+ method: "GET",
34
+ path: "/channels",
35
+ query: {
36
+ workspaceId: input.workspaceId,
37
+ cursor: input.cursor,
38
+ limit: input.limit === undefined ? undefined : String(input.limit),
39
+ },
40
+ });
41
+ }
42
+ /**
43
+ * The channel behind an id you already hold - a webhook's `channelId`, or a
44
+ * call record's.
45
+ *
46
+ * Throws `ConnectError` with `code: "not_found"` for an id that does not
47
+ * exist *or* belongs to another application.
48
+ */
49
+ get(channelId) {
50
+ return (0, http_js_1.request)(this.transport, {
51
+ method: "GET",
52
+ path: `/channels/${encodeURIComponent(channelId)}`,
53
+ });
54
+ }
55
+ /**
56
+ * Takes a number out of Connect's inventory and binds it to one of your
57
+ * subscribing workspaces.
58
+ *
59
+ * **You do not pick the number.** A workspace may hold exactly one, across
60
+ * every application it uses, and it is then fixed for the life of the
61
+ * engagement - there is no re-claim and no swap. Only a Connect operator can
62
+ * unassign one.
63
+ *
64
+ * Two different `409`s, and they need different handling: the workspace
65
+ * already holds a channel of this kind (fix the call), or the pool is empty
66
+ * right now (retry - Connect is notified, and this is not permanent).
67
+ *
68
+ * The claimed channel comes back without routing, because it has none yet.
69
+ * Call `setRouting` next, or the number refuses every call that reaches it.
70
+ */
71
+ claim(input) {
72
+ return (0, http_js_1.request)(this.transport, {
73
+ method: "POST",
74
+ path: "/channels/claim",
75
+ json: { workspaceId: input.workspaceId, kind: input.kind ?? "voice" },
76
+ });
77
+ }
78
+ /**
79
+ * Points a channel at wherever inbound conversations should go.
80
+ *
81
+ * `destination` is yours: Connect stores it verbatim and never reads into
82
+ * it. It **replaces** the stored record rather than merging, so send the
83
+ * whole destination each time; it must serialize to 16 KB or fewer.
84
+ *
85
+ * Throws `code: "conflict"` when the channel is suspended or released - use
86
+ * `clearRouting` to stop a suspended number answering, which is deliberately
87
+ * still allowed.
88
+ */
89
+ setRouting(channelId, destination) {
90
+ return (0, http_js_1.request)(this.transport, {
91
+ method: "PUT",
92
+ path: `/channels/${encodeURIComponent(channelId)}/routing`,
93
+ json: { destination },
94
+ });
95
+ }
96
+ /**
97
+ * Takes a number out of service without giving it up - the channel stays
98
+ * assigned to you and stays billable, and inbound calls are refused until
99
+ * you write routing again.
100
+ *
101
+ * This is the only way to say "stop answering": overwriting `destination`
102
+ * can only ever point the number somewhere *else*, and unassigning it is a
103
+ * Console action that returns the number to Connect's inventory.
104
+ *
105
+ * Idempotent - clearing already-clear routing answers `200` with
106
+ * `routing: null`, so a retry after a lost response is safe.
107
+ */
108
+ clearRouting(channelId) {
109
+ return (0, http_js_1.request)(this.transport, {
110
+ method: "DELETE",
111
+ path: `/channels/${encodeURIComponent(channelId)}/routing`,
112
+ });
113
+ }
114
+ }
115
+ exports.ChannelsResource = ChannelsResource;
@@ -0,0 +1,56 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.EnrichmentResource = void 0;
4
+ const acting_as_js_1 = require("./acting-as.js");
5
+ const http_js_1 = require("../http.js");
6
+ /**
7
+ * What Connect knows about the company at a domain.
8
+ *
9
+ * The thing behind "the account filled itself in": a record holding nothing
10
+ * but `priya@acme.com` can offer a name, an industry, a size band and an
11
+ * address instead of an empty form.
12
+ *
13
+ * Two properties are worth knowing before you wire it in, because they decide
14
+ * how you should render the result:
15
+ *
16
+ * - **Every value is cited, and nothing is recalled.** Connect reads the
17
+ * company's own website and returns the URL each claim came from. A field
18
+ * the pages never stated comes back `null` rather than as a plausible guess.
19
+ * - **It is a suggestion, not a save.** Nothing is written to your store, and
20
+ * nothing should be written to it without the person in front of it
21
+ * agreeing - which is also how HubSpot and Zoho present the same feature.
22
+ */
23
+ class EnrichmentResource {
24
+ transport;
25
+ constructor(transport) {
26
+ this.transport = transport;
27
+ }
28
+ /**
29
+ * Look a company up from a work email address or a domain.
30
+ *
31
+ * Send whichever you hold; `domain` wins when both are given. A consumer
32
+ * address (`founder@gmail.com`) is answered with
33
+ * `{ status: "skipped", reason: "consumer_domain" }` rather than an error -
34
+ * it names a person, not an employer.
35
+ *
36
+ * `asUser` attributes the generation to the signed-in caller's workspace,
37
+ * resolved from their session token. Without it the call is attributed to
38
+ * your application.
39
+ */
40
+ company(input, options = {}) {
41
+ return (0, http_js_1.request)(this.transport, {
42
+ method: "POST",
43
+ path: "/enrichment/company",
44
+ // `asUser` never reaches the body - it is a credential, and Connect
45
+ // reads it from the header.
46
+ json: {
47
+ ...(input.email ? { email: input.email } : {}),
48
+ ...(input.domain ? { domain: input.domain } : {}),
49
+ ...(input.refresh ? { refresh: true } : {}),
50
+ },
51
+ headers: (0, acting_as_js_1.actingAs)(input.asUser),
52
+ signal: options.signal,
53
+ });
54
+ }
55
+ }
56
+ exports.EnrichmentResource = EnrichmentResource;