@cosmoops/connect-sdk 0.33.0 → 0.34.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +159 -0
- package/README.md +17 -5
- package/dist/cjs/client/components/access-gate.js +98 -2
- package/dist/cjs/client/components/subscription.js +6 -0
- package/dist/cjs/client/provider.js +5 -2
- package/dist/cjs/client/use-checkout.js +3 -1
- package/dist/cjs/helpers.js +46 -0
- package/dist/cjs/index.js +5 -1
- package/dist/cjs/next/index.js +10 -3
- package/dist/cjs/server/client.js +1 -1
- package/dist/cjs/server/handler.js +162 -28
- package/dist/cjs/server/resources/billing.js +5 -0
- package/dist/cjs/server/resources/entitlements.js +47 -0
- package/dist/esm/client/components/access-gate.js +99 -3
- package/dist/esm/client/components/subscription.js +6 -0
- package/dist/esm/client/provider.js +5 -2
- package/dist/esm/client/use-checkout.js +3 -1
- package/dist/esm/helpers.js +43 -0
- package/dist/esm/index.js +1 -0
- package/dist/esm/next/index.js +10 -3
- package/dist/esm/server/client.js +1 -1
- package/dist/esm/server/handler.js +162 -28
- package/dist/esm/server/resources/billing.js +5 -0
- package/dist/esm/server/resources/entitlements.js +47 -0
- package/dist/types/client/components/access-gate.d.ts +9 -0
- package/dist/types/client/components/subscription.d.ts +6 -0
- package/dist/types/client/provider.d.ts +155 -51
- package/dist/types/client/use-checkout.d.ts +18 -1
- package/dist/types/helpers.d.ts +36 -1
- package/dist/types/index.d.ts +1 -0
- package/dist/types/server/client.d.ts +1 -1
- package/dist/types/server/resources/billing.d.ts +4 -0
- package/dist/types/server/resources/entitlements.d.ts +40 -1
- package/dist/types/types.d.ts +164 -0
- package/package.json +1 -1
|
@@ -55,6 +55,99 @@ function sameState(a, b) {
|
|
|
55
55
|
diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
|
|
56
56
|
return diff === 0;
|
|
57
57
|
}
|
|
58
|
+
/**
|
|
59
|
+
* How many sign-ins one browser may have in flight at once.
|
|
60
|
+
*
|
|
61
|
+
* The state cookie used to hold a *single* `state:next` pair, so a second
|
|
62
|
+
* `GET /signin` overwrote the first. Two tabs - or one abandoned attempt and
|
|
63
|
+
* one fresh one - and the older round trip came back to a cookie naming a
|
|
64
|
+
* state it had never heard of: `connect_error=state_mismatch`, which the host
|
|
65
|
+
* app's own session gate answers by silently firing another `/signin`, putting
|
|
66
|
+
* the visitor back on the sign-in screen with nothing on the page saying why.
|
|
67
|
+
*
|
|
68
|
+
* Five is far past what anyone opens by hand and still a cookie nobody
|
|
69
|
+
* notices - each entry is a 43-character state plus a short path. Entries are
|
|
70
|
+
* newest-first and the oldest fall off the end, so the tab a visitor is
|
|
71
|
+
* actually looking at is the last one to be forgotten.
|
|
72
|
+
*
|
|
73
|
+
* It does not weaken the CSRF check: a callback must still present one of
|
|
74
|
+
* these exact 256-bit values, and each still expires with the cookie.
|
|
75
|
+
*/
|
|
76
|
+
const MAX_PENDING_STATES = 5;
|
|
77
|
+
/** `-` and `r` are the only flag fields this format writes - see `decodePendingState`. */
|
|
78
|
+
const RETRIED_FLAG = "r";
|
|
79
|
+
const NO_FLAGS = "-";
|
|
80
|
+
/**
|
|
81
|
+
* The state cookie's wire format: one entry per in-flight sign-in, newest
|
|
82
|
+
* first, joined by `|`.
|
|
83
|
+
*
|
|
84
|
+
* An entry is `state:flags:next` with the destination **percent-encoded**
|
|
85
|
+
* rather than written raw, and that is what makes splitting on a delimiter
|
|
86
|
+
* safe: `state` is base64url so it can hold neither, and an encoded path
|
|
87
|
+
* cannot be read as a field boundary however many colons or pipes it
|
|
88
|
+
* contains. The raw single-pair format this replaces is exactly why `retried`
|
|
89
|
+
* and the silent marker had to live outside the cookie - the split was too
|
|
90
|
+
* fragile to carry a third field.
|
|
91
|
+
*/
|
|
92
|
+
function encodePendingStates(entries) {
|
|
93
|
+
return entries
|
|
94
|
+
.map((entry) => `${entry.state}:${entry.retried ? RETRIED_FLAG : NO_FLAGS}:${encodeURIComponent(entry.next)}`)
|
|
95
|
+
.join("|");
|
|
96
|
+
}
|
|
97
|
+
function decodePendingStates(raw) {
|
|
98
|
+
if (!raw)
|
|
99
|
+
return [];
|
|
100
|
+
const entries = [];
|
|
101
|
+
for (const part of raw.split("|")) {
|
|
102
|
+
const entry = decodePendingState(part);
|
|
103
|
+
if (entry)
|
|
104
|
+
entries.push(entry);
|
|
105
|
+
}
|
|
106
|
+
return entries;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* One entry, tolerating the format this replaced.
|
|
110
|
+
*
|
|
111
|
+
* A cookie minted by the previous version is a single raw `state:next` pair,
|
|
112
|
+
* and sign-ins in flight across a deploy carry one. The flag field is a known
|
|
113
|
+
* one-character set, so anything else in that position means the remainder is
|
|
114
|
+
* a legacy destination - including one that itself contains a colon. Dropping
|
|
115
|
+
* those instead would answer every mid-deploy callback with
|
|
116
|
+
* `state_mismatch`.
|
|
117
|
+
*/
|
|
118
|
+
function decodePendingState(part) {
|
|
119
|
+
const firstColon = part.indexOf(":");
|
|
120
|
+
if (firstColon <= 0)
|
|
121
|
+
return null;
|
|
122
|
+
const state = part.slice(0, firstColon);
|
|
123
|
+
const remainder = part.slice(firstColon + 1);
|
|
124
|
+
const secondColon = remainder.indexOf(":");
|
|
125
|
+
const flags = secondColon === -1 ? "" : remainder.slice(0, secondColon);
|
|
126
|
+
const hasFlags = flags === NO_FLAGS || flags === RETRIED_FLAG;
|
|
127
|
+
return {
|
|
128
|
+
state,
|
|
129
|
+
next: decodeDestination(hasFlags ? remainder.slice(secondColon + 1) : remainder),
|
|
130
|
+
retried: flags === RETRIED_FLAG,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* `decodeURIComponent` throws on a lone `%`, which a legacy raw destination
|
|
135
|
+
* may well hold. The undecoded string then goes through `safeRedirect` like
|
|
136
|
+
* any other candidate, so a malformed one costs the default destination
|
|
137
|
+
* rather than a 500.
|
|
138
|
+
*/
|
|
139
|
+
function decodeDestination(value) {
|
|
140
|
+
try {
|
|
141
|
+
return decodeURIComponent(value);
|
|
142
|
+
}
|
|
143
|
+
catch {
|
|
144
|
+
return value;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
/** Appends `connect_error=<code>` to a destination that may already carry a query. */
|
|
148
|
+
function withError(next, code) {
|
|
149
|
+
return `${next}${next.includes("?") ? "&" : "?"}connect_error=${encodeURIComponent(code)}`;
|
|
150
|
+
}
|
|
58
151
|
function readCookie(request, name) {
|
|
59
152
|
const header = request.headers.get("cookie");
|
|
60
153
|
if (!header)
|
|
@@ -127,6 +220,10 @@ export function createConnectHandler(options) {
|
|
|
127
220
|
* the cross-site redirect back from Connect, which is the one request that
|
|
128
221
|
* has to read it. Scoped to `basePath` so it is never attached to ordinary
|
|
129
222
|
* page loads.
|
|
223
|
+
*
|
|
224
|
+
* Holds every in-flight round trip rather than the latest one - see
|
|
225
|
+
* `MAX_PENDING_STATES` and `encodePendingStates`. An empty value clears it,
|
|
226
|
+
* which is what an exchange that consumed the last pending entry wants.
|
|
130
227
|
*/
|
|
131
228
|
function stateCookie(value, secure) {
|
|
132
229
|
const cleared = value === "";
|
|
@@ -956,6 +1053,17 @@ export function createConnectHandler(options) {
|
|
|
956
1053
|
* alongside the post-login destination, and bounces the browser to
|
|
957
1054
|
* Connect.
|
|
958
1055
|
*
|
|
1056
|
+
* The new entry is **prepended to whatever is already pending** rather
|
|
1057
|
+
* than replacing it, so opening the app in a second tab no longer
|
|
1058
|
+
* invalidates the first - see `MAX_PENDING_STATES`.
|
|
1059
|
+
*
|
|
1060
|
+
* `?connect_retry=1` is stamped onto the entry here, which is the only
|
|
1061
|
+
* place that marker can be read: it is a query parameter on *this* URL,
|
|
1062
|
+
* and the `redirect_uri` handed to Connect is a registered, exact-match
|
|
1063
|
+
* value that cannot carry it. The callback used to look for it on its own
|
|
1064
|
+
* URL, where it could never arrive - so both retry bounds below were
|
|
1065
|
+
* unreachable and `login_required` could loop indefinitely.
|
|
1066
|
+
*
|
|
959
1067
|
* Registered only when the session store can `write` - without that, the
|
|
960
1068
|
* callback would have nowhere to put the session it just earned.
|
|
961
1069
|
*/
|
|
@@ -971,9 +1079,13 @@ export function createConnectHandler(options) {
|
|
|
971
1079
|
state,
|
|
972
1080
|
...(prompt ? { prompt } : {}),
|
|
973
1081
|
});
|
|
974
|
-
// State and destination travel together in one
|
|
1082
|
+
// State and destination travel together in one entry so the callback
|
|
975
1083
|
// cannot end up with one and not the other.
|
|
976
|
-
const
|
|
1084
|
+
const pending = [
|
|
1085
|
+
{ state, next, retried: url.searchParams.get("connect_retry") === "1" },
|
|
1086
|
+
...decodePendingStates(readCookie(request, stateCookieName)),
|
|
1087
|
+
].slice(0, MAX_PENDING_STATES);
|
|
1088
|
+
const cookies = [stateCookie(encodePendingStates(pending), secure)];
|
|
977
1089
|
if (prompt === "none")
|
|
978
1090
|
cookies.push(silentCookie("1", secure));
|
|
979
1091
|
return redirect(authorizeUrl, cookies);
|
|
@@ -986,20 +1098,49 @@ export function createConnectHandler(options) {
|
|
|
986
1098
|
"GET /callback": async (request) => {
|
|
987
1099
|
const url = new URL(request.url);
|
|
988
1100
|
const secure = url.protocol === "https:";
|
|
989
|
-
const
|
|
990
|
-
const next = safeRedirect(destinationParts.join(":") || null, defaultRedirect);
|
|
991
|
-
const clearState = stateCookie("", secure);
|
|
1101
|
+
const pending = decodePendingStates(readCookie(request, stateCookieName));
|
|
992
1102
|
const returnedState = url.searchParams.get("state") ?? "";
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1103
|
+
const matched = pending.find((entry) => sameState(entry.state, returnedState)) ?? null;
|
|
1104
|
+
// Every *other* in-flight sign-in survives this one. Clearing the whole
|
|
1105
|
+
// cookie is what made a second tab collateral damage: finishing in one
|
|
1106
|
+
// tab left the other holding a state the server had just forgotten.
|
|
1107
|
+
const settleState = stateCookie(encodePendingStates(pending.filter((entry) => entry !== matched)), secure);
|
|
1108
|
+
// Only ever this flow's own destination. With no match there is no
|
|
1109
|
+
// destination to trust - a `next` belonging to a different round trip is
|
|
1110
|
+
// somebody else's business, so that case takes the default.
|
|
1111
|
+
const next = safeRedirect(matched?.next ?? null, defaultRedirect);
|
|
1112
|
+
// Read before the state check, not after. A `prompt=none` probe must
|
|
1113
|
+
// never escalate into a visible sign-in - including the retry below -
|
|
1114
|
+
// and the mismatch branch is reachable for a silent probe whose cookie
|
|
1115
|
+
// has expired.
|
|
1001
1116
|
const wasSilent = readCookie(request, silentCookieName) === "1";
|
|
1002
1117
|
const clearSilent = silentCookie("", secure);
|
|
1118
|
+
/** One more interactive attempt, marked so the next pass can tell. */
|
|
1119
|
+
const retrySignIn = () => {
|
|
1120
|
+
const retry = new URL(`${url.origin}${basePath}/signin`);
|
|
1121
|
+
retry.searchParams.set("next", next);
|
|
1122
|
+
retry.searchParams.set("connect_retry", "1");
|
|
1123
|
+
return redirect(retry.toString(), [settleState, clearSilent]);
|
|
1124
|
+
};
|
|
1125
|
+
if (!matched) {
|
|
1126
|
+
// A stale tab whose cookie has expired, more entries in flight than
|
|
1127
|
+
// `MAX_PENDING_STATES` holds, or a forged callback. None of them mints
|
|
1128
|
+
// a session and none is worth an error page.
|
|
1129
|
+
//
|
|
1130
|
+
// One retry, because the overwhelmingly common cause is a round trip
|
|
1131
|
+
// whose own entry is simply gone and whose visitor Connect has already
|
|
1132
|
+
// signed in - that completes in a single hop. The bound reads *any*
|
|
1133
|
+
// pending entry's marker rather than the matched one, there being
|
|
1134
|
+
// none: a browser that has already been bounced once gets the error
|
|
1135
|
+
// instead of another lap. Without it this is the silent infinite loop,
|
|
1136
|
+
// since the host app's session gate answers `/dashboard` with no
|
|
1137
|
+
// session by firing `/signin` again.
|
|
1138
|
+
if (wasSilent)
|
|
1139
|
+
return redirect(next, [settleState, clearSilent]);
|
|
1140
|
+
if (!pending.some((entry) => entry.retried))
|
|
1141
|
+
return retrySignIn();
|
|
1142
|
+
return redirect(withError(next, "state_mismatch"), [settleState, clearSilent]);
|
|
1143
|
+
}
|
|
1003
1144
|
const error = url.searchParams.get("error");
|
|
1004
1145
|
if (error) {
|
|
1005
1146
|
// A silent (`prompt=none`) probe that came back empty-handed. All
|
|
@@ -1014,27 +1155,20 @@ export function createConnectHandler(options) {
|
|
|
1014
1155
|
// if they're already here" into "force everyone to log in", which is
|
|
1015
1156
|
// the precise opposite of the feature.
|
|
1016
1157
|
if (wasSilent) {
|
|
1017
|
-
return redirect(next, [
|
|
1158
|
+
return redirect(next, [settleState, clearSilent]);
|
|
1018
1159
|
}
|
|
1019
1160
|
// `login_required` is normal traffic, not a fault: a new account gets
|
|
1020
1161
|
// pulled out to their inbox to verify, and the ~15 minute request
|
|
1021
1162
|
// expires before they return. Connect has already signed them in, so
|
|
1022
|
-
// one silent retry completes in a single hop.
|
|
1023
|
-
// it - a genuine loop must surface, not spin.
|
|
1024
|
-
if (error === "login_required" &&
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
retry.searchParams.set("connect_retry", "1");
|
|
1028
|
-
return redirect(retry.toString(), [clearState, clearSilent]);
|
|
1029
|
-
}
|
|
1030
|
-
return redirect(`${next}${next.includes("?") ? "&" : "?"}connect_error=${encodeURIComponent(error)}`, [
|
|
1031
|
-
clearState,
|
|
1032
|
-
clearSilent,
|
|
1033
|
-
]);
|
|
1163
|
+
// one silent retry completes in a single hop. The marker on this
|
|
1164
|
+
// flow's own entry bounds it - a genuine loop must surface, not spin.
|
|
1165
|
+
if (error === "login_required" && !matched.retried)
|
|
1166
|
+
return retrySignIn();
|
|
1167
|
+
return redirect(withError(next, error), [settleState, clearSilent]);
|
|
1034
1168
|
}
|
|
1035
1169
|
const code = url.searchParams.get("code");
|
|
1036
1170
|
if (!code) {
|
|
1037
|
-
return redirect(
|
|
1171
|
+
return redirect(withError(next, "missing_code"), [settleState, clearSilent]);
|
|
1038
1172
|
}
|
|
1039
1173
|
const result = await client.identity.exchangeCode({
|
|
1040
1174
|
code,
|
|
@@ -1043,7 +1177,7 @@ export function createConnectHandler(options) {
|
|
|
1043
1177
|
// Verified before the session is stored, not after - an unverifiable
|
|
1044
1178
|
// token must never become a signed-in user.
|
|
1045
1179
|
await client.identity.verifyIdToken(result.idToken);
|
|
1046
|
-
const response = redirect(next, [
|
|
1180
|
+
const response = redirect(next, [settleState, clearSilent]);
|
|
1047
1181
|
await session.write(request, response, result.sessionToken);
|
|
1048
1182
|
return response;
|
|
1049
1183
|
},
|
|
@@ -14,6 +14,10 @@ export class BillingResource {
|
|
|
14
14
|
* independently of the instance it talks to. Without the coercion a partner
|
|
15
15
|
* pointed at a Connect that has not upgraded yet renders a feature list of
|
|
16
16
|
* blanks - which is exactly what happened. See `normalizePlanFeatures`.
|
|
17
|
+
*
|
|
18
|
+
* `limits` is defaulted for the same reason, one version later: an instance
|
|
19
|
+
* that predates limits sends no such field, and a pricing grid mapping over
|
|
20
|
+
* `plan.limits` would throw rather than simply show no limit rows.
|
|
17
21
|
*/
|
|
18
22
|
async listPlans() {
|
|
19
23
|
const response = await request(this.transport, { method: "GET", path: "/plans" });
|
|
@@ -21,6 +25,7 @@ export class BillingResource {
|
|
|
21
25
|
plans: (response.plans ?? []).map((plan) => ({
|
|
22
26
|
...plan,
|
|
23
27
|
features: normalizePlanFeatures(plan.features),
|
|
28
|
+
limits: plan.limits ?? [],
|
|
24
29
|
})),
|
|
25
30
|
};
|
|
26
31
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { pathSegment, request } from "../http.js";
|
|
2
|
+
import { NO_PLAN_LIMIT, planLimitAllows, planLimitFor } from "../../helpers.js";
|
|
2
3
|
/**
|
|
3
4
|
* The two checks worth making on every request: may this workspace be here at
|
|
4
5
|
* all, and what is it paying for.
|
|
@@ -42,6 +43,52 @@ export class EntitlementsResource {
|
|
|
42
43
|
const result = await this.getSubscription(workspaceId);
|
|
43
44
|
return result.hasActiveSubscription && (result.plan?.features.includes(feature) ?? false);
|
|
44
45
|
}
|
|
46
|
+
/**
|
|
47
|
+
* How much of a metered resource this workspace may have - the numeric
|
|
48
|
+
* counterpart of `hasFeature`, for the resources your application counts
|
|
49
|
+
* itself (sites, projects, pipelines).
|
|
50
|
+
*
|
|
51
|
+
* **This is the effective ceiling, not the plan's own number.** Connect has
|
|
52
|
+
* already folded in whatever the add-ons on the subscription raise it by, so
|
|
53
|
+
* "Starter includes 1 site, they bought two 5-site packs" arrives here as
|
|
54
|
+
* `{ model: "limited", value: 11 }`. You never join the plan against the
|
|
55
|
+
* add-on list yourself, and a customer who buys another pack needs no change
|
|
56
|
+
* on your side.
|
|
57
|
+
*
|
|
58
|
+
* Returns `{ model: "none" }` rather than throwing when there is no active
|
|
59
|
+
* subscription, when the plan carries no answer for this key, and when the
|
|
60
|
+
* Connect instance predates limits entirely - the same reasoning
|
|
61
|
+
* `hasFeature` gives for answering `false`: "not entitled" is the same answer
|
|
62
|
+
* either way, and a limit check should not need a try/catch around it.
|
|
63
|
+
*
|
|
64
|
+
* Connect does not enforce this. It cannot see what you are counting, so it
|
|
65
|
+
* hands you the number and your code refuses the next row - `canAdd` is that
|
|
66
|
+
* comparison if you already have the count.
|
|
67
|
+
*/
|
|
68
|
+
async getLimit(workspaceId, key) {
|
|
69
|
+
const result = await this.getSubscription(workspaceId);
|
|
70
|
+
if (!result.hasActiveSubscription)
|
|
71
|
+
return NO_PLAN_LIMIT;
|
|
72
|
+
return planLimitFor(result.plan?.limits, key);
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Whether this workspace may add `additional` more of a limited resource,
|
|
76
|
+
* given how many it already has.
|
|
77
|
+
*
|
|
78
|
+
* You supply the count, because only your application can produce it. This
|
|
79
|
+
* is `getLimit` plus `planLimitAllows`, which exists so the comparison is
|
|
80
|
+
* not rewritten per call site - see that helper for the three ways of
|
|
81
|
+
* getting it wrong.
|
|
82
|
+
*
|
|
83
|
+
* ```ts
|
|
84
|
+
* if (!(await connect.entitlements.canAdd(workspaceId, "sites", sites.length))) {
|
|
85
|
+
* return refuse("Your plan's site limit is reached.");
|
|
86
|
+
* }
|
|
87
|
+
* ```
|
|
88
|
+
*/
|
|
89
|
+
async canAdd(workspaceId, key, currentCount, additional = 1) {
|
|
90
|
+
return planLimitAllows(await this.getLimit(workspaceId, key), currentCount, additional);
|
|
91
|
+
}
|
|
45
92
|
/**
|
|
46
93
|
* One page of the workspaces subscribed to your application, newest first.
|
|
47
94
|
*
|
|
@@ -23,6 +23,15 @@ export type ConnectAccessGateState = "loading"
|
|
|
23
23
|
| "clear";
|
|
24
24
|
export interface ConnectAccessGateResult {
|
|
25
25
|
state: ConnectAccessGateState;
|
|
26
|
+
/**
|
|
27
|
+
* Re-reads entitlements, evicting the cached answer first.
|
|
28
|
+
*
|
|
29
|
+
* Exposed because a blocker is almost always resolved **somewhere this tab
|
|
30
|
+
* cannot see** - on Connect, in another tab or on a phone - and nothing
|
|
31
|
+
* about that reaches this one. The gate wires it to the tab regaining
|
|
32
|
+
* focus; call it yourself if your app learns of a change some other way.
|
|
33
|
+
*/
|
|
34
|
+
refresh(): void;
|
|
26
35
|
/** The blocker driving a hard-block state (`suspended`/`no_subscription`/`onboarding_pending`/`blocked`). `null` otherwise. */
|
|
27
36
|
blocker: ReadinessBlocker | null;
|
|
28
37
|
/** The `payment_retry_pending` blocker, only when `state === "payment_retry"`. Carries `graceEndsAt`. */
|
|
@@ -61,6 +61,12 @@ export declare function ConnectSubscriptionCard({ plansHref, className }: Connec
|
|
|
61
61
|
* count against, so it renders nothing rather than a meter that is always
|
|
62
62
|
* empty. Under `"per-seat"` going over is billable rather than blocked, so it
|
|
63
63
|
* says what the extra costs instead of warning.
|
|
64
|
+
*
|
|
65
|
+
* `detail.seatCount` is a live count of the members holding a role for this
|
|
66
|
+
* application. It used to be the subscription's last-billed snapshot, which
|
|
67
|
+
* only ever moved on a `"per-seat"` plan - so a `"fixed"` single-seat plan
|
|
68
|
+
* read "0 of 1 seat used" permanently, including when its one seat was taken
|
|
69
|
+
* and Connect was already refusing the next assignment.
|
|
64
70
|
*/
|
|
65
71
|
export declare function ConnectSeatUsage({ plan, detail, className, }: Readonly<{
|
|
66
72
|
plan: SubscribedPlan;
|
|
@@ -196,7 +196,21 @@ export interface ConnectProviderProps {
|
|
|
196
196
|
* not a Connect URL - it is your backend that exchanges the code.
|
|
197
197
|
*/
|
|
198
198
|
redirectUri?: string;
|
|
199
|
-
/**
|
|
199
|
+
/**
|
|
200
|
+
* Override for non-production Connect instances. Defaults to
|
|
201
|
+
* `https://connect.cosmoops.com`.
|
|
202
|
+
*
|
|
203
|
+
* The twin of `CONNECT_APP_URL`, and *not* derived from it - that variable
|
|
204
|
+
* is read on your server and never reaches the browser. This value is where
|
|
205
|
+
* the user menu's Profile, Billing and Invoices destinations open, where
|
|
206
|
+
* `ConnectAppsGrid` sends an unsubscribed application, where checkout
|
|
207
|
+
* opens, and - if you pass `redirectUri` and so build the authorize URL
|
|
208
|
+
* here rather than in your own `/signin` route - where sign-in goes. Point
|
|
209
|
+
* the server somewhere else and leave this alone and the two halves
|
|
210
|
+
* disagree, so pass both or neither. Your root layout is a Server
|
|
211
|
+
* Component, so `appUrl={process.env.CONNECT_APP_URL}` is enough and needs
|
|
212
|
+
* no browser-visible variable.
|
|
213
|
+
*/
|
|
200
214
|
appUrl?: string;
|
|
201
215
|
/**
|
|
202
216
|
* Replace the `fetch` used to reach your bridge. The browser-side twin of
|
|
@@ -327,11 +341,20 @@ export declare function useConnectEntitlements(): {
|
|
|
327
341
|
isLoading: boolean;
|
|
328
342
|
error: string | null;
|
|
329
343
|
/**
|
|
330
|
-
* Forces a fresh read -
|
|
331
|
-
*
|
|
332
|
-
*
|
|
333
|
-
*
|
|
334
|
-
*
|
|
344
|
+
* Forces a fresh read - evicts this path's cache entry and re-runs the
|
|
345
|
+
* read.
|
|
346
|
+
*
|
|
347
|
+
* It used only to re-trigger the effect, on the assumption that the caller
|
|
348
|
+
* had just evicted the entry itself through `mutateResource`. That made it
|
|
349
|
+
* useless for the other reason to refetch: **something changed somewhere
|
|
350
|
+
* this tab cannot see.** The provider's cache is a `Map` of resolved
|
|
351
|
+
* promises with no expiry, so a `refetch()` without the eviction re-read
|
|
352
|
+
* the same settled promise and reported the same stale answer forever -
|
|
353
|
+
* which is what left `<ConnectAccessGate>` showing "Choose a plan" in a tab
|
|
354
|
+
* whose workspace had subscribed in a different one.
|
|
355
|
+
*
|
|
356
|
+
* Still safe after a `mutateResource` against the same path: evicting an
|
|
357
|
+
* entry that is already gone is a no-op.
|
|
335
358
|
*/
|
|
336
359
|
refetch(): void;
|
|
337
360
|
};
|
|
@@ -347,11 +370,20 @@ export declare function useConnectAddonUsage(): {
|
|
|
347
370
|
isLoading: boolean;
|
|
348
371
|
error: string | null;
|
|
349
372
|
/**
|
|
350
|
-
* Forces a fresh read -
|
|
351
|
-
*
|
|
352
|
-
*
|
|
353
|
-
*
|
|
354
|
-
*
|
|
373
|
+
* Forces a fresh read - evicts this path's cache entry and re-runs the
|
|
374
|
+
* read.
|
|
375
|
+
*
|
|
376
|
+
* It used only to re-trigger the effect, on the assumption that the caller
|
|
377
|
+
* had just evicted the entry itself through `mutateResource`. That made it
|
|
378
|
+
* useless for the other reason to refetch: **something changed somewhere
|
|
379
|
+
* this tab cannot see.** The provider's cache is a `Map` of resolved
|
|
380
|
+
* promises with no expiry, so a `refetch()` without the eviction re-read
|
|
381
|
+
* the same settled promise and reported the same stale answer forever -
|
|
382
|
+
* which is what left `<ConnectAccessGate>` showing "Choose a plan" in a tab
|
|
383
|
+
* whose workspace had subscribed in a different one.
|
|
384
|
+
*
|
|
385
|
+
* Still safe after a `mutateResource` against the same path: evicting an
|
|
386
|
+
* entry that is already gone is a no-op.
|
|
355
387
|
*/
|
|
356
388
|
refetch(): void;
|
|
357
389
|
};
|
|
@@ -361,11 +393,20 @@ export declare function useConnectPlans(): {
|
|
|
361
393
|
isLoading: boolean;
|
|
362
394
|
error: string | null;
|
|
363
395
|
/**
|
|
364
|
-
* Forces a fresh read -
|
|
365
|
-
*
|
|
366
|
-
*
|
|
367
|
-
*
|
|
368
|
-
*
|
|
396
|
+
* Forces a fresh read - evicts this path's cache entry and re-runs the
|
|
397
|
+
* read.
|
|
398
|
+
*
|
|
399
|
+
* It used only to re-trigger the effect, on the assumption that the caller
|
|
400
|
+
* had just evicted the entry itself through `mutateResource`. That made it
|
|
401
|
+
* useless for the other reason to refetch: **something changed somewhere
|
|
402
|
+
* this tab cannot see.** The provider's cache is a `Map` of resolved
|
|
403
|
+
* promises with no expiry, so a `refetch()` without the eviction re-read
|
|
404
|
+
* the same settled promise and reported the same stale answer forever -
|
|
405
|
+
* which is what left `<ConnectAccessGate>` showing "Choose a plan" in a tab
|
|
406
|
+
* whose workspace had subscribed in a different one.
|
|
407
|
+
*
|
|
408
|
+
* Still safe after a `mutateResource` against the same path: evicting an
|
|
409
|
+
* entry that is already gone is a no-op.
|
|
369
410
|
*/
|
|
370
411
|
refetch(): void;
|
|
371
412
|
};
|
|
@@ -384,11 +425,20 @@ export declare function useConnectCompanyProfile(): {
|
|
|
384
425
|
isLoading: boolean;
|
|
385
426
|
error: string | null;
|
|
386
427
|
/**
|
|
387
|
-
* Forces a fresh read -
|
|
388
|
-
*
|
|
389
|
-
*
|
|
390
|
-
*
|
|
391
|
-
*
|
|
428
|
+
* Forces a fresh read - evicts this path's cache entry and re-runs the
|
|
429
|
+
* read.
|
|
430
|
+
*
|
|
431
|
+
* It used only to re-trigger the effect, on the assumption that the caller
|
|
432
|
+
* had just evicted the entry itself through `mutateResource`. That made it
|
|
433
|
+
* useless for the other reason to refetch: **something changed somewhere
|
|
434
|
+
* this tab cannot see.** The provider's cache is a `Map` of resolved
|
|
435
|
+
* promises with no expiry, so a `refetch()` without the eviction re-read
|
|
436
|
+
* the same settled promise and reported the same stale answer forever -
|
|
437
|
+
* which is what left `<ConnectAccessGate>` showing "Choose a plan" in a tab
|
|
438
|
+
* whose workspace had subscribed in a different one.
|
|
439
|
+
*
|
|
440
|
+
* Still safe after a `mutateResource` against the same path: evicting an
|
|
441
|
+
* entry that is already gone is a no-op.
|
|
392
442
|
*/
|
|
393
443
|
refetch(): void;
|
|
394
444
|
};
|
|
@@ -400,11 +450,20 @@ export declare function useConnectApps(): {
|
|
|
400
450
|
isLoading: boolean;
|
|
401
451
|
error: string | null;
|
|
402
452
|
/**
|
|
403
|
-
* Forces a fresh read -
|
|
404
|
-
*
|
|
405
|
-
*
|
|
406
|
-
*
|
|
407
|
-
*
|
|
453
|
+
* Forces a fresh read - evicts this path's cache entry and re-runs the
|
|
454
|
+
* read.
|
|
455
|
+
*
|
|
456
|
+
* It used only to re-trigger the effect, on the assumption that the caller
|
|
457
|
+
* had just evicted the entry itself through `mutateResource`. That made it
|
|
458
|
+
* useless for the other reason to refetch: **something changed somewhere
|
|
459
|
+
* this tab cannot see.** The provider's cache is a `Map` of resolved
|
|
460
|
+
* promises with no expiry, so a `refetch()` without the eviction re-read
|
|
461
|
+
* the same settled promise and reported the same stale answer forever -
|
|
462
|
+
* which is what left `<ConnectAccessGate>` showing "Choose a plan" in a tab
|
|
463
|
+
* whose workspace had subscribed in a different one.
|
|
464
|
+
*
|
|
465
|
+
* Still safe after a `mutateResource` against the same path: evicting an
|
|
466
|
+
* entry that is already gone is a no-op.
|
|
408
467
|
*/
|
|
409
468
|
refetch(): void;
|
|
410
469
|
};
|
|
@@ -423,11 +482,20 @@ export declare function useConnectWorkspaces(): {
|
|
|
423
482
|
isLoading: boolean;
|
|
424
483
|
error: string | null;
|
|
425
484
|
/**
|
|
426
|
-
* Forces a fresh read -
|
|
427
|
-
*
|
|
428
|
-
*
|
|
429
|
-
*
|
|
430
|
-
*
|
|
485
|
+
* Forces a fresh read - evicts this path's cache entry and re-runs the
|
|
486
|
+
* read.
|
|
487
|
+
*
|
|
488
|
+
* It used only to re-trigger the effect, on the assumption that the caller
|
|
489
|
+
* had just evicted the entry itself through `mutateResource`. That made it
|
|
490
|
+
* useless for the other reason to refetch: **something changed somewhere
|
|
491
|
+
* this tab cannot see.** The provider's cache is a `Map` of resolved
|
|
492
|
+
* promises with no expiry, so a `refetch()` without the eviction re-read
|
|
493
|
+
* the same settled promise and reported the same stale answer forever -
|
|
494
|
+
* which is what left `<ConnectAccessGate>` showing "Choose a plan" in a tab
|
|
495
|
+
* whose workspace had subscribed in a different one.
|
|
496
|
+
*
|
|
497
|
+
* Still safe after a `mutateResource` against the same path: evicting an
|
|
498
|
+
* entry that is already gone is a no-op.
|
|
431
499
|
*/
|
|
432
500
|
refetch(): void;
|
|
433
501
|
};
|
|
@@ -442,11 +510,20 @@ export declare function useConnectWorkspaceMembers(): {
|
|
|
442
510
|
isLoading: boolean;
|
|
443
511
|
error: string | null;
|
|
444
512
|
/**
|
|
445
|
-
* Forces a fresh read -
|
|
446
|
-
*
|
|
447
|
-
*
|
|
448
|
-
*
|
|
449
|
-
*
|
|
513
|
+
* Forces a fresh read - evicts this path's cache entry and re-runs the
|
|
514
|
+
* read.
|
|
515
|
+
*
|
|
516
|
+
* It used only to re-trigger the effect, on the assumption that the caller
|
|
517
|
+
* had just evicted the entry itself through `mutateResource`. That made it
|
|
518
|
+
* useless for the other reason to refetch: **something changed somewhere
|
|
519
|
+
* this tab cannot see.** The provider's cache is a `Map` of resolved
|
|
520
|
+
* promises with no expiry, so a `refetch()` without the eviction re-read
|
|
521
|
+
* the same settled promise and reported the same stale answer forever -
|
|
522
|
+
* which is what left `<ConnectAccessGate>` showing "Choose a plan" in a tab
|
|
523
|
+
* whose workspace had subscribed in a different one.
|
|
524
|
+
*
|
|
525
|
+
* Still safe after a `mutateResource` against the same path: evicting an
|
|
526
|
+
* entry that is already gone is a no-op.
|
|
450
527
|
*/
|
|
451
528
|
refetch(): void;
|
|
452
529
|
};
|
|
@@ -456,11 +533,20 @@ export declare function useConnectIntegrations(): {
|
|
|
456
533
|
isLoading: boolean;
|
|
457
534
|
error: string | null;
|
|
458
535
|
/**
|
|
459
|
-
* Forces a fresh read -
|
|
460
|
-
*
|
|
461
|
-
*
|
|
462
|
-
*
|
|
463
|
-
*
|
|
536
|
+
* Forces a fresh read - evicts this path's cache entry and re-runs the
|
|
537
|
+
* read.
|
|
538
|
+
*
|
|
539
|
+
* It used only to re-trigger the effect, on the assumption that the caller
|
|
540
|
+
* had just evicted the entry itself through `mutateResource`. That made it
|
|
541
|
+
* useless for the other reason to refetch: **something changed somewhere
|
|
542
|
+
* this tab cannot see.** The provider's cache is a `Map` of resolved
|
|
543
|
+
* promises with no expiry, so a `refetch()` without the eviction re-read
|
|
544
|
+
* the same settled promise and reported the same stale answer forever -
|
|
545
|
+
* which is what left `<ConnectAccessGate>` showing "Choose a plan" in a tab
|
|
546
|
+
* whose workspace had subscribed in a different one.
|
|
547
|
+
*
|
|
548
|
+
* Still safe after a `mutateResource` against the same path: evicting an
|
|
549
|
+
* entry that is already gone is a no-op.
|
|
464
550
|
*/
|
|
465
551
|
refetch(): void;
|
|
466
552
|
};
|
|
@@ -476,11 +562,20 @@ export declare function useConnectSessions(): {
|
|
|
476
562
|
isLoading: boolean;
|
|
477
563
|
error: string | null;
|
|
478
564
|
/**
|
|
479
|
-
* Forces a fresh read -
|
|
480
|
-
*
|
|
481
|
-
*
|
|
482
|
-
*
|
|
483
|
-
*
|
|
565
|
+
* Forces a fresh read - evicts this path's cache entry and re-runs the
|
|
566
|
+
* read.
|
|
567
|
+
*
|
|
568
|
+
* It used only to re-trigger the effect, on the assumption that the caller
|
|
569
|
+
* had just evicted the entry itself through `mutateResource`. That made it
|
|
570
|
+
* useless for the other reason to refetch: **something changed somewhere
|
|
571
|
+
* this tab cannot see.** The provider's cache is a `Map` of resolved
|
|
572
|
+
* promises with no expiry, so a `refetch()` without the eviction re-read
|
|
573
|
+
* the same settled promise and reported the same stale answer forever -
|
|
574
|
+
* which is what left `<ConnectAccessGate>` showing "Choose a plan" in a tab
|
|
575
|
+
* whose workspace had subscribed in a different one.
|
|
576
|
+
*
|
|
577
|
+
* Still safe after a `mutateResource` against the same path: evicting an
|
|
578
|
+
* entry that is already gone is a no-op.
|
|
484
579
|
*/
|
|
485
580
|
refetch(): void;
|
|
486
581
|
};
|
|
@@ -512,11 +607,20 @@ export declare function useConnectResource<T>(path: string, options?: Readonly<U
|
|
|
512
607
|
isLoading: boolean;
|
|
513
608
|
error: string | null;
|
|
514
609
|
/**
|
|
515
|
-
* Forces a fresh read -
|
|
516
|
-
*
|
|
517
|
-
*
|
|
518
|
-
*
|
|
519
|
-
*
|
|
610
|
+
* Forces a fresh read - evicts this path's cache entry and re-runs the
|
|
611
|
+
* read.
|
|
612
|
+
*
|
|
613
|
+
* It used only to re-trigger the effect, on the assumption that the caller
|
|
614
|
+
* had just evicted the entry itself through `mutateResource`. That made it
|
|
615
|
+
* useless for the other reason to refetch: **something changed somewhere
|
|
616
|
+
* this tab cannot see.** The provider's cache is a `Map` of resolved
|
|
617
|
+
* promises with no expiry, so a `refetch()` without the eviction re-read
|
|
618
|
+
* the same settled promise and reported the same stale answer forever -
|
|
619
|
+
* which is what left `<ConnectAccessGate>` showing "Choose a plan" in a tab
|
|
620
|
+
* whose workspace had subscribed in a different one.
|
|
621
|
+
*
|
|
622
|
+
* Still safe after a `mutateResource` against the same path: evicting an
|
|
623
|
+
* entry that is already gone is a no-op.
|
|
520
624
|
*/
|
|
521
625
|
refetch(): void;
|
|
522
626
|
};
|