@cosmoops/connect-sdk 0.21.0 → 0.24.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -3,6 +3,207 @@
3
3
  `@cosmoops/connect-sdk` follows semver, with the 0.x convention that **minor
4
4
  versions may carry breaking changes** and patch versions never do.
5
5
 
6
+ ## 0.24.0
7
+
8
+ ### Fixed
9
+
10
+ - **`connect.integrations.initiateConnect()` could never succeed.** Connect's
11
+ `POST /integrations/connect` requires a `userId` - it stores it as the
12
+ connection's `connectedByUserId` - and the SDK sent `workspaceId`,
13
+ `toolkitSlug` and `returnUrl` and nothing else. Every call 400'd, including
14
+ the one `createConnectHandler`'s own `POST /integrations/connect` route
15
+ makes, so **`<ConnectIntegrationsModal />`'s Connect button was broken too**:
16
+ the browser reached the bridge, the bridge reached Connect, and Connect
17
+ refused with `Invalid input: expected string, received undefined` - a Zod
18
+ message that names no field, which is most of why this survived.
19
+
20
+ The conformance suite pinned the *response* shape and not the request, so
21
+ nothing failed the build. It now pins the request body against the
22
+ use-case command, and `src/conformance.test.ts` fails to compile if the
23
+ route grows a fourth required field.
24
+
25
+ ### Changed
26
+
27
+ - **`initiateConnect` takes an input object** - `initiateConnect(workspaceId,
28
+ { toolkitSlug, returnUrl, userId })`, replacing the three positional
29
+ strings. Breaking in name only: the old signature had no working call to
30
+ break. The object shape matches `execute()` on the same resource, and it is
31
+ what keeps `returnUrl` and `userId` from being silently transposable.
32
+
33
+ `userId` is the signed-in member starting the connection. Take it from your
34
+ own session, never from a form: the toolkit connection is shared by the
35
+ whole workspace, so this attribution is the only record of who turned it on.
36
+ The bridge already does this for you - it reads the session's user, and a
37
+ `userId` in the request body is ignored, the same way a crafted
38
+ `workspaceId` always was.
39
+
40
+ - **`InitiateIntegrationConnectionInput` is exported** from
41
+ `@cosmoops/connect-sdk/server`, beside `ExecuteIntegrationActionInput`.
42
+
43
+ ## 0.23.0
44
+
45
+ ### Added
46
+
47
+ - **`CompanyProfile.legalJurisdiction`** - the seat a governing-law clause
48
+ names, e.g. `"Chennai, Tamil Nadu"`.
49
+
50
+ **This is not the registered address**, and that is why it is its own field
51
+ rather than something to derive. An entity registered in one state routinely
52
+ names another as the venue for disputes, and the applications that kept
53
+ their own copies of this had reached for the registered address instead -
54
+ publishing two different venues on two terms pages operated by the same
55
+ company. `null` when the operator has not published one, in which case a
56
+ terms page should omit the venue rather than substitute anything.
57
+
58
+ - **`CompanyProfile.brandName` is now editable by the platform operator.** The
59
+ field is unchanged on the wire; what changed is that Connect served it from
60
+ a hardcoded constant until now, which made it the one company fact nobody
61
+ could correct without a deploy.
62
+
63
+ ### Changed
64
+
65
+ - **`GET /company-profile` can now answer `503 unavailable`.** Connect used to
66
+ resolve every field against hardcoded defaults, so this endpoint always
67
+ answered - including with a legal name and grievance officer that nobody had
68
+ checked against the operator's own record. Those defaults are gone, and an
69
+ unconfigured platform refuses the read instead.
70
+
71
+ Nothing you wrote needs changing: `<ConnectGrievanceNotice />` already
72
+ renders `null` on failure, and `useConnectCompanyProfile()` surfaces it as
73
+ an error like any other. **Do not add a fallback of your own** - a copy kept
74
+ for outages is still a copy, and going stale is the failure this endpoint
75
+ exists to prevent.
76
+
77
+ - **`connect.enrichment.company()`** - fill an account in from a work email
78
+ address, instead of asking somebody to type it.
79
+
80
+ ```ts
81
+ const result = await connect.enrichment.company({ email: "priya@acme.test" });
82
+ if (result.status === "enriched") {
83
+ form.setValues(result.profile); // then let the person confirm it
84
+ }
85
+ ```
86
+
87
+ Connect reads the company's **own website** - the domain your input yields,
88
+ plus About/Contact pages linked from its homepage - and returns what it found
89
+ with `citations` naming the URL each claim came from. It does not search the
90
+ web, and it never answers from a model's recall: a field the pages did not
91
+ state comes back `null`. An invented industry is indistinguishable from a
92
+ real one, and an invented address reaches an invoice.
93
+
94
+ `status: "skipped"` is a normal 200, not an error - most records legitimately
95
+ have nothing to enrich. `consumer_domain` (a `gmail.com` address names a
96
+ person, not an employer) and `blocked_domain` will never succeed on a retry;
97
+ `unreachable` and `no_signal` may. `confidence` is derived from how many
98
+ fields the pages actually supported, not from the model's opinion of itself,
99
+ so `low` means "offer this" and `high` means "safe to pre-fill".
100
+
101
+ Needs an API key scoped to the new **`enrichment:read`**. That is separate
102
+ from `ai:extract` because the two differ in what they touch rather than only
103
+ in what they cost: extraction reads a file you already hold, this makes
104
+ Connect fetch a third-party website on a string that reached you from a
105
+ stranger.
106
+
107
+ - **`connect.workspaces.access()`, `.invite()`, `.setAccess()` and
108
+ `.revokeInvitation()`** - manage the customer's roster from inside your own
109
+ application, instead of sending them to Connect's team page.
110
+
111
+ `members()` has always been a read, so a partner's own team screen could
112
+ show who had access and then had to hand off to Connect for every change.
113
+ These four close that loop, and all of them take `asUser`: the workspace and
114
+ the actor come from the caller's session token, never from a parameter.
115
+
116
+ ```ts
117
+ const { capacity, viewer } = await connect.workspaces.access({ asUser: token });
118
+ if (viewer.canInvite && capacity.canAdd) {
119
+ await connect.workspaces.invite({ inviteeEmail: "priya@acme.test", roleKey: "rep" }, { asUser: token });
120
+ }
121
+ await connect.workspaces.setAccess({ userId, roleKey: null }, { asUser: token }); // revoke
122
+ ```
123
+
124
+ **You can only ever move your own application's grant.** The wire carries a
125
+ `roleKey` from your catalog rather than a roles array, and Connect computes
126
+ the member's next roles as *what they hold now, minus any grant naming your
127
+ application, plus yours* - so nothing these methods can express reaches a
128
+ workspace-wide Connect role, another application's access, or removes
129
+ someone from the workspace.
130
+
131
+ Two gates apply on top of the API key's new `members:manage` scope, and both
132
+ are about the person rather than your application: the signed-in caller must
133
+ hold Connect's own `member:invite` / `member:update` on the workspace, and
134
+ they cannot grant access they do not hold themselves. `viewer` on the
135
+ `access()` response is the first of those, resolved, so you can render your
136
+ controls from it instead of letting a button 403.
137
+
138
+ **Seats are the workspace's, not your plan's.** `capacity.included` is a
139
+ baseline plus every active subscription's member allowance folded across
140
+ applications, and `used` counts pending invitations as well as members,
141
+ because an invitation is a seat already spoken for. `reason` separates "no
142
+ plan grants member seats" (`no_allowance` - sell them one) from "every seat
143
+ is taken" (`exhausted` - an upgrade, or a removal); those read alike and
144
+ need different words on screen. The read is advisory - the authoritative
145
+ check runs inside `invite()`, which is where two people claiming the last
146
+ seat at once is settled.
147
+
148
+ `revokeInvitation()` is not optional decoration: an invitation sent to a
149
+ mistyped address holds a seat for its full seven days, and an application
150
+ that can spend one but not release it leaves a customer paying for a seat
151
+ nobody can use.
152
+
153
+ **Server-side only, deliberately.** None of the four is exposed through
154
+ `createConnectHandler`, which stays four fixed reads plus sign-out and the
155
+ workspace switch. A bridge route is a capability handed to any script
156
+ running in your page, and "grant this account admin" is the highest-value
157
+ one there is; call these from your own server, where the API key already
158
+ lives.
159
+
160
+ ## 0.22.0
161
+
162
+ ### Fixed
163
+
164
+ - **`<ConnectGrievanceNotice />` rendered nothing for a signed-out visitor -
165
+ which is most of the people who read a privacy page.**
166
+
167
+ The bridge route `GET /company-profile` is deliberately unauthenticated: the
168
+ answer is a fact about the platform operator, identical for every caller and
169
+ already on the public web, and requiring a session would hide the redressal
170
+ channel at the one moment it most needs to be legible. The browser half
171
+ never got that exemption. `useConnectCompanyProfile` goes through
172
+ `useConnectResource`, which waits for `session.user` before it reads - right
173
+ for every other route, and the reason this component was blank on exactly
174
+ the public page the docs recommend putting it on.
175
+
176
+ `useConnectResource` now takes `{ requiresSession: false }`, and the company
177
+ profile is its only caller. It widens no access - the bridge decides that,
178
+ not the hook.
179
+
180
+ - **`<ConnectPlans />`: a card reading "Subscribe" opened Connect's *change
181
+ plan* screen, which answered a 404.**
182
+
183
+ The label and the URL were two separate decisions. `resolveAction` needs the
184
+ **current plan** to classify a change and falls back to `"subscribe"`
185
+ without one; the checkout URL was built from the mere presence of a
186
+ subscription *row*. Those disagree in a state Connect reports routinely:
187
+ `SubscriptionResponse` carries `plan: null` beside a live `subscription`
188
+ whenever that plan has been deleted from the application's catalogue - which
189
+ re-seeding an application does. The button then said "Subscribe" and opened
190
+ `/billing/subscriptions/{id}/change-plan/{planId}`, where Connect could not
191
+ read the current price either and rendered its not-found page.
192
+
193
+ Both now come from the same `PlanAction`. When the grid cannot classify a
194
+ change it hands off to the subscribe route, which re-reads the workspace's
195
+ live subscriptions server-side and redirects to the change-plan screen
196
+ itself when there is genuinely one to change - so Connect decides, and the
197
+ grid does not have to be sure.
198
+
199
+ ### Added
200
+
201
+ - **`checkoutSubscriptionId(action, current)`** - the other half of
202
+ `resolveAction`, exported alongside it. A partner writing its own pricing
203
+ grid has to turn that action into a checkout URL, and reading the
204
+ subscription id straight off the entitlements response is exactly the bug
205
+ above. Pass its result as `buildCheckoutUrl`'s `subscriptionId`.
206
+
6
207
  ## 0.21.0
