@cosmoops/connect-sdk 0.21.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.
package/CHANGELOG.md CHANGED
@@ -3,6 +3,170 @@
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.23.0
7
+
8
+ ### Added
9
+
10
+ - **`CompanyProfile.legalJurisdiction`** - the seat a governing-law clause
11
+ names, e.g. `"Chennai, Tamil Nadu"`.
12
+
13
+ **This is not the registered address**, and that is why it is its own field
14
+ rather than something to derive. An entity registered in one state routinely
15
+ names another as the venue for disputes, and the applications that kept
16
+ their own copies of this had reached for the registered address instead -
17
+ publishing two different venues on two terms pages operated by the same
18
+ company. `null` when the operator has not published one, in which case a
19
+ terms page should omit the venue rather than substitute anything.
20
+
21
+ - **`CompanyProfile.brandName` is now editable by the platform operator.** The
22
+ field is unchanged on the wire; what changed is that Connect served it from
23
+ a hardcoded constant until now, which made it the one company fact nobody
24
+ could correct without a deploy.
25
+
26
+ ### Changed
27
+
28
+ - **`GET /company-profile` can now answer `503 unavailable`.** Connect used to
29
+ resolve every field against hardcoded defaults, so this endpoint always
30
+ answered - including with a legal name and grievance officer that nobody had
31
+ checked against the operator's own record. Those defaults are gone, and an
32
+ unconfigured platform refuses the read instead.
33
+
34
+ Nothing you wrote needs changing: `<ConnectGrievanceNotice />` already
35
+ renders `null` on failure, and `useConnectCompanyProfile()` surfaces it as
36
+ an error like any other. **Do not add a fallback of your own** - a copy kept
37
+ for outages is still a copy, and going stale is the failure this endpoint
38
+ exists to prevent.
39
+
40
+ - **`connect.enrichment.company()`** - fill an account in from a work email
41
+ address, instead of asking somebody to type it.
42
+
43
+ ```ts
44
+ const result = await connect.enrichment.company({ email: "priya@acme.test" });
45
+ if (result.status === "enriched") {
46
+ form.setValues(result.profile); // then let the person confirm it
47
+ }
48
+ ```
49
+
50
+ Connect reads the company's **own website** - the domain your input yields,
51
+ plus About/Contact pages linked from its homepage - and returns what it found
52
+ with `citations` naming the URL each claim came from. It does not search the
53
+ web, and it never answers from a model's recall: a field the pages did not
54
+ state comes back `null`. An invented industry is indistinguishable from a
55
+ real one, and an invented address reaches an invoice.
56
+
57
+ `status: "skipped"` is a normal 200, not an error - most records legitimately
58
+ have nothing to enrich. `consumer_domain` (a `gmail.com` address names a
59
+ person, not an employer) and `blocked_domain` will never succeed on a retry;
60
+ `unreachable` and `no_signal` may. `confidence` is derived from how many
61
+ fields the pages actually supported, not from the model's opinion of itself,
62
+ so `low` means "offer this" and `high` means "safe to pre-fill".
63
+
64
+ Needs an API key scoped to the new **`enrichment:read`**. That is separate
65
+ from `ai:extract` because the two differ in what they touch rather than only
66
+ in what they cost: extraction reads a file you already hold, this makes
67
+ Connect fetch a third-party website on a string that reached you from a
68
+ stranger.
69
+
70
+ - **`connect.workspaces.access()`, `.invite()`, `.setAccess()` and
71
+ `.revokeInvitation()`** - manage the customer's roster from inside your own
72
+ application, instead of sending them to Connect's team page.
73
+
74
+ `members()` has always been a read, so a partner's own team screen could
75
+ show who had access and then had to hand off to Connect for every change.
76
+ These four close that loop, and all of them take `asUser`: the workspace and
77
+ the actor come from the caller's session token, never from a parameter.
78
+
79
+ ```ts
80
+ const { capacity, viewer } = await connect.workspaces.access({ asUser: token });
81
+ if (viewer.canInvite && capacity.canAdd) {
82
+ await connect.workspaces.invite({ inviteeEmail: "priya@acme.test", roleKey: "rep" }, { asUser: token });
83
+ }
84
+ await connect.workspaces.setAccess({ userId, roleKey: null }, { asUser: token }); // revoke
85
+ ```
86
+
87
+ **You can only ever move your own application's grant.** The wire carries a
88
+ `roleKey` from your catalog rather than a roles array, and Connect computes
89
+ the member's next roles as *what they hold now, minus any grant naming your
90
+ application, plus yours* - so nothing these methods can express reaches a
91
+ workspace-wide Connect role, another application's access, or removes
92
+ someone from the workspace.
93
+
94
+ Two gates apply on top of the API key's new `members:manage` scope, and both
95
+ are about the person rather than your application: the signed-in caller must
96
+ hold Connect's own `member:invite` / `member:update` on the workspace, and
97
+ they cannot grant access they do not hold themselves. `viewer` on the
98
+ `access()` response is the first of those, resolved, so you can render your
99
+ controls from it instead of letting a button 403.
100
+
101
+ **Seats are the workspace's, not your plan's.** `capacity.included` is a
102
+ baseline plus every active subscription's member allowance folded across
103
+ applications, and `used` counts pending invitations as well as members,
104
+ because an invitation is a seat already spoken for. `reason` separates "no
105
+ plan grants member seats" (`no_allowance` - sell them one) from "every seat
106
+ is taken" (`exhausted` - an upgrade, or a removal); those read alike and
107
+ need different words on screen. The read is advisory - the authoritative
108
+ check runs inside `invite()`, which is where two people claiming the last
109
+ seat at once is settled.
110
+
111
+ `revokeInvitation()` is not optional decoration: an invitation sent to a
112
+ mistyped address holds a seat for its full seven days, and an application
113
+ that can spend one but not release it leaves a customer paying for a seat
114
+ nobody can use.
115
+
116
+ **Server-side only, deliberately.** None of the four is exposed through
117
+ `createConnectHandler`, which stays four fixed reads plus sign-out and the
118
+ workspace switch. A bridge route is a capability handed to any script
119
+ running in your page, and "grant this account admin" is the highest-value
120
+ one there is; call these from your own server, where the API key already
121
+ lives.
122
+
123
+ ## 0.22.0
124
+
125
+ ### Fixed
126
+
127
+ - **`<ConnectGrievanceNotice />` rendered nothing for a signed-out visitor -
128
+ which is most of the people who read a privacy page.**
129
+
130
+ The bridge route `GET /company-profile` is deliberately unauthenticated: the
131
+ answer is a fact about the platform operator, identical for every caller and
132
+ already on the public web, and requiring a session would hide the redressal
133
+ channel at the one moment it most needs to be legible. The browser half
134
+ never got that exemption. `useConnectCompanyProfile` goes through
135
+ `useConnectResource`, which waits for `session.user` before it reads - right
136
+ for every other route, and the reason this component was blank on exactly
137
+ the public page the docs recommend putting it on.
138
+
139
+ `useConnectResource` now takes `{ requiresSession: false }`, and the company
140
+ profile is its only caller. It widens no access - the bridge decides that,
141
+ not the hook.
142
+
143
+ - **`<ConnectPlans />`: a card reading "Subscribe" opened Connect's *change
144
+ plan* screen, which answered a 404.**
145
+
146
+ The label and the URL were two separate decisions. `resolveAction` needs the
147
+ **current plan** to classify a change and falls back to `"subscribe"`
148
+ without one; the checkout URL was built from the mere presence of a
149
+ subscription *row*. Those disagree in a state Connect reports routinely:
150
+ `SubscriptionResponse` carries `plan: null` beside a live `subscription`
151
+ whenever that plan has been deleted from the application's catalogue - which
152
+ re-seeding an application does. The button then said "Subscribe" and opened
153
+ `/billing/subscriptions/{id}/change-plan/{planId}`, where Connect could not
154
+ read the current price either and rendered its not-found page.
155
+
156
+ Both now come from the same `PlanAction`. When the grid cannot classify a
157
+ change it hands off to the subscribe route, which re-reads the workspace's
158
+ live subscriptions server-side and redirects to the change-plan screen
159
+ itself when there is genuinely one to change - so Connect decides, and the
160
+ grid does not have to be sure.
161
+
162
+ ### Added
163
+
164
+ - **`checkoutSubscriptionId(action, current)`** - the other half of
165
+ `resolveAction`, exported alongside it. A partner writing its own pricing
166
+ grid has to turn that action into a checkout URL, and reading the
167
+ subscription id straight off the entitlements response is exactly the bug
168
+ above. Pass its result as `buildCheckoutUrl`'s `subscriptionId`.
169
+
6
170
  ## 0.21.0
