@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 +201 -0
- package/dist/cjs/client/components/plan-pricing.js +28 -0
- package/dist/cjs/client/components/plans.js +8 -3
- package/dist/cjs/client/index.js +7 -1
- package/dist/cjs/client/provider.js +14 -7
- package/dist/cjs/server/client.js +4 -1
- package/dist/cjs/server/handler.js +12 -2
- package/dist/cjs/server/resources/enrichment.js +56 -0
- package/dist/cjs/server/resources/integrations.js +7 -5
- package/dist/cjs/server/resources/workspaces.js +73 -2
- package/dist/esm/client/components/plan-pricing.js +27 -0
- package/dist/esm/client/components/plans.js +9 -4
- package/dist/esm/client/index.js +7 -1
- package/dist/esm/client/provider.js +14 -7
- package/dist/esm/server/client.js +4 -1
- package/dist/esm/server/handler.js +12 -2
- package/dist/esm/server/resources/enrichment.js +52 -0
- package/dist/esm/server/resources/integrations.js +7 -5
- package/dist/esm/server/resources/workspaces.js +73 -2
- package/dist/types/client/components/plan-pricing.d.ts +23 -0
- package/dist/types/client/index.d.ts +2 -2
- package/dist/types/client/provider.d.ts +20 -3
- package/dist/types/client/use-checkout.d.ts +9 -0
- package/dist/types/server/client.d.ts +3 -1
- package/dist/types/server/index.d.ts +2 -1
- package/dist/types/server/resources/enrichment.d.ts +44 -0
- package/dist/types/server/resources/integrations.d.ts +20 -4
- package/dist/types/server/resources/workspaces.d.ts +63 -3
- package/dist/types/types.d.ts +172 -1
- package/package.json +1 -1
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:
|
|
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.
|
package/dist/cjs/client/index.js
CHANGED
|
@@ -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
|
-
*
|
|
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
|
-
|
|
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.
|
|
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,
|
|
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
|
|
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,
|
|
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,
|
|
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,
|
|
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
|
+
}
|