7
208
 
8
209
  ### Added
@@ -28,6 +28,7 @@ exports.resolvePlanGroup = resolvePlanGroup;
28
28
  exports.groupPlansByTier = groupPlansByTier;
29
29
  exports.annualSaving = annualSaving;
30
30
  exports.resolveAction = resolveAction;
31
+ exports.checkoutSubscriptionId = checkoutSubscriptionId;
31
32
  /**
32
33
  * Whether a plan costs nothing - the SDK's copy of Connect's `isFreeSelection`
33
34
  * (`packages/data/src/payments/free-plan.ts`), which is what the server
@@ -207,3 +208,30 @@ exports.ACTION_LABEL = {
207
208
  "switch-to-monthly": "Switch to monthly",
208
209
  subscribe: "Subscribe",
209
210
  };
211
+ /**
212
+ * The subscription id a checkout hand-off should carry for `action` - or
213
+ * `null`, meaning "send them to Connect's *subscribe* screen".
214
+ *
215
+ * Paired with `resolveAction` deliberately: the button's label and the URL it
216
+ * opens are one decision, and deriving the URL from the mere *presence* of a
217
+ * subscription row instead is what sent a card reading "Subscribe" to
218
+ * Connect's change-plan screen, which answered `notFound()`.
219
+ *
220
+ * The gap is real rather than hypothetical. `resolveAction` needs the
221
+ * **current plan** to classify a change and answers `"subscribe"` without
222
+ * one - and `SubscriptionResponse` carries `plan: null` beside a perfectly
223
+ * live `subscription` whenever that plan has been deleted from the catalogue
224
+ * out from under it, which a re-seeded application does. Connect's change-plan
225
+ * route is stuck in the same place for the same reason: it cannot prorate
226
+ * against a price it cannot read.
227
+ *
228
+ * Sending those to `/apps/.../subscribe/...` is a real hand-off, not a guess:
229
+ * that route re-reads the workspace's live subscriptions server-side and
230
+ * redirects to the change-plan screen itself when there genuinely is one to
231
+ * change. Connect decides, so the grid does not have to be sure.
232
+ */
233
+ function checkoutSubscriptionId(action, current) {
234
+ if (action === "subscribe")
235
+ return null;
236
+ return current?.subscription?.id ?? null;
237
+ }
@@ -83,8 +83,13 @@ function ConnectPlans({ planIds, hideCoupon = false, className }) {
83
83
  * created in `"created"` and only flips to active when the payment webhook
84
84
  * lands, which can be minutes later. Holding a customer on a spinner until
85
85
  * then would misreport a completed checkout as an abandoned one.
86
+ *
87
+ * `action` is threaded through rather than re-derived: it is the same
88
+ * decision the card's label came from, and `checkoutSubscriptionId` is what
89
+ * keeps the two from disagreeing - see its doc comment for the 404 that
90
+ * disagreement produced.
86
91
  */
87
- function startCheckout(plan) {
92
+ function startCheckout(plan, action) {
88
93
  if (!applicationId)
89
94
  return;
90
95
  checkout.start({
@@ -92,7 +97,7 @@ function ConnectPlans({ planIds, hideCoupon = false, className }) {
92
97
  appUrl,
93
98
  applicationId,
94
99
  planId: plan.id,
95
- subscriptionId: subscription?.subscription?.id ?? null,
100
+ subscriptionId: (0, plan_pricing_js_1.checkoutSubscriptionId)(action, subscription),
96
101
  coupon,
97
102
  }),
98
103
  isSettled: (next) => next.subscription.plan?.id === plan.id,
@@ -142,7 +147,7 @@ function ConnectPlans({ planIds, hideCoupon = false, className }) {
142
147
  // plan would race the first and one of them would be
143
148
  // rejected on arrival.
144
149
  isDisabled: isCurrent || checkout.status === "waiting", onPress: () => setPending({ plan, action }), children: [plan_pricing_js_1.ACTION_LABEL[action], !isCurrent && (0, jsx_runtime_1.jsx)(lucide_react_1.ExternalLink, { className: "size-3.5" })] }) })] }) }, tier.planGroup));