7
171
 
8
172
  ### 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.23.0";
@@ -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;
@@ -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
+ }
@@ -7,7 +7,7 @@ import { useConnect, useConnectEntitlements, useConnectPlans } from "../provider
7
7
  import { buildCancelUrl, buildCheckoutUrl, useConnectCheckout, } from "../use-checkout.js";
8
8
  import { AnimatePresence, ENTER, m, MotionProvider } from "./motion.js";
9
9
  import { ConnectSubscribeDialog } from "./subscribe-dialog.js";
10
- import { ACTION_LABEL, discountLabel, formatPrice, groupPlansByTier, intervalLabel, isFreePlan, pricePerLabel, resolveAction, } from "./plan-pricing.js";
10
+ import { ACTION_LABEL, checkoutSubscriptionId, discountLabel, formatPrice, groupPlansByTier, intervalLabel, isFreePlan, pricePerLabel, resolveAction, } from "./plan-pricing.js";
11
11
  /**
12
12
  * The plan table: monthly/annual toggle, features, add-ons, the discount
13
13
  * actually in effect, and the subscribe / upgrade / downgrade flows.
@@ -80,8 +80,13 @@ export function ConnectPlans({ planIds, hideCoupon = false, className }) {
80
80
  * created in `"created"` and only flips to active when the payment webhook
81
81
  * lands, which can be minutes later. Holding a customer on a spinner until
82
82
  * then would misreport a completed checkout as an abandoned one.
83
+ *
84
+ * `action` is threaded through rather than re-derived: it is the same
85
+ * decision the card's label came from, and `checkoutSubscriptionId` is what
86
+ * keeps the two from disagreeing - see its doc comment for the 404 that
87
+ * disagreement produced.
83
88
  */
84
- function startCheckout(plan) {
89
+ function startCheckout(plan, action) {
85
90
  if (!applicationId)
86
91
  return;
87
92
  checkout.start({
@@ -89,7 +94,7 @@ export function ConnectPlans({ planIds, hideCoupon = false, className }) {
89
94
  appUrl,
90
95
  applicationId,
91
96
  planId: plan.id,
92
- subscriptionId: subscription?.subscription?.id ?? null,
97
+ subscriptionId: checkoutSubscriptionId(action, subscription),
93
98
  coupon,
94
99
  }),
95
100
  isSettled: (next) => next.subscription.plan?.id === plan.id,
@@ -139,7 +144,7 @@ export function ConnectPlans({ planIds, hideCoupon = false, className }) {
139
144
  // plan would race the first and one of them would be
140
145
  // rejected on arrival.
141
146
  isDisabled: isCurrent || checkout.status === "waiting", onPress: () => setPending({ plan, action }), children: [ACTION_LABEL[action], !isCurrent && _jsx(ExternalLink, { className: "size-3.5" })] }) })] }) }, tier.planGroup));
