@withone/connect 0.10.0 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +144 -26
- package/dist/button.d.ts +20 -14
- package/dist/constants.d.ts +5 -1
- package/dist/flow.d.ts +18 -0
- package/dist/index.cjs.js +1 -1
- package/dist/index.d.ts +7 -4
- package/dist/index.esm.js +1 -1
- package/dist/platforms.d.ts +1 -1
- package/dist/react.cjs.js +2 -1
- package/dist/react.d.ts +20 -1
- package/dist/react.esm.js +2 -1
- package/dist/return.d.ts +5 -2
- package/dist/server/index.cjs.js +157 -45
- package/dist/server/index.d.ts +11 -3
- package/dist/server/index.esm.js +157 -46
- package/dist/server/oauth.d.ts +4 -0
- package/dist/server/types.d.ts +47 -4
- package/dist/svelte.cjs.js +1 -1
- package/dist/svelte.d.ts +1 -1
- package/dist/svelte.esm.js +1 -1
- package/dist/types.d.ts +71 -29
- package/dist/vue.cjs.js +1 -1
- package/dist/vue.d.ts +71 -22
- package/dist/vue.esm.js +1 -1
- package/package.json +4 -1
- package/skills/one-connect/SKILL.md +55 -19
- package/src/button.ts +450 -295
- package/src/constants.ts +5 -1
- package/src/flow.ts +171 -0
- package/src/index.ts +17 -19
- package/src/platforms.ts +41 -6
- package/src/react.ts +119 -21
- package/src/return.ts +31 -9
- package/src/server/index.ts +188 -51
- package/src/server/oauth.ts +16 -0
- package/src/server/types.ts +49 -4
- package/src/svelte.ts +5 -13
- package/src/types.ts +73 -29
- package/src/vue.ts +42 -26
- package/dist/useOneConnect.d.ts +0 -13
- package/dist/wrapper-options.d.ts +0 -28
- package/src/useOneConnect.ts +0 -72
- package/src/wrapper-options.ts +0 -68
package/dist/server/index.esm.js
CHANGED
|
@@ -4,7 +4,8 @@ import { randomBytes, createHash } from 'node:crypto';
|
|
|
4
4
|
* The SDK reads them off the page URL when the tab comes home, so
|
|
5
5
|
* there is no completion page to build. */
|
|
6
6
|
const RETURN_STATUS_PARAM = "one_connect";
|
|
7
|
-
|
|
7
|
+
/** Why the flow failed, as a code (see ConnectFailureCode). */
|
|
8
|
+
const RETURN_ERROR_PARAM = "one_connect_error";
|
|
8
9
|
|
|
9
10
|
/** One's production API. Point `oneApiUrl` elsewhere for development. */
|
|
10
11
|
const DEFAULT_ONE_API_URL = "https://api.withone.ai";
|
|
@@ -70,6 +71,18 @@ function tenancyHeaders(accessToken) {
|
|
|
70
71
|
}
|
|
71
72
|
}
|
|
72
73
|
|
|
74
|
+
/** When a refresh token stops working, in epoch milliseconds, from its
|
|
75
|
+
* `exp` claim. Null when the token carries no readable expiry, in which
|
|
76
|
+
* case only One can say whether it still works. */
|
|
77
|
+
function refreshTokenExpiresAt(refreshToken) {
|
|
78
|
+
try {
|
|
79
|
+
const payload = JSON.parse(Buffer.from(refreshToken.split(".")[1], "base64url").toString());
|
|
80
|
+
return typeof payload.exp === "number" && Number.isFinite(payload.exp) ? payload.exp * 1000 : null;
|
|
81
|
+
} catch {
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
73
86
|
/** The scopes the token was granted, from its claims. Display only. */
|
|
74
87
|
function tokenScopes(accessToken) {
|
|
75
88
|
try {
|
|
@@ -89,8 +102,8 @@ function tokenScopes(accessToken) {
|
|
|
89
102
|
|
|
90
103
|
/**
|
|
91
104
|
* Where the app keeps each user's tokens: its database, a cache, an
|
|
92
|
-
* encrypted cookie. The SDK never sees a token outside these
|
|
93
|
-
*
|
|
105
|
+
* encrypted cookie. The SDK never sees a token outside these calls.
|
|
106
|
+
* `userId` is the app's own id for its user.
|
|
94
107
|
*/
|
|
95
108
|
|
|
96
109
|
/** The transaction cookie the authorize leg sets and the callback reads. */
|
|
@@ -99,6 +112,14 @@ function tokenScopes(accessToken) {
|
|
|
99
112
|
|
|
100
113
|
/** One catalog action for a platform. */
|
|
101
114
|
|
|
115
|
+
/**
|
|
116
|
+
* - `not_connected`: no tokens are stored for this user.
|
|
117
|
+
* - `refresh_failed`: One declared the grant dead (revoked, expired or
|
|
118
|
+
* reused). The tokens were cleared; ask the user to connect again.
|
|
119
|
+
* - `request_failed`: One answered with an error or could not be
|
|
120
|
+
* reached. During a refresh the tokens are kept, so retry later.
|
|
121
|
+
*/
|
|
122
|
+
|
|
102
123
|
class OneConnectError extends Error {
|
|
103
124
|
constructor(code, message, status) {
|
|
104
125
|
super(message);
|
|
@@ -126,12 +147,26 @@ class OneConnectError extends Error {
|
|
|
126
147
|
* const reply = await oneConnect.runAction(userId, { connectionKey, actionId, method, path });
|
|
127
148
|
*
|
|
128
149
|
* The Next.js and Node adapters turn the first two into route handlers.
|
|
150
|
+
*
|
|
151
|
+
* Refresh. One's access token lives an hour by default, its refresh token 30
|
|
152
|
+
* days; every refresh rotates both, and One treats a second use of a
|
|
153
|
+
* rotated refresh token as theft and revokes the whole grant. So the
|
|
154
|
+
* client refreshes one user at a time (in this process always, across
|
|
155
|
+
* processes through `tokenStore.withLock`), re-reads the store before
|
|
156
|
+
* spending a refresh token, clears tokens only when One declares the
|
|
157
|
+
* grant dead, and never lets a failing old pair delete a newer one.
|
|
129
158
|
*/
|
|
130
159
|
|
|
131
160
|
/** Refresh this long before expiry, so a call never races the clock. */
|
|
132
161
|
const REFRESH_MARGIN_MS = 60_000;
|
|
133
162
|
const CATALOG_PAGE_SIZE = 100;
|
|
134
163
|
const CATALOG_MAX_PAGES = 20;
|
|
164
|
+
|
|
165
|
+
/** What One's token endpoint answered. A network failure throws instead. */
|
|
166
|
+
|
|
167
|
+
/** The one refusal that means the grant is gone for good: revoked by the
|
|
168
|
+
* user, expired, or burned by a reused refresh token (RFC 6749 §5.2). */
|
|
169
|
+
const isDeadGrant = answer => !answer.ok && answer.status === 400 && answer.error === "invalid_grant";
|
|
135
170
|
function createOneConnect(config) {
|
|
136
171
|
const oneApiUrl = (config.oneApiUrl ?? DEFAULT_ONE_API_URL).replace(/\/+$/, "");
|
|
137
172
|
const authorizeUrl = `${oneApiUrl}/oauth/authorize`;
|
|
@@ -146,13 +181,13 @@ function createOneConnect(config) {
|
|
|
146
181
|
/** One refresh in flight per user: two concurrent refreshes with the
|
|
147
182
|
* same refresh token trip One's reuse detection. */
|
|
148
183
|
const refreshing = new Map();
|
|
149
|
-
const returnUrl = (status,
|
|
184
|
+
const returnUrl = (status, code) => {
|
|
150
185
|
const url = new URL(returnTo, config.redirectUri);
|
|
151
186
|
url.searchParams.set(RETURN_STATUS_PARAM, status);
|
|
152
|
-
if (
|
|
187
|
+
if (code) url.searchParams.set(RETURN_ERROR_PARAM, code);
|
|
153
188
|
return url.toString();
|
|
154
189
|
};
|
|
155
|
-
const
|
|
190
|
+
const postToken = async body => {
|
|
156
191
|
const response = await fetch(tokenUrl, {
|
|
157
192
|
method: "POST",
|
|
158
193
|
headers: {
|
|
@@ -161,10 +196,40 @@ function createOneConnect(config) {
|
|
|
161
196
|
},
|
|
162
197
|
body
|
|
163
198
|
});
|
|
164
|
-
if (
|
|
165
|
-
|
|
199
|
+
if (response.ok) return {
|
|
200
|
+
ok: true,
|
|
201
|
+
body: await response.json()
|
|
202
|
+
};
|
|
203
|
+
let error;
|
|
204
|
+
try {
|
|
205
|
+
const refusal = await response.json();
|
|
206
|
+
if (typeof refusal.error === "string") error = refusal.error;
|
|
207
|
+
} catch {
|
|
208
|
+
/* not an OAuth error body: a proxy page or a server error */
|
|
209
|
+
}
|
|
210
|
+
return {
|
|
211
|
+
ok: false,
|
|
212
|
+
status: response.status,
|
|
213
|
+
error
|
|
214
|
+
};
|
|
215
|
+
};
|
|
216
|
+
const exchange = async body => {
|
|
217
|
+
const answer = await postToken(body);
|
|
218
|
+
if (!answer.ok) {
|
|
219
|
+
throw new OneConnectError("request_failed", `One refused the token request (HTTP ${answer.status}).`, answer.status);
|
|
166
220
|
}
|
|
167
|
-
return
|
|
221
|
+
return answer.body;
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
/** Runs `run` under the app's cross-process lock for this user, when
|
|
225
|
+
* the store has one. */
|
|
226
|
+
const locked = (userId, run) => tokenStore.withLock ? tokenStore.withLock(userId, run) : run();
|
|
227
|
+
|
|
228
|
+
/** Whether either token of the pair stops working within `withinMs`. */
|
|
229
|
+
const expiresWithin = (tokens, withinMs) => {
|
|
230
|
+
const horizon = Date.now() + withinMs;
|
|
231
|
+
const refreshExpiresAt = refreshTokenExpiresAt(tokens.refreshToken);
|
|
232
|
+
return tokens.expiresAt <= horizon || refreshExpiresAt !== null && refreshExpiresAt <= horizon;
|
|
168
233
|
};
|
|
169
234
|
const toTokens = response => ({
|
|
170
235
|
accessToken: response.access_token,
|
|
@@ -196,17 +261,18 @@ function createOneConnect(config) {
|
|
|
196
261
|
const oauthError = params.get("error");
|
|
197
262
|
const cookieName = state ? txCookieName(state) : undefined;
|
|
198
263
|
const verifier = cookieName ? input.getCookie(cookieName) : undefined;
|
|
199
|
-
const fail = (
|
|
200
|
-
outcome,
|
|
264
|
+
const fail = (failure, message) => ({
|
|
265
|
+
outcome: failure === "declined" ? "declined" : "failed",
|
|
266
|
+
code: failure,
|
|
201
267
|
message,
|
|
202
|
-
redirectUrl: returnUrl("error",
|
|
268
|
+
redirectUrl: returnUrl("error", failure),
|
|
203
269
|
clearCookieName: cookieName
|
|
204
270
|
});
|
|
205
|
-
if (oauthError === "access_denied") return fail("declined", "
|
|
271
|
+
if (oauthError === "access_denied") return fail("declined", "The user cancelled on One's page.");
|
|
206
272
|
if (oauthError) return fail("failed", `One reported an error: ${oauthError}.`);
|
|
207
|
-
// The returned state names its own cookie. No cookie means a
|
|
208
|
-
// or
|
|
209
|
-
if (!code || !state || !verifier) return fail("
|
|
273
|
+
// The returned state names its own cookie. No cookie means a stale,
|
|
274
|
+
// foreign or forged state; the code is never exchanged in that case.
|
|
275
|
+
if (!code || !state || !verifier) return fail("expired", "The attempt expired, or its state cookie was missing.");
|
|
210
276
|
try {
|
|
211
277
|
const tokens = toTokens(await exchange(new URLSearchParams({
|
|
212
278
|
grant_type: "authorization_code",
|
|
@@ -214,7 +280,9 @@ function createOneConnect(config) {
|
|
|
214
280
|
redirect_uri: config.redirectUri,
|
|
215
281
|
code_verifier: verifier
|
|
216
282
|
})));
|
|
217
|
-
|
|
283
|
+
// Under the lock, so a refresh in flight on another server cannot
|
|
284
|
+
// interleave with this save.
|
|
285
|
+
await locked(input.userId, () => tokenStore.saveTokens(input.userId, tokens));
|
|
218
286
|
} catch (error) {
|
|
219
287
|
const status = error instanceof OneConnectError ? error.status : undefined;
|
|
220
288
|
return fail("failed", status ? `One rejected the code exchange (HTTP ${status}).` : "One could not be reached to complete the connection.");
|
|
@@ -225,39 +293,81 @@ function createOneConnect(config) {
|
|
|
225
293
|
clearCookieName: cookieName
|
|
226
294
|
};
|
|
227
295
|
};
|
|
228
|
-
const
|
|
296
|
+
const notConnected = () => new OneConnectError("not_connected", "This user is not connected.");
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* The grant behind `failed` is dead. Clears it, unless a newer pair
|
|
300
|
+
* landed while it was failing (a reconnect's callback, another
|
|
301
|
+
* server's refresh): that pair is returned instead, because the user
|
|
302
|
+
* did nothing wrong and deleting it would disconnect them.
|
|
303
|
+
*/
|
|
304
|
+
const retire = async (userId, failed, status) => {
|
|
305
|
+
const latest = await tokenStore.loadTokens(userId);
|
|
306
|
+
if (latest && latest.refreshToken !== failed.refreshToken) return latest;
|
|
307
|
+
await tokenStore.clearTokens(userId, failed);
|
|
308
|
+
throw new OneConnectError("refresh_failed", "The connection to One has expired or was revoked. Ask the user to connect again.", status);
|
|
309
|
+
};
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* The one place a refresh token is spent. Under the app's lock it
|
|
313
|
+
* re-reads the store, and refreshes only when `stillNeeded` says the
|
|
314
|
+
* stored pair still needs it: another process may have refreshed while
|
|
315
|
+
* this one waited, and spending the same refresh token twice makes One
|
|
316
|
+
* revoke the grant.
|
|
317
|
+
*/
|
|
318
|
+
const refreshUnderLock = (userId, stillNeeded) => locked(userId, async () => {
|
|
319
|
+
const current = await tokenStore.loadTokens(userId);
|
|
320
|
+
if (!current) throw notConnected();
|
|
321
|
+
if (!stillNeeded(current)) return current;
|
|
322
|
+
|
|
323
|
+
// An expired refresh token cannot work, and One answers one with a
|
|
324
|
+
// server error rather than invalid_grant, so settle it here.
|
|
325
|
+
const refreshExpiresAt = refreshTokenExpiresAt(current.refreshToken);
|
|
326
|
+
if (refreshExpiresAt !== null && refreshExpiresAt <= Date.now()) return retire(userId, current);
|
|
327
|
+
let answer;
|
|
328
|
+
try {
|
|
329
|
+
answer = await postToken(new URLSearchParams({
|
|
330
|
+
grant_type: "refresh_token",
|
|
331
|
+
refresh_token: current.refreshToken
|
|
332
|
+
}));
|
|
333
|
+
} catch {
|
|
334
|
+
throw new OneConnectError("request_failed", "One could not be reached to refresh the connection. The tokens were kept; try again.");
|
|
335
|
+
}
|
|
336
|
+
if (answer.ok) {
|
|
337
|
+
// Both tokens: One rotates the pair on every refresh.
|
|
338
|
+
const next = toTokens(answer.body);
|
|
339
|
+
await tokenStore.saveTokens(userId, next);
|
|
340
|
+
return next;
|
|
341
|
+
}
|
|
342
|
+
if (isDeadGrant(answer)) return retire(userId, current, answer.status);
|
|
343
|
+
// A server error, a rate limit, a misconfigured secret: nothing says
|
|
344
|
+
// the grant is gone, so keep the tokens and let the caller retry.
|
|
345
|
+
throw new OneConnectError("request_failed", `One could not refresh the connection (HTTP ${answer.status}). The tokens were kept; try again.`, answer.status);
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
/** One refresh per user in this process; concurrent callers share it. */
|
|
349
|
+
const singleFlight = (userId, job) => {
|
|
229
350
|
const inFlight = refreshing.get(userId);
|
|
230
351
|
if (inFlight) return inFlight;
|
|
231
|
-
const
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
try {
|
|
235
|
-
const next = toTokens(await exchange(new URLSearchParams({
|
|
236
|
-
grant_type: "refresh_token",
|
|
237
|
-
refresh_token: current.refreshToken
|
|
238
|
-
})));
|
|
239
|
-
// Both tokens: One rotates the pair on every refresh.
|
|
240
|
-
await tokenStore.saveTokens(userId, next);
|
|
241
|
-
return next;
|
|
242
|
-
} catch (error) {
|
|
243
|
-
// The family is dead: revoked, expired or reused. Keeping the
|
|
244
|
-
// pair would only fail again; the user has to reconnect.
|
|
245
|
-
await tokenStore.clearTokens(userId);
|
|
246
|
-
const status = error instanceof OneConnectError ? error.status : undefined;
|
|
247
|
-
throw new OneConnectError("refresh_failed", "The connection to One has expired or was revoked. Ask the user to connect again.", status);
|
|
248
|
-
} finally {
|
|
249
|
-
refreshing.delete(userId);
|
|
250
|
-
}
|
|
251
|
-
})();
|
|
252
|
-
refreshing.set(userId, job);
|
|
253
|
-
return job;
|
|
352
|
+
const running = job().finally(() => refreshing.delete(userId));
|
|
353
|
+
refreshing.set(userId, running);
|
|
354
|
+
return running;
|
|
254
355
|
};
|
|
255
|
-
const
|
|
356
|
+
const refreshTokens = userId => singleFlight(userId, async () => {
|
|
357
|
+
const before = await tokenStore.loadTokens(userId);
|
|
358
|
+
if (!before) throw notConnected();
|
|
359
|
+
// Rotate the pair seen now; a pair someone else rotated since is
|
|
360
|
+
// already fresh.
|
|
361
|
+
return refreshUnderLock(userId, current => current.refreshToken === before.refreshToken);
|
|
362
|
+
});
|
|
363
|
+
const refreshIfExpiring = async (userId, options = {}) => {
|
|
364
|
+
const withinMs = options.withinMs ?? REFRESH_MARGIN_MS;
|
|
256
365
|
const tokens = await tokenStore.loadTokens(userId);
|
|
257
|
-
if (!tokens) throw
|
|
258
|
-
if (
|
|
259
|
-
return (
|
|
366
|
+
if (!tokens) throw notConnected();
|
|
367
|
+
if (!expiresWithin(tokens, withinMs)) return tokens;
|
|
368
|
+
return singleFlight(userId, () => refreshUnderLock(userId, current => expiresWithin(current, withinMs)));
|
|
260
369
|
};
|
|
370
|
+
const getAccessToken = async userId => (await refreshIfExpiring(userId)).accessToken;
|
|
261
371
|
const oneFetch = async (userId, path, init = {}) => {
|
|
262
372
|
const accessToken = await getAccessToken(userId);
|
|
263
373
|
const headers = new Headers(init.headers);
|
|
@@ -333,6 +443,7 @@ function createOneConnect(config) {
|
|
|
333
443
|
getAccessToken,
|
|
334
444
|
getTokens: userId => tokenStore.loadTokens(userId),
|
|
335
445
|
refreshTokens,
|
|
446
|
+
refreshIfExpiring,
|
|
336
447
|
disconnect: userId => tokenStore.clearTokens(userId),
|
|
337
448
|
listConnections,
|
|
338
449
|
listActions,
|
|
@@ -341,4 +452,4 @@ function createOneConnect(config) {
|
|
|
341
452
|
};
|
|
342
453
|
}
|
|
343
454
|
|
|
344
|
-
export { OneConnectError, createOneConnect, tenancyHeaders, tokenScopes };
|
|
455
|
+
export { OneConnectError, createOneConnect, refreshTokenExpiresAt, tenancyHeaders, tokenScopes };
|
package/dist/server/oauth.d.ts
CHANGED
|
@@ -20,5 +20,9 @@ export declare function basicAuthorization(clientId: string, clientSecret: strin
|
|
|
20
20
|
* these headers, and without them the call runs in the user's personal
|
|
21
21
|
* space. The access token's claims name the tenant, so echo them. */
|
|
22
22
|
export declare function tenancyHeaders(accessToken: string): Record<string, string>;
|
|
23
|
+
/** When a refresh token stops working, in epoch milliseconds, from its
|
|
24
|
+
* `exp` claim. Null when the token carries no readable expiry, in which
|
|
25
|
+
* case only One can say whether it still works. */
|
|
26
|
+
export declare function refreshTokenExpiresAt(refreshToken: string): number | null;
|
|
23
27
|
/** The scopes the token was granted, from its claims. Display only. */
|
|
24
28
|
export declare function tokenScopes(accessToken: string): string[];
|
package/dist/server/types.d.ts
CHANGED
|
@@ -11,13 +11,44 @@ export interface OneConnectTokens {
|
|
|
11
11
|
}
|
|
12
12
|
/**
|
|
13
13
|
* Where the app keeps each user's tokens: its database, a cache, an
|
|
14
|
-
* encrypted cookie. The SDK never sees a token outside these
|
|
15
|
-
*
|
|
14
|
+
* encrypted cookie. The SDK never sees a token outside these calls.
|
|
15
|
+
* `userId` is the app's own id for its user.
|
|
16
16
|
*/
|
|
17
17
|
export interface OneConnectTokenStore {
|
|
18
18
|
saveTokens: (userId: string, tokens: OneConnectTokens) => Promise<void>;
|
|
19
19
|
loadTokens: (userId: string) => Promise<OneConnectTokens | null>;
|
|
20
|
-
|
|
20
|
+
/**
|
|
21
|
+
* Deletes the user's tokens.
|
|
22
|
+
*
|
|
23
|
+
* `failed` is set when the SDK clears because One declared that pair
|
|
24
|
+
* dead. Delete only when the stored refresh token is still
|
|
25
|
+
* `failed.refreshToken`: a newer pair saved in the meantime (a
|
|
26
|
+
* reconnect, another server's refresh) must survive. `failed` is
|
|
27
|
+
* undefined for `disconnect`, which always deletes.
|
|
28
|
+
*/
|
|
29
|
+
clearTokens: (userId: string, failed?: OneConnectTokens) => Promise<void>;
|
|
30
|
+
/**
|
|
31
|
+
* Runs `run` while holding a lock on this user that every server and
|
|
32
|
+
* worker of the app shares: a Postgres advisory lock, a Redis lock, a
|
|
33
|
+
* row lock. The SDK loads, refreshes and saves the user's tokens
|
|
34
|
+
* inside it.
|
|
35
|
+
*
|
|
36
|
+
* Required when the app runs more than one process (serverless,
|
|
37
|
+
* several instances, a background worker). One rotates the refresh
|
|
38
|
+
* token on every use and treats a second use of the old one as theft,
|
|
39
|
+
* revoking the whole grant, so two processes refreshing the same user
|
|
40
|
+
* at once disconnect that user. Without a lock the SDK can only stop
|
|
41
|
+
* that inside a single process.
|
|
42
|
+
*
|
|
43
|
+
* Hold it for at least 60 seconds before any timeout: it spans one
|
|
44
|
+
* call to One's token endpoint.
|
|
45
|
+
*/
|
|
46
|
+
withLock?: <T>(userId: string, run: () => Promise<T>) => Promise<T>;
|
|
47
|
+
}
|
|
48
|
+
export interface RefreshIfExpiringOptions {
|
|
49
|
+
/** Refresh when the access token or the refresh token expires within
|
|
50
|
+
* this many milliseconds. One minute when omitted. */
|
|
51
|
+
withinMs?: number;
|
|
21
52
|
}
|
|
22
53
|
export interface OneConnectServerConfig {
|
|
23
54
|
/** The app's client id from the dashboard. */
|
|
@@ -70,10 +101,15 @@ export interface CompleteAuthorizationInput {
|
|
|
70
101
|
/** Reads a cookie by name from the incoming request. */
|
|
71
102
|
getCookie: (name: string) => string | undefined;
|
|
72
103
|
}
|
|
104
|
+
import type { ConnectFailureCode } from "../types";
|
|
105
|
+
export type { ConnectFailureCode };
|
|
73
106
|
export type AuthorizationOutcome = "connected" | "declined" | "failed";
|
|
74
107
|
export interface CompleteAuthorizationResult {
|
|
75
108
|
outcome: AuthorizationOutcome;
|
|
76
|
-
/**
|
|
109
|
+
/** Why it failed, as the code the browser receives. Only the code goes
|
|
110
|
+
* on the return URL; the browser shows fixed text for it. */
|
|
111
|
+
code?: ConnectFailureCode;
|
|
112
|
+
/** What happened, for your logs. Never put it in front of the user. */
|
|
77
113
|
message?: string;
|
|
78
114
|
/** Send the browser here with a 302; it carries `?one_connect=…`. */
|
|
79
115
|
redirectUrl: string;
|
|
@@ -128,6 +164,13 @@ export interface RunActionResult {
|
|
|
128
164
|
blockedByGrant: boolean;
|
|
129
165
|
data: unknown;
|
|
130
166
|
}
|
|
167
|
+
/**
|
|
168
|
+
* - `not_connected`: no tokens are stored for this user.
|
|
169
|
+
* - `refresh_failed`: One declared the grant dead (revoked, expired or
|
|
170
|
+
* reused). The tokens were cleared; ask the user to connect again.
|
|
171
|
+
* - `request_failed`: One answered with an error or could not be
|
|
172
|
+
* reached. During a refresh the tokens are kept, so retry later.
|
|
173
|
+
*/
|
|
131
174
|
export type OneConnectErrorCode = "not_connected" | "refresh_failed" | "request_failed";
|
|
132
175
|
export declare class OneConnectError extends Error {
|
|
133
176
|
readonly code: OneConnectErrorCode;
|
package/dist/svelte.cjs.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";var t=require("@withone/connect");exports.connectButton=function(
|
|
1
|
+
"use strict";var t=require("@withone/connect");exports.connectButton=function(n,e){const o=t.mountConnectButton(n,e);return{update:t=>o.update(t),destroy:()=>o.destroy()}};
|
package/dist/svelte.d.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* Svelte compiler or dependency is involved:
|
|
6
6
|
*
|
|
7
7
|
* <div use:connectButton={{ authorizeUrl: "/api/one/authorize",
|
|
8
|
-
* platforms: ["stripe", "notion"],
|
|
8
|
+
* platforms: ["stripe", "notion"], connected: data.hasOneGrant,
|
|
9
9
|
* onSuccess: () => { ... } }} />
|
|
10
10
|
*/
|
|
11
11
|
import { type ConnectButtonProps } from "@withone/connect";
|
package/dist/svelte.esm.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{mountConnectButton as t
|
|
1
|
+
import{mountConnectButton as t}from"@withone/connect";function o(o,e){const n=t(o,e);return{update:t=>n.update(t),destroy:()=>n.destroy()}}export{o as connectButton};
|
package/dist/types.d.ts
CHANGED
|
@@ -8,32 +8,55 @@
|
|
|
8
8
|
*/
|
|
9
9
|
/** Theme of One's hosted connect page. */
|
|
10
10
|
export type OneConnectTheme = "light" | "dark";
|
|
11
|
-
|
|
11
|
+
/** Theme of the button: fixed, or following the visitor's setting. */
|
|
12
|
+
export type ConnectButtonTheme = "light" | "dark" | "auto";
|
|
13
|
+
/**
|
|
14
|
+
* Why a flow ended without a grant. The callback route puts only this
|
|
15
|
+
* code on the return URL; the text shown for it is fixed in the SDK, so
|
|
16
|
+
* a crafted link can never put its own words in front of the user.
|
|
17
|
+
*
|
|
18
|
+
* - `declined`: the user cancelled on One's page.
|
|
19
|
+
* - `expired`: the attempt took too long or was started elsewhere.
|
|
20
|
+
* - `failed`: One could not complete the connection.
|
|
21
|
+
*/
|
|
22
|
+
export type ConnectFailureCode = "declined" | "expired" | "failed";
|
|
23
|
+
export interface OneConnectFlowOptions {
|
|
12
24
|
/** The app's own backend authorize route. Relative paths such as
|
|
13
25
|
* "/api/one/authorize" resolve against the page's origin. */
|
|
14
26
|
authorizeUrl: string;
|
|
15
27
|
/** Theme for One's hosted page. Carried on the URL fragment, which
|
|
16
28
|
* survives the redirect chain, so the backend forwards nothing. */
|
|
29
|
+
connectTheme?: OneConnectTheme;
|
|
30
|
+
/** @deprecated Renamed to `connectTheme`; removed in the next minor. */
|
|
17
31
|
appTheme?: OneConnectTheme;
|
|
18
|
-
/** The grant completed and the backend stored the tokens.
|
|
32
|
+
/** The grant completed and the backend stored the tokens. Fires once
|
|
33
|
+
* per page load, on the first flow still mounted when the tab
|
|
34
|
+
* returns. Treat it as a hint to refetch: your server is the truth. */
|
|
19
35
|
onSuccess?: () => void;
|
|
20
|
-
/** The flow ended without a grant
|
|
21
|
-
*
|
|
22
|
-
onError?: (message: string) => void;
|
|
36
|
+
/** The flow ended without a grant. `message` is fixed text for
|
|
37
|
+
* `code`, safe to show. */
|
|
38
|
+
onError?: (message: string, code: ConnectFailureCode) => void;
|
|
39
|
+
/** The user came back with the browser's Back button before finishing
|
|
40
|
+
* (the page was restored from the back-forward cache). */
|
|
41
|
+
onCancel?: () => void;
|
|
23
42
|
}
|
|
24
|
-
export interface
|
|
43
|
+
export interface OneConnectFlow {
|
|
25
44
|
/** Navigates the tab to One's hosted connect flow. */
|
|
26
45
|
open: () => void;
|
|
46
|
+
/** Swaps the options (callbacks, theme) without losing the flow. */
|
|
47
|
+
update: (options: OneConnectFlowOptions) => void;
|
|
48
|
+
/** Stops listening: callbacks no longer fire for this flow. */
|
|
49
|
+
destroy: () => void;
|
|
27
50
|
}
|
|
28
|
-
/** How the
|
|
29
|
-
* redirect, read off the page URL when the tab returns. */
|
|
51
|
+
/** How the flow ended, read off the page URL when the tab returns. */
|
|
30
52
|
export interface OneConnectReturn {
|
|
31
53
|
status: "success" | "error";
|
|
54
|
+
code?: ConnectFailureCode;
|
|
32
55
|
message?: string;
|
|
33
56
|
}
|
|
34
57
|
/**
|
|
35
58
|
* A connector chip on the button. Pass One's connector slug ("stripe",
|
|
36
|
-
* "google-calendar") and the SDK shows
|
|
59
|
+
* "google-calendar") and the SDK shows its logo and name; pass an
|
|
37
60
|
* object to override either.
|
|
38
61
|
*/
|
|
39
62
|
export type ConnectButtonPlatformInput = string | {
|
|
@@ -48,31 +71,50 @@ export interface ConnectButtonPlatform {
|
|
|
48
71
|
imageUrl: string;
|
|
49
72
|
}
|
|
50
73
|
export type ConnectButtonVariant = "default" | "accent" | "block";
|
|
74
|
+
export type ConnectButtonSize = "sm" | "md" | "lg";
|
|
51
75
|
export type ConnectButtonState = "idle" | "connecting" | "connected";
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
* full-width card with a description and a "Secured by One" foot. */
|
|
60
|
-
variant?: ConnectButtonVariant;
|
|
61
|
-
/** Matches the host page, not One's page (that is connect.appTheme). */
|
|
62
|
-
theme?: OneConnectTheme;
|
|
63
|
-
/** Connector chips. The first three render; the rest fold into a "+N"
|
|
64
|
-
* chip, so that count only ever describes this list. */
|
|
76
|
+
/** One prop shape for every surface: React, Vue, Svelte, the custom
|
|
77
|
+
* element and `mountConnectButton`. */
|
|
78
|
+
export interface ConnectButtonProps {
|
|
79
|
+
/** The app's own backend authorize route; relative is fine. */
|
|
80
|
+
authorizeUrl: string;
|
|
81
|
+
/** Connector slugs, or objects that override the name or the logo.
|
|
82
|
+
* The first three draw as logos; the rest fold into a "+N" chip. */
|
|
65
83
|
platforms?: ConnectButtonPlatformInput[];
|
|
66
|
-
/**
|
|
67
|
-
|
|
68
|
-
|
|
84
|
+
/** Whether this user has a live grant, from your server. When set, it
|
|
85
|
+
* decides the Connected state. When omitted, the button shows
|
|
86
|
+
* Connected only right after a successful return. */
|
|
87
|
+
connected?: boolean;
|
|
88
|
+
/** Not clickable, for example until terms are accepted. */
|
|
89
|
+
disabled?: boolean;
|
|
90
|
+
/** default = neutral, accent = your brand colour, block = a card with
|
|
91
|
+
* a description and a "Secured by One" foot. */
|
|
92
|
+
variant?: ConnectButtonVariant;
|
|
93
|
+
size?: ConnectButtonSize;
|
|
94
|
+
/** Stretches to the width of its container. */
|
|
95
|
+
fullWidth?: boolean;
|
|
96
|
+
/** Matches the host page. "auto" follows the visitor's setting. */
|
|
97
|
+
theme?: ConnectButtonTheme;
|
|
98
|
+
/** Theme of One's hosted page. */
|
|
99
|
+
connectTheme?: OneConnectTheme;
|
|
100
|
+
/** @deprecated Renamed to `connectTheme`; removed in the next minor. */
|
|
101
|
+
appTheme?: OneConnectTheme;
|
|
102
|
+
/** Fill of the accent variant; One's lime when omitted. The label is
|
|
103
|
+
* black or white, whichever reads better on it. */
|
|
69
104
|
accentColor?: string;
|
|
70
|
-
/**
|
|
105
|
+
/** "Connect your apps" unless set. */
|
|
106
|
+
label?: string;
|
|
107
|
+
/** "Connected" unless set. */
|
|
71
108
|
connectedLabel?: string;
|
|
109
|
+
/** Sub-line on the block variant. */
|
|
110
|
+
description?: string;
|
|
111
|
+
onSuccess?: () => void;
|
|
112
|
+
onError?: (message: string, code: ConnectFailureCode) => void;
|
|
113
|
+
onCancel?: () => void;
|
|
72
114
|
}
|
|
73
115
|
export interface ConnectButtonHandle {
|
|
74
|
-
/**
|
|
75
|
-
|
|
76
|
-
/**
|
|
116
|
+
/** Applies new props in place, keeping the button's state. */
|
|
117
|
+
update: (props: ConnectButtonProps) => void;
|
|
118
|
+
/** Removes the button and stops its callbacks. */
|
|
77
119
|
destroy: () => void;
|
|
78
120
|
}
|
package/dist/vue.cjs.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";var e=require("vue"),t=require("@withone/connect");const n=e.defineComponent({name:"OneConnectButton",props:{authorizeUrl:{type:String,required:!0},
|
|
1
|
+
"use strict";var e=require("vue"),t=require("@withone/connect");const n={type:Boolean,default:void 0},o=e.defineComponent({name:"OneConnectButton",props:{authorizeUrl:{type:String,required:!0},platforms:{type:Array,default:void 0},connected:n,disabled:n,variant:{type:String,default:void 0},size:{type:String,default:void 0},fullWidth:n,theme:{type:String,default:void 0},connectTheme:{type:String,default:void 0},appTheme:{type:String,default:void 0},accentColor:{type:String,default:void 0},label:{type:String,default:void 0},connectedLabel:{type:String,default:void 0},description:{type:String,default:void 0}},emits:{success:()=>!0,error:(e,t)=>"string"==typeof e&&"string"==typeof t,cancel:()=>!0},setup(n,{emit:o}){const r=e.ref(null);let i=null;const d=()=>({...n,onSuccess:()=>o("success"),onError:(e,t)=>o("error",e,t),onCancel:()=>o("cancel")});return e.onMounted(()=>{r.value&&(i=t.mountConnectButton(r.value,d()))}),e.watch(()=>({...n}),()=>{var e;return null===(e=i)||void 0===e?void 0:e.update(d())},{deep:!0}),e.onBeforeUnmount(()=>{var e;null===(e=i)||void 0===e||e.destroy(),i=null}),()=>e.h("div",{ref:r})}});exports.ConnectButton=o;
|