@cosmoops/connect-sdk 0.1.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.
Files changed (126) hide show
  1. package/LICENSE +34 -0
  2. package/README.md +446 -0
  3. package/dist/cjs/cli/bin.js +16 -0
  4. package/dist/cjs/cli/init.js +179 -0
  5. package/dist/cjs/client/components/account-sessions.js +112 -0
  6. package/dist/cjs/client/components/apps-grid.js +46 -0
  7. package/dist/cjs/client/components/auth-buttons.js +42 -0
  8. package/dist/cjs/client/components/feature-gate.js +61 -0
  9. package/dist/cjs/client/components/link-button.js +24 -0
  10. package/dist/cjs/client/components/logo.js +27 -0
  11. package/dist/cjs/client/components/plan-pricing.js +166 -0
  12. package/dist/cjs/client/components/plans-page.js +39 -0
  13. package/dist/cjs/client/components/plans.js +212 -0
  14. package/dist/cjs/client/components/readiness.js +117 -0
  15. package/dist/cjs/client/components/subscribe-dialog.js +60 -0
  16. package/dist/cjs/client/components/subscription.js +189 -0
  17. package/dist/cjs/client/components/usage-meter.js +68 -0
  18. package/dist/cjs/client/components/user-menu.js +172 -0
  19. package/dist/cjs/client/index.js +99 -0
  20. package/dist/cjs/client/mark.js +16 -0
  21. package/dist/cjs/client/provider.js +259 -0
  22. package/dist/cjs/client/use-checkout.js +173 -0
  23. package/dist/cjs/errors.js +74 -0
  24. package/dist/cjs/format-relative-time.js +18 -0
  25. package/dist/cjs/helpers.js +30 -0
  26. package/dist/cjs/index.js +22 -0
  27. package/dist/cjs/next/index.js +311 -0
  28. package/dist/cjs/package.json +3 -0
  29. package/dist/cjs/server/client.js +114 -0
  30. package/dist/cjs/server/handler.js +333 -0
  31. package/dist/cjs/server/http.js +160 -0
  32. package/dist/cjs/server/index.js +18 -0
  33. package/dist/cjs/server/resources/access.js +28 -0
  34. package/dist/cjs/server/resources/ai.js +36 -0
  35. package/dist/cjs/server/resources/apps.js +30 -0
  36. package/dist/cjs/server/resources/billing.js +71 -0
  37. package/dist/cjs/server/resources/drive.js +39 -0
  38. package/dist/cjs/server/resources/entitlements.js +49 -0
  39. package/dist/cjs/server/resources/identity.js +194 -0
  40. package/dist/cjs/server/resources/logs.js +41 -0
  41. package/dist/cjs/server/resources/otp.js +29 -0
  42. package/dist/cjs/server/resources/sessions.js +82 -0
  43. package/dist/cjs/types.js +15 -0
  44. package/dist/esm/cli/bin.js +13 -0
  45. package/dist/esm/cli/init.js +174 -0
  46. package/dist/esm/client/components/account-sessions.js +109 -0
  47. package/dist/esm/client/components/apps-grid.js +43 -0
  48. package/dist/esm/client/components/auth-buttons.js +38 -0
  49. package/dist/esm/client/components/feature-gate.js +57 -0
  50. package/dist/esm/client/components/link-button.js +21 -0
  51. package/dist/esm/client/components/logo.js +24 -0
  52. package/dist/esm/client/components/plan-pricing.js +156 -0
  53. package/dist/esm/client/components/plans-page.js +36 -0
  54. package/dist/esm/client/components/plans.js +209 -0
  55. package/dist/esm/client/components/readiness.js +113 -0
  56. package/dist/esm/client/components/subscribe-dialog.js +57 -0
  57. package/dist/esm/client/components/subscription.js +181 -0
  58. package/dist/esm/client/components/usage-meter.js +65 -0
  59. package/dist/esm/client/components/user-menu.js +164 -0
  60. package/dist/esm/client/index.js +47 -0
  61. package/dist/esm/client/mark.js +13 -0
  62. package/dist/esm/client/provider.js +247 -0
  63. package/dist/esm/client/use-checkout.js +168 -0
  64. package/dist/esm/errors.js +69 -0
  65. package/dist/esm/format-relative-time.js +15 -0
  66. package/dist/esm/helpers.js +26 -0
  67. package/dist/esm/index.js +15 -0
  68. package/dist/esm/next/index.js +279 -0
  69. package/dist/esm/package.json +3 -0
  70. package/dist/esm/server/client.js +110 -0
  71. package/dist/esm/server/handler.js +330 -0
  72. package/dist/esm/server/http.js +156 -0
  73. package/dist/esm/server/index.js +10 -0
  74. package/dist/esm/server/resources/access.js +24 -0
  75. package/dist/esm/server/resources/ai.js +32 -0
  76. package/dist/esm/server/resources/apps.js +26 -0
  77. package/dist/esm/server/resources/billing.js +67 -0
  78. package/dist/esm/server/resources/drive.js +35 -0
  79. package/dist/esm/server/resources/entitlements.js +45 -0
  80. package/dist/esm/server/resources/identity.js +189 -0
  81. package/dist/esm/server/resources/logs.js +37 -0
  82. package/dist/esm/server/resources/otp.js +25 -0
  83. package/dist/esm/server/resources/sessions.js +78 -0
  84. package/dist/esm/types.js +14 -0
  85. package/dist/theme.css +84 -0
  86. package/dist/types/cli/bin.d.ts +1 -0
  87. package/dist/types/cli/init.d.ts +34 -0
  88. package/dist/types/client/components/account-sessions.d.ts +25 -0
  89. package/dist/types/client/components/apps-grid.d.ts +20 -0
  90. package/dist/types/client/components/auth-buttons.d.ts +31 -0
  91. package/dist/types/client/components/feature-gate.d.ts +57 -0
  92. package/dist/types/client/components/link-button.d.ts +20 -0
  93. package/dist/types/client/components/logo.d.ts +35 -0
  94. package/dist/types/client/components/plan-pricing.d.ts +96 -0
  95. package/dist/types/client/components/plans-page.d.ts +36 -0
  96. package/dist/types/client/components/plans.d.ts +34 -0
  97. package/dist/types/client/components/readiness.d.ts +52 -0
  98. package/dist/types/client/components/subscribe-dialog.d.ts +34 -0
  99. package/dist/types/client/components/subscription.d.ts +123 -0
  100. package/dist/types/client/components/usage-meter.d.ts +24 -0
  101. package/dist/types/client/components/user-menu.d.ts +86 -0
  102. package/dist/types/client/index.d.ts +47 -0
  103. package/dist/types/client/mark.d.ts +11 -0
  104. package/dist/types/client/provider.d.ts +209 -0
  105. package/dist/types/client/use-checkout.d.ts +93 -0
  106. package/dist/types/errors.d.ts +62 -0
  107. package/dist/types/format-relative-time.d.ts +2 -0
  108. package/dist/types/helpers.d.ts +33 -0
  109. package/dist/types/index.d.ts +17 -0
  110. package/dist/types/next/index.d.ts +136 -0
  111. package/dist/types/server/client.d.ts +83 -0
  112. package/dist/types/server/handler.d.ts +67 -0
  113. package/dist/types/server/http.d.ts +52 -0
  114. package/dist/types/server/index.d.ts +15 -0
  115. package/dist/types/server/resources/access.d.ts +17 -0
  116. package/dist/types/server/resources/ai.d.ts +24 -0
  117. package/dist/types/server/resources/apps.d.ts +12 -0
  118. package/dist/types/server/resources/billing.d.ts +41 -0
  119. package/dist/types/server/resources/drive.d.ts +26 -0
  120. package/dist/types/server/resources/entitlements.d.ts +31 -0
  121. package/dist/types/server/resources/identity.d.ts +76 -0
  122. package/dist/types/server/resources/logs.d.ts +20 -0
  123. package/dist/types/server/resources/otp.d.ts +17 -0
  124. package/dist/types/server/resources/sessions.d.ts +37 -0
  125. package/dist/types/types.d.ts +434 -0
  126. package/package.json +102 -0