142
- }) }), subscription?.subscription && (_jsx(ConnectUnsubscribeRow, { subscription: subscription.subscription, appUrl: appUrl, checkout: checkout })), _jsx(ConnectSubscribeDialog, { plan: pending?.plan ?? null, action: pending?.action ?? "subscribe", current: subscription, coupon: hideCoupon ? undefined : coupon, onClose: () => setPending(null), onConfirm: () => pending && startCheckout(pending.plan) })] }) }));
147
+ }) }), subscription?.subscription && (_jsx(ConnectUnsubscribeRow, { subscription: subscription.subscription, appUrl: appUrl, checkout: checkout })), _jsx(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) })] }) }));
143
148
  }
144
149
  /**
145
150
  * What the page says while the customer is off in Connect, and after.
@@ -105,7 +105,13 @@ export { ConnectConfirmDialog } from "./components/confirm-dialog.js";
105
105
  export { ConnectCursorPagination } from "./components/cursor-pagination.js";
106
106
  export { ConnectFilterTabs } from "./components/filter-tabs.js";
107
107
  export { useConnectCheckout, buildCheckoutUrl } from "./use-checkout.js";
108
- export { formatPrice, monthlyOf, resolveAction, annualSaving, discountLabel, ACTION_LABEL,
108
+ export { formatPrice, monthlyOf, resolveAction,
109
+ // The other half of `resolveAction`, and exported with it deliberately: a
110
+ // partner writing its own pricing grid has to turn that action into a
111
+ // checkout URL, and reading the subscription id straight off the
112
+ // entitlements response instead is what sent a "Subscribe" button to
113
+ // Connect's change-plan screen. See its doc comment.
114
+ checkoutSubscriptionId, annualSaving, discountLabel, ACTION_LABEL,
109
115
  // Exported for the same reason `groupPlansByTier` is: a partner writing its
110
116
  // own pricing grid has to draw a ₹0 plan as "Free" rather than as a price
111
117
  // with a cadence and a GST rate attached, and re-deriving "is this free"
@@ -389,7 +389,7 @@ export function useConnectPlans() {
389
389
  * `CompanyProfile`. `<ConnectGrievanceNotice>` renders it for you.
390
390
  */
391
391
  export function useConnectCompanyProfile() {
392
- return useConnectResource("/company-profile");
392
+ return useConnectResource("/company-profile", { requiresSession: false });
393
393
  }
394
394
  /** The apps this workspace can open. */
