@cosmoops/connect-sdk 0.1.0 → 0.3.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/README.md +78 -17
- package/dist/cjs/cli/doctor.js +209 -0
- package/dist/cjs/cli/init.js +22 -16
- package/dist/cjs/client/provider.js +1 -2
- package/dist/cjs/next/index.js +7 -51
- package/dist/cjs/server/client.js +1 -1
- package/dist/cjs/this-entry-is-server-only.js +31 -0
- package/dist/esm/cli/doctor.js +206 -0
- package/dist/esm/cli/init.js +22 -16
- package/dist/esm/client/provider.js +1 -2
- package/dist/esm/next/index.js +7 -50
- package/dist/esm/server/client.js +1 -1
- package/dist/esm/this-entry-is-server-only.js +30 -0
- package/dist/types/cli/doctor.d.ts +23 -0
- package/dist/types/cli/init.d.ts +2 -3
- package/dist/types/client/provider.d.ts +11 -5
- package/dist/types/next/index.d.ts +0 -34
- package/dist/types/server/client.d.ts +1 -1
- package/dist/types/server/index.d.ts +10 -2
- package/dist/types/this-entry-is-server-only.d.ts +1 -0
- package/package.json +13 -1
package/README.md
CHANGED
|
@@ -211,25 +211,15 @@ cookie, `/session`, `/entitlements`, `/plans`, `/addons`, `/apps`, `/sessions`,
|
|
|
211
211
|
| --- | --- | --- |
|
|
212
212
|
| `CONNECT_API_KEY` | yes | server-side credential |
|
|
213
213
|
| `CONNECT_CLIENT_ID` | yes | OAuth client id |
|
|
214
|
-
| `NEXT_PUBLIC_CONNECT_CLIENT_ID` | yes | same value, for `<ConnectProvider>` |
|
|
215
214
|
| `CONNECT_APP_URL` | no | non-production Connect instance |
|
|
216
215
|
| `CONNECT_SESSION_COOKIE` | no | session cookie name |
|
|
217
216
|
|
|
218
217
|
Register `{your origin}/api/connect/callback` as a redirect URI in the service's
|
|
219
218
|
Setup tab — the one step this cannot do for you.
|
|
220
219
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
```ts
|
|
225
|
-
import { withConnect } from "@cosmoops/connect-sdk/next";
|
|
226
|
-
export default withConnect({ });
|
|
227
|
-
```
|
|
228
|
-
|
|
229
|
-
That is all it does. It does **not** stand in for the route file — Next has no
|
|
230
|
-
plugin API that contributes a route from a package, so
|
|
231
|
-
`app/api/connect/[...connect]/route.ts` has to exist either way. The two are
|
|
232
|
-
independent; using both is just the default setup.
|
|
220
|
+
No `next.config.ts` changes needed, including `transpilePackages`: `dist/` is
|
|
221
|
+
already ES2022 with `"use client"` hoisted, so Turbopack and webpack both
|
|
222
|
+
consume it as-is.
|
|
233
223
|
|
|
234
224
|
Reading the session server-side — no argument, no `headers()` dance:
|
|
235
225
|
|
|
@@ -255,6 +245,68 @@ it above every boundary gives up its static shell.
|
|
|
255
245
|
You do **not** need `transpilePackages`. `dist/` is ES2022 with `"use client"`
|
|
256
246
|
already hoisted, so Turbopack and webpack consume it as-is.
|
|
257
247
|
|
|
248
|
+
## Route protection and `proxy.ts`
|
|
249
|
+
|
|
250
|
+
Nothing here is auto-wired, and nothing needs to be: this SDK ships no proxy and
|
|
251
|
+
never writes one. Next 16 renamed Middleware to **Proxy** — a single `proxy.ts`
|
|
252
|
+
at the project root (or in `src/`, beside `app/`).
|
|
253
|
+
|
|
254
|
+
You only need one if you want unauthenticated visitors bounced *before* a page
|
|
255
|
+
renders. If you are happy redirecting inside the page, `getConnectSession()`
|
|
256
|
+
above already does the job and there is nothing to add.
|
|
257
|
+
|
|
258
|
+
If you do add one, keep it to a cookie **presence** check:
|
|
259
|
+
|
|
260
|
+
```ts
|
|
261
|
+
// proxy.ts
|
|
262
|
+
import { NextResponse } from "next/server";
|
|
263
|
+
import type { NextRequest } from "next/server";
|
|
264
|
+
|
|
265
|
+
const COOKIE = process.env.CONNECT_SESSION_COOKIE ?? "connect_session";
|
|
266
|
+
|
|
267
|
+
export function proxy(request: NextRequest) {
|
|
268
|
+
if (request.cookies.has(COOKIE)) return NextResponse.next();
|
|
269
|
+
|
|
270
|
+
const signin = new URL("/api/connect/signin", request.url);
|
|
271
|
+
signin.searchParams.set("next", request.nextUrl.pathname);
|
|
272
|
+
return NextResponse.redirect(signin);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Must not match /api/connect/* — the sign-in route has no session cookie yet,
|
|
276
|
+
// so matching it would redirect the sign-in to itself.
|
|
277
|
+
export const config = { matcher: ["/dashboard/:path*", "/settings/:path*"] };
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
**A cookie is not a session, and this check is not authorization.** The cookie
|
|
281
|
+
holds an opaque Connect `sessionToken`; deciding whether it is still valid means
|
|
282
|
+
asking Connect, and proxy runs on every matched request — so a real check here
|
|
283
|
+
would put a network round trip in front of each one. Next's own guidance says
|
|
284
|
+
the same thing: proxy is for [optimistic
|
|
285
|
+
checks](https://nextjs.org/docs/app/guides/authentication#optimistic-checks-with-proxy-optional),
|
|
286
|
+
"not ... a full session management or authorization solution."
|
|
287
|
+
|
|
288
|
+
An expired or revoked token still arrives as a cookie and still passes the
|
|
289
|
+
snippet above. The authoritative check is `getConnectSession()` in the Server
|
|
290
|
+
Component, or `client.entitlements.*` on your server — treat the proxy purely as
|
|
291
|
+
a way to skip rendering a page the visitor cannot use.
|
|
292
|
+
|
|
293
|
+
### The server/client boundary
|
|
294
|
+
|
|
295
|
+
`@cosmoops/connect-sdk/server` and `/next` build a client around
|
|
296
|
+
`CONNECT_API_KEY`; `/client` holds no credential and calls your own origin. That
|
|
297
|
+
split is enforced, not just documented — both credential-holding entries resolve
|
|
298
|
+
to a stub in a browser bundle, so importing one from a client component fails the
|
|
299
|
+
build:
|
|
300
|
+
|
|
301
|
+
```
|
|
302
|
+
No matching export in ".../dist/esm/this-entry-is-server-only.js" for import "ConnectClient"
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
Import from `/client` instead, or use `import type`, which erases before any
|
|
306
|
+
bundler sees it. Server Components, Route Handlers, Server Actions and non-Next
|
|
307
|
+
backends are unaffected — the stub is wired to the `browser` export condition,
|
|
308
|
+
which only a client bundle resolves.
|
|
309
|
+
|
|
258
310
|
## Other frameworks
|
|
259
311
|
|
|
260
312
|
Mount the bridge once in your backend:
|
|
@@ -280,11 +332,14 @@ Then wrap your app:
|
|
|
280
332
|
```tsx
|
|
281
333
|
import { ConnectProvider } from "@cosmoops/connect-sdk/client";
|
|
282
334
|
|
|
283
|
-
<ConnectProvider
|
|
284
|
-
{children}
|
|
285
|
-
</ConnectProvider>
|
|
335
|
+
<ConnectProvider>{children}</ConnectProvider>
|
|
286
336
|
```
|
|
287
337
|
|
|
338
|
+
No `clientId` prop needed here either: the default sign-in flow bounces the
|
|
339
|
+
browser to your own `/signin` route, which builds the authorize URL
|
|
340
|
+
server-side. Pass `clientId` only if you also pass `redirectUri`, to build
|
|
341
|
+
that URL yourself instead.
|
|
342
|
+
|
|
288
343
|
### Components
|
|
289
344
|
|
|
290
345
|
```tsx
|
|
@@ -357,7 +412,13 @@ All three render `loading` — never the fallback — while the answer is unknow
|
|
|
357
412
|
npx @cosmoops/connect-sdk init
|
|
358
413
|
```
|
|
359
414
|
|
|
360
|
-
Writes `app/api/connect/[...connect]/route.ts`, `app/plans/page.tsx` and an `.env.local` template, then prints the
|
|
415
|
+
Writes `app/api/connect/[...connect]/route.ts`, `app/plans/page.tsx` and an `.env.local` template, then prints the steps it will not do for you (wrapping your layout in `<ConnectProvider>`, importing the theme, and registering the redirect URI in Connect). It never overwrites, so re-running is safe.
|
|
416
|
+
|
|
417
|
+
```bash
|
|
418
|
+
npx @cosmoops/connect-sdk doctor
|
|
419
|
+
```
|
|
420
|
+
|
|
421
|
+
Checks a wired-up project and names what is missing: the bridge route (wherever you mounted it) exporting both verbs, exactly one `<ConnectProvider>`, the theme imported *after* `@heroui/styles`, and both credentials actually set rather than declared empty. Exits non-zero on a real problem, so it works as a CI step. It is entirely offline — no credentials, no network — which is also its one limit: Connect exposes no endpoint listing a client's registered redirect URIs, so that step is reported as something only you can confirm.
|
|
361
422
|
|
|
362
423
|
Next has no plugin API that adds routes — `next.config` can rewrite and redirect, but both need a route to point at, and the App Router only discovers routes in your own `app/` directory. So this is a scaffolder rather than a plugin: `ConnectPlansPage` makes the file that must exist one line, and `init` writes that line.
|
|
363
424
|
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* `npx @cosmoops/connect-sdk doctor` - checks a wired-up project for the
|
|
4
|
+
* mistakes that `init` cannot prevent.
|
|
5
|
+
*
|
|
6
|
+
* `init` writes the two files it owns. Everything else in the setup is a step
|
|
7
|
+
* someone performs by hand in a file this package must not rewrite - the
|
|
8
|
+
* provider goes into a layout somebody else wrote, the theme import goes into
|
|
9
|
+
* their stylesheet in a specific position, and the redirect URI lives in
|
|
10
|
+
* Connect rather than in the repo. Those are where setup actually breaks, and
|
|
11
|
+
* each one fails in a way that does not name its cause: a missing provider
|
|
12
|
+
* throws from a hook, a mis-ordered theme import silently loses to HeroUI's
|
|
13
|
+
* defaults, and an unregistered redirect URI only shows up as an OAuth error
|
|
14
|
+
* after a round trip to Connect.
|
|
15
|
+
*
|
|
16
|
+
* Deliberately offline. Connect exposes no endpoint that lists a client's
|
|
17
|
+
* registered redirect URIs, so there is nothing to check remotely that would
|
|
18
|
+
* justify the cost - and an offline check needs no credentials, which is what
|
|
19
|
+
* lets this run in CI on a pull request.
|
|
20
|
+
*
|
|
21
|
+
* The exit code is the point: `fail` means the app is broken and returns 1,
|
|
22
|
+
* `warn` means this cannot tell from the outside and returns 0.
|
|
23
|
+
*/
|
|
24
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
25
|
+
exports.runDoctor = runDoctor;
|
|
26
|
+
const node_fs_1 = require("node:fs");
|
|
27
|
+
const node_path_1 = require("node:path");
|
|
28
|
+
/** Directories that are never a project's own source, and are large enough to matter. */
|
|
29
|
+
const SKIP = new Set(["node_modules", ".next", ".git", "dist", "build", ".turbo", "coverage"]);
|
|
30
|
+
function walk(dir, extensions, found = [], depth = 0) {
|
|
31
|
+
if (depth > 8 || !(0, node_fs_1.existsSync)(dir))
|
|
32
|
+
return found;
|
|
33
|
+
for (const entry of (0, node_fs_1.readdirSync)(dir)) {
|
|
34
|
+
if (SKIP.has(entry))
|
|
35
|
+
continue;
|
|
36
|
+
const path = (0, node_path_1.join)(dir, entry);
|
|
37
|
+
if ((0, node_fs_1.statSync)(path).isDirectory())
|
|
38
|
+
walk(path, extensions, found, depth + 1);
|
|
39
|
+
else if (extensions.some((extension) => entry.endsWith(extension)))
|
|
40
|
+
found.push(path);
|
|
41
|
+
}
|
|
42
|
+
return found;
|
|
43
|
+
}
|
|
44
|
+
function read(path) {
|
|
45
|
+
try {
|
|
46
|
+
return (0, node_fs_1.readFileSync)(path, "utf8");
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
return "";
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
function findAppDir(root) {
|
|
53
|
+
for (const candidate of [(0, node_path_1.join)(root, "app"), (0, node_path_1.join)(root, "src", "app")]) {
|
|
54
|
+
if ((0, node_fs_1.existsSync)(candidate))
|
|
55
|
+
return candidate;
|
|
56
|
+
}
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* The bridge is found by what it exports, not by where it sits.
|
|
61
|
+
*
|
|
62
|
+
* `createConnectHandlers({ basePath })` exists precisely so the route can be
|
|
63
|
+
* mounted somewhere other than `/api/connect`, so checking for that one path
|
|
64
|
+
* would report a false failure on every app that took the option.
|
|
65
|
+
*/
|
|
66
|
+
function checkBridge(appDir, root) {
|
|
67
|
+
const routes = walk(appDir, ["route.ts", "route.tsx", "route.js"]);
|
|
68
|
+
const bridge = routes.find((path) => /@cosmoops\/connect-sdk\/next/.test(read(path)));
|
|
69
|
+
if (!bridge) {
|
|
70
|
+
return {
|
|
71
|
+
status: "fail",
|
|
72
|
+
title: "Bridge route handler",
|
|
73
|
+
detail: 'No route re-exports the bridge. Create app/api/connect/[...connect]/route.ts containing:\n export { GET, POST } from "@cosmoops/connect-sdk/next";\n or run `npx @cosmoops/connect-sdk init`.',
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
const source = read(bridge);
|
|
77
|
+
const exportsBoth = /\bGET\b/.test(source) && /\bPOST\b/.test(source);
|
|
78
|
+
if (!exportsBoth) {
|
|
79
|
+
return {
|
|
80
|
+
status: "fail",
|
|
81
|
+
title: "Bridge route handler",
|
|
82
|
+
detail: `${(0, node_path_1.relative)(root, bridge)} exports only one verb. GET and POST are the same function - the bridge routes on method itself - and sign-out POSTs, so both must be exported.`,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
return { status: "pass", title: `Bridge route handler (${(0, node_path_1.relative)(root, bridge)})` };
|
|
86
|
+
}
|
|
87
|
+
function checkProvider(appDir, root) {
|
|
88
|
+
const layouts = walk(appDir, ["layout.tsx", "layout.jsx"]);
|
|
89
|
+
const hosting = layouts.filter((path) => /<ConnectProvider/.test(read(path)));
|
|
90
|
+
if (hosting.length === 0) {
|
|
91
|
+
return {
|
|
92
|
+
status: "fail",
|
|
93
|
+
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
|
+
};
|
|
96
|
+
}
|
|
97
|
+
// A nested provider re-fetches the session and keeps a second cache that the
|
|
98
|
+
// first one's mutations never invalidate, so a plan change updates one half
|
|
99
|
+
// of the page. Worth naming even though both layouts look correct alone.
|
|
100
|
+
if (hosting.length > 1) {
|
|
101
|
+
return {
|
|
102
|
+
status: "warn",
|
|
103
|
+
title: "ConnectProvider in a layout",
|
|
104
|
+
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
|
+
}
|
|
107
|
+
return { status: "pass", title: `ConnectProvider in a layout (${(0, node_path_1.relative)(root, hosting[0])})` };
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Order is the whole check.
|
|
111
|
+
*
|
|
112
|
+
* `theme.css` sets the HeroUI token values Connect's components read. Imported
|
|
113
|
+
* before `@heroui/styles`, HeroUI's own defaults are declared later, win on
|
|
114
|
+
* source order, and the theme silently does nothing - the app looks fine, just
|
|
115
|
+
* not themed. The import also has to live in the app's own stylesheet: from
|
|
116
|
+
* inside node_modules, Tailwind roots its content scan there and never scans
|
|
117
|
+
* the app.
|
|
118
|
+
*/
|
|
119
|
+
function checkTheme(root) {
|
|
120
|
+
const stylesheets = walk(root, [".css"]).filter((path) => !path.includes("node_modules"));
|
|
121
|
+
const importing = stylesheets.filter((path) => read(path).includes("@cosmoops/connect-sdk/theme.css"));
|
|
122
|
+
if (importing.length === 0) {
|
|
123
|
+
return {
|
|
124
|
+
status: "fail",
|
|
125
|
+
title: "Theme stylesheet imported",
|
|
126
|
+
detail: 'No stylesheet imports the Connect theme, so the components render with HeroUI defaults. Add it last of the three, in your own global CSS:\n @import "tailwindcss";\n @import "@heroui/styles";\n @import "@cosmoops/connect-sdk/theme.css";',
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
for (const path of importing) {
|
|
130
|
+
const source = read(path);
|
|
131
|
+
const connect = source.indexOf("@cosmoops/connect-sdk/theme.css");
|
|
132
|
+
const heroui = source.indexOf("@heroui/styles");
|
|
133
|
+
if (heroui !== -1 && heroui > connect) {
|
|
134
|
+
return {
|
|
135
|
+
status: "fail",
|
|
136
|
+
title: "Theme stylesheet imported",
|
|
137
|
+
detail: `${(0, node_path_1.relative)(root, path)} imports @heroui/styles *after* the Connect theme, so HeroUI's defaults win on source order and the theme has no effect. Put "@cosmoops/connect-sdk/theme.css" last.`,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return { status: "pass", title: `Theme stylesheet imported (${(0, node_path_1.relative)(root, importing[0])})` };
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* `.env.local` is read directly rather than trusting `process.env`, because
|
|
145
|
+
* the interesting case is a developer running this in a shell that never
|
|
146
|
+
* loaded it - exactly the shell `next dev` will inherit.
|
|
147
|
+
*/
|
|
148
|
+
function checkEnv(root) {
|
|
149
|
+
const files = [".env.local", ".env.development.local", ".env"].map((name) => (0, node_path_1.join)(root, name));
|
|
150
|
+
const combined = files.filter(node_fs_1.existsSync).map(read).join("\n");
|
|
151
|
+
return ["CONNECT_API_KEY", "CONNECT_CLIENT_ID"].map((name) => {
|
|
152
|
+
// Horizontal whitespace only. `\s*` also matches the newline, so a bare
|
|
153
|
+
// `CONNECT_API_KEY=` was satisfied by the *next* line's value and an empty
|
|
154
|
+
// credential reported as set - which is the exact failure this check exists
|
|
155
|
+
// to catch.
|
|
156
|
+
const assigned = new RegExp(`^[ \\t]*${name}[ \\t]*=[ \\t]*\\S`, "m").test(combined);
|
|
157
|
+
if (assigned || process.env[name]) {
|
|
158
|
+
return { status: "pass", title: `${name} set` };
|
|
159
|
+
}
|
|
160
|
+
const declaredEmpty = new RegExp(`^[ \\t]*${name}[ \\t]*=`, "m").test(combined);
|
|
161
|
+
return {
|
|
162
|
+
status: "fail",
|
|
163
|
+
title: `${name} set`,
|
|
164
|
+
detail: declaredEmpty
|
|
165
|
+
? `Declared but empty in .env.local. The value is in your service's Setup tab in Connect.`
|
|
166
|
+
: `Not found in .env.local or the environment. Both values are in your service's Setup tab in Connect.`,
|
|
167
|
+
};
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Nothing here can verify this - Connect has no endpoint listing a client's
|
|
172
|
+
* redirect URIs - but it is the single most common reason a correct setup
|
|
173
|
+
* still fails at sign-in, so it is worth a line that always prints.
|
|
174
|
+
*/
|
|
175
|
+
function checkRedirectUri() {
|
|
176
|
+
return {
|
|
177
|
+
status: "warn",
|
|
178
|
+
title: "Redirect URI registered in Connect",
|
|
179
|
+
detail: "Not checkable from here. Confirm {your origin}/api/connect/callback is listed as a redirect URI in your service's Setup tab - including the deployed origin, not just http://localhost:3000.",
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
const MARKER = { pass: " ok ", warn: " note ", fail: " FAIL " };
|
|
183
|
+
function runDoctor(argv = []) {
|
|
184
|
+
const root = (0, node_path_1.resolve)(argv[0] ?? process.cwd());
|
|
185
|
+
const appDir = findAppDir(root);
|
|
186
|
+
if (!appDir) {
|
|
187
|
+
console.error(`[connect-sdk] No App Router directory found in ${root} - looked for "app/" and "src/app/".\n` +
|
|
188
|
+
"Run this from your Next.js project root, or pass the path: npx @cosmoops/connect-sdk doctor ./my-app");
|
|
189
|
+
return 1;
|
|
190
|
+
}
|
|
191
|
+
console.log(`[connect-sdk] Checking ${(0, node_path_1.relative)(process.cwd(), root) || root}\n`);
|
|
192
|
+
const checks = [
|
|
193
|
+
checkBridge(appDir, root),
|
|
194
|
+
checkProvider(appDir, root),
|
|
195
|
+
checkTheme(root),
|
|
196
|
+
...checkEnv(root),
|
|
197
|
+
checkRedirectUri(),
|
|
198
|
+
];
|
|
199
|
+
for (const check of checks) {
|
|
200
|
+
console.log(` ${MARKER[check.status]} ${check.title}`);
|
|
201
|
+
if (check.detail)
|
|
202
|
+
console.log(` ${check.detail.replace(/\n/g, "\n ")}\n`);
|
|
203
|
+
}
|
|
204
|
+
const failed = checks.filter((check) => check.status === "fail").length;
|
|
205
|
+
console.log(failed === 0
|
|
206
|
+
? "\nNo problems found.\n"
|
|
207
|
+
: `\n${failed} problem${failed === 1 ? "" : "s"} to fix. Run \`npx @cosmoops/connect-sdk init\` to scaffold anything missing.\n`);
|
|
208
|
+
return failed === 0 ? 0 : 1;
|
|
209
|
+
}
|
package/dist/cjs/cli/init.js
CHANGED
|
@@ -27,6 +27,7 @@ exports.runInit = runInit;
|
|
|
27
27
|
exports.main = main;
|
|
28
28
|
const node_fs_1 = require("node:fs");
|
|
29
29
|
const node_path_1 = require("node:path");
|
|
30
|
+
const doctor_js_1 = require("./doctor.js");
|
|
30
31
|
const BRIDGE_ROUTE = `// The Connect bridge - the only place your API key is used.
|
|
31
32
|
//
|
|
32
33
|
// It answers your own origin, so the browser half of the SDK never holds a
|
|
@@ -46,8 +47,6 @@ export { ConnectPlansPage as default } from "@cosmoops/connect-sdk/client";
|
|
|
46
47
|
const ENV_TEMPLATE = `# CosmoOps Connect - values come from your service's Setup tab.
|
|
47
48
|
CONNECT_API_KEY=
|
|
48
49
|
CONNECT_CLIENT_ID=
|
|
49
|
-
# Same value as CONNECT_CLIENT_ID. Inlined at build time for <ConnectProvider>.
|
|
50
|
-
NEXT_PUBLIC_CONNECT_CLIENT_ID=
|
|
51
50
|
`;
|
|
52
51
|
const SCAFFOLDS = [
|
|
53
52
|
{
|
|
@@ -112,7 +111,7 @@ function runInit(argv = []) {
|
|
|
112
111
|
console.log(` ${marker} ${(0, node_path_1.relative)(root, target)} - ${scaffold.description}`);
|
|
113
112
|
}
|
|
114
113
|
const env = ensureEnv(root);
|
|
115
|
-
console.log(` ${env === "skipped" ? "exists " : env === "appended" ? "updated" : "created"} .env.local - fill in the
|
|
114
|
+
console.log(` ${env === "skipped" ? "exists " : env === "appended" ? "updated" : "created"} .env.local - fill in the two values from your service's Setup tab`);
|
|
116
115
|
// The two steps a scaffolder must not do for you: editing an existing layout
|
|
117
116
|
// means parsing and rewriting JSX someone else wrote, and the redirect URI
|
|
118
117
|
// lives in Connect, not in this repo.
|
|
@@ -125,7 +124,7 @@ Three things left, all one-time:
|
|
|
125
124
|
|
|
126
125
|
<ConnectProvider>{children}</ConnectProvider>
|
|
127
126
|
|
|
128
|
-
No props
|
|
127
|
+
No props needed.
|
|
129
128
|
|
|
130
129
|
2. Add the theme to your global stylesheet, last of the three - Tailwind
|
|
131
130
|
first, HeroUI's token layer next, so these values win:
|
|
@@ -140,26 +139,24 @@ Three things left, all one-time:
|
|
|
140
139
|
3. Register {your origin}/api/connect/callback as a redirect URI in your
|
|
141
140
|
service's Setup tab in Connect.
|
|
142
141
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
export default withConnect({ });
|
|
147
|
-
|
|
148
|
-
It checks NEXT_PUBLIC_CONNECT_CLIENT_ID at config load - the one variable whose
|
|
149
|
-
absence fails silently in the browser rather than loudly on the server.
|
|
142
|
+
No next.config.ts changes needed - not even transpilePackages: dist/ is
|
|
143
|
+
already ES2022 with "use client" hoisted, so Turbopack and webpack consume it
|
|
144
|
+
as-is.
|
|
150
145
|
|
|
151
146
|
Then <ConnectPlans />, <ConnectSubscriptionGate>, <ConnectFeatureGate> and the
|
|
152
147
|
rest work anywhere below the provider, and getConnectSession() reads the same
|
|
153
148
|
session from any Server Component.
|
|
149
|
+
|
|
150
|
+
Run \`npx @cosmoops/connect-sdk doctor\` when you have done those three - it
|
|
151
|
+
checks all of them and says which are still outstanding.
|
|
154
152
|
`);
|
|
155
153
|
return 0;
|
|
156
154
|
}
|
|
157
155
|
/**
|
|
158
|
-
*
|
|
159
|
-
* `check` later does not change how the existing one is invoked.
|
|
156
|
+
* `init` writes the wiring; `doctor` checks the parts `init` cannot write.
|
|
160
157
|
*
|
|
161
158
|
* A bare path runs `init` against it, so both `connect-sdk init ./my-app` and
|
|
162
|
-
* `connect-sdk ./my-app` work. A first argument that is neither
|
|
159
|
+
* `connect-sdk ./my-app` work. A first argument that is neither a command nor
|
|
163
160
|
* an existing directory is a typo, and saying so beats reporting that
|
|
164
161
|
* `./innit` contains no App Router - which is true, unhelpful, and points at
|
|
165
162
|
* the wrong mistake.
|
|
@@ -167,13 +164,22 @@ session from any Server Component.
|
|
|
167
164
|
function main(argv) {
|
|
168
165
|
const [first, ...rest] = argv;
|
|
169
166
|
if (first === "--help" || first === "-h") {
|
|
170
|
-
console.log(
|
|
167
|
+
console.log([
|
|
168
|
+
"Usage: connect-sdk <command> [project-dir]",
|
|
169
|
+
"",
|
|
170
|
+
" init scaffold the bridge route, /plans page and .env.local",
|
|
171
|
+
" doctor check an existing setup and report what is missing",
|
|
172
|
+
"",
|
|
173
|
+
"With no command, runs init against the given directory (default: cwd).",
|
|
174
|
+
].join("\n"));
|
|
171
175
|
return 0;
|
|
172
176
|
}
|
|
173
177
|
if (first === "init")
|
|
174
178
|
return runInit(rest);
|
|
179
|
+
if (first === "doctor" || first === "check")
|
|
180
|
+
return (0, doctor_js_1.runDoctor)(rest);
|
|
175
181
|
if (!first || (0, node_fs_1.existsSync)(first))
|
|
176
182
|
return runInit(argv);
|
|
177
|
-
console.error(`[connect-sdk] Unknown command "${first}". Try: connect-sdk init`);
|
|
183
|
+
console.error(`[connect-sdk] Unknown command "${first}". Try: connect-sdk init, or connect-sdk doctor`);
|
|
178
184
|
return 1;
|
|
179
185
|
}
|
|
@@ -24,13 +24,12 @@ const ConnectContext = (0, react_1.createContext)(null);
|
|
|
24
24
|
* origin, where `createConnectHandler` holds the credential.
|
|
25
25
|
*
|
|
26
26
|
* ```tsx
|
|
27
|
-
* // Next.js - clientId comes from NEXT_PUBLIC_CONNECT_CLIENT_ID
|
|
28
27
|
* <ConnectProvider>
|
|
29
28
|
* <App />
|
|
30
29
|
* </ConnectProvider>
|
|
31
30
|
* ```
|
|
32
31
|
*/
|
|
33
|
-
function ConnectProvider({ clientId =
|
|
32
|
+
function ConnectProvider({ clientId = "", basePath = "/api/connect", redirectUri, appUrl = exports.CONNECT_APP_URL, fetch: fetchImpl, children, }) {
|
|
34
33
|
// One state cell, not three. `isLoading` is *derived* from "no result yet"
|
|
35
34
|
// rather than set - a `setIsLoading(true)` at the top of an effect is a
|
|
36
35
|
// synchronous setState inside that effect, which cascades an extra render
|
package/dist/cjs/next/index.js
CHANGED
|
@@ -41,7 +41,6 @@ exports.createConnectHandlers = createConnectHandlers;
|
|
|
41
41
|
exports.GET = handle;
|
|
42
42
|
exports.POST = handle;
|
|
43
43
|
exports.getConnectSession = getConnectSession;
|
|
44
|
-
exports.withConnect = withConnect;
|
|
45
44
|
const client_js_1 = require("../server/client.js");
|
|
46
45
|
const handler_js_1 = require("../server/handler.js");
|
|
47
46
|
/**
|
|
@@ -66,27 +65,23 @@ const handler_js_1 = require("../server/handler.js");
|
|
|
66
65
|
* <ConnectProvider>{children}</ConnectProvider>
|
|
67
66
|
* ```
|
|
68
67
|
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
* ```ts
|
|
73
|
-
* import { withConnect } from "@cosmoops/connect-sdk/next";
|
|
74
|
-
* export default withConnect({});
|
|
75
|
-
* ```
|
|
76
|
-
*
|
|
77
|
-
* Configuration comes from the environment, so none of those files take
|
|
78
|
-
* arguments:
|
|
68
|
+
* No `next.config.ts` changes needed - configuration comes from the
|
|
69
|
+
* environment, so neither file takes arguments:
|
|
79
70
|
*
|
|
80
71
|
* | Variable | Required | Purpose |
|
|
81
72
|
* | --- | --- | --- |
|
|
82
73
|
* | `CONNECT_API_KEY` | yes | server-side credential |
|
|
83
74
|
* | `CONNECT_CLIENT_ID` | yes | OAuth client id |
|
|
84
|
-
* | `NEXT_PUBLIC_CONNECT_CLIENT_ID` | yes | same value, for `<ConnectProvider>` |
|
|
85
75
|
* | `CONNECT_APP_URL` | no | non-production Connect instance |
|
|
86
76
|
* | `CONNECT_SESSION_COOKIE` | no | session cookie name |
|
|
87
77
|
*
|
|
88
78
|
* Register `{your origin}/api/connect/callback` as a redirect URI in the
|
|
89
79
|
* service's Setup tab. That is the only step this cannot do for you.
|
|
80
|
+
*
|
|
81
|
+
* No `next.config.ts` changes needed either, including `transpilePackages`:
|
|
82
|
+
* `dist/` is already ES2022 with `"use client"` hoisted to the top of every
|
|
83
|
+
* browser file, so Turbopack and webpack both consume it as-is - adding the
|
|
84
|
+
* entry only costs build time.
|
|
90
85
|
*/
|
|
91
86
|
const DEFAULT_COOKIE = "connect_session";
|
|
92
87
|
function requireEnv(name) {
|
|
@@ -266,45 +261,6 @@ async function getConnectSession(request, options = {}) {
|
|
|
266
261
|
return null;
|
|
267
262
|
}
|
|
268
263
|
}
|
|
269
|
-
/**
|
|
270
|
-
* `next.config` wrapper.
|
|
271
|
-
*
|
|
272
|
-
* Asserts, at config load, that the public client id is present - the one
|
|
273
|
-
* variable whose absence fails silently in the browser instead of loudly on
|
|
274
|
-
* the server, because `NEXT_PUBLIC_*` is inlined at build time and an unset
|
|
275
|
-
* one simply becomes `undefined`. That is its whole job.
|
|
276
|
-
*
|
|
277
|
-
* In particular it does **not** replace the bridge route, and cannot: Next has
|
|
278
|
-
* no plugin API that contributes a route from a `node_modules` package - the
|
|
279
|
-
* App Router discovers routes by walking your own `app/` directory and nothing
|
|
280
|
-
* else, and a `next.config` rewrite still needs a real route to point at. So
|
|
281
|
-
* `app/api/connect/[...connect]/route.ts` has to exist whether or not you use
|
|
282
|
-
* this wrapper. It is one line, and `npx @cosmoops/connect-sdk init` writes it:
|
|
283
|
-
*
|
|
284
|
-
* ```ts
|
|
285
|
-
* export { GET, POST } from "@cosmoops/connect-sdk/next";
|
|
286
|
-
* ```
|
|
287
|
-
*
|
|
288
|
-
* The two are independent - the route works with a plain config, and this
|
|
289
|
-
* wrapper is useful with no route at all. Using both is just the default setup.
|
|
290
|
-
*
|
|
291
|
-
* It does **not** add this package to `transpilePackages`, and you should not
|
|
292
|
-
* either. `dist/` is already ES2022 with `"use client"` hoisted to the top of
|
|
293
|
-
* every browser file, so Turbopack and webpack both consume it as-is;
|
|
294
|
-
* transpiling it again only costs build time. An earlier version of this
|
|
295
|
-
* wrapper added the entry, which is why it may still be in your config.
|
|
296
|
-
*
|
|
297
|
-
* ```ts
|
|
298
|
-
* import { withConnect } from "@cosmoops/connect-sdk/next";
|
|
299
|
-
* export default withConnect({ });
|
|
300
|
-
* ```
|
|
301
|
-
*/
|
|
302
|
-
function withConnect(config = {}) {
|
|
303
|
-
if (!process.env.NEXT_PUBLIC_CONNECT_CLIENT_ID && process.env.NODE_ENV !== "test") {
|
|
304
|
-
console.warn("[@cosmoops/connect-sdk] NEXT_PUBLIC_CONNECT_CLIENT_ID is not set - <ConnectProvider> will not be able to start a sign-in.");
|
|
305
|
-
}
|
|
306
|
-
return config;
|
|
307
|
-
}
|
|
308
264
|
var handler_js_2 = require("../server/handler.js");
|
|
309
265
|
Object.defineProperty(exports, "createConnectHandler", { enumerable: true, get: function () { return handler_js_2.createConnectHandler; } });
|
|
310
266
|
var client_js_2 = require("../server/client.js");
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
/**
|
|
4
|
+
* The module a browser bundle gets if it reaches for `/server` or `/next`.
|
|
5
|
+
*
|
|
6
|
+
* Neither entry may run in a browser: both build a `ConnectClient` around
|
|
7
|
+
* `CONNECT_API_KEY`. Nothing enforced that before - the split was documented
|
|
8
|
+
* and unguarded, so a client component importing `@cosmoops/connect-sdk/server`
|
|
9
|
+
* compiled cleanly and failed later, somewhere else, for reasons that did not
|
|
10
|
+
* mention the import.
|
|
11
|
+
*
|
|
12
|
+
* The key itself does not leak: `CONNECT_API_KEY` has no `NEXT_PUBLIC_` prefix,
|
|
13
|
+
* so Next replaces the read with `undefined` in a client bundle rather than
|
|
14
|
+
* inlining the secret. What ships is the credential-handling code path and a
|
|
15
|
+
* client that authenticates with `undefined` - which 401s against Connect and
|
|
16
|
+
* looks like a server bug. This turns that into a build-time failure naming the
|
|
17
|
+
* entry to import instead.
|
|
18
|
+
*
|
|
19
|
+
* Wired through the `browser` export condition, which Next's client compiler
|
|
20
|
+
* resolves and its server, RSC and SSR passes do not - so the same import stays
|
|
21
|
+
* legal in a Route Handler, a Server Component and a non-Next backend.
|
|
22
|
+
*
|
|
23
|
+
* The filename is the error message. A bundler reaches this module while
|
|
24
|
+
* resolving a *named* import, so it reports "no matching export" and never
|
|
25
|
+
* evaluates the throw below - the path is the only part of that error carrying
|
|
26
|
+
* any meaning, so the path has to say what went wrong. The throw still covers
|
|
27
|
+
* side-effect, default and namespace imports, which do evaluate it.
|
|
28
|
+
*/
|
|
29
|
+
throw new Error('@cosmoops/connect-sdk: "@cosmoops/connect-sdk/server" and "@cosmoops/connect-sdk/next" hold your CONNECT_API_KEY and cannot be imported from browser code.\n' +
|
|
30
|
+
'Import "@cosmoops/connect-sdk/client" instead - its components and hooks call your own origin, and the bridge route reaches Connect for them.\n' +
|
|
31
|
+
'For types only, import from "@cosmoops/connect-sdk" (isomorphic) or use `import type`, which erases at compile time.');
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `npx @cosmoops/connect-sdk doctor` - checks a wired-up project for the
|
|
3
|
+
* mistakes that `init` cannot prevent.
|
|
4
|
+
*
|
|
5
|
+
* `init` writes the two files it owns. Everything else in the setup is a step
|
|
6
|
+
* someone performs by hand in a file this package must not rewrite - the
|
|
7
|
+
* provider goes into a layout somebody else wrote, the theme import goes into
|
|
8
|
+
* their stylesheet in a specific position, and the redirect URI lives in
|
|
9
|
+
* Connect rather than in the repo. Those are where setup actually breaks, and
|
|
10
|
+
* each one fails in a way that does not name its cause: a missing provider
|
|
11
|
+
* throws from a hook, a mis-ordered theme import silently loses to HeroUI's
|
|
12
|
+
* defaults, and an unregistered redirect URI only shows up as an OAuth error
|
|
13
|
+
* after a round trip to Connect.
|
|
14
|
+
*
|
|
15
|
+
* Deliberately offline. Connect exposes no endpoint that lists a client's
|
|
16
|
+
* registered redirect URIs, so there is nothing to check remotely that would
|
|
17
|
+
* justify the cost - and an offline check needs no credentials, which is what
|
|
18
|
+
* lets this run in CI on a pull request.
|
|
19
|
+
*
|
|
20
|
+
* The exit code is the point: `fail` means the app is broken and returns 1,
|
|
21
|
+
* `warn` means this cannot tell from the outside and returns 0.
|
|
22
|
+
*/
|
|
23
|
+
import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
|
|
24
|
+
import { join, relative, resolve } from "node:path";
|
|
25
|
+
/** Directories that are never a project's own source, and are large enough to matter. */
|
|
26
|
+
const SKIP = new Set(["node_modules", ".next", ".git", "dist", "build", ".turbo", "coverage"]);
|
|
27
|
+
function walk(dir, extensions, found = [], depth = 0) {
|
|
28
|
+
if (depth > 8 || !existsSync(dir))
|
|
29
|
+
return found;
|
|
30
|
+
for (const entry of readdirSync(dir)) {
|
|
31
|
+
if (SKIP.has(entry))
|
|
32
|
+
continue;
|
|
33
|
+
const path = join(dir, entry);
|
|
34
|
+
if (statSync(path).isDirectory())
|
|
35
|
+
walk(path, extensions, found, depth + 1);
|
|
36
|
+
else if (extensions.some((extension) => entry.endsWith(extension)))
|
|
37
|
+
found.push(path);
|
|
38
|
+
}
|
|
39
|
+
return found;
|
|
40
|
+
}
|
|
41
|
+
function read(path) {
|
|
42
|
+
try {
|
|
43
|
+
return readFileSync(path, "utf8");
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
return "";
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
function findAppDir(root) {
|
|
50
|
+
for (const candidate of [join(root, "app"), join(root, "src", "app")]) {
|
|
51
|
+
if (existsSync(candidate))
|
|
52
|
+
return candidate;
|
|
53
|
+
}
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* The bridge is found by what it exports, not by where it sits.
|
|
58
|
+
*
|
|
59
|
+
* `createConnectHandlers({ basePath })` exists precisely so the route can be
|
|
60
|
+
* mounted somewhere other than `/api/connect`, so checking for that one path
|
|
61
|
+
* would report a false failure on every app that took the option.
|
|
62
|
+
*/
|
|
63
|
+
function checkBridge(appDir, root) {
|
|
64
|
+
const routes = walk(appDir, ["route.ts", "route.tsx", "route.js"]);
|
|
65
|
+
const bridge = routes.find((path) => /@cosmoops\/connect-sdk\/next/.test(read(path)));
|
|
66
|
+
if (!bridge) {
|
|
67
|
+
return {
|
|
68
|
+
status: "fail",
|
|
69
|
+
title: "Bridge route handler",
|
|
70
|
+
detail: 'No route re-exports the bridge. Create app/api/connect/[...connect]/route.ts containing:\n export { GET, POST } from "@cosmoops/connect-sdk/next";\n or run `npx @cosmoops/connect-sdk init`.',
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
const source = read(bridge);
|
|
74
|
+
const exportsBoth = /\bGET\b/.test(source) && /\bPOST\b/.test(source);
|
|
75
|
+
if (!exportsBoth) {
|
|
76
|
+
return {
|
|
77
|
+
status: "fail",
|
|
78
|
+
title: "Bridge route handler",
|
|
79
|
+
detail: `${relative(root, bridge)} exports only one verb. GET and POST are the same function - the bridge routes on method itself - and sign-out POSTs, so both must be exported.`,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
return { status: "pass", title: `Bridge route handler (${relative(root, bridge)})` };
|
|
83
|
+
}
|
|
84
|
+
function checkProvider(appDir, root) {
|
|
85
|
+
const layouts = walk(appDir, ["layout.tsx", "layout.jsx"]);
|
|
86
|
+
const hosting = layouts.filter((path) => /<ConnectProvider/.test(read(path)));
|
|
87
|
+
if (hosting.length === 0) {
|
|
88
|
+
return {
|
|
89
|
+
status: "fail",
|
|
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>',
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
// A nested provider re-fetches the session and keeps a second cache that the
|
|
95
|
+
// first one's mutations never invalidate, so a plan change updates one half
|
|
96
|
+
// of the page. Worth naming even though both layouts look correct alone.
|
|
97
|
+
if (hosting.length > 1) {
|
|
98
|
+
return {
|
|
99
|
+
status: "warn",
|
|
100
|
+
title: "ConnectProvider in a layout",
|
|
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
|
+
};
|
|
103
|
+
}
|
|
104
|
+
return { status: "pass", title: `ConnectProvider in a layout (${relative(root, hosting[0])})` };
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Order is the whole check.
|
|
108
|
+
*
|
|
109
|
+
* `theme.css` sets the HeroUI token values Connect's components read. Imported
|
|
110
|
+
* before `@heroui/styles`, HeroUI's own defaults are declared later, win on
|
|
111
|
+
* source order, and the theme silently does nothing - the app looks fine, just
|
|
112
|
+
* not themed. The import also has to live in the app's own stylesheet: from
|
|
113
|
+
* inside node_modules, Tailwind roots its content scan there and never scans
|
|
114
|
+
* the app.
|
|
115
|
+
*/
|
|
116
|
+
function checkTheme(root) {
|
|
117
|
+
const stylesheets = walk(root, [".css"]).filter((path) => !path.includes("node_modules"));
|
|
118
|
+
const importing = stylesheets.filter((path) => read(path).includes("@cosmoops/connect-sdk/theme.css"));
|
|
119
|
+
if (importing.length === 0) {
|
|
120
|
+
return {
|
|
121
|
+
status: "fail",
|
|
122
|
+
title: "Theme stylesheet imported",
|
|
123
|
+
detail: 'No stylesheet imports the Connect theme, so the components render with HeroUI defaults. Add it last of the three, in your own global CSS:\n @import "tailwindcss";\n @import "@heroui/styles";\n @import "@cosmoops/connect-sdk/theme.css";',
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
for (const path of importing) {
|
|
127
|
+
const source = read(path);
|
|
128
|
+
const connect = source.indexOf("@cosmoops/connect-sdk/theme.css");
|
|
129
|
+
const heroui = source.indexOf("@heroui/styles");
|
|
130
|
+
if (heroui !== -1 && heroui > connect) {
|
|
131
|
+
return {
|
|
132
|
+
status: "fail",
|
|
133
|
+
title: "Theme stylesheet imported",
|
|
134
|
+
detail: `${relative(root, path)} imports @heroui/styles *after* the Connect theme, so HeroUI's defaults win on source order and the theme has no effect. Put "@cosmoops/connect-sdk/theme.css" last.`,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return { status: "pass", title: `Theme stylesheet imported (${relative(root, importing[0])})` };
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* `.env.local` is read directly rather than trusting `process.env`, because
|
|
142
|
+
* the interesting case is a developer running this in a shell that never
|
|
143
|
+
* loaded it - exactly the shell `next dev` will inherit.
|
|
144
|
+
*/
|
|
145
|
+
function checkEnv(root) {
|
|
146
|
+
const files = [".env.local", ".env.development.local", ".env"].map((name) => join(root, name));
|
|
147
|
+
const combined = files.filter(existsSync).map(read).join("\n");
|
|
148
|
+
return ["CONNECT_API_KEY", "CONNECT_CLIENT_ID"].map((name) => {
|
|
149
|
+
// Horizontal whitespace only. `\s*` also matches the newline, so a bare
|
|
150
|
+
// `CONNECT_API_KEY=` was satisfied by the *next* line's value and an empty
|
|
151
|
+
// credential reported as set - which is the exact failure this check exists
|
|
152
|
+
// to catch.
|
|
153
|
+
const assigned = new RegExp(`^[ \\t]*${name}[ \\t]*=[ \\t]*\\S`, "m").test(combined);
|
|
154
|
+
if (assigned || process.env[name]) {
|
|
155
|
+
return { status: "pass", title: `${name} set` };
|
|
156
|
+
}
|
|
157
|
+
const declaredEmpty = new RegExp(`^[ \\t]*${name}[ \\t]*=`, "m").test(combined);
|
|
158
|
+
return {
|
|
159
|
+
status: "fail",
|
|
160
|
+
title: `${name} set`,
|
|
161
|
+
detail: declaredEmpty
|
|
162
|
+
? `Declared but empty in .env.local. The value is in your service's Setup tab in Connect.`
|
|
163
|
+
: `Not found in .env.local or the environment. Both values are in your service's Setup tab in Connect.`,
|
|
164
|
+
};
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Nothing here can verify this - Connect has no endpoint listing a client's
|
|
169
|
+
* redirect URIs - but it is the single most common reason a correct setup
|
|
170
|
+
* still fails at sign-in, so it is worth a line that always prints.
|
|
171
|
+
*/
|
|
172
|
+
function checkRedirectUri() {
|
|
173
|
+
return {
|
|
174
|
+
status: "warn",
|
|
175
|
+
title: "Redirect URI registered in Connect",
|
|
176
|
+
detail: "Not checkable from here. Confirm {your origin}/api/connect/callback is listed as a redirect URI in your service's Setup tab - including the deployed origin, not just http://localhost:3000.",
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
const MARKER = { pass: " ok ", warn: " note ", fail: " FAIL " };
|
|
180
|
+
export function runDoctor(argv = []) {
|
|
181
|
+
const root = resolve(argv[0] ?? process.cwd());
|
|
182
|
+
const appDir = findAppDir(root);
|
|
183
|
+
if (!appDir) {
|
|
184
|
+
console.error(`[connect-sdk] No App Router directory found in ${root} - looked for "app/" and "src/app/".\n` +
|
|
185
|
+
"Run this from your Next.js project root, or pass the path: npx @cosmoops/connect-sdk doctor ./my-app");
|
|
186
|
+
return 1;
|
|
187
|
+
}
|
|
188
|
+
console.log(`[connect-sdk] Checking ${relative(process.cwd(), root) || root}\n`);
|
|
189
|
+
const checks = [
|
|
190
|
+
checkBridge(appDir, root),
|
|
191
|
+
checkProvider(appDir, root),
|
|
192
|
+
checkTheme(root),
|
|
193
|
+
...checkEnv(root),
|
|
194
|
+
checkRedirectUri(),
|
|
195
|
+
];
|
|
196
|
+
for (const check of checks) {
|
|
197
|
+
console.log(` ${MARKER[check.status]} ${check.title}`);
|
|
198
|
+
if (check.detail)
|
|
199
|
+
console.log(` ${check.detail.replace(/\n/g, "\n ")}\n`);
|
|
200
|
+
}
|
|
201
|
+
const failed = checks.filter((check) => check.status === "fail").length;
|
|
202
|
+
console.log(failed === 0
|
|
203
|
+
? "\nNo problems found.\n"
|
|
204
|
+
: `\n${failed} problem${failed === 1 ? "" : "s"} to fix. Run \`npx @cosmoops/connect-sdk init\` to scaffold anything missing.\n`);
|
|
205
|
+
return failed === 0 ? 0 : 1;
|
|
206
|
+
}
|
package/dist/esm/cli/init.js
CHANGED
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
*/
|
|
23
23
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
24
24
|
import { dirname, join, relative, resolve } from "node:path";
|
|
25
|
+
import { runDoctor } from "./doctor.js";
|
|
25
26
|
const BRIDGE_ROUTE = `// The Connect bridge - the only place your API key is used.
|
|
26
27
|
//
|
|
27
28
|
// It answers your own origin, so the browser half of the SDK never holds a
|
|
@@ -41,8 +42,6 @@ export { ConnectPlansPage as default } from "@cosmoops/connect-sdk/client";
|
|
|
41
42
|
const ENV_TEMPLATE = `# CosmoOps Connect - values come from your service's Setup tab.
|
|
42
43
|
CONNECT_API_KEY=
|
|
43
44
|
CONNECT_CLIENT_ID=
|
|
44
|
-
# Same value as CONNECT_CLIENT_ID. Inlined at build time for <ConnectProvider>.
|
|
45
|
-
NEXT_PUBLIC_CONNECT_CLIENT_ID=
|
|
46
45
|
`;
|
|
47
46
|
const SCAFFOLDS = [
|
|
48
47
|
{
|
|
@@ -107,7 +106,7 @@ export function runInit(argv = []) {
|
|
|
107
106
|
console.log(` ${marker} ${relative(root, target)} - ${scaffold.description}`);
|
|
108
107
|
}
|
|
109
108
|
const env = ensureEnv(root);
|
|
110
|
-
console.log(` ${env === "skipped" ? "exists " : env === "appended" ? "updated" : "created"} .env.local - fill in the
|
|
109
|
+
console.log(` ${env === "skipped" ? "exists " : env === "appended" ? "updated" : "created"} .env.local - fill in the two values from your service's Setup tab`);
|
|
111
110
|
// The two steps a scaffolder must not do for you: editing an existing layout
|
|
112
111
|
// means parsing and rewriting JSX someone else wrote, and the redirect URI
|
|
113
112
|
// lives in Connect, not in this repo.
|
|
@@ -120,7 +119,7 @@ Three things left, all one-time:
|
|
|
120
119
|
|
|
121
120
|
<ConnectProvider>{children}</ConnectProvider>
|
|
122
121
|
|
|
123
|
-
No props
|
|
122
|
+
No props needed.
|
|
124
123
|
|
|
125
124
|
2. Add the theme to your global stylesheet, last of the three - Tailwind
|
|
126
125
|
first, HeroUI's token layer next, so these values win:
|
|
@@ -135,26 +134,24 @@ Three things left, all one-time:
|
|
|
135
134
|
3. Register {your origin}/api/connect/callback as a redirect URI in your
|
|
136
135
|
service's Setup tab in Connect.
|
|
137
136
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
export default withConnect({ });
|
|
142
|
-
|
|
143
|
-
It checks NEXT_PUBLIC_CONNECT_CLIENT_ID at config load - the one variable whose
|
|
144
|
-
absence fails silently in the browser rather than loudly on the server.
|
|
137
|
+
No next.config.ts changes needed - not even transpilePackages: dist/ is
|
|
138
|
+
already ES2022 with "use client" hoisted, so Turbopack and webpack consume it
|
|
139
|
+
as-is.
|
|
145
140
|
|
|
146
141
|
Then <ConnectPlans />, <ConnectSubscriptionGate>, <ConnectFeatureGate> and the
|
|
147
142
|
rest work anywhere below the provider, and getConnectSession() reads the same
|
|
148
143
|
session from any Server Component.
|
|
144
|
+
|
|
145
|
+
Run \`npx @cosmoops/connect-sdk doctor\` when you have done those three - it
|
|
146
|
+
checks all of them and says which are still outstanding.
|
|
149
147
|
`);
|
|
150
148
|
return 0;
|
|
151
149
|
}
|
|
152
150
|
/**
|
|
153
|
-
*
|
|
154
|
-
* `check` later does not change how the existing one is invoked.
|
|
151
|
+
* `init` writes the wiring; `doctor` checks the parts `init` cannot write.
|
|
155
152
|
*
|
|
156
153
|
* A bare path runs `init` against it, so both `connect-sdk init ./my-app` and
|
|
157
|
-
* `connect-sdk ./my-app` work. A first argument that is neither
|
|
154
|
+
* `connect-sdk ./my-app` work. A first argument that is neither a command nor
|
|
158
155
|
* an existing directory is a typo, and saying so beats reporting that
|
|
159
156
|
* `./innit` contains no App Router - which is true, unhelpful, and points at
|
|
160
157
|
* the wrong mistake.
|
|
@@ -162,13 +159,22 @@ session from any Server Component.
|
|
|
162
159
|
export function main(argv) {
|
|
163
160
|
const [first, ...rest] = argv;
|
|
164
161
|
if (first === "--help" || first === "-h") {
|
|
165
|
-
console.log(
|
|
162
|
+
console.log([
|
|
163
|
+
"Usage: connect-sdk <command> [project-dir]",
|
|
164
|
+
"",
|
|
165
|
+
" init scaffold the bridge route, /plans page and .env.local",
|
|
166
|
+
" doctor check an existing setup and report what is missing",
|
|
167
|
+
"",
|
|
168
|
+
"With no command, runs init against the given directory (default: cwd).",
|
|
169
|
+
].join("\n"));
|
|
166
170
|
return 0;
|
|
167
171
|
}
|
|
168
172
|
if (first === "init")
|
|
169
173
|
return runInit(rest);
|
|
174
|
+
if (first === "doctor" || first === "check")
|
|
175
|
+
return runDoctor(rest);
|
|
170
176
|
if (!first || existsSync(first))
|
|
171
177
|
return runInit(argv);
|
|
172
|
-
console.error(`[connect-sdk] Unknown command "${first}". Try: connect-sdk init`);
|
|
178
|
+
console.error(`[connect-sdk] Unknown command "${first}". Try: connect-sdk init, or connect-sdk doctor`);
|
|
173
179
|
return 1;
|
|
174
180
|
}
|
|
@@ -12,13 +12,12 @@ const ConnectContext = createContext(null);
|
|
|
12
12
|
* origin, where `createConnectHandler` holds the credential.
|
|
13
13
|
*
|
|
14
14
|
* ```tsx
|
|
15
|
-
* // Next.js - clientId comes from NEXT_PUBLIC_CONNECT_CLIENT_ID
|
|
16
15
|
* <ConnectProvider>
|
|
17
16
|
* <App />
|
|
18
17
|
* </ConnectProvider>
|
|
19
18
|
* ```
|
|
20
19
|
*/
|
|
21
|
-
export function ConnectProvider({ clientId =
|
|
20
|
+
export function ConnectProvider({ clientId = "", basePath = "/api/connect", redirectUri, appUrl = CONNECT_APP_URL, fetch: fetchImpl, children, }) {
|
|
22
21
|
// One state cell, not three. `isLoading` is *derived* from "no result yet"
|
|
23
22
|
// rather than set - a `setIsLoading(true)` at the top of an effect is a
|
|
24
23
|
// synchronous setState inside that effect, which cascades an extra render
|
package/dist/esm/next/index.js
CHANGED
|
@@ -22,27 +22,23 @@ import { createConnectHandler } from "../server/handler.js";
|
|
|
22
22
|
* <ConnectProvider>{children}</ConnectProvider>
|
|
23
23
|
* ```
|
|
24
24
|
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
* ```ts
|
|
29
|
-
* import { withConnect } from "@cosmoops/connect-sdk/next";
|
|
30
|
-
* export default withConnect({});
|
|
31
|
-
* ```
|
|
32
|
-
*
|
|
33
|
-
* Configuration comes from the environment, so none of those files take
|
|
34
|
-
* arguments:
|
|
25
|
+
* No `next.config.ts` changes needed - configuration comes from the
|
|
26
|
+
* environment, so neither file takes arguments:
|
|
35
27
|
*
|
|
36
28
|
* | Variable | Required | Purpose |
|
|
37
29
|
* | --- | --- | --- |
|
|
38
30
|
* | `CONNECT_API_KEY` | yes | server-side credential |
|
|
39
31
|
* | `CONNECT_CLIENT_ID` | yes | OAuth client id |
|
|
40
|
-
* | `NEXT_PUBLIC_CONNECT_CLIENT_ID` | yes | same value, for `<ConnectProvider>` |
|
|
41
32
|
* | `CONNECT_APP_URL` | no | non-production Connect instance |
|
|
42
33
|
* | `CONNECT_SESSION_COOKIE` | no | session cookie name |
|
|
43
34
|
*
|
|
44
35
|
* Register `{your origin}/api/connect/callback` as a redirect URI in the
|
|
45
36
|
* service's Setup tab. That is the only step this cannot do for you.
|
|
37
|
+
*
|
|
38
|
+
* No `next.config.ts` changes needed either, including `transpilePackages`:
|
|
39
|
+
* `dist/` is already ES2022 with `"use client"` hoisted to the top of every
|
|
40
|
+
* browser file, so Turbopack and webpack both consume it as-is - adding the
|
|
41
|
+
* entry only costs build time.
|
|
46
42
|
*/
|
|
47
43
|
const DEFAULT_COOKIE = "connect_session";
|
|
48
44
|
function requireEnv(name) {
|
|
@@ -236,44 +232,5 @@ export async function getConnectSession(request, options = {}) {
|
|
|
236
232
|
return null;
|
|
237
233
|
}
|
|
238
234
|
}
|
|
239
|
-
/**
|
|
240
|
-
* `next.config` wrapper.
|
|
241
|
-
*
|
|
242
|
-
* Asserts, at config load, that the public client id is present - the one
|
|
243
|
-
* variable whose absence fails silently in the browser instead of loudly on
|
|
244
|
-
* the server, because `NEXT_PUBLIC_*` is inlined at build time and an unset
|
|
245
|
-
* one simply becomes `undefined`. That is its whole job.
|
|
246
|
-
*
|
|
247
|
-
* In particular it does **not** replace the bridge route, and cannot: Next has
|
|
248
|
-
* no plugin API that contributes a route from a `node_modules` package - the
|
|
249
|
-
* App Router discovers routes by walking your own `app/` directory and nothing
|
|
250
|
-
* else, and a `next.config` rewrite still needs a real route to point at. So
|
|
251
|
-
* `app/api/connect/[...connect]/route.ts` has to exist whether or not you use
|
|
252
|
-
* this wrapper. It is one line, and `npx @cosmoops/connect-sdk init` writes it:
|
|
253
|
-
*
|
|
254
|
-
* ```ts
|
|
255
|
-
* export { GET, POST } from "@cosmoops/connect-sdk/next";
|
|
256
|
-
* ```
|
|
257
|
-
*
|
|
258
|
-
* The two are independent - the route works with a plain config, and this
|
|
259
|
-
* wrapper is useful with no route at all. Using both is just the default setup.
|
|
260
|
-
*
|
|
261
|
-
* It does **not** add this package to `transpilePackages`, and you should not
|
|
262
|
-
* either. `dist/` is already ES2022 with `"use client"` hoisted to the top of
|
|
263
|
-
* every browser file, so Turbopack and webpack both consume it as-is;
|
|
264
|
-
* transpiling it again only costs build time. An earlier version of this
|
|
265
|
-
* wrapper added the entry, which is why it may still be in your config.
|
|
266
|
-
*
|
|
267
|
-
* ```ts
|
|
268
|
-
* import { withConnect } from "@cosmoops/connect-sdk/next";
|
|
269
|
-
* export default withConnect({ });
|
|
270
|
-
* ```
|
|
271
|
-
*/
|
|
272
|
-
export function withConnect(config = {}) {
|
|
273
|
-
if (!process.env.NEXT_PUBLIC_CONNECT_CLIENT_ID && process.env.NODE_ENV !== "test") {
|
|
274
|
-
console.warn("[@cosmoops/connect-sdk] NEXT_PUBLIC_CONNECT_CLIENT_ID is not set - <ConnectProvider> will not be able to start a sign-in.");
|
|
275
|
-
}
|
|
276
|
-
return config;
|
|
277
|
-
}
|
|
278
235
|
export { createConnectHandler } from "../server/handler.js";
|
|
279
236
|
export { ConnectClient } from "../server/client.js";
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The module a browser bundle gets if it reaches for `/server` or `/next`.
|
|
3
|
+
*
|
|
4
|
+
* Neither entry may run in a browser: both build a `ConnectClient` around
|
|
5
|
+
* `CONNECT_API_KEY`. Nothing enforced that before - the split was documented
|
|
6
|
+
* and unguarded, so a client component importing `@cosmoops/connect-sdk/server`
|
|
7
|
+
* compiled cleanly and failed later, somewhere else, for reasons that did not
|
|
8
|
+
* mention the import.
|
|
9
|
+
*
|
|
10
|
+
* The key itself does not leak: `CONNECT_API_KEY` has no `NEXT_PUBLIC_` prefix,
|
|
11
|
+
* so Next replaces the read with `undefined` in a client bundle rather than
|
|
12
|
+
* inlining the secret. What ships is the credential-handling code path and a
|
|
13
|
+
* client that authenticates with `undefined` - which 401s against Connect and
|
|
14
|
+
* looks like a server bug. This turns that into a build-time failure naming the
|
|
15
|
+
* entry to import instead.
|
|
16
|
+
*
|
|
17
|
+
* Wired through the `browser` export condition, which Next's client compiler
|
|
18
|
+
* resolves and its server, RSC and SSR passes do not - so the same import stays
|
|
19
|
+
* legal in a Route Handler, a Server Component and a non-Next backend.
|
|
20
|
+
*
|
|
21
|
+
* The filename is the error message. A bundler reaches this module while
|
|
22
|
+
* resolving a *named* import, so it reports "no matching export" and never
|
|
23
|
+
* evaluates the throw below - the path is the only part of that error carrying
|
|
24
|
+
* any meaning, so the path has to say what went wrong. The throw still covers
|
|
25
|
+
* side-effect, default and namespace imports, which do evaluate it.
|
|
26
|
+
*/
|
|
27
|
+
throw new Error('@cosmoops/connect-sdk: "@cosmoops/connect-sdk/server" and "@cosmoops/connect-sdk/next" hold your CONNECT_API_KEY and cannot be imported from browser code.\n' +
|
|
28
|
+
'Import "@cosmoops/connect-sdk/client" instead - its components and hooks call your own origin, and the bridge route reaches Connect for them.\n' +
|
|
29
|
+
'For types only, import from "@cosmoops/connect-sdk" (isomorphic) or use `import type`, which erases at compile time.');
|
|
30
|
+
export {};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `npx @cosmoops/connect-sdk doctor` - checks a wired-up project for the
|
|
3
|
+
* mistakes that `init` cannot prevent.
|
|
4
|
+
*
|
|
5
|
+
* `init` writes the two files it owns. Everything else in the setup is a step
|
|
6
|
+
* someone performs by hand in a file this package must not rewrite - the
|
|
7
|
+
* provider goes into a layout somebody else wrote, the theme import goes into
|
|
8
|
+
* their stylesheet in a specific position, and the redirect URI lives in
|
|
9
|
+
* Connect rather than in the repo. Those are where setup actually breaks, and
|
|
10
|
+
* each one fails in a way that does not name its cause: a missing provider
|
|
11
|
+
* throws from a hook, a mis-ordered theme import silently loses to HeroUI's
|
|
12
|
+
* defaults, and an unregistered redirect URI only shows up as an OAuth error
|
|
13
|
+
* after a round trip to Connect.
|
|
14
|
+
*
|
|
15
|
+
* Deliberately offline. Connect exposes no endpoint that lists a client's
|
|
16
|
+
* registered redirect URIs, so there is nothing to check remotely that would
|
|
17
|
+
* justify the cost - and an offline check needs no credentials, which is what
|
|
18
|
+
* lets this run in CI on a pull request.
|
|
19
|
+
*
|
|
20
|
+
* The exit code is the point: `fail` means the app is broken and returns 1,
|
|
21
|
+
* `warn` means this cannot tell from the outside and returns 0.
|
|
22
|
+
*/
|
|
23
|
+
export declare function runDoctor(argv?: string[]): number;
|
package/dist/types/cli/init.d.ts
CHANGED
|
@@ -22,11 +22,10 @@
|
|
|
22
22
|
*/
|
|
23
23
|
export declare function runInit(argv?: string[]): number;
|
|
24
24
|
/**
|
|
25
|
-
*
|
|
26
|
-
* `check` later does not change how the existing one is invoked.
|
|
25
|
+
* `init` writes the wiring; `doctor` checks the parts `init` cannot write.
|
|
27
26
|
*
|
|
28
27
|
* A bare path runs `init` against it, so both `connect-sdk init ./my-app` and
|
|
29
|
-
* `connect-sdk ./my-app` work. A first argument that is neither
|
|
28
|
+
* `connect-sdk ./my-app` work. A first argument that is neither a command nor
|
|
30
29
|
* an existing directory is a typo, and saying so beats reporting that
|
|
31
30
|
* `./innit` contains no App Router - which is true, unhelpful, and points at
|
|
32
31
|
* the wrong mistake.
|
|
@@ -7,7 +7,12 @@ export interface ConnectSession {
|
|
|
7
7
|
serviceRole: ServiceRole | null;
|
|
8
8
|
}
|
|
9
9
|
export interface ConnectContextValue {
|
|
10
|
-
/**
|
|
10
|
+
/**
|
|
11
|
+
* Public OAuth client id, if `clientId` was passed to `<ConnectProvider>` -
|
|
12
|
+
* empty string otherwise. Safe in a browser regardless: it only ever goes
|
|
13
|
+
* into the authorize URL. Most integrations never read this - it exists for
|
|
14
|
+
* the explicit-`redirectUri` flow, see `ConnectProviderProps.clientId`.
|
|
15
|
+
*/
|
|
11
16
|
clientId: string;
|
|
12
17
|
/** Connect's own origin, for deep links out to Profile, Billing and the catalog. */
|
|
13
18
|
appUrl: string;
|
|
@@ -58,9 +63,11 @@ export interface ConnectProviderProps {
|
|
|
58
63
|
/**
|
|
59
64
|
* This service's public `client_id`, from its Setup tab in Connect.
|
|
60
65
|
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
66
|
+
* Not needed for the default sign-in flow: `signIn()` with no `redirectUri`
|
|
67
|
+
* bounces the browser to your own `/signin` route, which already knows
|
|
68
|
+
* `client_id` server-side (`CONNECT_CLIENT_ID`) and builds the authorize URL
|
|
69
|
+
* itself - the browser never needs it. Pass this only alongside an explicit
|
|
70
|
+
* `redirectUri`, when you're building that URL yourself instead.
|
|
64
71
|
*/
|
|
65
72
|
clientId?: string;
|
|
66
73
|
/** Where `createConnectHandler` is mounted in your app. Must match its `basePath`. */
|
|
@@ -91,7 +98,6 @@ export interface ConnectProviderProps {
|
|
|
91
98
|
* origin, where `createConnectHandler` holds the credential.
|
|
92
99
|
*
|
|
93
100
|
* ```tsx
|
|
94
|
-
* // Next.js - clientId comes from NEXT_PUBLIC_CONNECT_CLIENT_ID
|
|
95
101
|
* <ConnectProvider>
|
|
96
102
|
* <App />
|
|
97
103
|
* </ConnectProvider>
|
|
@@ -97,40 +97,6 @@ export declare function getConnectSession(request?: Request, options?: {
|
|
|
97
97
|
client?: ConnectClient;
|
|
98
98
|
session?: ConnectSessionStore;
|
|
99
99
|
}): Promise<UserResponse | null>;
|
|
100
|
-
/**
|
|
101
|
-
* `next.config` wrapper.
|
|
102
|
-
*
|
|
103
|
-
* Asserts, at config load, that the public client id is present - the one
|
|
104
|
-
* variable whose absence fails silently in the browser instead of loudly on
|
|
105
|
-
* the server, because `NEXT_PUBLIC_*` is inlined at build time and an unset
|
|
106
|
-
* one simply becomes `undefined`. That is its whole job.
|
|
107
|
-
*
|
|
108
|
-
* In particular it does **not** replace the bridge route, and cannot: Next has
|
|
109
|
-
* no plugin API that contributes a route from a `node_modules` package - the
|
|
110
|
-
* App Router discovers routes by walking your own `app/` directory and nothing
|
|
111
|
-
* else, and a `next.config` rewrite still needs a real route to point at. So
|
|
112
|
-
* `app/api/connect/[...connect]/route.ts` has to exist whether or not you use
|
|
113
|
-
* this wrapper. It is one line, and `npx @cosmoops/connect-sdk init` writes it:
|
|
114
|
-
*
|
|
115
|
-
* ```ts
|
|
116
|
-
* export { GET, POST } from "@cosmoops/connect-sdk/next";
|
|
117
|
-
* ```
|
|
118
|
-
*
|
|
119
|
-
* The two are independent - the route works with a plain config, and this
|
|
120
|
-
* wrapper is useful with no route at all. Using both is just the default setup.
|
|
121
|
-
*
|
|
122
|
-
* It does **not** add this package to `transpilePackages`, and you should not
|
|
123
|
-
* either. `dist/` is already ES2022 with `"use client"` hoisted to the top of
|
|
124
|
-
* every browser file, so Turbopack and webpack both consume it as-is;
|
|
125
|
-
* transpiling it again only costs build time. An earlier version of this
|
|
126
|
-
* wrapper added the entry, which is why it may still be in your config.
|
|
127
|
-
*
|
|
128
|
-
* ```ts
|
|
129
|
-
* import { withConnect } from "@cosmoops/connect-sdk/next";
|
|
130
|
-
* export default withConnect({ });
|
|
131
|
-
* ```
|
|
132
|
-
*/
|
|
133
|
-
export declare function withConnect<T extends object>(config?: T): T;
|
|
134
100
|
export { createConnectHandler } from "../server/handler.js";
|
|
135
101
|
export type { ConnectSessionStore } from "../server/handler.js";
|
|
136
102
|
export { ConnectClient } from "../server/client.js";
|
|
@@ -10,6 +10,14 @@ export type { ConnectClientOptions } from "./client.js";
|
|
|
10
10
|
export { createConnectHandler } from "./handler.js";
|
|
11
11
|
export type { ConnectHandlerOptions, ConnectSessionStore } from "./handler.js";
|
|
12
12
|
export { createState } from "./resources/identity.js";
|
|
13
|
-
export type { AuthorizeUrlInput, ExchangeCodeInput } from "./resources/identity.js";
|
|
14
|
-
export type { UploadInput } from "./resources/drive.js";
|
|
13
|
+
export type { AuthorizeUrlInput, ExchangeCodeInput, IdentityResource } from "./resources/identity.js";
|
|
14
|
+
export type { UploadInput, DriveResource } from "./resources/drive.js";
|
|
15
15
|
export type { FetchLike } from "./http.js";
|
|
16
|
+
export type { AccessResource } from "./resources/access.js";
|
|
17
|
+
export type { AppsResource } from "./resources/apps.js";
|
|
18
|
+
export type { EntitlementsResource } from "./resources/entitlements.js";
|
|
19
|
+
export type { BillingResource } from "./resources/billing.js";
|
|
20
|
+
export type { AiResource } from "./resources/ai.js";
|
|
21
|
+
export type { OtpResource } from "./resources/otp.js";
|
|
22
|
+
export type { LogsResource } from "./resources/logs.js";
|
|
23
|
+
export type { SessionsResource } from "./resources/sessions.js";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cosmoops/connect-sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.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",
|
|
@@ -15,11 +15,19 @@
|
|
|
15
15
|
},
|
|
16
16
|
"./server": {
|
|
17
17
|
"types": "./dist/types/server/index.d.ts",
|
|
18
|
+
"browser": {
|
|
19
|
+
"import": "./dist/esm/this-entry-is-server-only.js",
|
|
20
|
+
"require": "./dist/cjs/this-entry-is-server-only.js"
|
|
21
|
+
},
|
|
18
22
|
"import": "./dist/esm/server/index.js",
|
|
19
23
|
"require": "./dist/cjs/server/index.js"
|
|
20
24
|
},
|
|
21
25
|
"./next": {
|
|
22
26
|
"types": "./dist/types/next/index.d.ts",
|
|
27
|
+
"browser": {
|
|
28
|
+
"import": "./dist/esm/this-entry-is-server-only.js",
|
|
29
|
+
"require": "./dist/cjs/this-entry-is-server-only.js"
|
|
30
|
+
},
|
|
23
31
|
"import": "./dist/esm/next/index.js",
|
|
24
32
|
"require": "./dist/cjs/next/index.js"
|
|
25
33
|
},
|
|
@@ -58,6 +66,8 @@
|
|
|
58
66
|
"verify:pack": "bun run scripts/verify-pack.ts",
|
|
59
67
|
"prepublishOnly": "bun run build && bun run verify:pack",
|
|
60
68
|
"test": "bun test",
|
|
69
|
+
"docs": "typedoc",
|
|
70
|
+
"docs:md": "typedoc --options typedoc.markdown.json",
|
|
61
71
|
"storybook": "storybook dev -p 6106",
|
|
62
72
|
"build:storybook": "storybook build -o storybook-static"
|
|
63
73
|
},
|
|
@@ -76,6 +86,8 @@
|
|
|
76
86
|
"react-dom": "19.2.8",
|
|
77
87
|
"storybook": "^10.5.10",
|
|
78
88
|
"tailwindcss": "^4.3.3",
|
|
89
|
+
"typedoc": "^0.28.20",
|
|
90
|
+
"typedoc-plugin-markdown": "^4.12.0",
|
|
79
91
|
"typescript": "^5",
|
|
80
92
|
"vite": "^8.2.2"
|
|
81
93
|
},
|