package/LICENSE ADDED
@@ -0,0 +1,34 @@
1
+ CosmoOps Proprietary License
2
+
3
+ Copyright (c) 2026 CosmoOps. All rights reserved.
4
+
5
+ This software and its accompanying documentation (the "Software") are the
6
+ confidential and proprietary property of CosmoOps.
7
+
8
+ NO LICENSE IS GRANTED.
9
+
10
+ Except for CosmoOps and those it has authorised in writing, no person or
11
+ entity may, in whole or in part, for any purpose whether personal,
12
+ educational, non-commercial or commercial:
13
+
14
+ (a) use, run, execute or evaluate the Software;
15
+ (b) copy, reproduce, publish, distribute, sublicense, sell, rent, lease or
16
+ otherwise transfer the Software;
17
+ (c) modify, adapt, translate, or create derivative works of the Software;
18
+ (d) reverse engineer, decompile or disassemble the Software, except to the
19
+ extent that this restriction is prohibited by applicable law; or
20
+ (e) remove, obscure or alter this notice or any proprietary marking.
21
+
22
+ Publication of the Software to a public package registry is a distribution
23
+ convenience for CosmoOps and its authorised integrators. It is not an offer,
24
+ and it does not place the Software in the public domain, make it open source,
25
+ or grant any licence by implication, estoppel or otherwise.
26
+
27
+ To request authorisation, contact legal@cosmoops.com.
28
+
29
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
30
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
31
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
32
+ COSMOOPS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
33
+ ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
34
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,446 @@
1
+ # @cosmoops/connect-sdk
2
+
3
+ Official client for the **CosmoOps Connect service-partner API** (`/api/v1/service/*`).
4
+
5
+ ```bash
6
+ npm install @cosmoops/connect-sdk
7
+ ```
8
+
9
+ Two entry points, and the split is a security boundary, not a preference:
10
+
11
+ | Import | Runs in | Holds the API key |
12
+ | --- | --- | --- |
13
+ | `@cosmoops/connect-sdk/server` | your backend | **yes** |
14
+ | `@cosmoops/connect-sdk/client` | the browser | never |
15
+ | `@cosmoops/connect-sdk/next` | your backend | **yes** (Next.js plug-and-play) |
16
+ | `@cosmoops/connect-sdk` | anywhere | types + `ConnectError` only |
17
+
18
+ The browser half never sees a credential. It calls your own origin, and
19
+ `createConnectHandler` — mounted in your backend — is what talks to Connect.
20
+
21
+ ```
22
+ Browser ──► your backend ──► Connect
23
+ (client) (createConnectHandler + server client, holds the key)
24
+ ```
25
+
26
+ ## Server setup
27
+
28
+ One credential does three jobs — the API key plaintext from your service's Setup tab is also your OAuth2 `client_secret` and the key your `id_token` is signed with. Revoking it revokes all three.
29
+
30
+ ```ts
31
+ import { ConnectClient } from "@cosmoops/connect-sdk/server";
32
+
33
+ export const connect = new ConnectClient({
34
+ apiKey: process.env.CONNECT_API_KEY!,
35
+ clientId: process.env.CONNECT_CLIENT_ID, // only needed for identity.*
36
+ });
37
+ ```
38
+
39
+ An API key or a `sessionToken` in browser code is a leaked credential.
40
+
41
+ ## Login with Connect
42
+
43
+ **1. Send the user out.**
44
+
45
+ ```ts
46
+ import { createState } from "@cosmoops/connect-sdk/server";
47
+
48
+ const state = createState();
49
+ await saveToSession({ state });
50
+
51
+ redirect(connect.identity.authorizeUrl({
52
+ redirectUri: "https://you.example/callback",
53
+ state,
54
+ // prompt: "create" ← opens registration instead of sign-in
55
+ }));
56
+ ```
57
+
58
+ **2. Handle the callback.**
59
+
60
+ ```ts
61
+ if (query.state !== savedState) throw new Error("state mismatch");
62
+
63
+ if (query.error === "login_required") return retrySignIn(); // normal — not an error
64
+ if (query.error) return showCancelled();
65
+
66
+ const session = await connect.identity.exchangeCode({
67
+ code: query.code,
68
+ redirectUri: "https://you.example/callback",
69
+ });
70
+
71
+ await connect.identity.verifyIdToken(session.idToken);
72
+
73
+ await saveToSession({ sessionToken: session.sessionToken });
74
+ ```
75
+
76
+ **3. On each later request.**
77
+
78
+ ```ts
79
+ const { user, serviceRole } = await connect.identity.getUser(sessionToken);
80
+
81
+ if (!serviceRole.granted.includes("invoice:write")) return forbidden();
82
+ ```
83
+
84
+ **4. On logout.**
85
+
86
+ ```ts
87
+ await connect.identity.logout(sessionToken);
88
+ ```
89
+
90
+ ## Gate on entitlements
91
+
92
+ ```ts
93
+ import { blockingBlockers } from "@cosmoops/connect-sdk"; // isomorphic
94
+
95
+ const readiness = await connect.entitlements.getReadiness(orgId);
96
+
97
+ if (!readiness.ready) {
98
+ const [blocker] = blockingBlockers(readiness);
99
+ return showBlocked(blocker.message, blocker.resolveUrl); // resolveUrl links into Connect
100
+ }
101
+
102
+ if (await connect.entitlements.hasFeature(orgId, "campaign-module")) {
103
+ // ...
104
+ }
105
+ ```
106
+
107
+ Branch on `blocker.code`, never on `message`. Treat an unrecognised code as blocking — the list grows.
108
+
109
+ ## Meter usage
110
+
111
+ ```ts
112
+ const { balance } = await connect.billing.consumeAddon({
113
+ organizationId: orgId,
114
+ addonId: "api-calls",
115
+ quantity: 1,
116
+ });
117
+ ```
118
+
119
+ An `Idempotency-Key` is sent on every consume, so a retry can't double-debit.
120
+
121
+ ## Errors
122
+
123
+ Everything throws `ConnectError` with a stable `code`.
124
+
125
+ ```ts
126
+ import { isConnectError } from "@cosmoops/connect-sdk"; // isomorphic
127
+
128
+ try {
129
+ await connect.identity.getUser(sessionToken);
130
+ } catch (error) {
131
+ if (isConnectError(error) && error.isSessionExpired) return redirectToSignIn();
132
+ throw error;
133
+ }
134
+ ```
135
+
136
+ `isRetryable` covers throttling, 5xx and network failures. The SDK already retries those for safe requests; the flag is for deciding whether to queue your own.
137
+
138
+ ## Namespaces
139
+
140
+ | | |
141
+ | --- | --- |
142
+ | `identity` | `authorizeUrl` · `exchangeCode` · `verifyIdToken` · `getUser` · `logout` |
143
+ | `access` | `getUserRoles` · `listRoles` |
144
+ | `entitlements` | `getReadiness` · `getSubscription` · `hasFeature` |
145
+ | `billing` | `listPlans` · `getAddonUsage` · `consumeAddon` |
146
+ | `drive` | `upload` · `getFileUrl` · `getPreviewUrl` |
147
+ | `ai` | `chat` · `extract` |
148
+ | `otp` | `send` · `verify` |
149
+ | `logs` | `write` |
150
+ | `apps` | `list` |
151
+ | `sessions` | `list` · `revoke` · `revokeOthers` |
152
+
153
+ ### Attributing a log to a user
154
+
155
+ ```ts
156
+ await client.logs.write({
157
+ message: "campaign sent",
158
+ metadata: { campaignId },
159
+ sessionToken, // that user's Connect session token
160
+ });
161
+ ```
162
+
163
+ Both scopes on the stored entry come from something verified, never from your payload: `serviceId` from your API key, and `userId` from this token — which Connect checks is unrevoked, unexpired, and **minted for your service** before attributing anything. **There is no `userId` field to send.** An actor a caller can type is not an actor worth recording, and a conformance test fails the build if one ever appears in the server schema.
164
+
165
+ `write` returns `{ delivered, attributed }` and never throws. `attributed: false` with `delivered: true` means the token had expired (~15 days) or been revoked and the line was stored *without* a user — deliberately kept rather than rejected, because losing telemetry over a failed attribution is the worse of the two outcomes.
166
+
167
+ ## Options
168
+
169
+ ```ts
170
+ new ConnectClient({
171
+ apiKey,
172
+ clientId,
173
+ baseUrl, // default https://connect.cosmoops.com
174
+ timeoutMs, // default 20000, per attempt
175
+ maxRetries, // default 2, safe requests only
176
+ fetch, // bring your own instrumented fetch
177
+ });
178
+ ```
179
+
180
+ ## Next.js — plug and play
181
+
182
+ Two files, no arguments.
183
+
184
+ ```ts
185
+ // app/api/connect/[...connect]/route.ts
186
+ export { GET, POST } from "@cosmoops/connect-sdk/next";
187
+ ```
188
+
189
+ ```tsx
190
+ // app/layout.tsx
191
+ import { ConnectProvider } from "@cosmoops/connect-sdk/client";
192
+
193
+ <ConnectProvider>{children}</ConnectProvider>
194
+ ```
195
+
196
+ ```css
197
+ /* app/globals.css */
198
+ @import "tailwindcss";
199
+ @import "@heroui/styles";
200
+ @import "@cosmoops/connect-sdk/theme.css";
201
+ ```
202
+
203
+ `npx @cosmoops/connect-sdk init` writes the route file and a `/plans` page for
204
+ you; the provider and the stylesheet are the two it won't edit on your behalf.
205
+
206
+ That gives you the whole round trip — `/signin`, `/callback`, an httpOnly session
207
+ cookie, `/session`, `/entitlements`, `/plans`, `/addons`, `/apps`, `/sessions`,
208
+ `/logout`. Config comes from the environment:
209
+
210
+ | Variable | Required | Purpose |
211
+ | --- | --- | --- |
212
+ | `CONNECT_API_KEY` | yes | server-side credential |
213
+ | `CONNECT_CLIENT_ID` | yes | OAuth client id |
214
+ | `NEXT_PUBLIC_CONNECT_CLIENT_ID` | yes | same value, for `<ConnectProvider>` |
215
+ | `CONNECT_APP_URL` | no | non-production Connect instance |
216
+ | `CONNECT_SESSION_COOKIE` | no | session cookie name |
217
+
218
+ Register `{your origin}/api/connect/callback` as a redirect URI in the service's
219
+ Setup tab — the one step this cannot do for you.
220
+
221
+ Optional `next.config` wrapper, which warns at config load if the public client
222
+ id is missing (a `NEXT_PUBLIC_*` that isn't set fails silently in the browser):
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.
233
+
234
+ Reading the session server-side — no argument, no `headers()` dance:
235
+
236
+ ```tsx
237
+ // app/dashboard/page.tsx
238
+ import { getConnectSession } from "@cosmoops/connect-sdk/next";
239
+ import { redirect } from "next/navigation";
240
+
241
+ export default async function DashboardPage() {
242
+ const session = await getConnectSession(); // UserResponse | null
243
+ if (!session) redirect("/api/connect/signin?next=/dashboard");
244
+ return <h1>{session.user.organizationName}</h1>;
245
+ }
246
+ ```
247
+
248
+ Pass a `Request` — `getConnectSession(request)` — anywhere `next/headers` does
249
+ not exist: a framework-agnostic handler, a custom server, a test.
250
+
251
+ It reads `cookies()` transitively, so under `cacheComponents` it belongs inside
252
+ a `<Suspense>` boundary like any other request-scoped read. A page that awaits
253
+ it above every boundary gives up its static shell.
254
+
255
+ You do **not** need `transpilePackages`. `dist/` is ES2022 with `"use client"`
256
+ already hoisted, so Turbopack and webpack consume it as-is.
257
+
258
+ ## Other frameworks
259
+
260
+ Mount the bridge once in your backend:
261
+
262
+ ```ts
263
+ // app/api/connect/[...route]/route.ts
264
+ import { createConnectHandler } from "@cosmoops/connect-sdk/server";
265
+ import { connect } from "@/lib/connect";
266
+
267
+ const handler = createConnectHandler({
268
+ client: connect,
269
+ session: {
270
+ read: async (req) => (await cookies()).get("connect_session")?.value ?? null,
271
+ clear: (req, res) => res.headers.append("set-cookie", "connect_session=; Max-Age=0; Path=/"),
272
+ },
273
+ });
274
+
275
+ export { handler as GET, handler as POST };
276
+ ```
277
+
278
+ Then wrap your app:
279
+
280
+ ```tsx
281
+ import { ConnectProvider } from "@cosmoops/connect-sdk/client";
282
+
283
+ <ConnectProvider clientId={process.env.NEXT_PUBLIC_CONNECT_CLIENT_ID!}>
284
+ {children}
285
+ </ConnectProvider>
286
+ ```
287
+
288
+ ### Components
289
+
290
+ ```tsx
291
+ import {
292
+ ConnectLoginButton, ConnectRegisterButton,
293
+ ConnectUserMenu, ConnectAppsGrid, ConnectPlans,
294
+ } from "@cosmoops/connect-sdk/client";
295
+
296
+ // Top bar — the pair swaps automatically on sign-in state
297
+ <ConnectAppsGrid />
298
+ <ConnectLoginButton />
299
+ <ConnectUserMenu />
300
+
301
+ // Pricing page
302
+ <ConnectPlans />
303
+ ```
304
+
305
+ | Component | What it does |
306
+ | --- | --- |
307
+ | `ConnectLoginButton` · `ConnectRegisterButton` | "Continue with Connect", in the shape people know from Google. Render nothing when signed in. |
308
+ | `ConnectUserMenu` | Avatar, name, workspace. Profile, Manage subscription and Open Connect each open Connect in a new tab; Sign out acts in place. Renders nothing when signed out. |
309
+ | `ConnectAppsGrid` | Google-style launcher — grid icon opening a modal of every app this workspace can open. |
310
+ | `ConnectPlans` | Monthly/annual toggle, features, add-ons, live discount, coupon field, and the subscribe / upgrade / downgrade / cancel flow. |
311
+ | `ConnectPlansPage` | A whole `/plans` page — readiness banner, current subscription, and the grid. What `connect-sdk init` scaffolds. |
312
+ | `ConnectSubscribeDialog` | The review step `ConnectPlans` opens — plan, GST, what happens to the current cycle. Exported so you can drive the flow from your own pricing page. |
313
+ | `ConnectSubscriptionGate` | Renders your module for a subscribed workspace, and `ConnectNoSubscriptionCard` for everyone else. Never flashes the upsell while loading. |
314
+ | `ConnectNoSubscriptionCard` · `ConnectSubscriptionCard` | The same slot, unsubscribed and subscribed. Plan, status, renewal date, seats, and one way into Connect. |
315
+ | `ConnectReadinessBanner` · `ConnectReadinessGate` | Every gate between this workspace and your service — KYC, payment method, seat headroom — each with the Connect screen that fixes it. |
316
+ | `ConnectFeatureGate` | Renders a module only when `plan.features` includes its key. |
317
+ | `ConnectTrialBanner` | A running trial and the date it converts. Renders nothing otherwise. |
318
+ | `ConnectAccountSessions` | Every device signed into this Connect account, with revoke. |
319
+ | `ConnectUsageMeter` | Remaining balance of each consumable add-on, with overage and carry-forward. |
320
+ | `ConnectManageSubscriptionButton` · `ConnectInvoicesButton` · `ConnectProfileButton` · `ConnectHomeButton` · `ConnectSignOutButton` | The menu items as standalone buttons — Connect's Billing, Invoices and Profile. |
321
+ | `ConnectLogo` | The shared lockup — your `icon` on the CosmoOps badge, `cosmo·ops` beside it, your `name` underneath. |
322
+
323
+ ### Hooks
324
+
325
+ `useConnect` · `useConnectUser` · `useConnectEntitlements` · `useConnectPlans` · `useConnectApps` · `useConnectAddonUsage` · `useConnectSessions` · `useConnectResource` · `useConnectFeature` · `useConnectCheckout`
326
+
327
+ ### Metering
328
+
329
+ ```tsx
330
+ await client.billing.consumeAddon({ organizationId, addonId: "addon_sms", quantity: 1 }); // server
331
+ <ConnectUsageMeter /> // browser
332
+ ```
333
+
334
+ The meter reads the same balance `consumeAddon` debits (`addons:read`, separate from `addons:consume` so a key that only renders a meter cannot spend the balance down). It shows `balance` against `includedPerCycle` and never a "used" figure: with `carryForward` a balance survives the cycle boundary, so `included − balance` can go negative — a meter running backwards.
335
+
336
+ Invoices stay a deep link (`ConnectInvoicesButton`). A GST tax invoice is a legal document emailed and downloaded from one place; a partner-rendered copy would be a second version of a record that must have exactly one.
337
+
338
+ ### Plan features: label vs key
339
+
340
+ The catalogue read (`useConnectPlans`, `<ConnectPlans />`) reports `features` as `{ key, name, description }` — **render `name`, gate on `key`**. The subscription read (`useConnectEntitlements`, `hasFeature`, `<ConnectFeatureGate>`) reports `features` as a bare `string[]` of keys, and stays that way: that one is a contract your backend compares against, and a display label is neither stable nor comparable. `name` falls back to the key for a plan older than its service's feature catalog.
341
+
342
+ ### Gates
343
+
344
+ ```tsx
345
+ <ConnectReadinessBanner /> {/* KYC, payment method, seats — each with a fix */}
346
+ <ConnectSubscriptionGate><Dashboard /></ConnectSubscriptionGate>
347
+ <ConnectFeatureGate feature="advanced_reports"><Reports /></ConnectFeatureGate>
348
+ ```
349
+
350
+ All three render `loading` — never the fallback — while the answer is unknown, so a paying customer is never flashed an upsell for something they already have.
351
+
352
+ **They are UX gates, not security boundaries.** Everything they read reaches your own origin and a browser can ignore it. The authoritative check is on your server: `client.entitlements.getReadiness(orgId)` and `client.entitlements.hasFeature(orgId, feature)`.
353
+
354
+ ### Scaffolding
355
+
356
+ ```bash
357
+ npx @cosmoops/connect-sdk init
358
+ ```
359
+
360
+ Writes `app/api/connect/[...connect]/route.ts`, `app/plans/page.tsx` and an `.env.local` template, then prints the two steps it will not do for you (wrapping your layout in `<ConnectProvider>`, and registering the redirect URI in Connect). It never overwrites, so re-running is safe.
361
+
362
+ 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
+
364
+ ### Subscribe, upgrade, downgrade, cancel
365
+
366
+ `<ConnectPlans />` runs all three. Pressing a plan's action opens a review step — the plan, its GST-inclusive price, and what happens to the cycle the customer is part-way through — and confirming opens Connect on the confirmation screen **for that exact plan**, not a catalogue they have to search again. The page then watches `/entitlements` and repaints itself once the change lands, so nobody has to be told to refresh.
367
+
368
+ To drive it from your own pricing page instead, the two pieces are exported:
369
+
370
+ ```tsx
371
+ import { useConnectCheckout, buildCheckoutUrl, useConnect, useConnectEntitlements }
372
+ from "@cosmoops/connect-sdk/client";
373
+
374
+ const { appUrl } = useConnect();
375
+ const { data } = useConnectEntitlements();
376
+ const checkout = useConnectCheckout();
377
+
378
+ <button
379
+ onClick={() =>
380
+ checkout.start({
381
+ url: buildCheckoutUrl({
382
+ appUrl,
383
+ serviceId: data!.subscription.serviceId,
384
+ planId: "plan_growth_m",
385
+ // Present ⇒ change this subscription's plan; absent ⇒ first subscribe.
386
+ subscriptionId: data!.subscription.subscription?.id ?? null,
387
+ }),
388
+ isSettled: (next) => next.subscription.plan?.id === "plan_growth_m",
389
+ })
390
+ }
391
+ >
392
+ Upgrade
393
+ </button>
394
+ ```
395
+
396
+ `checkout.status` moves `idle → waiting → completed | abandoned`, plus `blocked` if the browser refused the window (render `checkout.url` as a link). **A closed window is never read as success** — only the entitlement changing is, because "paid and closed it" and "opened it and thought better of it" look identical from here.
397
+
398
+ Cancelling uses the same machinery via `buildCancelUrl`. There is no *resume*: Connect has no uncancel path, so a subscription already set to cancel gets a statement of when access ends, not a button that cannot work.
399
+
400
+ ### Logo
401
+
402
+ ```tsx
403
+ import { ConnectLogo } from "@cosmoops/connect-sdk/client";
404
+ import { Truck } from "lucide-react";
405
+
406
+ <ConnectLogo name="Dispatch" icon={<Truck />} />
407
+ ```
408
+
409
+ Each platform passes only what differs — `name` and `icon`. Mark geometry, wordmark, separator and accent come from one place, so the family reads as one product. Sizes are `em`-based: `className="text-xl"` scales the whole lockup, which is why there's no `size` prop. `collapsed` gives the mark alone.
410
+
411
+ ### Theming
412
+
413
+ ```css
414
+ @import "tailwindcss";
415
+ @import "@heroui/styles";
416
+ @import "@cosmoops/connect-sdk/theme.css";
417
+ ```
418
+
419
+ One shared **HeroUI v3** theme for every service platform. The components paint with semantic tokens only — `accent`, `muted`, `surface`, `separator`, `danger` — never a hardcoded colour, so one edit to the theme restyles all of them everywhere at once. To diverge on brand, redefine individual tokens *after* the import rather than forking the file.
420
+
421
+ Requires `@heroui/react` and `lucide-react` as peers (optional — server-only consumers need neither).
422
+
423
+ ### Why payment happens on Connect
424
+
425
+ The *flow* is in your app; the *payment* is not. A plan change is not one write — it re-runs the readiness gates, prices proration against accumulated credit, re-checks the RBI mandate ceiling for the new amount, and issues a GST invoice. Connect owns that sequence end to end, and a partner-side implementation would be a second copy of it that drifts. It would also mean a browser holding your credentials could move money, which is the one thing the bridge exists to prevent.
426
+
427
+ So the hand-off is a URL, and Connect renders it as a modal over the relevant page (an intercepted parallel route) — the customer lands on the confirmation for the plan they picked, and returns to a subscription that is already correct.
428
+
429
+ ## Development
430
+
431
+ ```bash
432
+ bun test # server suite - hermetic, no network
433
+ bun run storybook # client components against a mock bridge, port 6106
434
+ bun run build # dual CJS/ESM + types
435
+ ```
436
+
437
+ Server tests drive the injectable `fetch` seam, so nothing reaches Connect.
438
+ Storybook renders the real `ConnectProvider` and substitutes only `fetch` — the
439
+ same seam the production bridge sits behind — so loading, error and empty
440
+ states are exercised for real. `.storybook/theme.css` is a sample host theme:
441
+ change it and every story should move together. If one doesn't, it has a
442
+ hardcoded colour.
443
+
444
+ ## License
445
+
446
+ Proprietary. See [LICENSE](./LICENSE) — no rights are granted to anyone outside CosmoOps and its authorised integrators.
@@ -0,0 +1,16 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ /**
5
+ * The `connect-sdk` executable.
6
+ *
7
+ * A thin wrapper so `init.ts` stays importable and testable - a module that
8
+ * calls `process.exit` at import time cannot be unit tested, and this is the
9
+ * only file that does.
10
+ *
11
+ * The shebang is added by `scripts/finalize-dist.ts`: TypeScript strips leading
12
+ * comments it does not recognise as directives, and a `bin` without one is run
13
+ * by the shell rather than by Node.
14
+ */
15
+ const init_js_1 = require("./init.js");
16
+ process.exit((0, init_js_1.main)(process.argv.slice(2)));
@@ -0,0 +1,179 @@
1
+ "use client";
2
+ "use strict";
3
+ /**
4
+ * `npx @cosmoops/connect-sdk init` - writes the Connect wiring into a Next.js
5
+ * App Router project.
6
+ *
7
+ * ## Why this is a CLI and not a plugin
8
+ *
9
+ * Next has no plugin API that injects routes. `next.config` can rewrite and
10
+ * redirect, but both need a real route to point at, and there is no hook that
11
+ * contributes a `page.tsx` from a node_module - the App Router discovers routes
12
+ * by walking the project's own `app/` directory and nothing else. So a package
13
+ * cannot add `/plans` to an app by being installed, however much one would like
14
+ * it to.
15
+ *
16
+ * What it *can* do is make the file that must exist a single line, and then
17
+ * write that file for you. `ConnectPlansPage` is the first half; this is the
18
+ * second. The result is the same as a plugin from the developer's side - one
19
+ * command and the page is there - without pretending an extension point exists.
20
+ *
21
+ * Nothing here overwrites. Every file that already exists is reported as
22
+ * skipped, so re-running after a partial setup is safe and running it on an
23
+ * app that already has Connect wired up does nothing at all.
24
+ */
25
+ Object.defineProperty(exports, "__esModule", { value: true });
26
+ exports.runInit = runInit;
27
+ exports.main = main;
28
+ const node_fs_1 = require("node:fs");
29
+ const node_path_1 = require("node:path");
30
+ const BRIDGE_ROUTE = `// The Connect bridge - the only place your API key is used.
31
+ //
32
+ // It answers your own origin, so the browser half of the SDK never holds a
33
+ // credential. GET and POST are the same function; the bridge routes on method
34
+ // and path itself.
35
+ export { GET, POST } from "@cosmoops/connect-sdk/next";
36
+ `;
37
+ const PLANS_PAGE = `"use client";
38
+
39
+ // The full pricing page: readiness banner, current subscription, plan grid, and
40
+ // the subscribe / upgrade / downgrade flow.
41
+ //
42
+ // Swap this for \`<ConnectPlansPage title="..." />\` (or compose \`ConnectPlans\`
43
+ // with the other exports) whenever you want your own copy or layout.
44
+ export { ConnectPlansPage as default } from "@cosmoops/connect-sdk/client";
45
+ `;
46
+ const ENV_TEMPLATE = `# CosmoOps Connect - values come from your service's Setup tab.
47
+ CONNECT_API_KEY=
48
+ CONNECT_CLIENT_ID=
49
+ # Same value as CONNECT_CLIENT_ID. Inlined at build time for <ConnectProvider>.
50
+ NEXT_PUBLIC_CONNECT_CLIENT_ID=
51
+ `;
52
+ const SCAFFOLDS = [
53
+ {
54
+ path: (0, node_path_1.join)("api", "connect", "[...connect]", "route.ts"),
55
+ contents: BRIDGE_ROUTE,
56
+ description: "Connect bridge route handler",
57
+ },
58
+ {
59
+ path: (0, node_path_1.join)("plans", "page.tsx"),
60
+ contents: PLANS_PAGE,
61
+ description: "/plans pricing page",
62
+ },
63
+ ];
64
+ /**
65
+ * `app/` or `src/app/`, whichever this project uses.
66
+ *
67
+ * Both are official layouts and neither is more correct, so guessing wrong
68
+ * would scatter routes into a directory Next never reads - a failure that looks
69
+ * like "the page just 404s" and takes a while to trace. Refusing to guess is
70
+ * better than a default.
71
+ */
72
+ function findAppDir(root) {
73
+ for (const candidate of [(0, node_path_1.join)(root, "app"), (0, node_path_1.join)(root, "src", "app")]) {
74
+ if ((0, node_fs_1.existsSync)(candidate))
75
+ return candidate;
76
+ }
77
+ return null;
78
+ }
79
+ function write(file, contents) {
80
+ if ((0, node_fs_1.existsSync)(file))
81
+ return "skipped";
82
+ (0, node_fs_1.mkdirSync)((0, node_path_1.dirname)(file), { recursive: true });
83
+ (0, node_fs_1.writeFileSync)(file, contents);
84
+ return "written";
85
+ }
86
+ /** Appends the Connect block to `.env.local`, or creates it - never rewrites an existing value. */
87
+ function ensureEnv(root) {
88
+ const file = (0, node_path_1.join)(root, ".env.local");
89
+ if (!(0, node_fs_1.existsSync)(file)) {
90
+ (0, node_fs_1.writeFileSync)(file, ENV_TEMPLATE);
91
+ return "written";
92
+ }
93
+ const current = (0, node_fs_1.readFileSync)(file, "utf8");
94
+ if (current.includes("CONNECT_CLIENT_ID"))
95
+ return "skipped";
96
+ (0, node_fs_1.writeFileSync)(file, `${current.trimEnd()}\n\n${ENV_TEMPLATE}`);
97
+ return "appended";
98
+ }
99
+ function runInit(argv = []) {
100
+ const root = (0, node_path_1.resolve)(argv[0] ?? process.cwd());
101
+ const appDir = findAppDir(root);
102
+ if (!appDir) {
103
+ console.error(`[connect-sdk] No App Router directory found in ${root} - looked for "app/" and "src/app/".\n` +
104
+ "Run this from your Next.js project root, or pass the path: npx @cosmoops/connect-sdk init ./my-app");
105
+ return 1;
106
+ }
107
+ console.log(`[connect-sdk] Wiring Connect into ${(0, node_path_1.relative)(process.cwd(), appDir) || appDir}\n`);
108
+ for (const scaffold of SCAFFOLDS) {
109
+ const target = (0, node_path_1.join)(appDir, scaffold.path);
110
+ const result = write(target, scaffold.contents);
111
+ const marker = result === "written" ? "created" : "exists ";
112
+ console.log(` ${marker} ${(0, node_path_1.relative)(root, target)} - ${scaffold.description}`);
113
+ }
114
+ const env = ensureEnv(root);
115
+ console.log(` ${env === "skipped" ? "exists " : env === "appended" ? "updated" : "created"} .env.local - fill in the three values from your service's Setup tab`);
116
+ // The two steps a scaffolder must not do for you: editing an existing layout
117
+ // means parsing and rewriting JSX someone else wrote, and the redirect URI
118
+ // lives in Connect, not in this repo.
119
+ console.log(`
120
+ Three things left, all one-time:
121
+
122
+ 1. Wrap your app in the provider - app/layout.tsx:
123
+
124
+ import { ConnectProvider } from "@cosmoops/connect-sdk/client";
125
+
126
+ <ConnectProvider>{children}</ConnectProvider>
127
+
128
+ No props: it reads NEXT_PUBLIC_CONNECT_CLIENT_ID.
129
+
130
+ 2. Add the theme to your global stylesheet, last of the three - Tailwind
131
+ first, HeroUI's token layer next, so these values win:
132
+
133
+ @import "tailwindcss";
134
+ @import "@heroui/styles";
135
+ @import "@cosmoops/connect-sdk/theme.css";
136
+
137
+ It has to be imported from your *own* CSS, not from node_modules, or
138
+ Tailwind roots its content scan inside node_modules and skips your app.
139
+
140
+ 3. Register {your origin}/api/connect/callback as a redirect URI in your
141
+ service's Setup tab in Connect.
142
+
143
+ Optionally, in next.config.ts:
144
+
145
+ import { withConnect } from "@cosmoops/connect-sdk/next";
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.
150
+
151
+ Then <ConnectPlans />, <ConnectSubscriptionGate>, <ConnectFeatureGate> and the
152
+ rest work anywhere below the provider, and getConnectSession() reads the same
153
+ session from any Server Component.
154
+ `);
155
+ return 0;
156
+ }
157
+ /**
158
+ * Only `init` today. Parsed as a subcommand anyway so adding `doctor` or
159
+ * `check` later does not change how the existing one is invoked.
160
+ *
161
+ * 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 the command nor
163
+ * an existing directory is a typo, and saying so beats reporting that
164
+ * `./innit` contains no App Router - which is true, unhelpful, and points at
165
+ * the wrong mistake.
166
+ */
167
+ function main(argv) {
168
+ const [first, ...rest] = argv;
169
+ if (first === "--help" || first === "-h") {
170
+ console.log("Usage: connect-sdk init [project-dir]");
171
+ return 0;
172
+ }
173
+ if (first === "init")
174
+ return runInit(rest);
175
+ if (!first || (0, node_fs_1.existsSync)(first))
176
+ return runInit(argv);
177
+ console.error(`[connect-sdk] Unknown command "${first}". Try: connect-sdk init`);
178
+ return 1;
179
+ }