395
395
  export function useConnectApps() {
@@ -430,10 +430,15 @@ export function useConnectSessions() {
430
430
  /**
431
431
  * One bridge read, shared through the provider's cache.
432
432
  *
433
- * Only fetches once a user is known: every resource route needs a session, so
434
- * firing before one exists just buys a guaranteed 401.
433
+ * Waits for a session by default: every resource route but one needs it, so
434
+ * firing before one exists just buys a guaranteed 401. `requiresSession:
435
+ * false` opts out, and the reason it exists is the grievance notice - a
436
+ * statutory contact is at its most useful on a public page a signed-out
437
+ * visitor is reading, so a hook that answered `null` until someone logged in
438
+ * would render nothing at exactly the moment that matters.
435
439
  */
436
- export function useConnectResource(path) {
440
+ export function useConnectResource(path, options) {
441
+ const requiresSession = options?.requiresSession ?? true;
437
442
  const { fetchResource, session, isLoading: sessionLoading } = useConnect();
438
443
  // Stamped with the `path` it belongs to, so switching paths reports loading
439
444
  // without a synchronous `setIsLoading(true)` - and, more usefully, so a
@@ -444,7 +449,7 @@ export function useConnectResource(path) {
444
449
  // Signed out is handled by derivation below rather than by writing state
445
450
  // here - a synchronous setState inside an effect costs an extra render pass
446
451
  // and is what `react-hooks/set-state-in-effect` exists to catch.
447
- if (sessionLoading || !session?.user)
452
+ if (requiresSession && (sessionLoading || !session?.user))
448
453
  return;
449
454
  let active = true;
450
455
  fetchResource(path)
@@ -463,8 +468,10 @@ export function useConnectResource(path) {
463
468
  // `version` is otherwise unread - it exists purely to force this effect
464
469
  // to re-run on `refetch()`, after the caller has already evicted this
465
470
  // path's cache entry via `mutateResource`.
466
- }, [fetchResource, path, session?.user, sessionLoading, version]);
467
- const signedOut = !sessionLoading && !session?.user;
471
+ }, [fetchResource, path, requiresSession, session?.user, sessionLoading, version]);
472
+ // A public read is never settled by the absence of a session - it is
473
+ // genuinely pending until the fetch lands.
474
+ const signedOut = requiresSession && !sessionLoading && !session?.user;
468
475
  const loaded = entry?.path === path ? entry : null;
469
476
  return {
470
477
  data: loaded?.data ?? null,
@@ -1,6 +1,7 @@
1
1
  import { AccessResource } from "./resources/access.js";
2
2
  import { AppsResource } from "./resources/apps.js";
3
3
  import { AiResource } from "./resources/ai.js";
4
+ import { EnrichmentResource } from "./resources/enrichment.js";
4
5
  import { DataResource, storePassthrough } from "./resources/data.js";
5
6
  import { WorkspacesResource } from "./resources/workspaces.js";
6
7
  import { SchedulingResource } from "./resources/scheduling.js";
@@ -90,6 +91,7 @@ export class ConnectClient {
90
91
  calls;
91
92
  /** RAG chat and structured extraction. */
92
93
  ai;
94
+ enrichment;
93
95
  /** Your application's own document store - see `DataResource`. */
94
96
  data;
95
97
  /** Your application's key/value store - see `KvResource`. */
@@ -141,6 +143,7 @@ export class ConnectClient {
141
143
  this.channels = new ChannelsResource(transport);
142
144
  this.calls = new CallsResource(transport);
143
145
  this.ai = new AiResource(transport);
146
+ this.enrichment = new EnrichmentResource(transport);
144
147
  this.data = new DataResource(transport);
145
148
  this.kv = new KvResource(transport);
146
149
  this.otp = new OtpResource(transport);
@@ -154,4 +157,4 @@ export class ConnectClient {
154
157
  }
155
158
  }
156
159
  /** Kept in step with `package.json` by `sdk.test.ts`. */
157
- export const SDK_VERSION = "0.21.0";
160
+ export const SDK_VERSION = "0.23.0";
@@ -0,0 +1,52 @@
1
+ import { actingAs } from "./acting-as.js";
2
+ import { request } from "../http.js";
3
+ /**
4
+ * What Connect knows about the company at a domain.
5
+ *
6
+ * The thing behind "the account filled itself in": a record holding nothing
7
+ * but `priya@acme.com` can offer a name, an industry, a size band and an
8
+ * address instead of an empty form.
9
+ *
10
+ * Two properties are worth knowing before you wire it in, because they decide
11
+ * how you should render the result:
12
+ *
13
+ * - **Every value is cited, and nothing is recalled.** Connect reads the
14
+ * company's own website and returns the URL each claim came from. A field
15
+ * the pages never stated comes back `null` rather than as a plausible guess.
16
+ * - **It is a suggestion, not a save.** Nothing is written to your store, and
17
+ * nothing should be written to it without the person in front of it
18
+ * agreeing - which is also how HubSpot and Zoho present the same feature.
19
+ */
20
+ export class EnrichmentResource {
21
+ transport;
22
+ constructor(transport) {
23
+ this.transport = transport;
24
+ }
25
+ /**
26
+ * Look a company up from a work email address or a domain.
27
+ *
28
+ * Send whichever you hold; `domain` wins when both are given. A consumer
29
+ * address (`founder@gmail.com`) is answered with
30
+ * `{ status: "skipped", reason: "consumer_domain" }` rather than an error -
31
+ * it names a person, not an employer.
32
+ *
33
+ * `asUser` attributes the generation to the signed-in caller's workspace,
34
+ * resolved from their session token. Without it the call is attributed to
35
+ * your application.
36
+ */
37
+ company(input, options = {}) {
38
+ return request(this.transport, {
39
+ method: "POST",
40
+ path: "/enrichment/company",
41
+ // `asUser` never reaches the body - it is a credential, and Connect
42
+ // reads it from the header.
43
+ json: {
44
+ ...(input.email ? { email: input.email } : {}),
45
+ ...(input.domain ? { domain: input.domain } : {}),
46
+ ...(input.refresh ? { refresh: true } : {}),
47
+ },
48
+ headers: actingAs(input.asUser),
49
+ signal: options.signal,
50
+ });
51
+ }
52
+ }
@@ -1,8 +1,8 @@
1
1
  import { actingAs } from "./acting-as.js";
2
2
  import { request } from "../http.js";
3
3
  /**
4
- * Which workspaces the signed-in user belongs to, moving between them, and who
5
- * else is on the team.
4
+ * Which workspaces the signed-in user belongs to, moving between them, who
5
+ * else is on the team, and inviting or granting access to your application.
6
6
  *
7
7
  * **Every call here needs `asUser`** - the signed-in user's opaque
8
8
  * `sessionToken`. There is no application-wide form of any of these: they take
@@ -67,4 +67,75 @@ export class WorkspacesResource {
67
67
  });
68
68
  return result.members;
69
69
  }
70
+ /**
71
+ * Seats, what the caller may do with them, and the invitations already
72
+ * holding one - everything a team screen needs to decide what to draw.
73
+ *
74
+ * Behind `members:manage` rather than `workspaces:read`, and gated on the
75
+ * caller's own `member:invite`: it names people who are not members yet.
76
+ */
77
+ access(options) {
78
+ return request(this.transport, {
79
+ method: "GET",
80
+ path: "/workspaces/members/access",
81
+ headers: actingAs(options.asUser),
82
+ });
83
+ }
84
+ /**
85
+ * Invite someone into the workspace, with access to your application.
86
+ *
87
+ * `roleKey` names a role from your own catalog (or the reserved `"admin"`);
88
+ * omit it to add someone to the workspace with no access to your
89
+ * application. You cannot send a Connect role - the grant is built server
90
+ * side as `[member, app:<yourKey>:<roleKey>]`, so nothing this method can
91
+ * express reaches outside your application.
92
+ *
93
+ * Spends a member seat when the invitation is created, not when it is
94
+ * accepted. Throws `ConnectError` with status 409 and a message written to
95
+ * be shown when there is no seat, and 403 when the caller is inviting
96
+ * someone with access they do not hold themselves.
97
+ */
98
+ invite(input, options) {
99
+ return request(this.transport, {
100
+ method: "POST",
101
+ path: "/workspaces/members/invite",
102
+ headers: actingAs(options.asUser),
103
+ json: input,
104
+ });
105
+ }
106
+ /**
107
+ * Grant or revoke **your application's** role for an existing member.
108
+ * `roleKey: null` revokes it.
109
+ *
110
+ * Moves only your own grant: Connect recomputes the member's roles as what
111
+ * they hold now, minus any grant naming your application, plus the one you
112
+ * asked for. Their workspace role and every other application they use are
113
+ * untouched, and there is no form of this that removes them from the
114
+ * workspace.
115
+ *
116
+ * Re-sending the role they already have is a no-op rather than an error, so
117
+ * a form that submits whatever is on screen is safe.
118
+ */
119
+ setAccess(input, options) {
120
+ return request(this.transport, {
121
+ method: "POST",
122
+ path: "/workspaces/members/access",
123
+ headers: actingAs(options.asUser),
124
+ json: input,
125
+ });
126
+ }
127
+ /**
128
+ * Withdraw a pending invitation your application sent, freeing its seat.
129
+ *
130
+ * Only yours - an invitation carrying another application's grant is a 404,
131
+ * deliberately indistinguishable from one that does not exist.
132
+ */
133
+ async revokeInvitation(input, options) {
134
+ await request(this.transport, {
135
+ method: "POST",
136
+ path: "/workspaces/invitations/revoke",
137
+ headers: actingAs(options.asUser),
138
+ json: input,
139
+ });
140
+ }
70
141
  }
@@ -117,3 +117,26 @@ export declare function annualSaving(monthly: Plan | null | undefined, annual: P
117
117
  export type PlanAction = "current" | "upgrade" | "downgrade" | "switch-to-annual" | "switch-to-monthly" | "subscribe";
118
118
  export declare function resolveAction(plan: Plan, current: SubscriptionResponse | null): PlanAction;
119
119
  export declare const ACTION_LABEL: Record<PlanAction, string>;
120
+ /**
121
+ * The subscription id a checkout hand-off should carry for `action` - or
122
+ * `null`, meaning "send them to Connect's *subscribe* screen".
123
+ *
124
+ * Paired with `resolveAction` deliberately: the button's label and the URL it
125
+ * opens are one decision, and deriving the URL from the mere *presence* of a
126
+ * subscription row instead is what sent a card reading "Subscribe" to
127
+ * Connect's change-plan screen, which answered `notFound()`.
128
+ *
129
+ * The gap is real rather than hypothetical. `resolveAction` needs the
130
+ * **current plan** to classify a change and answers `"subscribe"` without
131
+ * one - and `SubscriptionResponse` carries `plan: null` beside a perfectly
132
+ * live `subscription` whenever that plan has been deleted from the catalogue
133
+ * out from under it, which a re-seeded application does. Connect's change-plan
134
+ * route is stuck in the same place for the same reason: it cannot prorate
135
+ * against a price it cannot read.
136
+ *
137
+ * Sending those to `/apps/.../subscribe/...` is a real hand-off, not a guess:
138
+ * that route re-reads the workspace's live subscriptions server-side and
139
+ * redirects to the change-plan screen itself when there genuinely is one to
140
+ * change. Connect decides, so the grid does not have to be sure.
141
+ */
142
+ export declare function checkoutSubscriptionId(action: PlanAction, current: SubscriptionResponse | null): string | null;
@@ -11,7 +11,7 @@
11
11
  * change lands identically in all of them.
12
12
  */
13
13
  export { ConnectProvider, useConnect, useConnectUser, useConnectWorkspaceType, useConnectAudience, useConnectPlatformAccess, useConnectEntitlements, useConnectPlans, useConnectApps, useConnectCompanyProfile, useConnectAddonUsage, useConnectSessions, useConnectIntegrations, useConnectResource, CONNECT_APP_URL, } from "./provider.js";
14
- export type { ConnectProviderProps, ConnectContextValue, ConnectSession, ConnectOneTapHint, ConnectOneTapFailure, } from "./provider.js";
14
+ export type { ConnectProviderProps, ConnectContextValue, ConnectSession, ConnectOneTapHint, ConnectOneTapFailure, UseConnectResourceOptions, } from "./provider.js";
15
15
  export { blockingBlockers, normalizePlanFeatures } from "../helpers.js";
16
16
  export type { WirePlanFeature } from "../helpers.js";
17
17
  export { ConnectError, isConnectError } from "../errors.js";
@@ -84,7 +84,7 @@ export { ConnectFilterTabs } from "./components/filter-tabs.js";
84
84
  export type { ConnectFilterTabsProps, ConnectFilterTabsOption } from "./components/filter-tabs.js";
85
85
  export { useConnectCheckout, buildCheckoutUrl } from "./use-checkout.js";
86
86
  export type { ConnectCheckoutStatus, ConnectCheckoutState, ConnectEntitlements, StartCheckoutOptions, CheckoutUrlOptions, } from "./use-checkout.js";
87
- export { formatPrice, monthlyOf, resolveAction, annualSaving, discountLabel, ACTION_LABEL, isFreePlan, groupPlansByTier, resolvePlanGroup, } from "./components/plan-pricing.js";
87
+ export { formatPrice, monthlyOf, resolveAction, checkoutSubscriptionId, annualSaving, discountLabel, ACTION_LABEL, isFreePlan, groupPlansByTier, resolvePlanGroup, } from "./components/plan-pricing.js";
88
88
  export type { PlanAction, PlanTier, ComparablePlan } from "./components/plan-pricing.js";
89
89
  export { ConnectWorkspaceSwitcher } from "./components/workspace-switcher.js";
90
90
  export type { ConnectWorkspaceSwitcherProps } from "./components/workspace-switcher.js";
@@ -456,13 +456,30 @@ export declare function useConnectSessions(): {
456
456
  */
457
457
  refetch(): void;
458
458
  };
459
+ /** Options for {@link useConnectResource}. */
460
+ export interface UseConnectResourceOptions {
461
+ /**
462
+ * Whether the read has to wait for a session. `true` by default, and right
463
+ * for every route but one.
464
+ *
465
+ * Pass `false` only for a bridge route that takes neither a session nor a
466
+ * workspace - today that is `GET /company-profile` alone, and its handler
467
+ * says why. Passing it for anything else does not widen access (the bridge
468
+ * still refuses), it just buys a guaranteed 401 sooner.
469
+ */
470
+ requiresSession?: boolean;
471
+ }
459
472
  /**
460
473
  * One bridge read, shared through the provider's cache.
461
474
  *
462
- * Only fetches once a user is known: every resource route needs a session, so
463
- * firing before one exists just buys a guaranteed 401.
475
+ * Waits for a session by default: every resource route but one needs it, so
476
+ * firing before one exists just buys a guaranteed 401. `requiresSession:
477
+ * false` opts out, and the reason it exists is the grievance notice - a
478
+ * statutory contact is at its most useful on a public page a signed-out
479
+ * visitor is reading, so a hook that answered `null` until someone logged in
480
+ * would render nothing at exactly the moment that matters.
464
481
  */
465
- export declare function useConnectResource<T>(path: string): {
482
+ export declare function useConnectResource<T>(path: string, options?: Readonly<UseConnectResourceOptions>): {
466
483
  data: T | null;
467
484
  isLoading: boolean;
468
485
  error: string | null;
@@ -64,6 +64,15 @@ export interface CheckoutUrlOptions {
64
64
  * plan on this subscription"; absent means "subscribe for the first time" -
65
65
  * and the two are separate screens in Connect, because only one of them can
66
66
  * price proration against a cycle already paid for.
67
+ *
68
+ * Pass it only when you know **which plan the subscription is on today**,
69
+ * which is what makes the change classifiable. `checkoutSubscriptionId`
70
+ * derives it from the action you are already showing on the button; a bare
71
+ * `subscription?.subscription?.id` does not, and a live subscription whose
72
+ * plan has been deleted from the catalogue is the case where the two differ
73
+ * and the change-plan screen answers `notFound()`. Leaving it `null` is
74
+ * always safe: the subscribe route redirects to the change-plan screen
75
+ * itself when Connect finds a subscription to change.
67
76
  */
68
77
  subscriptionId?: string | null;
69
78
  /** Carried into Connect's checkout, where it is validated against the real cart. */
@@ -2,6 +2,7 @@ import type { FetchLike } from "./http.js";
2
2
  import { AccessResource } from "./resources/access.js";
3
3
  import { AppsResource } from "./resources/apps.js";
4
4
  import { AiResource } from "./resources/ai.js";
5
+ import { EnrichmentResource } from "./resources/enrichment.js";
5
6
  import { DataResource } from "./resources/data.js";
6
7
  import { WorkspacesResource } from "./resources/workspaces.js";
7
8
  import { SchedulingResource } from "./resources/scheduling.js";
@@ -89,6 +90,7 @@ export declare class ConnectClient {
89
90
  readonly calls: CallsResource;
90
91
  /** RAG chat and structured extraction. */
91
92
  readonly ai: AiResource;
93
+ readonly enrichment: EnrichmentResource;
92
94
  /** Your application's own document store - see `DataResource`. */
93
95
  readonly data: DataResource;
94
96
  /** Your application's key/value store - see `KvResource`. */
@@ -121,4 +123,4 @@ export declare class ConnectClient {
121
123
  constructor(options: ConnectClientOptions);
122
124
  }
123
125
  /** Kept in step with `package.json` by `sdk.test.ts`. */
124
- export declare const SDK_VERSION = "0.21.0";
126
+ export declare const SDK_VERSION = "0.23.0";
@@ -18,6 +18,7 @@ export type { AppsResource } from "./resources/apps.js";
18
18
  export type { EntitlementsResource } from "./resources/entitlements.js";
19
19
  export type { BillingResource } from "./resources/billing.js";
20
20
  export type { AiResource } from "./resources/ai.js";
21
+ export type { EnrichmentResource } from "./resources/enrichment.js";
21
22
  export type { DataResource } from "./resources/data.js";
22
23
  export type { KvResource } from "./resources/kv.js";
23
24
  export type { WorkspacesResource } from "./resources/workspaces.js";
@@ -0,0 +1,44 @@
1
+ import { type TransportOptions } from "../http.js";
2
+ import type { CompanyEnrichmentResult } from "../../types.js";
3
+ /**
4
+ * What Connect knows about the company at a domain.
5
+ *
6
+ * The thing behind "the account filled itself in": a record holding nothing
7
+ * but `priya@acme.com` can offer a name, an industry, a size band and an
8
+ * address instead of an empty form.
9
+ *
10
+ * Two properties are worth knowing before you wire it in, because they decide
11
+ * how you should render the result:
12
+ *
13
+ * - **Every value is cited, and nothing is recalled.** Connect reads the
14
+ * company's own website and returns the URL each claim came from. A field
15
+ * the pages never stated comes back `null` rather than as a plausible guess.
16
+ * - **It is a suggestion, not a save.** Nothing is written to your store, and
17
+ * nothing should be written to it without the person in front of it
18
+ * agreeing - which is also how HubSpot and Zoho present the same feature.
19
+ */
20
+ export declare class EnrichmentResource {
21
+ private readonly transport;
22
+ constructor(transport: TransportOptions);
23
+ /**
24
+ * Look a company up from a work email address or a domain.
25
+ *
26
+ * Send whichever you hold; `domain` wins when both are given. A consumer
27
+ * address (`founder@gmail.com`) is answered with
28
+ * `{ status: "skipped", reason: "consumer_domain" }` rather than an error -
29
+ * it names a person, not an employer.
30
+ *
31
+ * `asUser` attributes the generation to the signed-in caller's workspace,
32
+ * resolved from their session token. Without it the call is attributed to
33
+ * your application.
34
+ */
35
+ company(input: {
36
+ email?: string;
37
+ domain?: string;
38
+ asUser?: string;
39
+ /** Re-read the site even if Connect has a fresh profile cached. For an operator pressing "refresh" - not for automatic calls, which should take the cache. */
40
+ refresh?: boolean;
41
+ }, options?: {
42
+ signal?: AbortSignal;
43
+ }): Promise<CompanyEnrichmentResult>;
44
+ }
@@ -1,8 +1,8 @@
1
1
  import { type TransportOptions } from "../http.js";
2
- import type { ConnectWorkspaceMember, UserWorkspace, WorkspaceSwitchResult } from "../../types.js";
2
+ import type { ConnectInvitation, ConnectMemberAccess, ConnectMemberAccessResult, ConnectWorkspaceMember, UserWorkspace, WorkspaceSwitchResult } from "../../types.js";
3
3
  /**
4
- * Which workspaces the signed-in user belongs to, moving between them, and who
5
- * else is on the team.
4
+ * Which workspaces the signed-in user belongs to, moving between them, who
5
+ * else is on the team, and inviting or granting access to your application.
6
6
  *
7
7
  * **Every call here needs `asUser`** - the signed-in user's opaque
8
8
  * `sessionToken`. There is no application-wide form of any of these: they take
@@ -52,4 +52,64 @@ export declare class WorkspacesResource {
52
52
  members(options: {
53
53
  asUser: string;
54
54
  }): Promise<ConnectWorkspaceMember[]>;
55
+ /**
56
+ * Seats, what the caller may do with them, and the invitations already
57
+ * holding one - everything a team screen needs to decide what to draw.
58
+ *
59
+ * Behind `members:manage` rather than `workspaces:read`, and gated on the
60
+ * caller's own `member:invite`: it names people who are not members yet.
61
+ */
62
+ access(options: {
63
+ asUser: string;
64
+ }): Promise<ConnectMemberAccess>;
65
+ /**
66
+ * Invite someone into the workspace, with access to your application.
67
+ *
68
+ * `roleKey` names a role from your own catalog (or the reserved `"admin"`);
69
+ * omit it to add someone to the workspace with no access to your
70
+ * application. You cannot send a Connect role - the grant is built server
71
+ * side as `[member, app:<yourKey>:<roleKey>]`, so nothing this method can
72
+ * express reaches outside your application.
73
+ *
74
+ * Spends a member seat when the invitation is created, not when it is
75
+ * accepted. Throws `ConnectError` with status 409 and a message written to
76
+ * be shown when there is no seat, and 403 when the caller is inviting
77
+ * someone with access they do not hold themselves.
78
+ */
79
+ invite(input: {
80
+ inviteeEmail: string;
81
+ roleKey?: string | null;
82
+ }, options: {
83
+ asUser: string;
84
+ }): Promise<ConnectInvitation>;
85
+ /**
86
+ * Grant or revoke **your application's** role for an existing member.
87
+ * `roleKey: null` revokes it.
88
+ *
89
+ * Moves only your own grant: Connect recomputes the member's roles as what
90
+ * they hold now, minus any grant naming your application, plus the one you
91
+ * asked for. Their workspace role and every other application they use are
92
+ * untouched, and there is no form of this that removes them from the
93
+ * workspace.
94
+ *
95
+ * Re-sending the role they already have is a no-op rather than an error, so
96
+ * a form that submits whatever is on screen is safe.
97
+ */
98
+ setAccess(input: {
99
+ userId: string;
100
+ roleKey: string | null;
101
+ }, options: {
102
+ asUser: string;
103
+ }): Promise<ConnectMemberAccessResult>;
104
+ /**
105
+ * Withdraw a pending invitation your application sent, freeing its seat.
106
+ *
107
+ * Only yours - an invitation carrying another application's grant is a 404,
108
+ * deliberately indistinguishable from one that does not exist.
109
+ */
110
+ revokeInvitation(input: {
111
+ invitationId: string;
112
+ }, options: {
113
+ asUser: string;
114
+ }): Promise<void>;
55
115
  }
@@ -597,13 +597,30 @@ export interface GrievanceChannel {
597
597
  */
598
598
  export interface CompanyProfile {
599
599
  legalName: string;
600
- /** Trading name, for prose the full registered name reads oddly in. */
600
+ /**
601
+ * Trading name, for prose the full registered name reads oddly in.
602
+ *
603
+ * Editable by the platform operator since SDK 0.23. It was a hardcoded
604
+ * constant on Connect's side before that - the one company fact no
605
+ * operator could correct without a deploy.
606
+ */
601
607
  brandName: string;
602
608
  registeredAddress: string;
603
609
  contactEmail: string;
604
610
  website: string | null;
605
611
  /** GSTIN, `null` if the operating entity is not GST-registered. */
606
612
  gstNumber: string | null;
613
+ /**
614
+ * The seat a governing-law clause names - `"Chennai, Tamil Nadu"`.
615
+ *
616
+ * **Not the registered address**, and the distinction is the reason it is
617
+ * its own field: an entity registered in one state routinely names another
618
+ * as the venue for disputes, and the two copies of this that applications
619
+ * used to keep disagreed about which. `null` when the operator has not
620
+ * published one, in which case a terms page should omit the venue rather
621
+ * than reach for `registeredAddress` instead.
622
+ */
623
+ legalJurisdiction: string | null;
607
624
  grievance: GrievanceChannel;
608
625
  }
609
626
  export interface CompanyProfileResponse {
@@ -1153,6 +1170,71 @@ export interface ConnectWorkspaceMember {
1153
1170
  granted: string[];
1154
1171
  role: string | null;
1155
1172
  }
1173
+ /**
1174
+ * How many member seats the workspace has, and whether there is another.
1175
+ *
1176
+ * The number is the **workspace's**, not your plan's: a baseline plus
1177
+ * `allowances.members` from every subscription it holds, folded across
1178
+ * applications - so a seat bought through another product is a seat you may
1179
+ * fill. `included: null` means unlimited. `used` counts active members *and*
1180
+ * pending invitations, because an invitation is a seat already spoken for.
1181
+ *
1182
+ * Advisory. The authoritative check runs inside `invite`, which is where two
1183
+ * people claiming the last seat at once is settled.
1184
+ */
1185
+ export interface ConnectMemberCapacity {
1186
+ included: number | null;
1187
+ used: number;
1188
+ canAdd: boolean;
1189
+ /**
1190
+ * `no_allowance` - nothing the workspace subscribes to grants member seats,
1191
+ * and the answer is a plan. `exhausted` - every seat is taken, and the
1192
+ * answer is an upgrade or a removal. They read alike and need different
1193
+ * next steps, so word them differently.
1194
+ */
1195
+ reason: "no_allowance" | "exhausted" | "hard_ceiling" | null;
1196
+ }
1197
+ /**
1198
+ * What the **signed-in caller** may do with the roster, from their Connect
1199
+ * permissions - not your application's.
1200
+ *
1201
+ * Render your Invite and Change-role controls from this. Without it the only
1202
+ * way to find out is to try, which is a button that exists to fail.
1203
+ */
1204
+ export interface ConnectMemberViewer {
1205
+ canInvite: boolean;
1206
+ canManageAccess: boolean;
1207
+ }
1208
+ /** An invitation that has been sent and not yet accepted - and is holding a seat. */
1209
+ export interface ConnectPendingInvitation {
1210
+ invitationId: string;
1211
+ inviteeEmail: string;
1212
+ /** The role on *your* application it carries, or `null`. */
1213
+ roleKey: string | null;
1214
+ /** ISO 8601. Seven days from when it was sent. */
1215
+ expiresAt: string;
1216
+ }
1217
+ export interface ConnectMemberAccess {
1218
+ capacity: ConnectMemberCapacity;
1219
+ viewer: ConnectMemberViewer;
1220
+ invitations: ConnectPendingInvitation[];
1221
+ }
1222
+ export interface ConnectInvitation {
1223
+ invitationId: string;
1224
+ inviteeEmail: string;
1225
+ /** ISO 8601. */
1226
+ expiresAt: string;
1227
+ /** Always present, so you can offer copy/share when the email did not go out. */
1228
+ inviteUrl: string;
1229
+ emailSent: boolean;
1230
+ }
1231
+ export interface ConnectMemberAccessResult {
1232
+ userId: string;
1233
+ /** The role on your application after the change - `null` when access was revoked. */
1234
+ roleKey: string | null;
1235
+ /** Your application's permissions this member now holds, re-resolved. */
1236
+ granted: string[];
1237
+ }
1156
1238
  /**
1157
1239
  * One bookable thing a workspace offers - "15-minute intro call", "site
1158
1240
  * survey".
@@ -1282,6 +1364,95 @@ export interface ConnectContact {
1282
1364
  createdAt: string;
1283
1365
  updatedAt: string;
1284
1366
  }
1367
+ /** Coarse on purpose - a headcount read off a homepage is a claim, not a measurement. */
1368
+ export type CompanySizeBand = "1-10" | "11-50" | "51-200" | "201-500" | "500+";
1369
+ /**
1370
+ * Where a value came from.
1371
+ *
1372
+ * - `domain` - the input itself. `url` is `null`, because there is no page.
1373
+ * - `website` - a page on the company's own site, at `url`.
1374
+ * - `registry` - a statutory register. Reserved; nothing returns it yet.
1375
+ */
1376
+ export type EnrichmentSource = "domain" | "registry" | "website";
1377
+ export interface EnrichmentCitation {
1378
+ source: EnrichmentSource;
1379
+ /** The exact page read. `null` only for `domain`. */
1380
+ url: string | null;
1381
+ /** ISO-8601. */
1382
+ retrievedAt: string;
1383
+ }
1384
+ /**
1385
+ * Every field nullable, including the ones your own record calls required.
1386
+ *
1387
+ * An enrichment is partial by nature: a site with no address printed on it
1388
+ * yields no address. A shape that demanded a city would only be forcing one to
1389
+ * be invented.
1390
+ */
1391
+ export interface EnrichedAddress {
1392
+ line1: string | null;
1393
+ line2: string | null;
1394
+ city: string | null;
1395
+ /** The state or region's *name* as written, not a code. */
1396
+ state: string | null;
1397
+ postalCode: string | null;
1398
+ /** ISO 3166-1 alpha-2, uppercase. */
1399
+ country: string | null;
1400
+ }
1401
+ /**
1402
+ * What Connect read about a company, and where each part came from.
1403
+ *
1404
+ * Every value is a claim from a page named in `citations`. Nothing here is
1405
+ * recalled by a model: a field the pages did not state is `null`, which is why
1406
+ * `null` is common and should be rendered as "not found" rather than as an
1407
+ * empty input somebody has to notice.
1408
+ */
1409
+ export interface EnrichedCompany {
1410
+ /** Normalised - lowercase, no scheme, no `www.`. */
1411
+ domain: string;
1412
+ name: string | null;
1413
+ /**
1414
+ * The registered entity name.
1415
+ *
1416
+ * Always `null` today. It is reserved for a statutory register, and
1417
+ * deliberately never filled from a website - a footer says "Acme" where the
1418
+ * register says "Acme Industries Private Limited", and this is the one field
1419
+ * here that would end up on a contract.
1420
+ */
1421
+ legalName: string | null;
1422
+ description: string | null;
1423
+ industry: string | null;
1424
+ sizeBand: CompanySizeBand | null;
1425
+ phone: string | null;
1426
+ email: string | null;
1427
+ website: string | null;
1428
+ address: EnrichedAddress | null;
1429
+ /**
1430
+ * Derived from how many fields the pages actually supported - not the
1431
+ * model's opinion of itself, which reflects the confidence of the prose it
1432
+ * read. Treat `low` as "offer this", `high` as "safe to pre-fill".
1433
+ */
1434
+ confidence: "high" | "medium" | "low";
1435
+ citations: EnrichmentCitation[];
1436
+ /** ISO-8601. Age the profile against this. */
1437
+ retrievedAt: string;
1438
+ }
1439
+ /**
1440
+ * Why there is nothing to report. A normal outcome, not an error - most
1441
+ * records legitimately have nothing to enrich.
1442
+ *
1443
+ * `consumer_domain` and `blocked_domain` will never succeed on retry.
1444
+ * `unreachable` and `no_signal` may, later.
1445
+ */
1446
+ export type EnrichmentSkipReason = "no_domain" | "consumer_domain" | "blocked_domain" | "unreachable" | "no_signal";
1447
+ export type CompanyEnrichmentResult = {
1448
+ status: "enriched";
1449
+ profile: EnrichedCompany;
1450
+ /** Whether Connect served this from its platform-wide cache. The profile is the same either way. */
1451
+ cached: boolean;
1452
+ } | {
1453
+ status: "skipped";
1454
+ reason: EnrichmentSkipReason;
1455
+ };
1285
1456
  /**
1286
1457
  * What kind of conversation a channel carries. `voice` is the only kind
1287
1458
  * Connect stocks inventory for today - `whatsapp` and `meeting` are modelled
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cosmoops/connect-sdk",
3
- "version": "0.21.0",
3
+ "version": "0.23.0",
4
4
  "description": "Official server-side SDK for the CosmoOps Connect platform API - Login with Connect, entitlements, billing and metering.",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "type": "commonjs",