@cosmoops/connect-sdk 0.34.1 → 0.36.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 +222 -0
- package/dist/cjs/cli/doctor.js +39 -5
- package/dist/cjs/client/components/shell-context.js +35 -6
- package/dist/cjs/client/components/sidebar.js +6 -1
- package/dist/cjs/next/index.js +96 -1
- package/dist/cjs/server/client.js +1 -1
- package/dist/cjs/server/resources/workspaces.js +33 -0
- package/dist/esm/cli/doctor.js +38 -5
- package/dist/esm/client/components/shell-context.js +35 -6
- package/dist/esm/client/components/sidebar.js +6 -1
- package/dist/esm/next/index.js +95 -2
- package/dist/esm/server/client.js +1 -1
- package/dist/esm/server/resources/workspaces.js +33 -0
- package/dist/types/client/components/shell-context.d.ts +39 -7
- package/dist/types/client/components/sidebar.d.ts +2 -2
- package/dist/types/next/index.d.ts +87 -0
- package/dist/types/server/client.d.ts +1 -1
- package/dist/types/server/resources/scheduling.d.ts +6 -1
- package/dist/types/server/resources/workspaces.d.ts +29 -1
- package/dist/types/types.d.ts +76 -3
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,228 @@
|
|
|
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.36.0
|
|
7
|
+
|
|
8
|
+
### A workspace's billing details can be read, so a tax id is typed once
|
|
9
|
+
|
|
10
|
+
`workspaces.billingProfile({ asUser })` returns the legal name, GSTIN,
|
|
11
|
+
registered address and entity form the customer gave CosmoOps so we can
|
|
12
|
+
invoice them.
|
|
13
|
+
|
|
14
|
+
It exists for one shape of problem: an application that prints documents - a
|
|
15
|
+
CRM's quotation letterhead - needs the same three facts about the same legal
|
|
16
|
+
entity, and the only way to get them was to ask the customer again. Two copies
|
|
17
|
+
of a GSTIN diverge, and on an Indian quotation the first two characters of that
|
|
18
|
+
GSTIN are what decide CGST+SGST versus IGST.
|
|
19
|
+
|
|
20
|
+
```ts
|
|
21
|
+
const token = await getConnectSessionToken();
|
|
22
|
+
const billing = token ? await connect.workspaces.billingProfile({ asUser: token }) : null;
|
|
23
|
+
// offer "Use my billing details" — then keep your own copy
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
**It is a prefill source, not a shared record.** This is the *Bill To* side -
|
|
27
|
+
who we invoice - and your letterhead is the *Bill From* side. They are the same
|
|
28
|
+
facts for most customers without being the same field: a group that bills us
|
|
29
|
+
through a holding company while quoting under a trading subsidiary has to stay
|
|
30
|
+
expressible. So copy it on first setup and keep your copy authoritative. There
|
|
31
|
+
is deliberately **no write form** - an application must not edit the details
|
|
32
|
+
its own invoices are raised against.
|
|
33
|
+
|
|
34
|
+
**Nothing in it is verified.** It is self-declared in the customer's billing
|
|
35
|
+
settings, not the KYB record; no PAN check stands behind it. A convenience,
|
|
36
|
+
never evidence of identity.
|
|
37
|
+
|
|
38
|
+
**It needs `billing:read` on the signed-in member**, on top of `workspaces:read`
|
|
39
|
+
on your key. A tax identity belongs to whoever can already see it in Connect's
|
|
40
|
+
billing screens - an owner, admin or manager - and it cannot be granted through
|
|
41
|
+
a role on your application. So an ordinary member gets a `403` here where they
|
|
42
|
+
get a roster from `members()`: **treat that as a normal branch**, offer the
|
|
43
|
+
prefill to whoever can use it, and let everyone else type their own details.
|
|
44
|
+
|
|
45
|
+
`razorpayCustomerId` is not returned. It is a payment-provider handle no
|
|
46
|
+
partner has a use for.
|
|
47
|
+
|
|
48
|
+
### `getConnectSessionToken()`, so `asUser` works from a Server Component
|
|
49
|
+
|
|
50
|
+
Every `asUser` call needs the caller's opaque session token, and there was no
|
|
51
|
+
way to obtain one inside a Server Component: `getConnectSession()` answers
|
|
52
|
+
*who* the caller is and deliberately does not hand back the credential. A page
|
|
53
|
+
that wanted its own workspace's roster had to reimplement the cookie read or
|
|
54
|
+
route the call through the browser.
|
|
55
|
+
|
|
56
|
+
```tsx
|
|
57
|
+
const token = await getConnectSessionToken();
|
|
58
|
+
const members = token ? await connect.workspaces.members({ asUser: token }) : [];
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
**It is the cookie, unverified** - no round trip, and no check that the session
|
|
62
|
+
is still live. A non-null answer means a cookie was present, not that it works;
|
|
63
|
+
Connect is what answers `401` for a revoked one. Use `getConnectSession()` for
|
|
64
|
+
"is somebody here" and this for "hand Connect the credential they arrived
|
|
65
|
+
with".
|
|
66
|
+
|
|
67
|
+
### `SDK_VERSION` matched the published version again
|
|
68
|
+
|
|
69
|
+
It still read `0.35.0` after the version bump, so every request from this
|
|
70
|
+
release would have carried a `User-Agent` a version behind. `src/sdk.test.ts`
|
|
71
|
+
is what caught it, and is the reason the constant is pinned to
|
|
72
|
+
`package.json` rather than trusted.
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
One field, `ConnectWorkspaceMember.memberId`, without which no integration
|
|
76
|
+
could say who an appointment belongs to.
|
|
77
|
+
|
|
78
|
+
### A diary and a roster could not be joined
|
|
79
|
+
|
|
80
|
+
`Booking.hostMemberIds` holds `WorkspaceMember.id` values, and so does
|
|
81
|
+
`listBookings`' `hostMemberId` filter. `workspaces.members()` returned
|
|
82
|
+
`userId` and no member id at all - so the two sets of ids had nothing in
|
|
83
|
+
common, and a partner could filter a diary by a value it had no way to read
|
|
84
|
+
back.
|
|
85
|
+
|
|
86
|
+
That made two obvious features unreachable rather than merely awkward: a host
|
|
87
|
+
column on a calendar, and narrowing a shared calendar to one person. Both fail
|
|
88
|
+
*quietly* if you reach for `userId` instead, because a filter nobody matches
|
|
89
|
+
returns an empty window, which reads as a free afternoon rather than a bug.
|
|
90
|
+
|
|
91
|
+
```ts
|
|
92
|
+
const { members } = await connect.workspaces.members({ asUser });
|
|
93
|
+
const byMemberId = new Map(members.map((m) => [m.memberId, m]));
|
|
94
|
+
|
|
95
|
+
const { bookings } = await connect.scheduling.listBookings({ workspaceId, asUser });
|
|
96
|
+
bookings.map((b) => b.hostMemberIds.map((id) => byMemberId.get(id)?.displayName));
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
The two ids are not interchangeable and neither is derived from the other:
|
|
100
|
+
`userId` identifies the person everywhere else in this API, `memberId` is what
|
|
101
|
+
the calendar speaks in. It is workspace-scoped and opaque - do not address a
|
|
102
|
+
member by it anywhere else.
|
|
103
|
+
|
|
104
|
+
Additive, so nothing breaks. `Booking.hostMemberIds`' own doc comment claimed
|
|
105
|
+
these were already the ids `workspaces.listMembers` returns, which was wrong
|
|
106
|
+
twice over - that method is `workspaces.members()`, and until now its rows
|
|
107
|
+
carried no id these could be matched against.
|
|
108
|
+
|
|
109
|
+
## 0.35.0
|
|
110
|
+
|
|
111
|
+
One addition, `connectProviderProps()`, for a mistake both applications built
|
|
112
|
+
on this SDK shipped before anyone noticed.
|
|
113
|
+
|
|
114
|
+
### Your server and your browser can address different Connect instances
|
|
115
|
+
|
|
116
|
+
`CONNECT_APP_URL` points the server at a non-production Connect. It is not
|
|
117
|
+
`NEXT_PUBLIC_`, so it never reaches the browser, and `ConnectProvider` defaults
|
|
118
|
+
`appUrl` to the public instance independently. Point one and not the other and
|
|
119
|
+
nothing fails: your API reads go to staging while every destination the browser
|
|
120
|
+
builds opens **production** - the user menu's Profile and Billing rows, the app
|
|
121
|
+
launcher's link for an unsubscribed application, `ConnectAccessGate`'s subscribe
|
|
122
|
+
target, and checkout.
|
|
123
|
+
|
|
124
|
+
The docs said to pass both. That was a rule to remember rather than a shape to
|
|
125
|
+
follow, and the sentence next to it - "the provider takes no props, which is the
|
|
126
|
+
point" - was about `clientId` and read as being about all of them.
|
|
127
|
+
|
|
128
|
+
```tsx title="app/layout.tsx"
|
|
129
|
+
import { connectProviderProps } from "@cosmoops/connect-sdk/next";
|
|
130
|
+
import { ConnectProvider } from "@cosmoops/connect-sdk/client";
|
|
131
|
+
|
|
132
|
+
<ConnectProvider {...connectProviderProps()}>{children}</ConnectProvider>
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
It returns `{ appUrl }`, resolved from `CONNECT_APP_URL` and falling back to the
|
|
136
|
+
production origin - so the line is correct in every environment and you write it
|
|
137
|
+
once. Props you pass yourself still win, as long as the spread comes first.
|
|
138
|
+
|
|
139
|
+
**Call it in the Server Component, not in your shell.** It reads a server-side
|
|
140
|
+
variable, so it ships from `/next`, which a Client Component cannot import -
|
|
141
|
+
attempting it fails the build naming `this-entry-is-server-only`. A shell that
|
|
142
|
+
needs `usePathname()` takes the result as a prop; `{ appUrl }` is a plain
|
|
143
|
+
serializable object, so that costs nothing.
|
|
144
|
+
|
|
145
|
+
Already have a helper that owns this origin - because you also build your own
|
|
146
|
+
links into Connect - keep using it. One function owning the read beats this one
|
|
147
|
+
sitting beside it.
|
|
148
|
+
|
|
149
|
+
Nothing changes if you were already passing `appUrl`, and nothing breaks if you
|
|
150
|
+
were not: the default is unchanged.
|
|
151
|
+
|
|
152
|
+
**This was never a security issue**, which is worth saying because it sounds
|
|
153
|
+
like one. `appUrl` is a navigation target you set from your own environment: it
|
|
154
|
+
carries no credential, no gate or permission check reads it, it is never
|
|
155
|
+
reachable from a URL or a request body, and redirect URIs are still matched
|
|
156
|
+
exactly server-side. The worst case is a real charge in the wrong instance, not
|
|
157
|
+
a way in.
|
|
158
|
+
|
|
159
|
+
### The sidebar's nav rows are 36px, so a ten-route rail stops scrolling
|
|
160
|
+
|
|
161
|
+
`AppShell`'s rail is the only part of the shell that grows with *your* route
|
|
162
|
+
count, and `ConnectSidebarBody` is the only part that scrolls - so a rail that
|
|
163
|
+
overflows overflows there. Ten destinations at the old 40px came to 436px of
|
|
164
|
+
nav against 273px of fixed chrome (header, workspace switcher, secondary-nav
|
|
165
|
+
footer, account footer), which needs a 709px viewport to sit still; a 1366x768
|
|
166
|
+
screen has about 678px of it. The nav scrolled under its own footer on the most
|
|
167
|
+
common small laptop, which reads as a rail with destinations missing from it.
|
|
168
|
+
|
|
169
|
+
Nav rows - and every 20px-glyph row, so the secondary nav and the app launcher
|
|
170
|
+
too - are now **36px**, and the scrolling region's own vertical padding drops
|
|
171
|
+
from 8px to 4px a side. That is 56px back on a twelve-row integration, enough
|
|
172
|
+
that the same rail sits still. 36px is still past WCAG 2.5.8's 24px target
|
|
173
|
+
minimum, with the row's full width as the hit area.
|
|
174
|
+
|
|
175
|
+
**The two 32px-glyph rows stay at 40px** - the workspace switcher and the
|
|
176
|
+
account row. There are exactly two of them, neither scales with your nav, and
|
|
177
|
+
both need the height: a 32px avatar in a 36px row has 2px of air, and the
|
|
178
|
+
account row's label is two lines.
|
|
179
|
+
|
|
180
|
+
**Shrinking the icons would not have done it.** The glyph box is a flex
|
|
181
|
+
centring container for an icon *you* supply at your own size, so a smaller box
|
|
182
|
+
buys no vertical space and only pushes the icon off the 32px horizontal centre
|
|
183
|
+
the collapsed rail is built on - `RAIL_GUTTER + inset + glyph/2 = RAIL_COLLAPSED / 2`
|
|
184
|
+
is what lets the rail collapse without anything drifting sideways.
|
|
185
|
+
|
|
186
|
+
Nothing in your code changes. If your nav is long enough to still overflow, the
|
|
187
|
+
levers are the row count and `secondaryNav` - a rail is navigation, and past a
|
|
188
|
+
dozen rows a scrolling one is a sitemap.
|
|
189
|
+
|
|
190
|
+
### `doctor` now catches it
|
|
191
|
+
|
|
192
|
+
`npx @cosmoops/connect-sdk doctor` reports a **note** when `CONNECT_APP_URL` is
|
|
193
|
+
set and the layout holding `<ConnectProvider>` passes no `appUrl`, with the fix
|
|
194
|
+
in the output. A note rather than a failure, so it cannot break a CI run: the
|
|
195
|
+
prop may be threaded from the layout into a `"use client"` shell, which reading
|
|
196
|
+
the layout cannot see.
|
|
197
|
+
|
|
198
|
+
### The rail fits a ten-destination nav on a laptop
|
|
199
|
+
|
|
200
|
+
`AppShell`'s nav rows, its launcher and its secondary-nav rows are **36px**
|
|
201
|
+
rather than 40px, and the one region that scrolls (`ConnectSidebarBody`) carries
|
|
202
|
+
`py-1` rather than `py-2`. The two rows with a 32px glyph - the account row and
|
|
203
|
+
the workspace switcher - stay at 40px: there are exactly two of them, neither
|
|
204
|
+
grows with your nav, and both need the height (a 32px tile in a 36px row has 2px
|
|
205
|
+
of air, and the account row's name-over-email label measures ~32px on its own).
|
|
206
|
+
|
|
207
|
+
Ten destinations at 40px came to 436px of nav against 273px of fixed chrome,
|
|
208
|
+
which needs a 709px viewport to sit still - a 1366x768 screen has about 678px of
|
|
209
|
+
it, so the nav scrolled on a perfectly ordinary laptop. Four pixels a row is 48px
|
|
210
|
+
back across the twelve rows a real integration draws. 36px is still comfortably
|
|
211
|
+
past WCAG 2.5.8's 24px target minimum, with the row's full width as the hit area.
|
|
212
|
+
|
|
213
|
+
The glyph box is deliberately unchanged at 20px. It is a flex centring container
|
|
214
|
+
for an icon **you** supply at your own size, so shrinking it buys no vertical
|
|
215
|
+
space and only pushes the icon off the 32px centre the collapsed rail is built
|
|
216
|
+
on. If you pass an icon larger than 20px it will now sit in a slightly tighter
|
|
217
|
+
row; `size-5` remains the recommendation.
|
|
218
|
+
|
|
219
|
+
### Fixed: the User-Agent reported the wrong version
|
|
220
|
+
|
|
221
|
+
`SDK_VERSION` was left at `0.34.0` when 0.34.1 shipped, so every request this
|
|
222
|
+
package made identified itself as the previous release. It is in step again, and
|
|
223
|
+
the assertion that pins it to `package.json` is back to green - it had been
|
|
224
|
+
failing since that release, unnoticed because the publish guard runs the build
|
|
225
|
+
and the pack check rather than the test suite. Diagnostic only; nothing behaved
|
|
226
|
+
differently.
|
|
227
|
+
|
|
6
228
|
## 0.34.1
|
|
7
229
|
|
|
8
230
|
Two tabs, or one abandoned sign-in and one fresh one, could not both work -
|
package/dist/cjs/cli/doctor.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
"use client";
|
|
1
2
|
"use strict";
|
|
2
3
|
/**
|
|
3
4
|
* `npx @cosmoops/connect-sdk doctor` - checks a wired-up project for the
|
|
@@ -84,14 +85,14 @@ function checkBridge(appDir, root) {
|
|
|
84
85
|
}
|
|
85
86
|
return { status: "pass", title: `Bridge route handler (${(0, node_path_1.relative)(root, bridge)})` };
|
|
86
87
|
}
|
|
87
|
-
function checkProvider(appDir, root) {
|
|
88
|
+
function checkProvider(appDir, root, env) {
|
|
88
89
|
const layouts = walk(appDir, ["layout.tsx", "layout.jsx"]);
|
|
89
90
|
const hosting = layouts.filter((path) => /<ConnectProvider/.test(read(path)));
|
|
90
91
|
if (hosting.length === 0) {
|
|
91
92
|
return {
|
|
92
93
|
status: "fail",
|
|
93
94
|
title: "ConnectProvider in a layout",
|
|
94
|
-
detail: 'Nothing renders <ConnectProvider>. Every client hook and component below it throws without one. In app/layout.tsx:\n import { ConnectProvider } from "@cosmoops/connect-sdk/client";\n <ConnectProvider>{children}</ConnectProvider>',
|
|
95
|
+
detail: 'Nothing renders <ConnectProvider>. Every client hook and component below it throws without one. In app/layout.tsx:\n import { connectProviderProps } from "@cosmoops/connect-sdk/next";\n import { ConnectProvider } from "@cosmoops/connect-sdk/client";\n <ConnectProvider {...connectProviderProps()}>{children}</ConnectProvider>',
|
|
95
96
|
};
|
|
96
97
|
}
|
|
97
98
|
// A nested provider re-fetches the session and keeps a second cache that the
|
|
@@ -104,8 +105,42 @@ function checkProvider(appDir, root) {
|
|
|
104
105
|
detail: `Found in ${hosting.length} layouts (${hosting.map((path) => (0, node_path_1.relative)(root, path)).join(", ")}). Nested providers keep separate caches, so a mutation in one is invisible to the other. Keep the outermost one only.`,
|
|
105
106
|
};
|
|
106
107
|
}
|
|
108
|
+
/*
|
|
109
|
+
* The split integration, and the reason this check exists at all.
|
|
110
|
+
*
|
|
111
|
+
* `CONNECT_APP_URL` points the *server* at a non-production Connect. It is
|
|
112
|
+
* not `NEXT_PUBLIC_`, so `ConnectProvider` cannot read it and defaults
|
|
113
|
+
* `appUrl` to production independently - and nothing fails. The API reads go
|
|
114
|
+
* to the configured instance while every destination the browser builds
|
|
115
|
+
* opens production: the user menu's Profile and Billing rows, the launcher's
|
|
116
|
+
* link for an unsubscribed application, the access gate's subscribe target,
|
|
117
|
+
* and checkout. Both applications built on this SDK shipped exactly that.
|
|
118
|
+
*
|
|
119
|
+
* `warn` rather than `fail`: the prop may be threaded from a parent Server
|
|
120
|
+
* Component into a `"use client"` shell, which is the documented shape for a
|
|
121
|
+
* shell needing `usePathname()` - and that assignment is not visible in the
|
|
122
|
+
* layout this reads. So this cannot tell from the outside, which is what
|
|
123
|
+
* `warn` means here.
|
|
124
|
+
*/
|
|
125
|
+
const pointedElsewhere = /^[ \t]*CONNECT_APP_URL[ \t]*=[ \t]*\S/m.test(env);
|
|
126
|
+
const carriesOrigin = /appUrl|connectProviderProps/.test(read(hosting[0]));
|
|
127
|
+
if (pointedElsewhere && !carriesOrigin) {
|
|
128
|
+
return {
|
|
129
|
+
status: "warn",
|
|
130
|
+
title: "ConnectProvider in a layout",
|
|
131
|
+
detail: `CONNECT_APP_URL is set, but ${(0, node_path_1.relative)(root, hosting[0])} passes no appUrl - so your server talks to that instance while every link the browser builds opens production Connect. Spread it in:\n <ConnectProvider {...connectProviderProps()}> // from "@cosmoops/connect-sdk/next"\n Already threading it into a "use client" shell? Then this is fine and you can ignore it.`,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
107
134
|
return { status: "pass", title: `ConnectProvider in a layout (${(0, node_path_1.relative)(root, hosting[0])})` };
|
|
108
135
|
}
|
|
136
|
+
/** The env files `checkEnv` reads, as one string, shared with `checkProvider`. */
|
|
137
|
+
function envText(root) {
|
|
138
|
+
return [".env.local", ".env.development.local", ".env"]
|
|
139
|
+
.map((name) => (0, node_path_1.join)(root, name))
|
|
140
|
+
.filter(node_fs_1.existsSync)
|
|
141
|
+
.map(read)
|
|
142
|
+
.join("\n");
|
|
143
|
+
}
|
|
109
144
|
/**
|
|
110
145
|
* Order is the whole check.
|
|
111
146
|
*
|
|
@@ -146,8 +181,7 @@ function checkTheme(root) {
|
|
|
146
181
|
* loaded it - exactly the shell `next dev` will inherit.
|
|
147
182
|
*/
|
|
148
183
|
function checkEnv(root) {
|
|
149
|
-
const
|
|
150
|
-
const combined = files.filter(node_fs_1.existsSync).map(read).join("\n");
|
|
184
|
+
const combined = envText(root);
|
|
151
185
|
return ["CONNECT_API_KEY", "CONNECT_CLIENT_ID"].map((name) => {
|
|
152
186
|
// Horizontal whitespace only. `\s*` also matches the newline, so a bare
|
|
153
187
|
// `CONNECT_API_KEY=` was satisfied by the *next* line's value and an empty
|
|
@@ -191,7 +225,7 @@ function runDoctor(argv = []) {
|
|
|
191
225
|
console.log(`[connect-sdk] Checking ${(0, node_path_1.relative)(process.cwd(), root) || root}\n`);
|
|
192
226
|
const checks = [
|
|
193
227
|
checkBridge(appDir, root),
|
|
194
|
-
checkProvider(appDir, root),
|
|
228
|
+
checkProvider(appDir, root, envText(root)),
|
|
195
229
|
checkTheme(root),
|
|
196
230
|
...checkEnv(root),
|
|
197
231
|
checkRedirectUri(),
|
|
@@ -36,22 +36,51 @@ exports.RAIL_GUTTER = 12;
|
|
|
36
36
|
* The row shape every sidebar control shares - nav items, the workspace tile,
|
|
37
37
|
* the launcher, the user button.
|
|
38
38
|
*
|
|
39
|
+
* **The height is not here.** It rides on `SIDEBAR_ROW_ICON` /
|
|
40
|
+
* `SIDEBAR_ROW_TILE` instead, because the two glyph sizes want different
|
|
41
|
+
* heights and only one of them is in the region that overflows - see those
|
|
42
|
+
* two constants.
|
|
43
|
+
*
|
|
39
44
|
* `overflow-hidden` + `whitespace-nowrap` is what makes the collapse smooth:
|
|
40
45
|
* the label is *clipped* by the shrinking rail rather than re-wrapped or
|
|
41
46
|
* re-measured, so a width animation costs no reflow. Fading the label with
|
|
42
47
|
* `SidebarLabel` on top of that hides the clip itself.
|
|
43
48
|
*/
|
|
44
|
-
exports.SIDEBAR_ROW = "group/row relative flex
|
|
49
|
+
exports.SIDEBAR_ROW = "group/row relative flex w-full shrink-0 items-center justify-start gap-3 overflow-hidden whitespace-nowrap rounded-lg pe-2 text-sm font-normal transition-colors focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-focus";
|
|
45
50
|
/**
|
|
46
|
-
* Leading inset for a row whose glyph is 20px wide (a nav icon,
|
|
51
|
+
* Leading inset *and height* for a row whose glyph is 20px wide (a nav icon,
|
|
52
|
+
* the launcher, a secondary-nav row).
|
|
53
|
+
*
|
|
47
54
|
* `10 + 10 = 20`, and `RAIL_GUTTER + 20 = 32`. See `RAIL_COLLAPSED`.
|
|
55
|
+
*
|
|
56
|
+
* **36px, not 40px, and the height belongs on this axis rather than on
|
|
57
|
+
* `SIDEBAR_ROW`.** The nav is the only region of the rail that grows with the
|
|
58
|
+
* host's route count, and `ConnectSidebarBody` is the only region that
|
|
59
|
+
* scrolls - so a rail that overflows overflows here. Ten destinations at 40px
|
|
60
|
+
* came to 436px of nav against 273px of fixed chrome (header, switcher, two
|
|
61
|
+
* footers), which needs a 709px viewport to sit still; a 1366x768 screen has
|
|
62
|
+
* about 678px of it. Four pixels a row is 48px back across the twelve rows a
|
|
63
|
+
* real integration draws, and 36px is still comfortably past the 24px
|
|
64
|
+
* WCAG 2.5.8 target minimum with the row's full width as the hit area.
|
|
65
|
+
*
|
|
66
|
+
* It is deliberately *not* a smaller glyph. The 20px box is a flex centring
|
|
67
|
+
* container for an icon the **host** supplies at its own size, so shrinking
|
|
68
|
+
* the box buys no vertical space and only pushes the icon off the 32px centre
|
|
69
|
+
* the collapsed rail is built on.
|
|
48
70
|
*/
|
|
49
|
-
exports.SIDEBAR_ROW_ICON = "ps-[10px]";
|
|
71
|
+
exports.SIDEBAR_ROW_ICON = "h-9 ps-[10px]";
|
|
50
72
|
/**
|
|
51
|
-
* Leading inset for a row whose glyph is 32px wide (the avatar, the
|
|
52
|
-
* tile). `4 + 16 = 20`, landing on the same 32px centre as
|
|
73
|
+
* Leading inset and height for a row whose glyph is 32px wide (the avatar, the
|
|
74
|
+
* workspace tile). `4 + 16 = 20`, landing on the same 32px centre as
|
|
75
|
+
* `SIDEBAR_ROW_ICON`.
|
|
76
|
+
*
|
|
77
|
+
* **These stay at 40px.** There are exactly two of them and neither scales
|
|
78
|
+
* with the host's nav, so they are not what overflows - and both need the
|
|
79
|
+
* height: a 32px tile in a 36px row has 2px of air, and the account row's
|
|
80
|
+
* label is two lines (name over email, `leading-tight`) which measures ~32px
|
|
81
|
+
* on its own.
|
|
53
82
|
*/
|
|
54
|
-
exports.SIDEBAR_ROW_TILE = "ps-[4px]";
|
|
83
|
+
exports.SIDEBAR_ROW_TILE = "h-10 ps-[4px]";
|
|
55
84
|
/**
|
|
56
85
|
* The class list for one sidebar row, in one place.
|
|
57
86
|
*
|
|
@@ -116,7 +116,12 @@ function ConnectSidebarBody({ children }) {
|
|
|
116
116
|
// zoom away over a region of text is a WCAG 1.4.4 failure, and nothing
|
|
117
117
|
// here needs it suppressed. On the desktop rail, where no ancestor
|
|
118
118
|
// restricts anything, the pair is equivalent to the default.
|
|
119
|
-
|
|
119
|
+
// `py-1` rather than the `py-2` every other region carries: this is the
|
|
120
|
+
// one region that scrolls, so it is the first place to give up air when
|
|
121
|
+
// the nav is a row or two from overflowing. The rows paint their own
|
|
122
|
+
// rounded background, so the hairline above the footer is still clear of
|
|
123
|
+
// them. See `SIDEBAR_ROW_ICON` for the arithmetic this is part of.
|
|
124
|
+
(0, jsx_runtime_1.jsx)("div", { className: "flex min-h-0 flex-1 touch-pan-y touch-pinch-zoom flex-col gap-1 overflow-y-auto overflow-x-hidden px-3 py-1", children: children }));
|
|
120
125
|
}
|
|
121
126
|
/**
|
|
122
127
|
* A pinned region at the foot of the rail - the secondary nav, the launcher,
|
package/dist/cjs/next/index.js
CHANGED
|
@@ -36,10 +36,12 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
36
36
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
37
37
|
exports.ConnectClient = exports.createConnectHandler = void 0;
|
|
38
38
|
exports.connectClient = connectClient;
|
|
39
|
+
exports.connectProviderProps = connectProviderProps;
|
|
39
40
|
exports.cookieSessionStore = cookieSessionStore;
|
|
40
41
|
exports.createConnectHandlers = createConnectHandlers;
|
|
41
42
|
exports.GET = handle;
|
|
42
43
|
exports.POST = handle;
|
|
44
|
+
exports.getConnectSessionToken = getConnectSessionToken;
|
|
43
45
|
exports.getConnectSession = getConnectSession;
|
|
44
46
|
exports.requireConnectSession = requireConnectSession;
|
|
45
47
|
const react_1 = require("react");
|
|
@@ -68,8 +70,9 @@ const errors_js_1 = require("../errors.js");
|
|
|
68
70
|
*
|
|
69
71
|
* ```tsx
|
|
70
72
|
* // app/layout.tsx
|
|
73
|
+
* import { connectProviderProps } from "@cosmoops/connect-sdk/next";
|
|
71
74
|
* import { ConnectProvider } from "@cosmoops/connect-sdk/client";
|
|
72
|
-
* <ConnectProvider>{children}</ConnectProvider>
|
|
75
|
+
* <ConnectProvider {...connectProviderProps()}>{children}</ConnectProvider>
|
|
73
76
|
* ```
|
|
74
77
|
*
|
|
75
78
|
* No `next.config.ts` changes needed - configuration comes from the
|
|
@@ -86,6 +89,10 @@ const errors_js_1 = require("../errors.js");
|
|
|
86
89
|
* `ConnectProvider` defaults `appUrl` to the production origin independently,
|
|
87
90
|
* so an app pointed at another instance must pass that prop as well or its
|
|
88
91
|
* portal links and its browser-built authorize URL still go to production.
|
|
92
|
+
* That is what the `connectProviderProps()` spread above is for, and why the
|
|
93
|
+
* layout is two imports rather than one - leave it off and the server half and
|
|
94
|
+
* the browser half of the same integration address different instances, with
|
|
95
|
+
* nothing failing to say so. See that function's own docs.
|
|
89
96
|
*
|
|
90
97
|
* Register `{your origin}/api/connect/callback` as a redirect URI in the
|
|
91
98
|
* application's Connect client section. That is the only step this cannot do
|
|
@@ -126,6 +133,55 @@ function connectClient(overrides = {}) {
|
|
|
126
133
|
cached = client;
|
|
127
134
|
return client;
|
|
128
135
|
}
|
|
136
|
+
/**
|
|
137
|
+
* The `ConnectProvider` props that come from the environment.
|
|
138
|
+
*
|
|
139
|
+
* ```tsx
|
|
140
|
+
* // app/layout.tsx - a Server Component
|
|
141
|
+
* import { connectProviderProps } from "@cosmoops/connect-sdk/next";
|
|
142
|
+
* import { ConnectProvider } from "@cosmoops/connect-sdk/client";
|
|
143
|
+
*
|
|
144
|
+
* <ConnectProvider {...connectProviderProps()}>{children}</ConnectProvider>
|
|
145
|
+
* ```
|
|
146
|
+
*
|
|
147
|
+
* ## What it is for
|
|
148
|
+
*
|
|
149
|
+
* `CONNECT_APP_URL` points the *server* at a non-production Connect. It is not
|
|
150
|
+
* `NEXT_PUBLIC_`, so it never reaches the browser, and `ConnectProvider`
|
|
151
|
+
* defaults `appUrl` to the production origin independently - so pointing the
|
|
152
|
+
* server somewhere else and leaving the prop off splits the integration in
|
|
153
|
+
* half. The server talks to dev and every destination the browser builds opens
|
|
154
|
+
* production: the user menu's Profile and Billing rows, the app launcher's link
|
|
155
|
+
* for an unsubscribed application, `ConnectAccessGate`'s subscribe target, and
|
|
156
|
+
* checkout. Nothing errors. The customer is simply asked to buy a second
|
|
157
|
+
* subscription in an instance where they have none.
|
|
158
|
+
*
|
|
159
|
+
* Both apps built on this SDK shipped that bug before anyone noticed, from
|
|
160
|
+
* reading "the provider takes no props" as a rule rather than as a remark about
|
|
161
|
+
* `clientId`. A root layout is a Server Component, so one spread fixes it with
|
|
162
|
+
* no browser-visible copy of the variable.
|
|
163
|
+
*
|
|
164
|
+
* ## What it deliberately leaves out
|
|
165
|
+
*
|
|
166
|
+
* `clientId` and `basePath`. The first is only needed alongside an explicit
|
|
167
|
+
* `redirectUri`, when you build the authorize URL in the browser rather than
|
|
168
|
+
* letting `signIn()` bounce through your own `/signin` route - and handing the
|
|
169
|
+
* browser a value it does not need is surface for nothing. The second is where
|
|
170
|
+
* *you* mounted the handler, which the environment does not know. Pass either
|
|
171
|
+
* yourself; a prop you pass always wins over this spread, whichever order you
|
|
172
|
+
* write them in, as long as the spread comes first.
|
|
173
|
+
*
|
|
174
|
+
* ## If you already have an origin helper
|
|
175
|
+
*
|
|
176
|
+
* Use it instead. An app that builds its own links into Connect (a "Manage
|
|
177
|
+
* team" button, a pricing page's subscribe target) needs that origin on the
|
|
178
|
+
* server too, and one function owning the read is better than this helper
|
|
179
|
+
* beside it - two readers of one variable is the shape that caused this. This
|
|
180
|
+
* exists for the integration that has no such helper, which is most of them.
|
|
181
|
+
*/
|
|
182
|
+
function connectProviderProps() {
|
|
183
|
+
return { appUrl: (process.env.CONNECT_APP_URL ?? client_js_1.DEFAULT_BASE_URL).replace(/\/+$/, "") };
|
|
184
|
+
}
|
|
129
185
|
/**
|
|
130
186
|
* An httpOnly cookie holding the opaque `sessionToken`.
|
|
131
187
|
*
|
|
@@ -297,6 +353,45 @@ async function readSession(request, options) {
|
|
|
297
353
|
const cachedAmbientSession = (0, react_1.cache)(async () => {
|
|
298
354
|
return readSession(await requestFromHeaders(), {});
|
|
299
355
|
});
|
|
356
|
+
/**
|
|
357
|
+
* The caller's opaque Connect **session token**, or `null` when signed out.
|
|
358
|
+
*
|
|
359
|
+
* ## What it is for
|
|
360
|
+
*
|
|
361
|
+
* Every `asUser` call on the server client needs one - `workspaces.*`,
|
|
362
|
+
* `scheduling.*`, the `/data` store's per-user form - and until this existed
|
|
363
|
+
* there was no way to obtain it inside a Server Component. `getConnectSession`
|
|
364
|
+
* answers *who* the caller is and deliberately does not hand back the
|
|
365
|
+
* credential, so a page that wanted to read its own workspace's roster had to
|
|
366
|
+
* either reimplement the cookie read or route the call through the browser.
|
|
367
|
+
*
|
|
368
|
+
* ```tsx
|
|
369
|
+
* // app/settings/page.tsx
|
|
370
|
+
* const token = await getConnectSessionToken();
|
|
371
|
+
* const members = token ? await connect.workspaces.members({ asUser: token }) : [];
|
|
372
|
+
* ```
|
|
373
|
+
*
|
|
374
|
+
* ## It is the cookie, unverified
|
|
375
|
+
*
|
|
376
|
+
* This reads the session store and nothing else - **no round trip, and no
|
|
377
|
+
* check that the token is still live.** A token from a revoked or expired
|
|
378
|
+
* session is a string that looks exactly like a good one, and Connect is what
|
|
379
|
+
* answers `401` for it. So this is not a sign-in check: use
|
|
380
|
+
* `getConnectSession()` for "is somebody here", and this for "hand Connect the
|
|
381
|
+
* credential they arrived with". A non-null answer means a cookie was present,
|
|
382
|
+
* not that it works.
|
|
383
|
+
*
|
|
384
|
+
* Reads `cookies()` transitively, so under `cacheComponents` it belongs inside
|
|
385
|
+
* a `<Suspense>` boundary like any other request-scoped read.
|
|
386
|
+
*
|
|
387
|
+
* Pass a `Request` to use it where `next/headers` does not exist, and a
|
|
388
|
+
* `session` store if you did not use the default cookie - the same two escape
|
|
389
|
+
* hatches `getConnectSession` takes, for the same reasons.
|
|
390
|
+
*/
|
|
391
|
+
async function getConnectSessionToken(request, options = {}) {
|
|
392
|
+
const store = options.session ?? cookieSessionStore();
|
|
393
|
+
return (await store.read(request ?? (await requestFromHeaders()))) ?? null;
|
|
394
|
+
}
|
|
300
395
|
/**
|
|
301
396
|
* The signed-in user for a Server Component, Route Handler or Server Action,
|
|
302
397
|
* or `null`.
|
|
@@ -54,6 +54,39 @@ class WorkspacesResource {
|
|
|
54
54
|
json: input,
|
|
55
55
|
});
|
|
56
56
|
}
|
|
57
|
+
/**
|
|
58
|
+
* The billing identity of the workspace this session is on - its legal
|
|
59
|
+
* name, GSTIN, registered address and entity form.
|
|
60
|
+
*
|
|
61
|
+
* **For prefilling your own settings, once.** See
|
|
62
|
+
* `WorkspaceBillingProfile`, which carries the whole argument: this is the
|
|
63
|
+
* *Bill To* side, yours is the *Bill From* side, and they are the same facts
|
|
64
|
+
* for most customers without being the same field. There is no write form.
|
|
65
|
+
*
|
|
66
|
+
* ```ts
|
|
67
|
+
* // app/settings/page.tsx - offer "Use my billing details"
|
|
68
|
+
* const token = await getConnectSessionToken();
|
|
69
|
+
* const billing = token ? await connect.workspaces.billingProfile({ asUser: token }) : null;
|
|
70
|
+
* ```
|
|
71
|
+
*
|
|
72
|
+
* **Needs two things, not one.** `workspaces:read` on your key, *and*
|
|
73
|
+
* `billing:read` on the signed-in member - a tax identity belongs to
|
|
74
|
+
* whoever can already see it in Connect's billing screens, so an ordinary
|
|
75
|
+
* member gets a 403 here where they get a roster from `members()`. That
|
|
76
|
+
* cannot be granted through a role on your application, deliberately.
|
|
77
|
+
*
|
|
78
|
+
* So **treat the 403 as a normal branch**, not an error: offer the prefill
|
|
79
|
+
* to whoever can use it and let everyone else type their own details. A
|
|
80
|
+
* workspace admin is the person who sets this up anyway.
|
|
81
|
+
*/
|
|
82
|
+
async billingProfile(options) {
|
|
83
|
+
const result = await (0, http_js_1.request)(this.transport, {
|
|
84
|
+
method: "GET",
|
|
85
|
+
path: "/workspaces/billing-profile",
|
|
86
|
+
headers: (0, acting_as_js_1.actingAs)(options.asUser),
|
|
87
|
+
});
|
|
88
|
+
return result.billingProfile;
|
|
89
|
+
}
|
|
57
90
|
/**
|
|
58
91
|
* The team of the workspace this session is on.
|
|
59
92
|
*
|
package/dist/esm/cli/doctor.js
CHANGED
|
@@ -81,14 +81,14 @@ function checkBridge(appDir, root) {
|
|
|
81
81
|
}
|
|
82
82
|
return { status: "pass", title: `Bridge route handler (${relative(root, bridge)})` };
|
|
83
83
|
}
|
|
84
|
-
function checkProvider(appDir, root) {
|
|
84
|
+
function checkProvider(appDir, root, env) {
|
|
85
85
|
const layouts = walk(appDir, ["layout.tsx", "layout.jsx"]);
|
|
86
86
|
const hosting = layouts.filter((path) => /<ConnectProvider/.test(read(path)));
|
|
87
87
|
if (hosting.length === 0) {
|
|
88
88
|
return {
|
|
89
89
|
status: "fail",
|
|
90
90
|
title: "ConnectProvider in a layout",
|
|
91
|
-
detail: 'Nothing renders <ConnectProvider>. Every client hook and component below it throws without one. In app/layout.tsx:\n import { ConnectProvider } from "@cosmoops/connect-sdk/client";\n <ConnectProvider>{children}</ConnectProvider>',
|
|
91
|
+
detail: 'Nothing renders <ConnectProvider>. Every client hook and component below it throws without one. In app/layout.tsx:\n import { connectProviderProps } from "@cosmoops/connect-sdk/next";\n import { ConnectProvider } from "@cosmoops/connect-sdk/client";\n <ConnectProvider {...connectProviderProps()}>{children}</ConnectProvider>',
|
|
92
92
|
};
|
|
93
93
|
}
|
|
94
94
|
// A nested provider re-fetches the session and keeps a second cache that the
|
|
@@ -101,8 +101,42 @@ function checkProvider(appDir, root) {
|
|
|
101
101
|
detail: `Found in ${hosting.length} layouts (${hosting.map((path) => relative(root, path)).join(", ")}). Nested providers keep separate caches, so a mutation in one is invisible to the other. Keep the outermost one only.`,
|
|
102
102
|
};
|
|
103
103
|
}
|
|
104
|
+
/*
|
|
105
|
+
* The split integration, and the reason this check exists at all.
|
|
106
|
+
*
|
|
107
|
+
* `CONNECT_APP_URL` points the *server* at a non-production Connect. It is
|
|
108
|
+
* not `NEXT_PUBLIC_`, so `ConnectProvider` cannot read it and defaults
|
|
109
|
+
* `appUrl` to production independently - and nothing fails. The API reads go
|
|
110
|
+
* to the configured instance while every destination the browser builds
|
|
111
|
+
* opens production: the user menu's Profile and Billing rows, the launcher's
|
|
112
|
+
* link for an unsubscribed application, the access gate's subscribe target,
|
|
113
|
+
* and checkout. Both applications built on this SDK shipped exactly that.
|
|
114
|
+
*
|
|
115
|
+
* `warn` rather than `fail`: the prop may be threaded from a parent Server
|
|
116
|
+
* Component into a `"use client"` shell, which is the documented shape for a
|
|
117
|
+
* shell needing `usePathname()` - and that assignment is not visible in the
|
|
118
|
+
* layout this reads. So this cannot tell from the outside, which is what
|
|
119
|
+
* `warn` means here.
|
|
120
|
+
*/
|
|
121
|
+
const pointedElsewhere = /^[ \t]*CONNECT_APP_URL[ \t]*=[ \t]*\S/m.test(env);
|
|
122
|
+
const carriesOrigin = /appUrl|connectProviderProps/.test(read(hosting[0]));
|
|
123
|
+
if (pointedElsewhere && !carriesOrigin) {
|
|
124
|
+
return {
|
|
125
|
+
status: "warn",
|
|
126
|
+
title: "ConnectProvider in a layout",
|
|
127
|
+
detail: `CONNECT_APP_URL is set, but ${relative(root, hosting[0])} passes no appUrl - so your server talks to that instance while every link the browser builds opens production Connect. Spread it in:\n <ConnectProvider {...connectProviderProps()}> // from "@cosmoops/connect-sdk/next"\n Already threading it into a "use client" shell? Then this is fine and you can ignore it.`,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
104
130
|
return { status: "pass", title: `ConnectProvider in a layout (${relative(root, hosting[0])})` };
|
|
105
131
|
}
|
|
132
|
+
/** The env files `checkEnv` reads, as one string, shared with `checkProvider`. */
|
|
133
|
+
function envText(root) {
|
|
134
|
+
return [".env.local", ".env.development.local", ".env"]
|
|
135
|
+
.map((name) => join(root, name))
|
|
136
|
+
.filter(existsSync)
|
|
137
|
+
.map(read)
|
|
138
|
+
.join("\n");
|
|
139
|
+
}
|
|
106
140
|
/**
|
|
107
141
|
* Order is the whole check.
|
|
108
142
|
*
|
|
@@ -143,8 +177,7 @@ function checkTheme(root) {
|
|
|
143
177
|
* loaded it - exactly the shell `next dev` will inherit.
|
|
144
178
|
*/
|
|
145
179
|
function checkEnv(root) {
|
|
146
|
-
const
|
|
147
|
-
const combined = files.filter(existsSync).map(read).join("\n");
|
|
180
|
+
const combined = envText(root);
|
|
148
181
|
return ["CONNECT_API_KEY", "CONNECT_CLIENT_ID"].map((name) => {
|
|
149
182
|
// Horizontal whitespace only. `\s*` also matches the newline, so a bare
|
|
150
183
|
// `CONNECT_API_KEY=` was satisfied by the *next* line's value and an empty
|
|
@@ -188,7 +221,7 @@ export function runDoctor(argv = []) {
|
|
|
188
221
|
console.log(`[connect-sdk] Checking ${relative(process.cwd(), root) || root}\n`);
|
|
189
222
|
const checks = [
|
|
190
223
|
checkBridge(appDir, root),
|
|
191
|
-
checkProvider(appDir, root),
|
|
224
|
+
checkProvider(appDir, root, envText(root)),
|
|
192
225
|
checkTheme(root),
|
|
193
226
|
...checkEnv(root),
|
|
194
227
|
checkRedirectUri(),
|
|
@@ -28,22 +28,51 @@ export const RAIL_GUTTER = 12;
|
|
|
28
28
|
* The row shape every sidebar control shares - nav items, the workspace tile,
|
|
29
29
|
* the launcher, the user button.
|
|
30
30
|
*
|
|
31
|
+
* **The height is not here.** It rides on `SIDEBAR_ROW_ICON` /
|
|
32
|
+
* `SIDEBAR_ROW_TILE` instead, because the two glyph sizes want different
|
|
33
|
+
* heights and only one of them is in the region that overflows - see those
|
|
34
|
+
* two constants.
|
|
35
|
+
*
|
|
31
36
|
* `overflow-hidden` + `whitespace-nowrap` is what makes the collapse smooth:
|
|
32
37
|
* the label is *clipped* by the shrinking rail rather than re-wrapped or
|
|
33
38
|
* re-measured, so a width animation costs no reflow. Fading the label with
|
|
34
39
|
* `SidebarLabel` on top of that hides the clip itself.
|
|
35
40
|
*/
|
|
36
|
-
export const SIDEBAR_ROW = "group/row relative flex
|
|
41
|
+
export const SIDEBAR_ROW = "group/row relative flex w-full shrink-0 items-center justify-start gap-3 overflow-hidden whitespace-nowrap rounded-lg pe-2 text-sm font-normal transition-colors focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-focus";
|
|
37
42
|
/**
|
|
38
|
-
* Leading inset for a row whose glyph is 20px wide (a nav icon,
|
|
43
|
+
* Leading inset *and height* for a row whose glyph is 20px wide (a nav icon,
|
|
44
|
+
* the launcher, a secondary-nav row).
|
|
45
|
+
*
|
|
39
46
|
* `10 + 10 = 20`, and `RAIL_GUTTER + 20 = 32`. See `RAIL_COLLAPSED`.
|
|
47
|
+
*
|
|
48
|
+
* **36px, not 40px, and the height belongs on this axis rather than on
|
|
49
|
+
* `SIDEBAR_ROW`.** The nav is the only region of the rail that grows with the
|
|
50
|
+
* host's route count, and `ConnectSidebarBody` is the only region that
|
|
51
|
+
* scrolls - so a rail that overflows overflows here. Ten destinations at 40px
|
|
52
|
+
* came to 436px of nav against 273px of fixed chrome (header, switcher, two
|
|
53
|
+
* footers), which needs a 709px viewport to sit still; a 1366x768 screen has
|
|
54
|
+
* about 678px of it. Four pixels a row is 48px back across the twelve rows a
|
|
55
|
+
* real integration draws, and 36px is still comfortably past the 24px
|
|
56
|
+
* WCAG 2.5.8 target minimum with the row's full width as the hit area.
|
|
57
|
+
*
|
|
58
|
+
* It is deliberately *not* a smaller glyph. The 20px box is a flex centring
|
|
59
|
+
* container for an icon the **host** supplies at its own size, so shrinking
|
|
60
|
+
* the box buys no vertical space and only pushes the icon off the 32px centre
|
|
61
|
+
* the collapsed rail is built on.
|
|
40
62
|
*/
|
|
41
|
-
export const SIDEBAR_ROW_ICON = "ps-[10px]";
|
|
63
|
+
export const SIDEBAR_ROW_ICON = "h-9 ps-[10px]";
|
|
42
64
|
/**
|
|
43
|
-
* Leading inset for a row whose glyph is 32px wide (the avatar, the
|
|
44
|
-
* tile). `4 + 16 = 20`, landing on the same 32px centre as
|
|
65
|
+
* Leading inset and height for a row whose glyph is 32px wide (the avatar, the
|
|
66
|
+
* workspace tile). `4 + 16 = 20`, landing on the same 32px centre as
|
|
67
|
+
* `SIDEBAR_ROW_ICON`.
|
|
68
|
+
*
|
|
69
|
+
* **These stay at 40px.** There are exactly two of them and neither scales
|
|
70
|
+
* with the host's nav, so they are not what overflows - and both need the
|
|
71
|
+
* height: a 32px tile in a 36px row has 2px of air, and the account row's
|
|
72
|
+
* label is two lines (name over email, `leading-tight`) which measures ~32px
|
|
73
|
+
* on its own.
|
|
45
74
|
*/
|
|
46
|
-
export const SIDEBAR_ROW_TILE = "ps-[4px]";
|
|
75
|
+
export const SIDEBAR_ROW_TILE = "h-10 ps-[4px]";
|
|
47
76
|
/**
|
|
48
77
|
* The class list for one sidebar row, in one place.
|
|
49
78
|
*
|
|
@@ -109,7 +109,12 @@ export function ConnectSidebarBody({ children }) {
|
|
|
109
109
|
// zoom away over a region of text is a WCAG 1.4.4 failure, and nothing
|
|
110
110
|
// here needs it suppressed. On the desktop rail, where no ancestor
|
|
111
111
|
// restricts anything, the pair is equivalent to the default.
|
|
112
|
-
|
|
112
|
+
// `py-1` rather than the `py-2` every other region carries: this is the
|
|
113
|
+
// one region that scrolls, so it is the first place to give up air when
|
|
114
|
+
// the nav is a row or two from overflowing. The rows paint their own
|
|
115
|
+
// rounded background, so the hairline above the footer is still clear of
|
|
116
|
+
// them. See `SIDEBAR_ROW_ICON` for the arithmetic this is part of.
|
|
117
|
+
_jsx("div", { className: "flex min-h-0 flex-1 touch-pan-y touch-pinch-zoom flex-col gap-1 overflow-y-auto overflow-x-hidden px-3 py-1", children: children }));
|
|
113
118
|
}
|
|
114
119
|
/**
|
|
115
120
|
* A pinned region at the foot of the rail - the secondary nav, the launcher,
|
package/dist/esm/next/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { cache } from "react";
|
|
2
|
-
import { ConnectClient } from "../server/client.js";
|
|
2
|
+
import { ConnectClient, DEFAULT_BASE_URL } from "../server/client.js";
|
|
3
3
|
import { createConnectHandler } from "../server/handler.js";
|
|
4
4
|
import { ConnectError } from "../errors.js";
|
|
5
5
|
/**
|
|
@@ -24,8 +24,9 @@ import { ConnectError } from "../errors.js";
|
|
|
24
24
|
*
|
|
25
25
|
* ```tsx
|
|
26
26
|
* // app/layout.tsx
|
|
27
|
+
* import { connectProviderProps } from "@cosmoops/connect-sdk/next";
|
|
27
28
|
* import { ConnectProvider } from "@cosmoops/connect-sdk/client";
|
|
28
|
-
* <ConnectProvider>{children}</ConnectProvider>
|
|
29
|
+
* <ConnectProvider {...connectProviderProps()}>{children}</ConnectProvider>
|
|
29
30
|
* ```
|
|
30
31
|
*
|
|
31
32
|
* No `next.config.ts` changes needed - configuration comes from the
|
|
@@ -42,6 +43,10 @@ import { ConnectError } from "../errors.js";
|
|
|
42
43
|
* `ConnectProvider` defaults `appUrl` to the production origin independently,
|
|
43
44
|
* so an app pointed at another instance must pass that prop as well or its
|
|
44
45
|
* portal links and its browser-built authorize URL still go to production.
|
|
46
|
+
* That is what the `connectProviderProps()` spread above is for, and why the
|
|
47
|
+
* layout is two imports rather than one - leave it off and the server half and
|
|
48
|
+
* the browser half of the same integration address different instances, with
|
|
49
|
+
* nothing failing to say so. See that function's own docs.
|
|
45
50
|
*
|
|
46
51
|
* Register `{your origin}/api/connect/callback` as a redirect URI in the
|
|
47
52
|
* application's Connect client section. That is the only step this cannot do
|
|
@@ -82,6 +87,55 @@ export function connectClient(overrides = {}) {
|
|
|
82
87
|
cached = client;
|
|
83
88
|
return client;
|
|
84
89
|
}
|
|
90
|
+
/**
|
|
91
|
+
* The `ConnectProvider` props that come from the environment.
|
|
92
|
+
*
|
|
93
|
+
* ```tsx
|
|
94
|
+
* // app/layout.tsx - a Server Component
|
|
95
|
+
* import { connectProviderProps } from "@cosmoops/connect-sdk/next";
|
|
96
|
+
* import { ConnectProvider } from "@cosmoops/connect-sdk/client";
|
|
97
|
+
*
|
|
98
|
+
* <ConnectProvider {...connectProviderProps()}>{children}</ConnectProvider>
|
|
99
|
+
* ```
|
|
100
|
+
*
|
|
101
|
+
* ## What it is for
|
|
102
|
+
*
|
|
103
|
+
* `CONNECT_APP_URL` points the *server* at a non-production Connect. It is not
|
|
104
|
+
* `NEXT_PUBLIC_`, so it never reaches the browser, and `ConnectProvider`
|
|
105
|
+
* defaults `appUrl` to the production origin independently - so pointing the
|
|
106
|
+
* server somewhere else and leaving the prop off splits the integration in
|
|
107
|
+
* half. The server talks to dev and every destination the browser builds opens
|
|
108
|
+
* production: the user menu's Profile and Billing rows, the app launcher's link
|
|
109
|
+
* for an unsubscribed application, `ConnectAccessGate`'s subscribe target, and
|
|
110
|
+
* checkout. Nothing errors. The customer is simply asked to buy a second
|
|
111
|
+
* subscription in an instance where they have none.
|
|
112
|
+
*
|
|
113
|
+
* Both apps built on this SDK shipped that bug before anyone noticed, from
|
|
114
|
+
* reading "the provider takes no props" as a rule rather than as a remark about
|
|
115
|
+
* `clientId`. A root layout is a Server Component, so one spread fixes it with
|
|
116
|
+
* no browser-visible copy of the variable.
|
|
117
|
+
*
|
|
118
|
+
* ## What it deliberately leaves out
|
|
119
|
+
*
|
|
120
|
+
* `clientId` and `basePath`. The first is only needed alongside an explicit
|
|
121
|
+
* `redirectUri`, when you build the authorize URL in the browser rather than
|
|
122
|
+
* letting `signIn()` bounce through your own `/signin` route - and handing the
|
|
123
|
+
* browser a value it does not need is surface for nothing. The second is where
|
|
124
|
+
* *you* mounted the handler, which the environment does not know. Pass either
|
|
125
|
+
* yourself; a prop you pass always wins over this spread, whichever order you
|
|
126
|
+
* write them in, as long as the spread comes first.
|
|
127
|
+
*
|
|
128
|
+
* ## If you already have an origin helper
|
|
129
|
+
*
|
|
130
|
+
* Use it instead. An app that builds its own links into Connect (a "Manage
|
|
131
|
+
* team" button, a pricing page's subscribe target) needs that origin on the
|
|
132
|
+
* server too, and one function owning the read is better than this helper
|
|
133
|
+
* beside it - two readers of one variable is the shape that caused this. This
|
|
134
|
+
* exists for the integration that has no such helper, which is most of them.
|
|
135
|
+
*/
|
|
136
|
+
export function connectProviderProps() {
|
|
137
|
+
return { appUrl: (process.env.CONNECT_APP_URL ?? DEFAULT_BASE_URL).replace(/\/+$/, "") };
|
|
138
|
+
}
|
|
85
139
|
/**
|
|
86
140
|
* An httpOnly cookie holding the opaque `sessionToken`.
|
|
87
141
|
*
|
|
@@ -267,6 +321,45 @@ async function readSession(request, options) {
|
|
|
267
321
|
const cachedAmbientSession = cache(async () => {
|
|
268
322
|
return readSession(await requestFromHeaders(), {});
|
|
269
323
|
});
|
|
324
|
+
/**
|
|
325
|
+
* The caller's opaque Connect **session token**, or `null` when signed out.
|
|
326
|
+
*
|
|
327
|
+
* ## What it is for
|
|
328
|
+
*
|
|
329
|
+
* Every `asUser` call on the server client needs one - `workspaces.*`,
|
|
330
|
+
* `scheduling.*`, the `/data` store's per-user form - and until this existed
|
|
331
|
+
* there was no way to obtain it inside a Server Component. `getConnectSession`
|
|
332
|
+
* answers *who* the caller is and deliberately does not hand back the
|
|
333
|
+
* credential, so a page that wanted to read its own workspace's roster had to
|
|
334
|
+
* either reimplement the cookie read or route the call through the browser.
|
|
335
|
+
*
|
|
336
|
+
* ```tsx
|
|
337
|
+
* // app/settings/page.tsx
|
|
338
|
+
* const token = await getConnectSessionToken();
|
|
339
|
+
* const members = token ? await connect.workspaces.members({ asUser: token }) : [];
|
|
340
|
+
* ```
|
|
341
|
+
*
|
|
342
|
+
* ## It is the cookie, unverified
|
|
343
|
+
*
|
|
344
|
+
* This reads the session store and nothing else - **no round trip, and no
|
|
345
|
+
* check that the token is still live.** A token from a revoked or expired
|
|
346
|
+
* session is a string that looks exactly like a good one, and Connect is what
|
|
347
|
+
* answers `401` for it. So this is not a sign-in check: use
|
|
348
|
+
* `getConnectSession()` for "is somebody here", and this for "hand Connect the
|
|
349
|
+
* credential they arrived with". A non-null answer means a cookie was present,
|
|
350
|
+
* not that it works.
|
|
351
|
+
*
|
|
352
|
+
* Reads `cookies()` transitively, so under `cacheComponents` it belongs inside
|
|
353
|
+
* a `<Suspense>` boundary like any other request-scoped read.
|
|
354
|
+
*
|
|
355
|
+
* Pass a `Request` to use it where `next/headers` does not exist, and a
|
|
356
|
+
* `session` store if you did not use the default cookie - the same two escape
|
|
357
|
+
* hatches `getConnectSession` takes, for the same reasons.
|
|
358
|
+
*/
|
|
359
|
+
export async function getConnectSessionToken(request, options = {}) {
|
|
360
|
+
const store = options.session ?? cookieSessionStore();
|
|
361
|
+
return (await store.read(request ?? (await requestFromHeaders()))) ?? null;
|
|
362
|
+
}
|
|
270
363
|
/**
|
|
271
364
|
* The signed-in user for a Server Component, Route Handler or Server Action,
|
|
272
365
|
* or `null`.
|
|
@@ -51,6 +51,39 @@ export class WorkspacesResource {
|
|
|
51
51
|
json: input,
|
|
52
52
|
});
|
|
53
53
|
}
|
|
54
|
+
/**
|
|
55
|
+
* The billing identity of the workspace this session is on - its legal
|
|
56
|
+
* name, GSTIN, registered address and entity form.
|
|
57
|
+
*
|
|
58
|
+
* **For prefilling your own settings, once.** See
|
|
59
|
+
* `WorkspaceBillingProfile`, which carries the whole argument: this is the
|
|
60
|
+
* *Bill To* side, yours is the *Bill From* side, and they are the same facts
|
|
61
|
+
* for most customers without being the same field. There is no write form.
|
|
62
|
+
*
|
|
63
|
+
* ```ts
|
|
64
|
+
* // app/settings/page.tsx - offer "Use my billing details"
|
|
65
|
+
* const token = await getConnectSessionToken();
|
|
66
|
+
* const billing = token ? await connect.workspaces.billingProfile({ asUser: token }) : null;
|
|
67
|
+
* ```
|
|
68
|
+
*
|
|
69
|
+
* **Needs two things, not one.** `workspaces:read` on your key, *and*
|
|
70
|
+
* `billing:read` on the signed-in member - a tax identity belongs to
|
|
71
|
+
* whoever can already see it in Connect's billing screens, so an ordinary
|
|
72
|
+
* member gets a 403 here where they get a roster from `members()`. That
|
|
73
|
+
* cannot be granted through a role on your application, deliberately.
|
|
74
|
+
*
|
|
75
|
+
* So **treat the 403 as a normal branch**, not an error: offer the prefill
|
|
76
|
+
* to whoever can use it and let everyone else type their own details. A
|
|
77
|
+
* workspace admin is the person who sets this up anyway.
|
|
78
|
+
*/
|
|
79
|
+
async billingProfile(options) {
|
|
80
|
+
const result = await request(this.transport, {
|
|
81
|
+
method: "GET",
|
|
82
|
+
path: "/workspaces/billing-profile",
|
|
83
|
+
headers: actingAs(options.asUser),
|
|
84
|
+
});
|
|
85
|
+
return result.billingProfile;
|
|
86
|
+
}
|
|
54
87
|
/**
|
|
55
88
|
* The team of the workspace this session is on.
|
|
56
89
|
*
|
|
@@ -25,22 +25,51 @@ export declare const RAIL_GUTTER = 12;
|
|
|
25
25
|
* The row shape every sidebar control shares - nav items, the workspace tile,
|
|
26
26
|
* the launcher, the user button.
|
|
27
27
|
*
|
|
28
|
+
* **The height is not here.** It rides on `SIDEBAR_ROW_ICON` /
|
|
29
|
+
* `SIDEBAR_ROW_TILE` instead, because the two glyph sizes want different
|
|
30
|
+
* heights and only one of them is in the region that overflows - see those
|
|
31
|
+
* two constants.
|
|
32
|
+
*
|
|
28
33
|
* `overflow-hidden` + `whitespace-nowrap` is what makes the collapse smooth:
|
|
29
34
|
* the label is *clipped* by the shrinking rail rather than re-wrapped or
|
|
30
35
|
* re-measured, so a width animation costs no reflow. Fading the label with
|
|
31
36
|
* `SidebarLabel` on top of that hides the clip itself.
|
|
32
37
|
*/
|
|
33
|
-
export declare const SIDEBAR_ROW = "group/row relative flex
|
|
38
|
+
export declare const SIDEBAR_ROW = "group/row relative flex w-full shrink-0 items-center justify-start gap-3 overflow-hidden whitespace-nowrap rounded-lg pe-2 text-sm font-normal transition-colors focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-focus";
|
|
34
39
|
/**
|
|
35
|
-
* Leading inset for a row whose glyph is 20px wide (a nav icon,
|
|
40
|
+
* Leading inset *and height* for a row whose glyph is 20px wide (a nav icon,
|
|
41
|
+
* the launcher, a secondary-nav row).
|
|
42
|
+
*
|
|
36
43
|
* `10 + 10 = 20`, and `RAIL_GUTTER + 20 = 32`. See `RAIL_COLLAPSED`.
|
|
44
|
+
*
|
|
45
|
+
* **36px, not 40px, and the height belongs on this axis rather than on
|
|
46
|
+
* `SIDEBAR_ROW`.** The nav is the only region of the rail that grows with the
|
|
47
|
+
* host's route count, and `ConnectSidebarBody` is the only region that
|
|
48
|
+
* scrolls - so a rail that overflows overflows here. Ten destinations at 40px
|
|
49
|
+
* came to 436px of nav against 273px of fixed chrome (header, switcher, two
|
|
50
|
+
* footers), which needs a 709px viewport to sit still; a 1366x768 screen has
|
|
51
|
+
* about 678px of it. Four pixels a row is 48px back across the twelve rows a
|
|
52
|
+
* real integration draws, and 36px is still comfortably past the 24px
|
|
53
|
+
* WCAG 2.5.8 target minimum with the row's full width as the hit area.
|
|
54
|
+
*
|
|
55
|
+
* It is deliberately *not* a smaller glyph. The 20px box is a flex centring
|
|
56
|
+
* container for an icon the **host** supplies at its own size, so shrinking
|
|
57
|
+
* the box buys no vertical space and only pushes the icon off the 32px centre
|
|
58
|
+
* the collapsed rail is built on.
|
|
37
59
|
*/
|
|
38
|
-
export declare const SIDEBAR_ROW_ICON = "ps-[10px]";
|
|
60
|
+
export declare const SIDEBAR_ROW_ICON = "h-9 ps-[10px]";
|
|
39
61
|
/**
|
|
40
|
-
* Leading inset for a row whose glyph is 32px wide (the avatar, the
|
|
41
|
-
* tile). `4 + 16 = 20`, landing on the same 32px centre as
|
|
62
|
+
* Leading inset and height for a row whose glyph is 32px wide (the avatar, the
|
|
63
|
+
* workspace tile). `4 + 16 = 20`, landing on the same 32px centre as
|
|
64
|
+
* `SIDEBAR_ROW_ICON`.
|
|
65
|
+
*
|
|
66
|
+
* **These stay at 40px.** There are exactly two of them and neither scales
|
|
67
|
+
* with the host's nav, so they are not what overflows - and both need the
|
|
68
|
+
* height: a 32px tile in a 36px row has 2px of air, and the account row's
|
|
69
|
+
* label is two lines (name over email, `leading-tight`) which measures ~32px
|
|
70
|
+
* on its own.
|
|
42
71
|
*/
|
|
43
|
-
export declare const SIDEBAR_ROW_TILE = "ps-[4px]";
|
|
72
|
+
export declare const SIDEBAR_ROW_TILE = "h-10 ps-[4px]";
|
|
44
73
|
/**
|
|
45
74
|
* The class list for one sidebar row, in one place.
|
|
46
75
|
*
|
|
@@ -57,7 +86,10 @@ export declare const SIDEBAR_ROW_TILE = "ps-[4px]";
|
|
|
57
86
|
* lets a `w-fit`, centred, `px-`-padded button take this shape at all.
|
|
58
87
|
*/
|
|
59
88
|
export declare function sidebarRowClass(options?: Readonly<{
|
|
60
|
-
/**
|
|
89
|
+
/**
|
|
90
|
+
* The glyph is 32px (an avatar, a workspace tile) rather than 20px - which
|
|
91
|
+
* also makes the row 40px rather than 36px. See `SIDEBAR_ROW_TILE`.
|
|
92
|
+
*/
|
|
61
93
|
tile?: boolean;
|
|
62
94
|
/** Paint the active treatment. */
|
|
63
95
|
active?: boolean;
|
|
@@ -73,13 +73,13 @@ export declare function ConnectSidebarFooter({ children, separated, }: Readonly<
|
|
|
73
73
|
separated?: boolean;
|
|
74
74
|
}>): import("react").JSX.Element;
|
|
75
75
|
export type ConnectSidebarRowProps = Readonly<{
|
|
76
|
-
/** The glyph. 20px by default; 32px when `tile`. */
|
|
76
|
+
/** The glyph. 20px in a 36px row by default; 32px in a 40px row when `tile`. */
|
|
77
77
|
icon: ReactNode;
|
|
78
78
|
/** The row's text, and - when it is a string - its accessible name. */
|
|
79
79
|
label: ReactNode;
|
|
80
80
|
/** Overrides the accessible name. Required when `label` is not a string. */
|
|
81
81
|
"aria-label"?: string;
|
|
82
|
-
/** The glyph is 32px (an avatar, a workspace tile) rather than 20px. */
|
|
82
|
+
/** The glyph is 32px (an avatar, a workspace tile) rather than 20px, in a 40px row. */
|
|
83
83
|
tile?: boolean;
|
|
84
84
|
/** Trailing content, e.g. a chevron. Clipped with the label while collapsed. */
|
|
85
85
|
trailing?: ReactNode;
|
|
@@ -9,6 +9,55 @@ import type { UserResponse } from "../types.js";
|
|
|
9
9
|
* are frequently absent.
|
|
10
10
|
*/
|
|
11
11
|
export declare function connectClient(overrides?: Partial<ConnectClientOptions>): ConnectClient;
|
|
12
|
+
/**
|
|
13
|
+
* The `ConnectProvider` props that come from the environment.
|
|
14
|
+
*
|
|
15
|
+
* ```tsx
|
|
16
|
+
* // app/layout.tsx - a Server Component
|
|
17
|
+
* import { connectProviderProps } from "@cosmoops/connect-sdk/next";
|
|
18
|
+
* import { ConnectProvider } from "@cosmoops/connect-sdk/client";
|
|
19
|
+
*
|
|
20
|
+
* <ConnectProvider {...connectProviderProps()}>{children}</ConnectProvider>
|
|
21
|
+
* ```
|
|
22
|
+
*
|
|
23
|
+
* ## What it is for
|
|
24
|
+
*
|
|
25
|
+
* `CONNECT_APP_URL` points the *server* at a non-production Connect. It is not
|
|
26
|
+
* `NEXT_PUBLIC_`, so it never reaches the browser, and `ConnectProvider`
|
|
27
|
+
* defaults `appUrl` to the production origin independently - so pointing the
|
|
28
|
+
* server somewhere else and leaving the prop off splits the integration in
|
|
29
|
+
* half. The server talks to dev and every destination the browser builds opens
|
|
30
|
+
* production: the user menu's Profile and Billing rows, the app launcher's link
|
|
31
|
+
* for an unsubscribed application, `ConnectAccessGate`'s subscribe target, and
|
|
32
|
+
* checkout. Nothing errors. The customer is simply asked to buy a second
|
|
33
|
+
* subscription in an instance where they have none.
|
|
34
|
+
*
|
|
35
|
+
* Both apps built on this SDK shipped that bug before anyone noticed, from
|
|
36
|
+
* reading "the provider takes no props" as a rule rather than as a remark about
|
|
37
|
+
* `clientId`. A root layout is a Server Component, so one spread fixes it with
|
|
38
|
+
* no browser-visible copy of the variable.
|
|
39
|
+
*
|
|
40
|
+
* ## What it deliberately leaves out
|
|
41
|
+
*
|
|
42
|
+
* `clientId` and `basePath`. The first is only needed alongside an explicit
|
|
43
|
+
* `redirectUri`, when you build the authorize URL in the browser rather than
|
|
44
|
+
* letting `signIn()` bounce through your own `/signin` route - and handing the
|
|
45
|
+
* browser a value it does not need is surface for nothing. The second is where
|
|
46
|
+
* *you* mounted the handler, which the environment does not know. Pass either
|
|
47
|
+
* yourself; a prop you pass always wins over this spread, whichever order you
|
|
48
|
+
* write them in, as long as the spread comes first.
|
|
49
|
+
*
|
|
50
|
+
* ## If you already have an origin helper
|
|
51
|
+
*
|
|
52
|
+
* Use it instead. An app that builds its own links into Connect (a "Manage
|
|
53
|
+
* team" button, a pricing page's subscribe target) needs that origin on the
|
|
54
|
+
* server too, and one function owning the read is better than this helper
|
|
55
|
+
* beside it - two readers of one variable is the shape that caused this. This
|
|
56
|
+
* exists for the integration that has no such helper, which is most of them.
|
|
57
|
+
*/
|
|
58
|
+
export declare function connectProviderProps(): {
|
|
59
|
+
appUrl: string;
|
|
60
|
+
};
|
|
12
61
|
/**
|
|
13
62
|
* An httpOnly cookie holding the opaque `sessionToken`.
|
|
14
63
|
*
|
|
@@ -71,6 +120,44 @@ declare function handle(request: Request): Promise<Response>;
|
|
|
71
120
|
* when you need to configure one of them.
|
|
72
121
|
*/
|
|
73
122
|
export { handle as GET, handle as POST };
|
|
123
|
+
/**
|
|
124
|
+
* The caller's opaque Connect **session token**, or `null` when signed out.
|
|
125
|
+
*
|
|
126
|
+
* ## What it is for
|
|
127
|
+
*
|
|
128
|
+
* Every `asUser` call on the server client needs one - `workspaces.*`,
|
|
129
|
+
* `scheduling.*`, the `/data` store's per-user form - and until this existed
|
|
130
|
+
* there was no way to obtain it inside a Server Component. `getConnectSession`
|
|
131
|
+
* answers *who* the caller is and deliberately does not hand back the
|
|
132
|
+
* credential, so a page that wanted to read its own workspace's roster had to
|
|
133
|
+
* either reimplement the cookie read or route the call through the browser.
|
|
134
|
+
*
|
|
135
|
+
* ```tsx
|
|
136
|
+
* // app/settings/page.tsx
|
|
137
|
+
* const token = await getConnectSessionToken();
|
|
138
|
+
* const members = token ? await connect.workspaces.members({ asUser: token }) : [];
|
|
139
|
+
* ```
|
|
140
|
+
*
|
|
141
|
+
* ## It is the cookie, unverified
|
|
142
|
+
*
|
|
143
|
+
* This reads the session store and nothing else - **no round trip, and no
|
|
144
|
+
* check that the token is still live.** A token from a revoked or expired
|
|
145
|
+
* session is a string that looks exactly like a good one, and Connect is what
|
|
146
|
+
* answers `401` for it. So this is not a sign-in check: use
|
|
147
|
+
* `getConnectSession()` for "is somebody here", and this for "hand Connect the
|
|
148
|
+
* credential they arrived with". A non-null answer means a cookie was present,
|
|
149
|
+
* not that it works.
|
|
150
|
+
*
|
|
151
|
+
* Reads `cookies()` transitively, so under `cacheComponents` it belongs inside
|
|
152
|
+
* a `<Suspense>` boundary like any other request-scoped read.
|
|
153
|
+
*
|
|
154
|
+
* Pass a `Request` to use it where `next/headers` does not exist, and a
|
|
155
|
+
* `session` store if you did not use the default cookie - the same two escape
|
|
156
|
+
* hatches `getConnectSession` takes, for the same reasons.
|
|
157
|
+
*/
|
|
158
|
+
export declare function getConnectSessionToken(request?: Request, options?: {
|
|
159
|
+
session?: ConnectSessionStore;
|
|
160
|
+
}): Promise<string | null>;
|
|
74
161
|
/**
|
|
75
162
|
* The signed-in user for a Server Component, Route Handler or Server Action,
|
|
76
163
|
* or `null`.
|
|
@@ -42,7 +42,12 @@ export interface RescheduleBookingInput extends ActingAsOption {
|
|
|
42
42
|
export interface ListBookingsInput extends ActingAsOption {
|
|
43
43
|
workspaceId: string;
|
|
44
44
|
status?: BookingStatus;
|
|
45
|
-
/**
|
|
45
|
+
/**
|
|
46
|
+
* A `ConnectWorkspaceMember.memberId` - only bookings that committed them.
|
|
47
|
+
*
|
|
48
|
+
* **Not a `userId`.** Pass one and the filter matches nothing, which comes
|
|
49
|
+
* back as an empty diary rather than an error.
|
|
50
|
+
*/
|
|
46
51
|
hostMemberId?: string;
|
|
47
52
|
from?: string | Date;
|
|
48
53
|
to?: string | Date;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type TransportOptions } from "../http.js";
|
|
2
|
-
import type { ConnectInvitation, ConnectMemberAccess, ConnectMemberAccessResult, ConnectWorkspaceMember, UserWorkspace, WorkspaceSwitchResult } from "../../types.js";
|
|
2
|
+
import type { ConnectInvitation, ConnectMemberAccess, ConnectMemberAccessResult, ConnectWorkspaceMember, UserWorkspace, WorkspaceBillingProfile, WorkspaceSwitchResult } from "../../types.js";
|
|
3
3
|
/**
|
|
4
4
|
* Which workspaces the signed-in user belongs to, moving between them, who
|
|
5
5
|
* else is on the team, and inviting or granting access to your application.
|
|
@@ -41,6 +41,34 @@ export declare class WorkspacesResource {
|
|
|
41
41
|
}, options: {
|
|
42
42
|
asUser: string;
|
|
43
43
|
}): Promise<WorkspaceSwitchResult>;
|
|
44
|
+
/**
|
|
45
|
+
* The billing identity of the workspace this session is on - its legal
|
|
46
|
+
* name, GSTIN, registered address and entity form.
|
|
47
|
+
*
|
|
48
|
+
* **For prefilling your own settings, once.** See
|
|
49
|
+
* `WorkspaceBillingProfile`, which carries the whole argument: this is the
|
|
50
|
+
* *Bill To* side, yours is the *Bill From* side, and they are the same facts
|
|
51
|
+
* for most customers without being the same field. There is no write form.
|
|
52
|
+
*
|
|
53
|
+
* ```ts
|
|
54
|
+
* // app/settings/page.tsx - offer "Use my billing details"
|
|
55
|
+
* const token = await getConnectSessionToken();
|
|
56
|
+
* const billing = token ? await connect.workspaces.billingProfile({ asUser: token }) : null;
|
|
57
|
+
* ```
|
|
58
|
+
*
|
|
59
|
+
* **Needs two things, not one.** `workspaces:read` on your key, *and*
|
|
60
|
+
* `billing:read` on the signed-in member - a tax identity belongs to
|
|
61
|
+
* whoever can already see it in Connect's billing screens, so an ordinary
|
|
62
|
+
* member gets a 403 here where they get a roster from `members()`. That
|
|
63
|
+
* cannot be granted through a role on your application, deliberately.
|
|
64
|
+
*
|
|
65
|
+
* So **treat the 403 as a normal branch**, not an error: offer the prefill
|
|
66
|
+
* to whoever can use it and let everyone else type their own details. A
|
|
67
|
+
* workspace admin is the person who sets this up anyway.
|
|
68
|
+
*/
|
|
69
|
+
billingProfile(options: {
|
|
70
|
+
asUser: string;
|
|
71
|
+
}): Promise<WorkspaceBillingProfile>;
|
|
44
72
|
/**
|
|
45
73
|
* The team of the workspace this session is on.
|
|
46
74
|
*
|
package/dist/types/types.d.ts
CHANGED
|
@@ -1741,6 +1741,59 @@ export interface UserWorkspace {
|
|
|
1741
1741
|
/** Whether it holds an entitling subscription to your application. */
|
|
1742
1742
|
subscribed: boolean;
|
|
1743
1743
|
}
|
|
1744
|
+
/**
|
|
1745
|
+
* A workspace's registered address, as its billing profile reports it.
|
|
1746
|
+
*
|
|
1747
|
+
* `stateCode` is the Indian state code, and it is the field that matters most
|
|
1748
|
+
* of the six: it decides CGST+SGST versus IGST on anything you raise against
|
|
1749
|
+
* this entity.
|
|
1750
|
+
*/
|
|
1751
|
+
export interface WorkspaceBillingAddress {
|
|
1752
|
+
line1: string | null;
|
|
1753
|
+
line2: string | null;
|
|
1754
|
+
city: string | null;
|
|
1755
|
+
stateCode: string | null;
|
|
1756
|
+
postalCode: string | null;
|
|
1757
|
+
countryCode: string;
|
|
1758
|
+
}
|
|
1759
|
+
/**
|
|
1760
|
+
* A workspace's **self-declared** billing identity - what the customer gave
|
|
1761
|
+
* CosmoOps so we can invoice them.
|
|
1762
|
+
*
|
|
1763
|
+
* ## Read it to prefill, not to share
|
|
1764
|
+
*
|
|
1765
|
+
* This is the *Bill To* side: who we invoice for the subscription. Your own
|
|
1766
|
+
* application's letterhead is the *Bill From* side. For a single-entity
|
|
1767
|
+
* customer they are the same three facts, which is the whole reason this is
|
|
1768
|
+
* readable - a GSTIN typed twice is a GSTIN that diverges, and the copy on a
|
|
1769
|
+
* quotation is the one that decides how the tax splits.
|
|
1770
|
+
*
|
|
1771
|
+
* They are **not the same field**, though. A group that bills us through a
|
|
1772
|
+
* holding company while quoting under a trading subsidiary has to stay
|
|
1773
|
+
* expressible, so copy this into your own settings on first setup and keep
|
|
1774
|
+
* your copy authoritative. There is deliberately no write form: an
|
|
1775
|
+
* application must not be able to edit the details its own invoices are
|
|
1776
|
+
* raised against.
|
|
1777
|
+
*
|
|
1778
|
+
* ## Nothing here is verified
|
|
1779
|
+
*
|
|
1780
|
+
* It is what the customer typed, not a KYB record - no PAN check stands
|
|
1781
|
+
* behind it. A convenience, never evidence of identity.
|
|
1782
|
+
*
|
|
1783
|
+
* Every field is nullable, because every one is optional to a customer who has
|
|
1784
|
+
* not needed an invoice yet. Fall back to the workspace name you already hold
|
|
1785
|
+
* on the session rather than printing an empty letterhead.
|
|
1786
|
+
*/
|
|
1787
|
+
export interface WorkspaceBillingProfile {
|
|
1788
|
+
/** The registered entity name, or `null` - most workspaces never set one. */
|
|
1789
|
+
legalName: string | null;
|
|
1790
|
+
/** GSTIN, 15 characters. `null` for an unregistered customer. */
|
|
1791
|
+
taxId: string | null;
|
|
1792
|
+
/** `null` as a whole when none was given - one check, not six. */
|
|
1793
|
+
billingAddress: WorkspaceBillingAddress | null;
|
|
1794
|
+
/** The legal entity form, self-declared. `null` for an individual workspace. */
|
|
1795
|
+
businessType: string | null;
|
|
1796
|
+
}
|
|
1744
1797
|
/**
|
|
1745
1798
|
* The result of a workspace switch.
|
|
1746
1799
|
*
|
|
@@ -1761,6 +1814,20 @@ export interface WorkspaceSwitchResult {
|
|
|
1761
1814
|
* catalog, not the member's Connect roles.
|
|
1762
1815
|
*/
|
|
1763
1816
|
export interface ConnectWorkspaceMember {
|
|
1817
|
+
/**
|
|
1818
|
+
* The id the **calendar** speaks in - `Booking.hostMemberIds` holds these,
|
|
1819
|
+
* and so does `listBookings`' `hostMemberId` filter.
|
|
1820
|
+
*
|
|
1821
|
+
* Carried beside `userId` because the two are different identifiers and
|
|
1822
|
+
* scheduling is the one surface that uses this one: joining a diary to a
|
|
1823
|
+
* roster, filtering it to one host, or putting a name against an
|
|
1824
|
+
* appointment all go through `memberId`. Everywhere else a member is a
|
|
1825
|
+
* `userId`.
|
|
1826
|
+
*
|
|
1827
|
+
* Opaque and workspace-scoped. Do not derive it from `userId` or address a
|
|
1828
|
+
* member by it elsewhere in this API.
|
|
1829
|
+
*/
|
|
1830
|
+
memberId: string;
|
|
1764
1831
|
userId: string;
|
|
1765
1832
|
email: string;
|
|
1766
1833
|
displayName: string | null;
|
|
@@ -1885,9 +1952,15 @@ export interface Booking {
|
|
|
1885
1952
|
/**
|
|
1886
1953
|
* The workspace members committed to this appointment.
|
|
1887
1954
|
*
|
|
1888
|
-
* These are `
|
|
1889
|
-
* returns and `listBookings`'
|
|
1890
|
-
*
|
|
1955
|
+
* These are `ConnectWorkspaceMember.memberId` values - what
|
|
1956
|
+
* `workspaces.members()` returns as `memberId` and what `listBookings`'
|
|
1957
|
+
* `hostMemberId` filter takes, so a host column is a lookup against a roster
|
|
1958
|
+
* you already have rather than another round trip.
|
|
1959
|
+
*
|
|
1960
|
+
* **Not `userId`.** An earlier version of this comment said these were the
|
|
1961
|
+
* ids `workspaces.listMembers` returns, which was wrong twice over: that
|
|
1962
|
+
* method is `workspaces.members()`, and until `memberId` was added to its
|
|
1963
|
+
* rows it returned no id these could be matched against at all.
|
|
1891
1964
|
*
|
|
1892
1965
|
* Resolved when the slot was held and frozen there: a round-robin event type
|
|
1893
1966
|
* picks one host, a collective one commits the whole group, and re-pointing
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cosmoops/connect-sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.36.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",
|
|
@@ -79,7 +79,7 @@
|
|
|
79
79
|
"clean": "rm -rf dist",
|
|
80
80
|
"build": "bun run clean && tsc -p tsconfig.cjs.json && tsc -p tsconfig.esm.json && tsc -p tsconfig.types.json && bun run scripts/finalize-dist.ts",
|
|
81
81
|
"verify:pack": "bun run scripts/verify-pack.ts",
|
|
82
|
-
"prepublishOnly": "bun run build && bun run verify:pack",
|
|
82
|
+
"prepublishOnly": "bun run build && bun run verify:pack && bun test",
|
|
83
83
|
"test": "bun test",
|
|
84
84
|
"docs:md": "typedoc --options typedoc.markdown.json",
|
|
85
85
|
"storybook": "storybook dev -p 6106",
|