145
- }) }), subscription?.subscription && ((0, jsx_runtime_1.jsx)(ConnectUnsubscribeRow, { subscription: subscription.subscription, appUrl: appUrl, checkout: checkout })), (0, jsx_runtime_1.jsx)(subscribe_dialog_js_1.ConnectSubscribeDialog, { plan: pending?.plan ?? null, action: pending?.action ?? "subscribe", current: subscription, coupon: hideCoupon ? undefined : coupon, onClose: () => setPending(null), onConfirm: () => pending && startCheckout(pending.plan) })] }) }));
150
+ }) }), subscription?.subscription && ((0, jsx_runtime_1.jsx)(ConnectUnsubscribeRow, { subscription: subscription.subscription, appUrl: appUrl, checkout: checkout })), (0, jsx_runtime_1.jsx)(subscribe_dialog_js_1.ConnectSubscribeDialog, { plan: pending?.plan ?? null, action: pending?.action ?? "subscribe", current: subscription, coupon: hideCoupon ? undefined : coupon, onClose: () => setPending(null), onConfirm: () => pending && startCheckout(pending.plan, pending.action) })] }) }));
146
151
  }
147
152
  /**
148
153
  * What the page says while the customer is off in Connect, and after.
@@ -3,7 +3,7 @@
3
3
  Object.defineProperty(exports, "__esModule", { value: true });
4
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
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 = 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
  *
@@ -210,6 +210,12 @@ var plan_pricing_js_1 = require("./components/plan-pricing.js");
210
210
  Object.defineProperty(exports, "formatPrice", { enumerable: true, get: function () { return plan_pricing_js_1.formatPrice; } });
211
211
  Object.defineProperty(exports, "monthlyOf", { enumerable: true, get: function () { return plan_pricing_js_1.monthlyOf; } });
212
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; } });
213
219
  Object.defineProperty(exports, "annualSaving", { enumerable: true, get: function () { return plan_pricing_js_1.annualSaving; } });
214
220
  Object.defineProperty(exports, "discountLabel", { enumerable: true, get: function () { return plan_pricing_js_1.discountLabel; } });
215
221
  Object.defineProperty(exports, "ACTION_LABEL", { enumerable: true, get: function () { return plan_pricing_js_1.ACTION_LABEL; } });
@@ -408,7 +408,7 @@ function useConnectPlans() {
408
408
  * `CompanyProfile`. `<ConnectGrievanceNotice>` renders it for you.
409
409
  */
