@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.
- package/LICENSE +34 -0
- package/README.md +446 -0
- package/dist/cjs/cli/bin.js +16 -0
- package/dist/cjs/cli/init.js +179 -0
- package/dist/cjs/client/components/account-sessions.js +112 -0
- package/dist/cjs/client/components/apps-grid.js +46 -0
- package/dist/cjs/client/components/auth-buttons.js +42 -0
- package/dist/cjs/client/components/feature-gate.js +61 -0
- package/dist/cjs/client/components/link-button.js +24 -0
- package/dist/cjs/client/components/logo.js +27 -0
- package/dist/cjs/client/components/plan-pricing.js +166 -0
- package/dist/cjs/client/components/plans-page.js +39 -0
- package/dist/cjs/client/components/plans.js +212 -0
- package/dist/cjs/client/components/readiness.js +117 -0
- package/dist/cjs/client/components/subscribe-dialog.js +60 -0
- package/dist/cjs/client/components/subscription.js +189 -0
- package/dist/cjs/client/components/usage-meter.js +68 -0
- package/dist/cjs/client/components/user-menu.js +172 -0
- package/dist/cjs/client/index.js +99 -0
- package/dist/cjs/client/mark.js +16 -0
- package/dist/cjs/client/provider.js +259 -0
- package/dist/cjs/client/use-checkout.js +173 -0
- package/dist/cjs/errors.js +74 -0
- package/dist/cjs/format-relative-time.js +18 -0
- package/dist/cjs/helpers.js +30 -0
- package/dist/cjs/index.js +22 -0
- package/dist/cjs/next/index.js +311 -0
- package/dist/cjs/package.json +3 -0
- package/dist/cjs/server/client.js +114 -0
- package/dist/cjs/server/handler.js +333 -0
- package/dist/cjs/server/http.js +160 -0
- package/dist/cjs/server/index.js +18 -0
- package/dist/cjs/server/resources/access.js +28 -0
- package/dist/cjs/server/resources/ai.js +36 -0
- package/dist/cjs/server/resources/apps.js +30 -0
- package/dist/cjs/server/resources/billing.js +71 -0
- package/dist/cjs/server/resources/drive.js +39 -0
- package/dist/cjs/server/resources/entitlements.js +49 -0
- package/dist/cjs/server/resources/identity.js +194 -0
- package/dist/cjs/server/resources/logs.js +41 -0
- package/dist/cjs/server/resources/otp.js +29 -0
- package/dist/cjs/server/resources/sessions.js +82 -0
- package/dist/cjs/types.js +15 -0
- package/dist/esm/cli/bin.js +13 -0
- package/dist/esm/cli/init.js +174 -0
- package/dist/esm/client/components/account-sessions.js +109 -0
- package/dist/esm/client/components/apps-grid.js +43 -0
- package/dist/esm/client/components/auth-buttons.js +38 -0
- package/dist/esm/client/components/feature-gate.js +57 -0
- package/dist/esm/client/components/link-button.js +21 -0
- package/dist/esm/client/components/logo.js +24 -0
- package/dist/esm/client/components/plan-pricing.js +156 -0
- package/dist/esm/client/components/plans-page.js +36 -0
- package/dist/esm/client/components/plans.js +209 -0
- package/dist/esm/client/components/readiness.js +113 -0
- package/dist/esm/client/components/subscribe-dialog.js +57 -0
- package/dist/esm/client/components/subscription.js +181 -0
- package/dist/esm/client/components/usage-meter.js +65 -0
- package/dist/esm/client/components/user-menu.js +164 -0
- package/dist/esm/client/index.js +47 -0
- package/dist/esm/client/mark.js +13 -0
- package/dist/esm/client/provider.js +247 -0
- package/dist/esm/client/use-checkout.js +168 -0
- package/dist/esm/errors.js +69 -0
- package/dist/esm/format-relative-time.js +15 -0
- package/dist/esm/helpers.js +26 -0
- package/dist/esm/index.js +15 -0
- package/dist/esm/next/index.js +279 -0
- package/dist/esm/package.json +3 -0
- package/dist/esm/server/client.js +110 -0
- package/dist/esm/server/handler.js +330 -0
- package/dist/esm/server/http.js +156 -0
- package/dist/esm/server/index.js +10 -0
- package/dist/esm/server/resources/access.js +24 -0
- package/dist/esm/server/resources/ai.js +32 -0
- package/dist/esm/server/resources/apps.js +26 -0
- package/dist/esm/server/resources/billing.js +67 -0
- package/dist/esm/server/resources/drive.js +35 -0
- package/dist/esm/server/resources/entitlements.js +45 -0
- package/dist/esm/server/resources/identity.js +189 -0
- package/dist/esm/server/resources/logs.js +37 -0
- package/dist/esm/server/resources/otp.js +25 -0
- package/dist/esm/server/resources/sessions.js +78 -0
- package/dist/esm/types.js +14 -0
- package/dist/theme.css +84 -0
- package/dist/types/cli/bin.d.ts +1 -0
- package/dist/types/cli/init.d.ts +34 -0
- package/dist/types/client/components/account-sessions.d.ts +25 -0
- package/dist/types/client/components/apps-grid.d.ts +20 -0
- package/dist/types/client/components/auth-buttons.d.ts +31 -0
- package/dist/types/client/components/feature-gate.d.ts +57 -0
- package/dist/types/client/components/link-button.d.ts +20 -0
- package/dist/types/client/components/logo.d.ts +35 -0
- package/dist/types/client/components/plan-pricing.d.ts +96 -0
- package/dist/types/client/components/plans-page.d.ts +36 -0
- package/dist/types/client/components/plans.d.ts +34 -0
- package/dist/types/client/components/readiness.d.ts +52 -0
- package/dist/types/client/components/subscribe-dialog.d.ts +34 -0
- package/dist/types/client/components/subscription.d.ts +123 -0
- package/dist/types/client/components/usage-meter.d.ts +24 -0
- package/dist/types/client/components/user-menu.d.ts +86 -0
- package/dist/types/client/index.d.ts +47 -0
- package/dist/types/client/mark.d.ts +11 -0
- package/dist/types/client/provider.d.ts +209 -0
- package/dist/types/client/use-checkout.d.ts +93 -0
- package/dist/types/errors.d.ts +62 -0
- package/dist/types/format-relative-time.d.ts +2 -0
- package/dist/types/helpers.d.ts +33 -0
- package/dist/types/index.d.ts +17 -0
- package/dist/types/next/index.d.ts +136 -0
- package/dist/types/server/client.d.ts +83 -0
- package/dist/types/server/handler.d.ts +67 -0
- package/dist/types/server/http.d.ts +52 -0
- package/dist/types/server/index.d.ts +15 -0
- package/dist/types/server/resources/access.d.ts +17 -0
- package/dist/types/server/resources/ai.d.ts +24 -0
- package/dist/types/server/resources/apps.d.ts +12 -0
- package/dist/types/server/resources/billing.d.ts +41 -0
- package/dist/types/server/resources/drive.d.ts +26 -0
- package/dist/types/server/resources/entitlements.d.ts +31 -0
- package/dist/types/server/resources/identity.d.ts +76 -0
- package/dist/types/server/resources/logs.d.ts +20 -0
- package/dist/types/server/resources/otp.d.ts +17 -0
- package/dist/types/server/resources/sessions.d.ts +37 -0
- package/dist/types/types.d.ts +434 -0
- package/package.json +102 -0
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
"use strict";
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
exports.useConnectCheckout = useConnectCheckout;
|
|
5
|
+
exports.buildCancelUrl = buildCancelUrl;
|
|
6
|
+
exports.buildCheckoutUrl = buildCheckoutUrl;
|
|
7
|
+
const react_1 = require("react");
|
|
8
|
+
const provider_js_1 = require("./provider.js");
|
|
9
|
+
/** How often `/entitlements` is re-read while a checkout is open. */
|
|
10
|
+
const POLL_INTERVAL_MS = 3_000;
|
|
11
|
+
/** Give up polling after this long. Long enough for a card 3-D Secure detour, short enough not to poll a forgotten tab all day. */
|
|
12
|
+
const POLL_DEADLINE_MS = 15 * 60 * 1000;
|
|
13
|
+
/**
|
|
14
|
+
* Grace period after the Connect window closes before calling it abandoned.
|
|
15
|
+
*
|
|
16
|
+
* The window can close a beat before Connect's own write lands - and on the
|
|
17
|
+
* paid path the subscription is created `"created"` and confirmed by a
|
|
18
|
+
* webhook, so "closed" and "settled" are genuinely different moments. A few
|
|
19
|
+
* extra polls here is the difference between a correct success and telling a
|
|
20
|
+
* customer who just paid that nothing happened.
|
|
21
|
+
*/
|
|
22
|
+
const CLOSE_GRACE_MS = 12_000;
|
|
23
|
+
/**
|
|
24
|
+
* Drives a checkout that completes on Connect, not here.
|
|
25
|
+
*
|
|
26
|
+
* The partner's page cannot complete a subscription itself - see
|
|
27
|
+
* `ConnectPlans`' doc comment for why that is deliberate - so the flow is:
|
|
28
|
+
* open Connect, watch `/entitlements` until the entitlement changes, then
|
|
29
|
+
* refresh the session so every Connect component on the page repaints.
|
|
30
|
+
*
|
|
31
|
+
* **The result is read from Connect's own state, never from the window.**
|
|
32
|
+
* A closed window proves nothing: the customer may have paid and closed it,
|
|
33
|
+
* or opened it, thought better of it, and closed it. Polling is what tells
|
|
34
|
+
* the two apart, which is why the window closing only starts a short grace
|
|
35
|
+
* period rather than ending the run.
|
|
36
|
+
*/
|
|
37
|
+
function useConnectCheckout() {
|
|
38
|
+
const { fetchResource, invalidateResource, refresh } = (0, provider_js_1.useConnect)();
|
|
39
|
+
const [state, setState] = (0, react_1.useState)({ status: "idle", url: null });
|
|
40
|
+
// Everything the poll loop needs lives in a ref: it runs from a timer, and
|
|
41
|
+
// re-creating the loop whenever a piece of it changed identity would
|
|
42
|
+
// restart the very interval that is meant to survive re-renders.
|
|
43
|
+
const run = (0, react_1.useRef)(null);
|
|
44
|
+
const generation = (0, react_1.useRef)(0);
|
|
45
|
+
const stop = (0, react_1.useCallback)(() => {
|
|
46
|
+
generation.current += 1;
|
|
47
|
+
run.current = null;
|
|
48
|
+
}, []);
|
|
49
|
+
const reset = (0, react_1.useCallback)(() => {
|
|
50
|
+
stop();
|
|
51
|
+
setState({ status: "idle", url: null });
|
|
52
|
+
}, [stop]);
|
|
53
|
+
const start = (0, react_1.useCallback)(({ url, isSettled }) => {
|
|
54
|
+
stop();
|
|
55
|
+
// Opened synchronously inside the press handler - a `window.open` after
|
|
56
|
+
// an await is what pop-up blockers exist to stop, and it would be
|
|
57
|
+
// blocked in every browser regardless of the user's settings.
|
|
58
|
+
//
|
|
59
|
+
// No `noopener`: with it set, `window.open` returns `null` by
|
|
60
|
+
// specification, and this hook would then read every successfully
|
|
61
|
+
// opened checkout as blocked. The handle is the whole point - it is
|
|
62
|
+
// how the grace period below knows the customer closed the window.
|
|
63
|
+
// The window it hands an `opener` to is Connect's own origin, and
|
|
64
|
+
// cross-origin access is limited to navigating this page, which
|
|
65
|
+
// Connect does not do.
|
|
66
|
+
const opened = window.open(url, "connect_checkout", "popup,width=520,height=780");
|
|
67
|
+
if (!opened) {
|
|
68
|
+
// Not a failure to report as an error: the customer can still get
|
|
69
|
+
// there, they just have to click the link themselves.
|
|
70
|
+
setState({ status: "blocked", url });
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
run.current = {
|
|
74
|
+
url,
|
|
75
|
+
isSettled,
|
|
76
|
+
window: opened,
|
|
77
|
+
closedAt: null,
|
|
78
|
+
startedAt: Date.now(),
|
|
79
|
+
generation: generation.current,
|
|
80
|
+
};
|
|
81
|
+
setState({ status: "waiting", url });
|
|
82
|
+
}, [stop]);
|
|
83
|
+
(0, react_1.useEffect)(() => {
|
|
84
|
+
// One interval for the hook's whole life rather than one per run: it does
|
|
85
|
+
// nothing while `run.current` is null, and a run that starts from a press
|
|
86
|
+
// handler picks it up on the next tick without re-creating the timer.
|
|
87
|
+
let inFlight = false;
|
|
88
|
+
const poll = async () => {
|
|
89
|
+
const current = run.current;
|
|
90
|
+
if (!current || inFlight)
|
|
91
|
+
return;
|
|
92
|
+
const now = Date.now();
|
|
93
|
+
const expired = now - current.startedAt > POLL_DEADLINE_MS;
|
|
94
|
+
// `closed` is readable cross-origin; nothing else about the window is.
|
|
95
|
+
if (current.window?.closed && current.closedAt === null)
|
|
96
|
+
current.closedAt = now;
|
|
97
|
+
const graceElapsed = current.closedAt !== null && now - current.closedAt > CLOSE_GRACE_MS;
|
|
98
|
+
inFlight = true;
|
|
99
|
+
let settled = false;
|
|
100
|
+
try {
|
|
101
|
+
invalidateResource("/entitlements");
|
|
102
|
+
settled = current.isSettled(await fetchResource("/entitlements"));
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
// A bridge blip mid-checkout is not an answer either way - keep
|
|
106
|
+
// polling and let the deadline decide.
|
|
107
|
+
}
|
|
108
|
+
finally {
|
|
109
|
+
inFlight = false;
|
|
110
|
+
}
|
|
111
|
+
// The run may have been superseded or reset while that read was in
|
|
112
|
+
// flight; writing state from it now would resurrect a dead run.
|
|
113
|
+
if (run.current?.generation !== current.generation)
|
|
114
|
+
return;
|
|
115
|
+
if (settled) {
|
|
116
|
+
stop();
|
|
117
|
+
setState({ status: "completed", url: current.url });
|
|
118
|
+
// Session-wide, not just this path: `/session` carries its own
|
|
119
|
+
// `subscription` block, and every other Connect component on the
|
|
120
|
+
// page reads through the same cache.
|
|
121
|
+
void refresh();
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
if (graceElapsed || expired) {
|
|
125
|
+
stop();
|
|
126
|
+
setState({ status: "abandoned", url: current.url });
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
const interval = setInterval(() => void poll(), POLL_INTERVAL_MS);
|
|
130
|
+
// Coming back to this tab is the strongest hint available that something
|
|
131
|
+
// happened in the other one - poll immediately rather than waiting out
|
|
132
|
+
// the interval the customer is now staring at.
|
|
133
|
+
const onVisible = () => {
|
|
134
|
+
if (document.visibilityState === "visible")
|
|
135
|
+
void poll();
|
|
136
|
+
};
|
|
137
|
+
document.addEventListener("visibilitychange", onVisible);
|
|
138
|
+
return () => {
|
|
139
|
+
clearInterval(interval);
|
|
140
|
+
document.removeEventListener("visibilitychange", onVisible);
|
|
141
|
+
};
|
|
142
|
+
}, [fetchResource, invalidateResource, refresh, stop]);
|
|
143
|
+
return { ...state, start, reset };
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* The Connect URL that cancels a subscription.
|
|
147
|
+
*
|
|
148
|
+
* Its own builder rather than a mode on `buildCheckoutUrl`, because it takes
|
|
149
|
+
* no plan: cancelling is about the subscription, not about what to move it to.
|
|
150
|
+
* Lands on the same kind of intercepted modal the other two do
|
|
151
|
+
* (`app/@modal/(.)billing/.../cancel`).
|
|
152
|
+
*/
|
|
153
|
+
function buildCancelUrl({ appUrl, subscriptionId }) {
|
|
154
|
+
return new URL(`/billing/subscriptions/${encodeURIComponent(subscriptionId)}/cancel`, appUrl).toString();
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* The Connect URL that completes a subscribe or a plan change.
|
|
158
|
+
*
|
|
159
|
+
* Both land on a modal Connect renders over the relevant page (an intercepted
|
|
160
|
+
* parallel route - `app/@modal/(.)services/...` and
|
|
161
|
+
* `app/@modal/(.)billing/...`), so the customer arrives already looking at the
|
|
162
|
+
* confirmation for the plan they picked here, not at a catalogue they have to
|
|
163
|
+
* find it in again.
|
|
164
|
+
*/
|
|
165
|
+
function buildCheckoutUrl({ appUrl, serviceId, planId, subscriptionId, coupon, }) {
|
|
166
|
+
const path = subscriptionId
|
|
167
|
+
? `/billing/subscriptions/${encodeURIComponent(subscriptionId)}/change-plan/${encodeURIComponent(planId)}`
|
|
168
|
+
: `/services/${encodeURIComponent(serviceId)}/subscribe/${encodeURIComponent(planId)}`;
|
|
169
|
+
const url = new URL(path, appUrl);
|
|
170
|
+
if (coupon?.trim())
|
|
171
|
+
url.searchParams.set("coupon", coupon.trim());
|
|
172
|
+
return url.toString();
|
|
173
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ConnectError = void 0;
|
|
4
|
+
exports.isConnectError = isConnectError;
|
|
5
|
+
/**
|
|
6
|
+
* Every failure the Connect service API can report, as one class.
|
|
7
|
+
*
|
|
8
|
+
* The API speaks two error dialects and this collapses them: the
|
|
9
|
+
* `/api/v1/service/*` endpoints use `{ error: { code, message } }`, while the
|
|
10
|
+
* three `/sso/*` ones use OAuth2's `{ error, error_description }` (RFC 6749
|
|
11
|
+
* §5.2). Both land here with `code` populated, so integrating code branches on
|
|
12
|
+
* one field either way.
|
|
13
|
+
*/
|
|
14
|
+
class ConnectError extends Error {
|
|
15
|
+
name = "ConnectError";
|
|
16
|
+
/** HTTP status. `0` when the request never got a response (network failure, timeout). */
|
|
17
|
+
status;
|
|
18
|
+
/** `rate_limited`, `forbidden`, `invalid_token`, ... - branch on this, never on `message`. */
|
|
19
|
+
code;
|
|
20
|
+
/** From `Retry-After` on a 429, in milliseconds. `null` otherwise. */
|
|
21
|
+
retryAfterMs;
|
|
22
|
+
/** The endpoint that failed, e.g. `POST /sso/token` - present on every error. */
|
|
23
|
+
endpoint;
|
|
24
|
+
/** Parsed response body, when there was one. */
|
|
25
|
+
body;
|
|
26
|
+
constructor(init) {
|
|
27
|
+
super(init.message, init.cause === undefined ? undefined : { cause: init.cause });
|
|
28
|
+
this.status = init.status;
|
|
29
|
+
this.code = init.code;
|
|
30
|
+
this.endpoint = init.endpoint;
|
|
31
|
+
this.retryAfterMs = init.retryAfterMs ?? null;
|
|
32
|
+
this.body = init.body;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Whether trying the exact same request again could plausibly succeed -
|
|
36
|
+
* throttling, a transient server fault, or no response at all. The SDK
|
|
37
|
+
* already retries these internally (see `ConnectClientOptions.maxRetries`);
|
|
38
|
+
* this is for callers deciding whether to queue their own retry after the
|
|
39
|
+
* SDK has given up.
|
|
40
|
+
*/
|
|
41
|
+
get isRetryable() {
|
|
42
|
+
return this.status === 0 || this.status === 429 || this.status >= 500;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Whether the user's Connect sign-in is over and they need to go back
|
|
46
|
+
* through `identity.authorizeUrl()`. Collapses every session-side failure,
|
|
47
|
+
* matching how the API reports them - unknown, expired, revoked, or issued
|
|
48
|
+
* to a different service all arrive as one `invalid_token`.
|
|
49
|
+
*/
|
|
50
|
+
get isSessionExpired() {
|
|
51
|
+
return this.code === "invalid_token";
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Whether *your own API key* is dead - revoked or past its expiry - rather
|
|
55
|
+
* than the request being wrong.
|
|
56
|
+
*
|
|
57
|
+
* Connect answers `403` for every credential failure and keeps the code
|
|
58
|
+
* uniform (`forbidden`) whenever telling them apart would help someone
|
|
59
|
+
* guessing keys. These two are the exception: reaching them required
|
|
60
|
+
* presenting a key whose hash matched, so you already hold it, and what you
|
|
61
|
+
* need to know is that no retry and no scope change will fix this - the key
|
|
62
|
+
* has to be reissued from the service's Setup tab.
|
|
63
|
+
*
|
|
64
|
+
* Worth alerting on. It is the one failure that will not resolve itself and
|
|
65
|
+
* takes down every call your integration makes.
|
|
66
|
+
*/
|
|
67
|
+
get isKeyRejected() {
|
|
68
|
+
return this.code === "key_revoked" || this.code === "key_expired";
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
exports.ConnectError = ConnectError;
|
|
72
|
+
function isConnectError(error) {
|
|
73
|
+
return error instanceof ConnectError;
|
|
74
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.formatRelativeTime = formatRelativeTime;
|
|
4
|
+
/** "3m ago" / "2h ago" / "5d ago", falling back to a plain date past 30 days. */
|
|
5
|
+
function formatRelativeTime(date) {
|
|
6
|
+
const diffMinutes = Math.round((Date.now() - date.getTime()) / 60_000);
|
|
7
|
+
if (diffMinutes < 1)
|
|
8
|
+
return "just now";
|
|
9
|
+
if (diffMinutes < 60)
|
|
10
|
+
return `${diffMinutes}m ago`;
|
|
11
|
+
const diffHours = Math.round(diffMinutes / 60);
|
|
12
|
+
if (diffHours < 24)
|
|
13
|
+
return `${diffHours}h ago`;
|
|
14
|
+
const diffDays = Math.round(diffHours / 24);
|
|
15
|
+
if (diffDays < 30)
|
|
16
|
+
return `${diffDays}d ago`;
|
|
17
|
+
return date.toLocaleDateString();
|
|
18
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.blockingBlockers = blockingBlockers;
|
|
4
|
+
exports.normalizePlanFeatures = normalizePlanFeatures;
|
|
5
|
+
/**
|
|
6
|
+
* The blockers that actually deny access, in the order to show them.
|
|
7
|
+
*
|
|
8
|
+
* Advisory ones (no saved payment method mid-subscription, say) are worth
|
|
9
|
+
* surfacing but are not a reason to lock anyone out today - filtering them here
|
|
10
|
+
* keeps that judgement in one place rather than in every call site.
|
|
11
|
+
*/
|
|
12
|
+
function blockingBlockers(readiness) {
|
|
13
|
+
return readiness.readiness.blockers.filter((blocker) => blocker.severity === "blocking");
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Coerces either wire shape into `PlanFeature[]`.
|
|
17
|
+
*
|
|
18
|
+
* A bare key becomes `{ key, name: key, description: "" }` - the same
|
|
19
|
+
* fallback the server applies for a plan older than its service's feature
|
|
20
|
+
* catalog, so an un-upgraded Connect renders exactly what it always did
|
|
21
|
+
* rather than a column of blanks.
|
|
22
|
+
*
|
|
23
|
+
* Applied inside `BillingResource.listPlans`, so anything reading a plan
|
|
24
|
+
* through the SDK or its bridge already has the normalised shape and no
|
|
25
|
+
* component needs to know this ambiguity exists. Exported for a partner who
|
|
26
|
+
* hand-rolled a bridge instead of using `createConnectHandler`.
|
|
27
|
+
*/
|
|
28
|
+
function normalizePlanFeatures(features) {
|
|
29
|
+
return (features ?? []).map((feature) => typeof feature === "string" ? { key: feature, name: feature, description: "" } : feature);
|
|
30
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.normalizePlanFeatures = exports.blockingBlockers = exports.isConnectError = exports.ConnectError = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* `@cosmoops/connect-sdk` - the official client for the CosmoOps Connect
|
|
6
|
+
* **service-partner API** (`/api/v1/service/*`).
|
|
7
|
+
*
|
|
8
|
+
* This root entry is the isomorphic surface: wire types, the error class, and
|
|
9
|
+
* pure helpers. It pulls in neither Node-only code nor React, so it is safe to
|
|
10
|
+
* import from anywhere.
|
|
11
|
+
*
|
|
12
|
+
* - `@cosmoops/connect-sdk/server` - `ConnectClient` and `createConnectHandler`.
|
|
13
|
+
* Holds the API key. Never import this from browser code.
|
|
14
|
+
* - `@cosmoops/connect-sdk/client` - React components and hooks. Talks to your
|
|
15
|
+
* own origin only.
|
|
16
|
+
*/
|
|
17
|
+
var errors_js_1 = require("./errors.js");
|
|
18
|
+
Object.defineProperty(exports, "ConnectError", { enumerable: true, get: function () { return errors_js_1.ConnectError; } });
|
|
19
|
+
Object.defineProperty(exports, "isConnectError", { enumerable: true, get: function () { return errors_js_1.isConnectError; } });
|
|
20
|
+
var helpers_js_1 = require("./helpers.js");
|
|
21
|
+
Object.defineProperty(exports, "blockingBlockers", { enumerable: true, get: function () { return helpers_js_1.blockingBlockers; } });
|
|
22
|
+
Object.defineProperty(exports, "normalizePlanFeatures", { enumerable: true, get: function () { return helpers_js_1.normalizePlanFeatures; } });
|
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
"use strict";
|
|
3
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
4
|
+
if (k2 === undefined) k2 = k;
|
|
5
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
6
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
7
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
8
|
+
}
|
|
9
|
+
Object.defineProperty(o, k2, desc);
|
|
10
|
+
}) : (function(o, m, k, k2) {
|
|
11
|
+
if (k2 === undefined) k2 = k;
|
|
12
|
+
o[k2] = m[k];
|
|
13
|
+
}));
|
|
14
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
15
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
16
|
+
}) : function(o, v) {
|
|
17
|
+
o["default"] = v;
|
|
18
|
+
});
|
|
19
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
20
|
+
var ownKeys = function(o) {
|
|
21
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
22
|
+
var ar = [];
|
|
23
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
24
|
+
return ar;
|
|
25
|
+
};
|
|
26
|
+
return ownKeys(o);
|
|
27
|
+
};
|
|
28
|
+
return function (mod) {
|
|
29
|
+
if (mod && mod.__esModule) return mod;
|
|
30
|
+
var result = {};
|
|
31
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
32
|
+
__setModuleDefault(result, mod);
|
|
33
|
+
return result;
|
|
34
|
+
};
|
|
35
|
+
})();
|
|
36
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
37
|
+
exports.ConnectClient = exports.createConnectHandler = void 0;
|
|
38
|
+
exports.connectClient = connectClient;
|
|
39
|
+
exports.cookieSessionStore = cookieSessionStore;
|
|
40
|
+
exports.createConnectHandlers = createConnectHandlers;
|
|
41
|
+
exports.GET = handle;
|
|
42
|
+
exports.POST = handle;
|
|
43
|
+
exports.getConnectSession = getConnectSession;
|
|
44
|
+
exports.withConnect = withConnect;
|
|
45
|
+
const client_js_1 = require("../server/client.js");
|
|
46
|
+
const handler_js_1 = require("../server/handler.js");
|
|
47
|
+
/**
|
|
48
|
+
* Plug-and-play wiring for Next.js.
|
|
49
|
+
*
|
|
50
|
+
* Everything below is server-side. Nothing here is *statically* imported from
|
|
51
|
+
* `next`, so this module loads in App Router route handlers, the Node runtime
|
|
52
|
+
* and the edge runtime alike - and needs no peer dependency. The one place
|
|
53
|
+
* that does reach for `next/headers` does it behind a dynamic import, inside
|
|
54
|
+
* the function that needs it, and says so when it isn't there.
|
|
55
|
+
*
|
|
56
|
+
* The whole integration is two files:
|
|
57
|
+
*
|
|
58
|
+
* ```ts
|
|
59
|
+
* // app/api/connect/[...connect]/route.ts
|
|
60
|
+
* export { GET, POST } from "@cosmoops/connect-sdk/next";
|
|
61
|
+
* ```
|
|
62
|
+
*
|
|
63
|
+
* ```tsx
|
|
64
|
+
* // app/layout.tsx
|
|
65
|
+
* import { ConnectProvider } from "@cosmoops/connect-sdk/client";
|
|
66
|
+
* <ConnectProvider>{children}</ConnectProvider>
|
|
67
|
+
* ```
|
|
68
|
+
*
|
|
69
|
+
* Plus one line in `next.config.ts`, which is optional but catches the single
|
|
70
|
+
* misconfiguration that fails silently:
|
|
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:
|
|
79
|
+
*
|
|
80
|
+
* | Variable | Required | Purpose |
|
|
81
|
+
* | --- | --- | --- |
|
|
82
|
+
* | `CONNECT_API_KEY` | yes | server-side credential |
|
|
83
|
+
* | `CONNECT_CLIENT_ID` | yes | OAuth client id |
|
|
84
|
+
* | `NEXT_PUBLIC_CONNECT_CLIENT_ID` | yes | same value, for `<ConnectProvider>` |
|
|
85
|
+
* | `CONNECT_APP_URL` | no | non-production Connect instance |
|
|
86
|
+
* | `CONNECT_SESSION_COOKIE` | no | session cookie name |
|
|
87
|
+
*
|
|
88
|
+
* Register `{your origin}/api/connect/callback` as a redirect URI in the
|
|
89
|
+
* service's Setup tab. That is the only step this cannot do for you.
|
|
90
|
+
*/
|
|
91
|
+
const DEFAULT_COOKIE = "connect_session";
|
|
92
|
+
function requireEnv(name) {
|
|
93
|
+
const value = process.env[name];
|
|
94
|
+
if (!value) {
|
|
95
|
+
throw new Error(`@cosmoops/connect-sdk/next: ${name} is not set. Add it to your environment - see the module docs for the full list.`);
|
|
96
|
+
}
|
|
97
|
+
return value;
|
|
98
|
+
}
|
|
99
|
+
let cached = null;
|
|
100
|
+
/**
|
|
101
|
+
* The shared server client, built from the environment on first use.
|
|
102
|
+
*
|
|
103
|
+
* Lazy on purpose: reading `CONNECT_API_KEY` at module load would throw during
|
|
104
|
+
* `next build`, where route modules are imported but never invoked and secrets
|
|
105
|
+
* are frequently absent.
|
|
106
|
+
*/
|
|
107
|
+
function connectClient(overrides = {}) {
|
|
108
|
+
if (cached && Object.keys(overrides).length === 0)
|
|
109
|
+
return cached;
|
|
110
|
+
const client = new client_js_1.ConnectClient({
|
|
111
|
+
apiKey: requireEnv("CONNECT_API_KEY"),
|
|
112
|
+
clientId: requireEnv("CONNECT_CLIENT_ID"),
|
|
113
|
+
...(process.env.CONNECT_APP_URL ? { baseUrl: process.env.CONNECT_APP_URL } : {}),
|
|
114
|
+
...overrides,
|
|
115
|
+
});
|
|
116
|
+
if (Object.keys(overrides).length === 0)
|
|
117
|
+
cached = client;
|
|
118
|
+
return client;
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* An httpOnly cookie holding the opaque `sessionToken`.
|
|
122
|
+
*
|
|
123
|
+
* Not signed or encrypted, deliberately. The token is opaque and useless
|
|
124
|
+
* without the `client_secret` that never leaves the server, so a tampered
|
|
125
|
+
* value buys an attacker a 401 rather than a forged identity - and a signing
|
|
126
|
+
* secret would be one more thing to rotate for protection already provided by
|
|
127
|
+
* the token's own design. `SameSite=Lax` because `Strict` would withhold the
|
|
128
|
+
* cookie on the redirect back from Connect.
|
|
129
|
+
*/
|
|
130
|
+
function cookieSessionStore(cookieName = process.env.CONNECT_SESSION_COOKIE ?? DEFAULT_COOKIE) {
|
|
131
|
+
const serialise = (value, maxAge, secure) => [
|
|
132
|
+
`${cookieName}=${encodeURIComponent(value)}`,
|
|
133
|
+
"Path=/",
|
|
134
|
+
"HttpOnly",
|
|
135
|
+
"SameSite=Lax",
|
|
136
|
+
secure ? "Secure" : "",
|
|
137
|
+
`Max-Age=${maxAge}`,
|
|
138
|
+
]
|
|
139
|
+
.filter(Boolean)
|
|
140
|
+
.join("; ");
|
|
141
|
+
const isSecure = (request) => new URL(request.url).protocol === "https:";
|
|
142
|
+
return {
|
|
143
|
+
read(request) {
|
|
144
|
+
const header = request.headers.get("cookie");
|
|
145
|
+
if (!header)
|
|
146
|
+
return null;
|
|
147
|
+
for (const part of header.split(";")) {
|
|
148
|
+
const [key, ...rest] = part.trim().split("=");
|
|
149
|
+
if (key === cookieName)
|
|
150
|
+
return decodeURIComponent(rest.join("=")) || null;
|
|
151
|
+
}
|
|
152
|
+
return null;
|
|
153
|
+
},
|
|
154
|
+
write(request, response, sessionToken) {
|
|
155
|
+
// ~15 days, matching the token's own lifetime - a cookie that outlives
|
|
156
|
+
// the token it carries only produces confident 401s.
|
|
157
|
+
response.headers.append("set-cookie", serialise(sessionToken, 15 * 24 * 60 * 60, isSecure(request)));
|
|
158
|
+
},
|
|
159
|
+
clear(request, response) {
|
|
160
|
+
response.headers.append("set-cookie", serialise("", 0, isSecure(request)));
|
|
161
|
+
},
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* The catch-all route handlers, ready to re-export.
|
|
166
|
+
*
|
|
167
|
+
* Only needed when you are changing something - a different mount point, a
|
|
168
|
+
* session store that isn't a cookie, a post-login destination that isn't `/`.
|
|
169
|
+
* The zero-configuration case is the `GET` / `POST` re-export below, which is
|
|
170
|
+
* this function with no arguments.
|
|
171
|
+
*
|
|
172
|
+
* ```ts
|
|
173
|
+
* // app/api/connect/[...connect]/route.ts
|
|
174
|
+
* import { createConnectHandlers } from "@cosmoops/connect-sdk/next";
|
|
175
|
+
* export const { GET, POST } = createConnectHandlers({ basePath: "/api/auth" });
|
|
176
|
+
* ```
|
|
177
|
+
*
|
|
178
|
+
* `GET` and `POST` are the same function - the bridge routes on method and
|
|
179
|
+
* path itself, so Next only needs to know both verbs reach it.
|
|
180
|
+
*/
|
|
181
|
+
function createConnectHandlers(options = {}) {
|
|
182
|
+
const handler = (0, handler_js_1.createConnectHandler)({
|
|
183
|
+
client: options.client ?? connectClient(),
|
|
184
|
+
session: options.session ?? cookieSessionStore(),
|
|
185
|
+
...(options.basePath ? { basePath: options.basePath } : {}),
|
|
186
|
+
...(options.defaultRedirect ? { defaultRedirect: options.defaultRedirect } : {}),
|
|
187
|
+
});
|
|
188
|
+
return { GET: handler, POST: handler };
|
|
189
|
+
}
|
|
190
|
+
let defaultHandler = null;
|
|
191
|
+
/**
|
|
192
|
+
* The bridge itself, for the default mount at `/api/connect`.
|
|
193
|
+
*
|
|
194
|
+
* Built on the first request rather than at module load, for the same reason
|
|
195
|
+
* `connectClient` is lazy: `next build` imports every route module, and an
|
|
196
|
+
* environment-reading constructor at the top level turns a missing secret in
|
|
197
|
+
* CI into a failed build of routes that were never going to run there.
|
|
198
|
+
*/
|
|
199
|
+
async function handle(request) {
|
|
200
|
+
defaultHandler ??= (0, handler_js_1.createConnectHandler)({
|
|
201
|
+
client: connectClient(),
|
|
202
|
+
session: cookieSessionStore(),
|
|
203
|
+
});
|
|
204
|
+
return defaultHandler(request);
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Rebuilds a `Request` from the headers of the Server Component, Route Handler
|
|
208
|
+
* or Server Action currently rendering.
|
|
209
|
+
*
|
|
210
|
+
* `next/headers` is imported dynamically and only from here, so this module
|
|
211
|
+
* still has no static dependency on `next` - which is what lets it load on the
|
|
212
|
+
* edge runtime and in a non-Next backend that only wants `createConnectHandler`.
|
|
213
|
+
*/
|
|
214
|
+
async function requestFromHeaders() {
|
|
215
|
+
let read;
|
|
216
|
+
try {
|
|
217
|
+
({ headers: read } = await Promise.resolve().then(() => __importStar(require("next/headers"))));
|
|
218
|
+
}
|
|
219
|
+
catch (cause) {
|
|
220
|
+
throw new Error("@cosmoops/connect-sdk/next: getConnectSession() with no arguments needs `next/headers`, which only exists inside a Server Component, Route Handler or Server Action. Pass the `Request` explicitly anywhere else.", { cause });
|
|
221
|
+
}
|
|
222
|
+
// Deliberately outside the try. `headers()` *throws* to tell Next this route
|
|
223
|
+
// is dynamic - `DYNAMIC_SERVER_USAGE` is control flow, not a fault - and a
|
|
224
|
+
// catch around it converts a route that should have opted out of prerendering
|
|
225
|
+
// into a failed build. Only the import is guarded, because only the import
|
|
226
|
+
// can be legitimately absent.
|
|
227
|
+
const incoming = (await read());
|
|
228
|
+
// Only the cookie header is actually read, but the URL has to be well-formed
|
|
229
|
+
// and its protocol is what a custom session store would use to decide
|
|
230
|
+
// `Secure` - so reconstruct the real one rather than hardcoding localhost.
|
|
231
|
+
const host = incoming.get("x-forwarded-host") ?? incoming.get("host") ?? "localhost";
|
|
232
|
+
const proto = incoming.get("x-forwarded-proto") ?? (host.startsWith("localhost") ? "http" : "https");
|
|
233
|
+
return new Request(`${proto}://${host}/`, { headers: incoming });
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* The signed-in user for a Server Component, Route Handler or Server Action,
|
|
237
|
+
* or `null`.
|
|
238
|
+
*
|
|
239
|
+
* ```tsx
|
|
240
|
+
* // app/dashboard/page.tsx
|
|
241
|
+
* const session = await getConnectSession();
|
|
242
|
+
* if (!session) redirect("/api/connect/signin?next=/dashboard");
|
|
243
|
+
* ```
|
|
244
|
+
*
|
|
245
|
+
* Pass a `Request` to use it somewhere `next/headers` does not exist - a
|
|
246
|
+
* framework-agnostic handler, a test, a custom server. Both forms take the
|
|
247
|
+
* same path afterwards; the argument is only how the cookie is found.
|
|
248
|
+
*
|
|
249
|
+
* Reads `cookies()` transitively, so under `cacheComponents` this belongs
|
|
250
|
+
* inside a `<Suspense>` boundary (or a `'use cache: private'` function) like
|
|
251
|
+
* any other request-scoped read - a page that awaits it above every boundary
|
|
252
|
+
* gives up its static shell.
|
|
253
|
+
*
|
|
254
|
+
* Returns `null` for every failure - an expired session and a missing one both
|
|
255
|
+
* mean "not signed in", and a page guard should not need a try/catch.
|
|
256
|
+
*/
|
|
257
|
+
async function getConnectSession(request, options = {}) {
|
|
258
|
+
const store = options.session ?? cookieSessionStore();
|
|
259
|
+
const token = await store.read(request ?? (await requestFromHeaders()));
|
|
260
|
+
if (!token)
|
|
261
|
+
return null;
|
|
262
|
+
try {
|
|
263
|
+
return await (options.client ?? connectClient()).identity.getUser(token);
|
|
264
|
+
}
|
|
265
|
+
catch {
|
|
266
|
+
return null;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
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
|
+
var handler_js_2 = require("../server/handler.js");
|
|
309
|
+
Object.defineProperty(exports, "createConnectHandler", { enumerable: true, get: function () { return handler_js_2.createConnectHandler; } });
|
|
310
|
+
var client_js_2 = require("../server/client.js");
|
|
311
|
+
Object.defineProperty(exports, "ConnectClient", { enumerable: true, get: function () { return client_js_2.ConnectClient; } });
|