410
410
  function useConnectCompanyProfile() {
411
- return useConnectResource("/company-profile");
411
+ return useConnectResource("/company-profile", { requiresSession: false });
412
412
  }
413
413
  /** The apps this workspace can open. */
414
414
  function useConnectApps() {
@@ -449,10 +449,15 @@ function useConnectSessions() {
449
449
  /**
450
450
  * One bridge read, shared through the provider's cache.
451
451
  *
452
- * Only fetches once a user is known: every resource route needs a session, so
453
- * 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.
454
458
  */
455
- function useConnectResource(path) {
459
+ function useConnectResource(path, options) {
460
+ const requiresSession = options?.requiresSession ?? true;
456
461
  const { fetchResource, session, isLoading: sessionLoading } = useConnect();
457
462
  // Stamped with the `path` it belongs to, so switching paths reports loading
458
463
  // without a synchronous `setIsLoading(true)` - and, more usefully, so a
@@ -463,7 +468,7 @@ function useConnectResource(path) {
463
468
  // Signed out is handled by derivation below rather than by writing state
464
469
  // here - a synchronous setState inside an effect costs an extra render pass
465
470
  // and is what `react-hooks/set-state-in-effect` exists to catch.
466
- if (sessionLoading || !session?.user)
471
+ if (requiresSession && (sessionLoading || !session?.user))
467
472
  return;
468
473
  let active = true;
469
474
  fetchResource(path)
@@ -482,8 +487,10 @@ function useConnectResource(path) {
482
487
  // `version` is otherwise unread - it exists purely to force this effect
483
488
  // to re-run on `refetch()`, after the caller has already evicted this
484
489
  // path's cache entry via `mutateResource`.
485
- }, [fetchResource, path, session?.user, sessionLoading, version]);
486
- 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;
487
494
  const loaded = entry?.path === path ? entry : null;
488
495
  return {
489
496
  data: loaded?.data ?? null,
@@ -4,6 +4,7 @@ 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");
@@ -93,6 +94,7 @@ class ConnectClient {
93
94
  calls;
94
95
  /** RAG chat and structured extraction. */
95
96
  ai;
97
+ enrichment;
96
98
  /** Your application's own document store - see `DataResource`. */
97
99
  data;
98
100
  /** Your application's key/value store - see `KvResource`. */
@@ -144,6 +146,7 @@ class ConnectClient {
144
146
  this.channels = new channels_js_1.ChannelsResource(transport);
145
147
  this.calls = new calls_js_1.CallsResource(transport);
146
148
  this.ai = new ai_js_1.AiResource(transport);
149
+ this.enrichment = new enrichment_js_1.EnrichmentResource(transport);
147
150
  this.data = new data_js_1.DataResource(transport);
148
151
  this.kv = new kv_js_1.KvResource(transport);
149
152
  this.otp = new otp_js_1.OtpResource(transport);
@@ -158,4 +161,4 @@ class ConnectClient {
158
161
  }
159
162
  exports.ConnectClient = ConnectClient;
160
163
  /** Kept in step with `package.json` by `sdk.test.ts`. */
161
- exports.SDK_VERSION = "0.21.0";
164
+ exports.SDK_VERSION = "0.24.0";
@@ -605,8 +605,14 @@ function createConnectHandler(options) {
605
605
  const { workspaceId } = await requireWorkspace(request);
606
606
  return json({ toolkits: await client.integrations.list(workspaceId) });
607
607
  },
608
+ /*
609
+ * `userId` comes from the session alongside `workspaceId`, never from the
610
+ * body - it is stored as `connectedByUserId`, so letting the browser name
611
+ * it would let anyone sign a workspace-shared connection with someone
612
+ * else's name.
613
+ */
608
614
  "POST /integrations/connect": async (request) => {
609
- const { workspaceId } = await requireWorkspace(request);
615
+ const { workspaceId, user } = await requireWorkspace(request);
610
616
  const body = (await request.json().catch(() => null));
611
617
  if (!body?.toolkitSlug || !body?.returnUrl) {
612
618
  throw new errors_js_1.ConnectError({
@@ -616,7 +622,11 @@ function createConnectHandler(options) {
616
622
  endpoint: "POST /integrations/connect",
617
623
  });
618
624
  }
619
- return json(await client.integrations.initiateConnect(workspaceId, body.toolkitSlug, body.returnUrl));
625
+ return json(await client.integrations.initiateConnect(workspaceId, {
626
+ toolkitSlug: body.toolkitSlug,
627
+ returnUrl: body.returnUrl,
628
+ userId: user.userId,
629
+ }));
620
630
  },
621
631
  "POST /integrations/disconnect": async (request) => {
622
632
  const { workspaceId } = await requireWorkspace(request);
@@ -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;
@@ -35,15 +35,17 @@ class IntegrationsResource {
35
35
  * itself. Reconnecting a disconnected/failed toolkit calls this same
36
36
  * method - there is no separate reconnect.
37
37
  *
38
- * Meant to be called on behalf of a signed-in workspace admin (this is what the
39
- * bridge's fixed `POST /integrations/connect` route does), not as a bare
40
- * background action with no admin present to complete the OAuth screen.
38
+ * Meant to be called on behalf of a signed-in workspace admin (this is what
39
+ * the bridge's fixed `POST /integrations/connect` route does), not as a bare
40
+ * background action with no admin present to complete the OAuth screen. That
41
+ * is why `userId` is part of the input rather than inferred - see
42
+ * `InitiateIntegrationConnectionInput`.
41
43
  */
42
- async initiateConnect(workspaceId, toolkitSlug, returnUrl) {
44
+ async initiateConnect(workspaceId, input) {
43
45
  return (0, http_js_1.request)(this.transport, {
44
46
  method: "POST",
45
47
  path: "/integrations/connect",
46
- json: { workspaceId, toolkitSlug, returnUrl },
48
+ json: { workspaceId, ...input },
47
49
  });
48
50
  }
49
51
  /** Idempotent - disconnecting a toolkit that was never connected, or is already disconnected, is a success. */
@@ -4,8 +4,8 @@ exports.WorkspacesResource = void 0;
4
4
  const acting_as_js_1 = require("./acting-as.js");
5
5
  const http_js_1 = require("../http.js");
6
6
  /**
7
- * Which workspaces the signed-in user belongs to, moving between them, and who
8
- * else is on the team.
7
+ * Which workspaces the signed-in user belongs to, moving between them, who
8
+ * else is on the team, and inviting or granting access to your application.
9
9
  *
10
10
  * **Every call here needs `asUser`** - the signed-in user's opaque
11
11
  * `sessionToken`. There is no application-wide form of any of these: they take
@@ -70,5 +70,76 @@ class WorkspacesResource {
70
70
  });
71
71
  return result.members;
72
72
  }
73
+ /**
74
+ * Seats, what the caller may do with them, and the invitations already
75
+ * holding one - everything a team screen needs to decide what to draw.
76
+ *
77
+ * Behind `members:manage` rather than `workspaces:read`, and gated on the
78
+ * caller's own `member:invite`: it names people who are not members yet.
79
+ */
80
+ access(options) {
81
+ return (0, http_js_1.request)(this.transport, {
82
+ method: "GET",
83
+ path: "/workspaces/members/access",
84
+ headers: (0, acting_as_js_1.actingAs)(options.asUser),
85
+ });
86
+ }
87
+ /**
88
+ * Invite someone into the workspace, with access to your application.
89
+ *
90
+ * `roleKey` names a role from your own catalog (or the reserved `"admin"`);
91
+ * omit it to add someone to the workspace with no access to your
92
+ * application. You cannot send a Connect role - the grant is built server
93
+ * side as `[member, app:<yourKey>:<roleKey>]`, so nothing this method can
94
+ * express reaches outside your application.
95
+ *
96
+ * Spends a member seat when the invitation is created, not when it is
97
+ * accepted. Throws `ConnectError` with status 409 and a message written to
98
+ * be shown when there is no seat, and 403 when the caller is inviting
99
+ * someone with access they do not hold themselves.
100
+ */
101
+ invite(input, options) {
102
+ return (0, http_js_1.request)(this.transport, {
103
+ method: "POST",
104
+ path: "/workspaces/members/invite",
105
+ headers: (0, acting_as_js_1.actingAs)(options.asUser),
106
+ json: input,
107
+ });
108
+ }
109
+ /**
110
+ * Grant or revoke **your application's** role for an existing member.
111
+ * `roleKey: null` revokes it.
112
+ *
113
+ * Moves only your own grant: Connect recomputes the member's roles as what
114
+ * they hold now, minus any grant naming your application, plus the one you
115
+ * asked for. Their workspace role and every other application they use are
116
+ * untouched, and there is no form of this that removes them from the
117
+ * workspace.
118
+ *
119
+ * Re-sending the role they already have is a no-op rather than an error, so
120
+ * a form that submits whatever is on screen is safe.
121
+ */
122
+ setAccess(input, options) {
123
+ return (0, http_js_1.request)(this.transport, {
124
+ method: "POST",
125
+ path: "/workspaces/members/access",
126
+ headers: (0, acting_as_js_1.actingAs)(options.asUser),
127
+ json: input,
128
+ });
129
+ }
130
+ /**
131
+ * Withdraw a pending invitation your application sent, freeing its seat.
132
+ *
133
+ * Only yours - an invitation carrying another application's grant is a 404,
134
+ * deliberately indistinguishable from one that does not exist.
135
+ */
136
+ async revokeInvitation(input, options) {
137
+ await (0, http_js_1.request)(this.transport, {
138
+ method: "POST",
139
+ path: "/workspaces/invitations/revoke",
140
+ headers: (0, acting_as_js_1.actingAs)(options.asUser),
141
+ json: input,
142
+ });
143
+ }
73
144
  }
74
145
  exports.WorkspacesResource = WorkspacesResource;
@@ -194,3 +194,30 @@ export const ACTION_LABEL = {
194
194
  "switch-to-monthly": "Switch to monthly",
195
195
  subscribe: "Subscribe",
196
196
  };
197
+ /**
198
+ * The subscription id a checkout hand-off should carry for `action` - or
199
+ * `null`, meaning "send them to Connect's *subscribe* screen".
200
+ *
201
+ * Paired with `resolveAction` deliberately: the button's label and the URL it
202
+ * opens are one decision, and deriving the URL from the mere *presence* of a
203
+ * subscription row instead is what sent a card reading "Subscribe" to
204
+ * Connect's change-plan screen, which answered `notFound()`.
205
+ *
206
+ * The gap is real rather than hypothetical. `resolveAction` needs the
207
+ * **current plan** to classify a change and answers `"subscribe"` without
208
+ * one - and `SubscriptionResponse` carries `plan: null` beside a perfectly
209
+ * live `subscription` whenever that plan has been deleted from the catalogue
210
+ * out from under it, which a re-seeded application does. Connect's change-plan
211
+ * route is stuck in the same place for the same reason: it cannot prorate
212
+ * against a price it cannot read.
213
+ *
214
+ * Sending those to `/apps/.../subscribe/...` is a real hand-off, not a guess:
215
+ * that route re-reads the workspace's live subscriptions server-side and
216
+ * redirects to the change-plan screen itself when there genuinely is one to
217
+ * change. Connect decides, so the grid does not have to be sure.
218
+ */
219
+ export function checkoutSubscriptionId(action, current) {
220
+ if (action === "subscribe")
221
+ return null;
222
+ return current?.subscription?.id ?? null;
223
+ }