@withone/connect 0.16.1 → 0.17.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/README.md +35 -2
- package/dist/constants.d.ts +8 -0
- package/dist/index.cjs.js +1 -1
- package/dist/index.esm.js +1 -1
- package/dist/next.cjs.js +13 -2
- package/dist/next.d.ts +0 -16
- package/dist/next.esm.js +13 -2
- package/dist/react.cjs.js +1 -1
- package/dist/react.esm.js +1 -1
- package/dist/server/index.cjs.js +74 -12
- package/dist/server/index.esm.js +74 -12
- package/dist/server/oauth.d.ts +13 -1
- package/dist/server/types.d.ts +24 -3
- package/dist/types.d.ts +15 -3
- package/dist/vue.cjs.js +1 -1
- package/dist/vue.d.ts +18 -0
- package/dist/vue.esm.js +1 -1
- package/package.json +1 -1
- package/skills/one-connect/SKILL.md +25 -6
- package/src/button.ts +6 -0
- package/src/constants.ts +10 -0
- package/src/flow.ts +11 -3
- package/src/next.ts +6 -1
- package/src/react.ts +2 -0
- package/src/server/index.ts +37 -15
- package/src/server/oauth.ts +32 -1
- package/src/server/types.ts +25 -3
- package/src/types.ts +15 -3
- package/src/vue.ts +2 -0
package/dist/server/index.cjs.js
CHANGED
|
@@ -34,14 +34,40 @@ function txCookieName(state) {
|
|
|
34
34
|
return `${TX_COOKIE_PREFIX}${state}`;
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
+
/** A return path this one flow may use: a path on the app itself. A
|
|
38
|
+
* full URL, a protocol-relative one or anything else is dropped, so
|
|
39
|
+
* the callback never sends a user to another site. */
|
|
40
|
+
function appPath(value) {
|
|
41
|
+
if (!value || !value.startsWith("/")) return undefined;
|
|
42
|
+
if (value.startsWith("//") || value.startsWith("/\\")) return undefined;
|
|
43
|
+
return value;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** The cookie value: the verifier, then the flow's own return path when
|
|
47
|
+
* it has one. A dot separates them; the verifier is base64url, which
|
|
48
|
+
* never contains one, so a value written before 0.17 still reads. */
|
|
49
|
+
function txCookieValue(verifier, returnTo) {
|
|
50
|
+
return returnTo ? `${verifier}.${Buffer.from(returnTo).toString("base64url")}` : verifier;
|
|
51
|
+
}
|
|
52
|
+
function parseTxCookieValue(value) {
|
|
53
|
+
const dot = value.indexOf(".");
|
|
54
|
+
if (dot < 0) return {
|
|
55
|
+
verifier: value
|
|
56
|
+
};
|
|
57
|
+
return {
|
|
58
|
+
verifier: value.slice(0, dot),
|
|
59
|
+
returnTo: appPath(Buffer.from(value.slice(dot + 1), "base64url").toString())
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
37
63
|
/** The cookie scoped to the routes that need it: the directory of the
|
|
38
64
|
* callback path, which is also where the authorize route lives. */
|
|
39
|
-
function txCookie(state, verifier, redirectUri) {
|
|
65
|
+
function txCookie(state, verifier, redirectUri, returnTo) {
|
|
40
66
|
const url = new URL(redirectUri);
|
|
41
67
|
const path = url.pathname.replace(/\/[^/]*$/, "") || "/";
|
|
42
68
|
return {
|
|
43
69
|
name: txCookieName(state),
|
|
44
|
-
value: verifier,
|
|
70
|
+
value: txCookieValue(verifier, returnTo),
|
|
45
71
|
options: {
|
|
46
72
|
httpOnly: true,
|
|
47
73
|
secure: url.protocol === "https:",
|
|
@@ -159,6 +185,18 @@ function tokenScopes(accessToken) {
|
|
|
159
185
|
* method and path the SDK runs it with.
|
|
160
186
|
*/
|
|
161
187
|
|
|
188
|
+
/**
|
|
189
|
+
* Why One itself turned a call down, when it did. A provider's own error
|
|
190
|
+
* (Gmail's 404, Stripe's 400) is not One's refusal and reads `null`.
|
|
191
|
+
*
|
|
192
|
+
* - `grant`: the call is outside what the user granted (403). Do not retry.
|
|
193
|
+
* - `plan_limit`: a plan limit was hit (402, "Please try again later or
|
|
194
|
+
* upgrade your plan"). Not about the grant: wait and retry later.
|
|
195
|
+
* - `rate_limit`: too many calls (429). Wait and retry later.
|
|
196
|
+
* - `one_error`: any other error from One (for example a 5xx). Retry with
|
|
197
|
+
* a backoff.
|
|
198
|
+
*/
|
|
199
|
+
|
|
162
200
|
/**
|
|
163
201
|
* - `not_connected`: nothing is stored for this user.
|
|
164
202
|
* - `reconnect_required` (key mode): One will not act for this user.
|
|
@@ -484,6 +522,23 @@ function createTokenCredential(tokenStore, postToken) {
|
|
|
484
522
|
* The Next.js and Node adapters turn the first two into route handlers.
|
|
485
523
|
* See `./key` and `./token` for what each mode stores and sends.
|
|
486
524
|
*/
|
|
525
|
+
|
|
526
|
+
/**
|
|
527
|
+
* Why One itself refused a call, from its reply. One's own errors carry a
|
|
528
|
+
* correlationId; a provider's error does not, so it reads `null`. Only a
|
|
529
|
+
* 403 is the grant: One also answers a plan limit (402), a rate limit
|
|
530
|
+
* (429) and its own failures (5xx) with a correlationId, and none of those
|
|
531
|
+
* mean the call is outside what the user granted.
|
|
532
|
+
*/
|
|
533
|
+
function oneRefusal(response, data) {
|
|
534
|
+
if (response.ok) return null;
|
|
535
|
+
const fromOne = typeof data === "object" && data !== null && "correlationId" in data;
|
|
536
|
+
if (!fromOne) return null;
|
|
537
|
+
if (response.status === 403) return "grant";
|
|
538
|
+
if (response.status === 402) return "plan_limit";
|
|
539
|
+
if (response.status === 429) return "rate_limit";
|
|
540
|
+
return "one_error";
|
|
541
|
+
}
|
|
487
542
|
const CATALOG_PAGE_SIZE = 100;
|
|
488
543
|
const CATALOG_MAX_PAGES = 20;
|
|
489
544
|
const SEARCH_DEFAULT_LIMIT = 5;
|
|
@@ -563,8 +618,8 @@ function createOneConnect(config) {
|
|
|
563
618
|
const apiUrl = `${oneApiUrl}/v1`;
|
|
564
619
|
const returnTo = config.returnTo ?? "/";
|
|
565
620
|
const scopes = config.scopes ?? DEFAULT_SCOPES;
|
|
566
|
-
const returnUrl = (status, code) => {
|
|
567
|
-
const url = new URL(returnTo, config.redirectUri);
|
|
621
|
+
const returnUrl = (status, code, flowReturnTo) => {
|
|
622
|
+
const url = new URL(flowReturnTo ?? returnTo, config.redirectUri);
|
|
568
623
|
url.searchParams.set(RETURN_STATUS_PARAM, status);
|
|
569
624
|
if (code) url.searchParams.set(RETURN_ERROR_PARAM, code);
|
|
570
625
|
return url.toString();
|
|
@@ -623,7 +678,7 @@ function createOneConnect(config) {
|
|
|
623
678
|
if (input.loginHint) url.searchParams.set("login_hint", input.loginHint);
|
|
624
679
|
return {
|
|
625
680
|
redirectUrl: url.toString(),
|
|
626
|
-
cookie: txCookie(state, verifier, config.redirectUri)
|
|
681
|
+
cookie: txCookie(state, verifier, config.redirectUri, appPath(input.returnTo))
|
|
627
682
|
};
|
|
628
683
|
};
|
|
629
684
|
const completeAuthorization = async input => {
|
|
@@ -632,12 +687,19 @@ function createOneConnect(config) {
|
|
|
632
687
|
const state = params.get("state");
|
|
633
688
|
const oauthError = params.get("error");
|
|
634
689
|
const cookieName = state ? txCookieName(state) : undefined;
|
|
635
|
-
const
|
|
690
|
+
const stored = cookieName ? input.getCookie(cookieName) : undefined;
|
|
691
|
+
const {
|
|
692
|
+
verifier,
|
|
693
|
+
returnTo: flowReturnTo
|
|
694
|
+
} = stored ? parseTxCookieValue(stored) : {
|
|
695
|
+
verifier: undefined,
|
|
696
|
+
returnTo: undefined
|
|
697
|
+
};
|
|
636
698
|
const fail = (failure, message) => ({
|
|
637
699
|
outcome: failure === "declined" ? "declined" : "failed",
|
|
638
700
|
code: failure,
|
|
639
701
|
message,
|
|
640
|
-
redirectUrl: returnUrl("error", failure),
|
|
702
|
+
redirectUrl: returnUrl("error", failure, flowReturnTo),
|
|
641
703
|
clearCookieName: cookieName
|
|
642
704
|
});
|
|
643
705
|
if (oauthError === "access_denied") return fail("declined", "The user cancelled on One's page.");
|
|
@@ -660,7 +722,7 @@ function createOneConnect(config) {
|
|
|
660
722
|
}
|
|
661
723
|
return {
|
|
662
724
|
outcome: "connected",
|
|
663
|
-
redirectUrl: returnUrl("success"),
|
|
725
|
+
redirectUrl: returnUrl("success", undefined, flowReturnTo),
|
|
664
726
|
clearCookieName: cookieName
|
|
665
727
|
};
|
|
666
728
|
};
|
|
@@ -827,6 +889,7 @@ function createOneConnect(config) {
|
|
|
827
889
|
status: response.status,
|
|
828
890
|
ok: false,
|
|
829
891
|
blockedByGrant: true,
|
|
892
|
+
refusal: "grant",
|
|
830
893
|
data: text
|
|
831
894
|
};
|
|
832
895
|
throw refused;
|
|
@@ -840,13 +903,12 @@ function createOneConnect(config) {
|
|
|
840
903
|
} catch {
|
|
841
904
|
/* the provider answered with something other than JSON */
|
|
842
905
|
}
|
|
843
|
-
|
|
844
|
-
// not. A One-shaped 403 is the grant working, not the provider.
|
|
845
|
-
const blockedByGrant = !response.ok && typeof data === "object" && data !== null && "correlationId" in data;
|
|
906
|
+
const refusal = oneRefusal(response, data);
|
|
846
907
|
return {
|
|
847
908
|
status: response.status,
|
|
848
909
|
ok: response.ok,
|
|
849
|
-
blockedByGrant,
|
|
910
|
+
blockedByGrant: refusal === "grant",
|
|
911
|
+
refusal,
|
|
850
912
|
data
|
|
851
913
|
};
|
|
852
914
|
};
|
package/dist/server/index.esm.js
CHANGED
|
@@ -32,14 +32,40 @@ function txCookieName(state) {
|
|
|
32
32
|
return `${TX_COOKIE_PREFIX}${state}`;
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
/** A return path this one flow may use: a path on the app itself. A
|
|
36
|
+
* full URL, a protocol-relative one or anything else is dropped, so
|
|
37
|
+
* the callback never sends a user to another site. */
|
|
38
|
+
function appPath(value) {
|
|
39
|
+
if (!value || !value.startsWith("/")) return undefined;
|
|
40
|
+
if (value.startsWith("//") || value.startsWith("/\\")) return undefined;
|
|
41
|
+
return value;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** The cookie value: the verifier, then the flow's own return path when
|
|
45
|
+
* it has one. A dot separates them; the verifier is base64url, which
|
|
46
|
+
* never contains one, so a value written before 0.17 still reads. */
|
|
47
|
+
function txCookieValue(verifier, returnTo) {
|
|
48
|
+
return returnTo ? `${verifier}.${Buffer.from(returnTo).toString("base64url")}` : verifier;
|
|
49
|
+
}
|
|
50
|
+
function parseTxCookieValue(value) {
|
|
51
|
+
const dot = value.indexOf(".");
|
|
52
|
+
if (dot < 0) return {
|
|
53
|
+
verifier: value
|
|
54
|
+
};
|
|
55
|
+
return {
|
|
56
|
+
verifier: value.slice(0, dot),
|
|
57
|
+
returnTo: appPath(Buffer.from(value.slice(dot + 1), "base64url").toString())
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
35
61
|
/** The cookie scoped to the routes that need it: the directory of the
|
|
36
62
|
* callback path, which is also where the authorize route lives. */
|
|
37
|
-
function txCookie(state, verifier, redirectUri) {
|
|
63
|
+
function txCookie(state, verifier, redirectUri, returnTo) {
|
|
38
64
|
const url = new URL(redirectUri);
|
|
39
65
|
const path = url.pathname.replace(/\/[^/]*$/, "") || "/";
|
|
40
66
|
return {
|
|
41
67
|
name: txCookieName(state),
|
|
42
|
-
value: verifier,
|
|
68
|
+
value: txCookieValue(verifier, returnTo),
|
|
43
69
|
options: {
|
|
44
70
|
httpOnly: true,
|
|
45
71
|
secure: url.protocol === "https:",
|
|
@@ -157,6 +183,18 @@ function tokenScopes(accessToken) {
|
|
|
157
183
|
* method and path the SDK runs it with.
|
|
158
184
|
*/
|
|
159
185
|
|
|
186
|
+
/**
|
|
187
|
+
* Why One itself turned a call down, when it did. A provider's own error
|
|
188
|
+
* (Gmail's 404, Stripe's 400) is not One's refusal and reads `null`.
|
|
189
|
+
*
|
|
190
|
+
* - `grant`: the call is outside what the user granted (403). Do not retry.
|
|
191
|
+
* - `plan_limit`: a plan limit was hit (402, "Please try again later or
|
|
192
|
+
* upgrade your plan"). Not about the grant: wait and retry later.
|
|
193
|
+
* - `rate_limit`: too many calls (429). Wait and retry later.
|
|
194
|
+
* - `one_error`: any other error from One (for example a 5xx). Retry with
|
|
195
|
+
* a backoff.
|
|
196
|
+
*/
|
|
197
|
+
|
|
160
198
|
/**
|
|
161
199
|
* - `not_connected`: nothing is stored for this user.
|
|
162
200
|
* - `reconnect_required` (key mode): One will not act for this user.
|
|
@@ -482,6 +520,23 @@ function createTokenCredential(tokenStore, postToken) {
|
|
|
482
520
|
* The Next.js and Node adapters turn the first two into route handlers.
|
|
483
521
|
* See `./key` and `./token` for what each mode stores and sends.
|
|
484
522
|
*/
|
|
523
|
+
|
|
524
|
+
/**
|
|
525
|
+
* Why One itself refused a call, from its reply. One's own errors carry a
|
|
526
|
+
* correlationId; a provider's error does not, so it reads `null`. Only a
|
|
527
|
+
* 403 is the grant: One also answers a plan limit (402), a rate limit
|
|
528
|
+
* (429) and its own failures (5xx) with a correlationId, and none of those
|
|
529
|
+
* mean the call is outside what the user granted.
|
|
530
|
+
*/
|
|
531
|
+
function oneRefusal(response, data) {
|
|
532
|
+
if (response.ok) return null;
|
|
533
|
+
const fromOne = typeof data === "object" && data !== null && "correlationId" in data;
|
|
534
|
+
if (!fromOne) return null;
|
|
535
|
+
if (response.status === 403) return "grant";
|
|
536
|
+
if (response.status === 402) return "plan_limit";
|
|
537
|
+
if (response.status === 429) return "rate_limit";
|
|
538
|
+
return "one_error";
|
|
539
|
+
}
|
|
485
540
|
const CATALOG_PAGE_SIZE = 100;
|
|
486
541
|
const CATALOG_MAX_PAGES = 20;
|
|
487
542
|
const SEARCH_DEFAULT_LIMIT = 5;
|
|
@@ -561,8 +616,8 @@ function createOneConnect(config) {
|
|
|
561
616
|
const apiUrl = `${oneApiUrl}/v1`;
|
|
562
617
|
const returnTo = config.returnTo ?? "/";
|
|
563
618
|
const scopes = config.scopes ?? DEFAULT_SCOPES;
|
|
564
|
-
const returnUrl = (status, code) => {
|
|
565
|
-
const url = new URL(returnTo, config.redirectUri);
|
|
619
|
+
const returnUrl = (status, code, flowReturnTo) => {
|
|
620
|
+
const url = new URL(flowReturnTo ?? returnTo, config.redirectUri);
|
|
566
621
|
url.searchParams.set(RETURN_STATUS_PARAM, status);
|
|
567
622
|
if (code) url.searchParams.set(RETURN_ERROR_PARAM, code);
|
|
568
623
|
return url.toString();
|
|
@@ -621,7 +676,7 @@ function createOneConnect(config) {
|
|
|
621
676
|
if (input.loginHint) url.searchParams.set("login_hint", input.loginHint);
|
|
622
677
|
return {
|
|
623
678
|
redirectUrl: url.toString(),
|
|
624
|
-
cookie: txCookie(state, verifier, config.redirectUri)
|
|
679
|
+
cookie: txCookie(state, verifier, config.redirectUri, appPath(input.returnTo))
|
|
625
680
|
};
|
|
626
681
|
};
|
|
627
682
|
const completeAuthorization = async input => {
|
|
@@ -630,12 +685,19 @@ function createOneConnect(config) {
|
|
|
630
685
|
const state = params.get("state");
|
|
631
686
|
const oauthError = params.get("error");
|
|
632
687
|
const cookieName = state ? txCookieName(state) : undefined;
|
|
633
|
-
const
|
|
688
|
+
const stored = cookieName ? input.getCookie(cookieName) : undefined;
|
|
689
|
+
const {
|
|
690
|
+
verifier,
|
|
691
|
+
returnTo: flowReturnTo
|
|
692
|
+
} = stored ? parseTxCookieValue(stored) : {
|
|
693
|
+
verifier: undefined,
|
|
694
|
+
returnTo: undefined
|
|
695
|
+
};
|
|
634
696
|
const fail = (failure, message) => ({
|
|
635
697
|
outcome: failure === "declined" ? "declined" : "failed",
|
|
636
698
|
code: failure,
|
|
637
699
|
message,
|
|
638
|
-
redirectUrl: returnUrl("error", failure),
|
|
700
|
+
redirectUrl: returnUrl("error", failure, flowReturnTo),
|
|
639
701
|
clearCookieName: cookieName
|
|
640
702
|
});
|
|
641
703
|
if (oauthError === "access_denied") return fail("declined", "The user cancelled on One's page.");
|
|
@@ -658,7 +720,7 @@ function createOneConnect(config) {
|
|
|
658
720
|
}
|
|
659
721
|
return {
|
|
660
722
|
outcome: "connected",
|
|
661
|
-
redirectUrl: returnUrl("success"),
|
|
723
|
+
redirectUrl: returnUrl("success", undefined, flowReturnTo),
|
|
662
724
|
clearCookieName: cookieName
|
|
663
725
|
};
|
|
664
726
|
};
|
|
@@ -825,6 +887,7 @@ function createOneConnect(config) {
|
|
|
825
887
|
status: response.status,
|
|
826
888
|
ok: false,
|
|
827
889
|
blockedByGrant: true,
|
|
890
|
+
refusal: "grant",
|
|
828
891
|
data: text
|
|
829
892
|
};
|
|
830
893
|
throw refused;
|
|
@@ -838,13 +901,12 @@ function createOneConnect(config) {
|
|
|
838
901
|
} catch {
|
|
839
902
|
/* the provider answered with something other than JSON */
|
|
840
903
|
}
|
|
841
|
-
|
|
842
|
-
// not. A One-shaped 403 is the grant working, not the provider.
|
|
843
|
-
const blockedByGrant = !response.ok && typeof data === "object" && data !== null && "correlationId" in data;
|
|
904
|
+
const refusal = oneRefusal(response, data);
|
|
844
905
|
return {
|
|
845
906
|
status: response.status,
|
|
846
907
|
ok: response.ok,
|
|
847
|
-
blockedByGrant,
|
|
908
|
+
blockedByGrant: refusal === "grant",
|
|
909
|
+
refusal,
|
|
848
910
|
data
|
|
849
911
|
};
|
|
850
912
|
};
|
package/dist/server/oauth.d.ts
CHANGED
|
@@ -11,9 +11,21 @@ export declare function createState(): string;
|
|
|
11
11
|
export declare function createPkceVerifier(): string;
|
|
12
12
|
export declare function pkceChallenge(verifier: string): string;
|
|
13
13
|
export declare function txCookieName(state: string): string;
|
|
14
|
+
/** A return path this one flow may use: a path on the app itself. A
|
|
15
|
+
* full URL, a protocol-relative one or anything else is dropped, so
|
|
16
|
+
* the callback never sends a user to another site. */
|
|
17
|
+
export declare function appPath(value: string | undefined): string | undefined;
|
|
18
|
+
/** The cookie value: the verifier, then the flow's own return path when
|
|
19
|
+
* it has one. A dot separates them; the verifier is base64url, which
|
|
20
|
+
* never contains one, so a value written before 0.17 still reads. */
|
|
21
|
+
export declare function txCookieValue(verifier: string, returnTo?: string): string;
|
|
22
|
+
export declare function parseTxCookieValue(value: string): {
|
|
23
|
+
verifier: string;
|
|
24
|
+
returnTo?: string;
|
|
25
|
+
};
|
|
14
26
|
/** The cookie scoped to the routes that need it: the directory of the
|
|
15
27
|
* callback path, which is also where the authorize route lives. */
|
|
16
|
-
export declare function txCookie(state: string, verifier: string, redirectUri: string): OneConnectCookie;
|
|
28
|
+
export declare function txCookie(state: string, verifier: string, redirectUri: string, returnTo?: string): OneConnectCookie;
|
|
17
29
|
export declare function basicAuthorization(clientId: string, clientSecret: string): string;
|
|
18
30
|
/** The tenant headers a grant needs on every call. A grant made into an
|
|
19
31
|
* organization or project lives there; One resolves the tenant from
|
package/dist/server/types.d.ts
CHANGED
|
@@ -149,6 +149,10 @@ export interface OneConnectCookie {
|
|
|
149
149
|
export interface StartAuthorizationInput {
|
|
150
150
|
/** Pre-fills the sign-in email on One's page. Never locks it. */
|
|
151
151
|
loginHint?: string;
|
|
152
|
+
/** Where this one flow returns to: a path on your app ("/chat/42").
|
|
153
|
+
* Kept in the flow's cookie, never sent to One. Anything that is not
|
|
154
|
+
* a plain path is ignored and the configured `returnTo` applies. */
|
|
155
|
+
returnTo?: string;
|
|
152
156
|
}
|
|
153
157
|
export interface StartAuthorizationResult {
|
|
154
158
|
/** Send the browser here with a 302. */
|
|
@@ -256,13 +260,30 @@ export interface RunActionInput {
|
|
|
256
260
|
* notation); "multipart" for file-style uploads. */
|
|
257
261
|
encoding?: ActionBodyEncoding;
|
|
258
262
|
}
|
|
263
|
+
/**
|
|
264
|
+
* Why One itself turned a call down, when it did. A provider's own error
|
|
265
|
+
* (Gmail's 404, Stripe's 400) is not One's refusal and reads `null`.
|
|
266
|
+
*
|
|
267
|
+
* - `grant`: the call is outside what the user granted (403). Do not retry.
|
|
268
|
+
* - `plan_limit`: a plan limit was hit (402, "Please try again later or
|
|
269
|
+
* upgrade your plan"). Not about the grant: wait and retry later.
|
|
270
|
+
* - `rate_limit`: too many calls (429). Wait and retry later.
|
|
271
|
+
* - `one_error`: any other error from One (for example a 5xx). Retry with
|
|
272
|
+
* a backoff.
|
|
273
|
+
*/
|
|
274
|
+
export type OneRefusal = "grant" | "plan_limit" | "rate_limit" | "one_error";
|
|
259
275
|
export interface RunActionResult {
|
|
260
276
|
status: number;
|
|
261
277
|
ok: boolean;
|
|
262
|
-
/** True when One refused the call because it is outside the grant
|
|
263
|
-
* The provider was never called. Do not retry. Key mode sets
|
|
264
|
-
* token mode it stays false today, so treat any 403 there as
|
|
278
|
+
/** True only when One refused the call because it is outside the grant
|
|
279
|
+
* (a 403). The provider was never called. Do not retry. Key mode sets
|
|
280
|
+
* it; in token mode it stays false today, so treat any 403 there as
|
|
281
|
+
* refused. One's other refusals (plan limit, rate limit, its own
|
|
282
|
+
* errors) are not about the grant: see `refusal`. */
|
|
265
283
|
blockedByGrant: boolean;
|
|
284
|
+
/** Why One turned the call down, or `null` when it didn't (the call
|
|
285
|
+
* succeeded, or the provider answered with its own error). */
|
|
286
|
+
refusal: OneRefusal | null;
|
|
266
287
|
data: unknown;
|
|
267
288
|
}
|
|
268
289
|
/**
|
package/dist/types.d.ts
CHANGED
|
@@ -27,6 +27,14 @@ export interface OneConnectFlowOptions {
|
|
|
27
27
|
/** Theme for One's hosted page. Carried on the URL fragment, which
|
|
28
28
|
* survives the redirect chain, so the backend forwards nothing. */
|
|
29
29
|
connectTheme?: OneConnectTheme;
|
|
30
|
+
/** A connector slug ("gmail"). One's page opens that connector's
|
|
31
|
+
* connect screen first, then shows the full list as usual. Carried
|
|
32
|
+
* on the URL fragment like the theme. */
|
|
33
|
+
connector?: string;
|
|
34
|
+
/** Where the user lands afterwards, for this flow only: a path on
|
|
35
|
+
* your app such as "/chat/42". Your authorize route reads it and
|
|
36
|
+
* the callback uses it instead of the server's `returnTo`. */
|
|
37
|
+
returnTo?: string;
|
|
30
38
|
/** The grant completed and the backend stored the tokens. Fires once
|
|
31
39
|
* per page load, on the first flow still mounted when the tab
|
|
32
40
|
* returns. Treat it as a hint to refetch: your server is the truth. */
|
|
@@ -69,9 +77,9 @@ export interface ConnectButtonLogo {
|
|
|
69
77
|
name: string;
|
|
70
78
|
imageUrl: string;
|
|
71
79
|
}
|
|
72
|
-
/** @deprecated Renamed to `ConnectButtonLogoInput`; removed in 0.
|
|
80
|
+
/** @deprecated Renamed to `ConnectButtonLogoInput`; removed in 0.18.0. */
|
|
73
81
|
export type ConnectButtonPlatformInput = ConnectButtonLogoInput;
|
|
74
|
-
/** @deprecated Renamed to `ConnectButtonLogo`; removed in 0.
|
|
82
|
+
/** @deprecated Renamed to `ConnectButtonLogo`; removed in 0.18.0. */
|
|
75
83
|
export type ConnectButtonPlatform = ConnectButtonLogo;
|
|
76
84
|
export type ConnectButtonVariant = "default" | "accent" | "block";
|
|
77
85
|
export type ConnectButtonSize = "sm" | "md" | "lg";
|
|
@@ -86,7 +94,7 @@ export interface ConnectButtonProps {
|
|
|
86
94
|
* into a "+N" chip. Decoration only; the permission set decides what
|
|
87
95
|
* One asks for. */
|
|
88
96
|
logos?: ConnectButtonLogoInput[];
|
|
89
|
-
/** @deprecated Renamed to `logos`; removed in 0.
|
|
97
|
+
/** @deprecated Renamed to `logos`; removed in 0.18.0. */
|
|
90
98
|
platforms?: ConnectButtonLogoInput[];
|
|
91
99
|
/** Whether this user has a live grant, from your server. When set, it
|
|
92
100
|
* decides the Connected state. When omitted, the button shows
|
|
@@ -105,6 +113,10 @@ export interface ConnectButtonProps {
|
|
|
105
113
|
theme?: ConnectButtonTheme;
|
|
106
114
|
/** Theme of One's hosted page. */
|
|
107
115
|
connectTheme?: OneConnectTheme;
|
|
116
|
+
/** A connector slug ("gmail") to open first on One's page. */
|
|
117
|
+
connector?: string;
|
|
118
|
+
/** Where this flow returns to: a path on your app. */
|
|
119
|
+
returnTo?: string;
|
|
108
120
|
/** "Connect your apps" unless set. */
|
|
109
121
|
label?: string;
|
|
110
122
|
/** "Connected" unless set. */
|
package/dist/vue.cjs.js
CHANGED
|
@@ -1 +1 @@
|
|
|
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},logos:{type:Array,default:void 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},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
|
|
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},logos:{type:Array,default:void 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},connector:{type:String,default:void 0},returnTo:{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.renderConnectButton(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}),()=>{var t;return e.h("span",{ref:r,class:"one-connect","data-variant":null!==(t=n.variant)&&void 0!==t?t:"default","data-full-width":n.fullWidth?"":void 0})}}});exports.ConnectButton=o;
|
package/dist/vue.d.ts
CHANGED
|
@@ -42,6 +42,14 @@ export declare const ConnectButton: import("vue").DefineComponent<import("vue").
|
|
|
42
42
|
type: PropType<OneConnectTheme>;
|
|
43
43
|
default: undefined;
|
|
44
44
|
};
|
|
45
|
+
connector: {
|
|
46
|
+
type: StringConstructor;
|
|
47
|
+
default: undefined;
|
|
48
|
+
};
|
|
49
|
+
returnTo: {
|
|
50
|
+
type: StringConstructor;
|
|
51
|
+
default: undefined;
|
|
52
|
+
};
|
|
45
53
|
label: {
|
|
46
54
|
type: StringConstructor;
|
|
47
55
|
default: undefined;
|
|
@@ -102,6 +110,14 @@ export declare const ConnectButton: import("vue").DefineComponent<import("vue").
|
|
|
102
110
|
type: PropType<OneConnectTheme>;
|
|
103
111
|
default: undefined;
|
|
104
112
|
};
|
|
113
|
+
connector: {
|
|
114
|
+
type: StringConstructor;
|
|
115
|
+
default: undefined;
|
|
116
|
+
};
|
|
117
|
+
returnTo: {
|
|
118
|
+
type: StringConstructor;
|
|
119
|
+
default: undefined;
|
|
120
|
+
};
|
|
105
121
|
label: {
|
|
106
122
|
type: StringConstructor;
|
|
107
123
|
default: undefined;
|
|
@@ -123,6 +139,8 @@ export declare const ConnectButton: import("vue").DefineComponent<import("vue").
|
|
|
123
139
|
label: string;
|
|
124
140
|
disabled: boolean;
|
|
125
141
|
connectTheme: OneConnectTheme;
|
|
142
|
+
connector: string;
|
|
143
|
+
returnTo: string;
|
|
126
144
|
logos: ConnectButtonLogoInput[];
|
|
127
145
|
platforms: ConnectButtonLogoInput[];
|
|
128
146
|
variant: ConnectButtonVariant;
|
package/dist/vue.esm.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{defineComponent as e,ref as t,onMounted as n,watch as o,onBeforeUnmount as r,h as
|
|
1
|
+
import{defineComponent as e,ref as t,onMounted as n,watch as o,onBeforeUnmount as r,h as i}from"vue";import{renderConnectButton as l}from"@withone/connect";const d={type:Boolean,default:void 0},a=e({name:"OneConnectButton",props:{authorizeUrl:{type:String,required:!0},logos:{type:Array,default:void 0},platforms:{type:Array,default:void 0},connected:d,disabled:d,variant:{type:String,default:void 0},size:{type:String,default:void 0},fullWidth:d,theme:{type:String,default:void 0},connectTheme:{type:String,default:void 0},connector:{type:String,default:void 0},returnTo:{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(e,{emit:d}){const a=t(null);let u=null;const c=()=>({...e,onSuccess:()=>d("success"),onError:(e,t)=>d("error",e,t),onCancel:()=>d("cancel")});return n(()=>{a.value&&(u=l(a.value,c()))}),o(()=>({...e}),()=>{var e;return null===(e=u)||void 0===e?void 0:e.update(c())},{deep:!0}),r(()=>{var e;null===(e=u)||void 0===e||e.destroy(),u=null}),()=>{var t;return i("span",{ref:a,class:"one-connect","data-variant":null!==(t=e.variant)&&void 0!==t?t:"default","data-full-width":e.fullWidth?"":void 0})}}});export{a as ConnectButton};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@withone/connect",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.17.1",
|
|
4
4
|
"description": "One Connect for your app: the button your users press, the two backend routes as one import, and a server client that calls One with the grant. Users keep their connections in One; your app holds only what they granted.",
|
|
5
5
|
"files": [
|
|
6
6
|
"dist",
|
|
@@ -149,8 +149,10 @@ import { ConnectButton } from "@withone/connect/react";
|
|
|
149
149
|
|
|
150
150
|
Optional props: `variant` ("default" | "accent" | "block"), `size` ("sm" |
|
|
151
151
|
"md" | "lg"), `fullWidth`, `theme` ("light" | "dark" | "auto"),
|
|
152
|
-
`connectTheme` ("light" | "dark", One's page), `
|
|
153
|
-
|
|
152
|
+
`connectTheme` ("light" | "dark", One's page), `connector` (a slug such as
|
|
153
|
+
"gmail": One's page opens that connector's connect screen first, then the
|
|
154
|
+
usual list), `returnTo` (a path on the app this flow returns to), `label`,
|
|
155
|
+
`connectedLabel`, `description`, `disabled`, `onCancel`. The accent variant's colours come
|
|
154
156
|
from the host's `--one-connect-accent` and `--one-connect-accent-fg` CSS
|
|
155
157
|
variables.
|
|
156
158
|
|
|
@@ -177,8 +179,15 @@ keep it and wire the flow to it instead of adding a second style:
|
|
|
177
179
|
button.addEventListener("click", () => flow.open());
|
|
178
180
|
```
|
|
179
181
|
|
|
182
|
+
- One connector at a time (a chat that says "Connect Gmail", a settings
|
|
183
|
+
row): `useOneConnect({ authorizeUrl, connector: "gmail", returnTo: "/chat/42" })`.
|
|
184
|
+
One's page still signs the user in and asks for the space, then opens
|
|
185
|
+
Gmail's connect screen instead of the list; the list follows with
|
|
186
|
+
everything connected. `returnTo` must be a path on the app.
|
|
187
|
+
|
|
180
188
|
- No SDK in the browser (server-rendered pages): a plain link,
|
|
181
|
-
`<a href="/api/one/authorize"
|
|
189
|
+
`<a href="/api/one/authorize">` (add `#one_connector=gmail` and
|
|
190
|
+
`?one_return_to=%2Fchat%2F42` for the above). The user returns with
|
|
182
191
|
`?one_connect=success`, or `?one_connect=error&one_connect_error=` with
|
|
183
192
|
`declined`, `expired` or `failed`. Show the app's own text per code, never
|
|
184
193
|
text from the URL, and remove the parameters after reading them.
|
|
@@ -205,7 +214,7 @@ const reply = await oneConnect.runAction(userId, {
|
|
|
205
214
|
encoding: "json", // or "form" / "multipart", as the guide says
|
|
206
215
|
headers: {}, // only when the guide names one
|
|
207
216
|
});
|
|
208
|
-
// { status, ok, blockedByGrant, data }
|
|
217
|
+
// { status, ok, blockedByGrant, refusal, data }
|
|
209
218
|
```
|
|
210
219
|
|
|
211
220
|
Each connection's `access` is `{ policy: "full" }`, `{ policy: "methods",
|
|
@@ -221,8 +230,18 @@ user's id to every call it makes.
|
|
|
221
230
|
|
|
222
231
|
## 8 - Errors
|
|
223
232
|
|
|
224
|
-
|
|
225
|
-
|
|
233
|
+
`refusal` says why One itself turned a call down:
|
|
234
|
+
|
|
235
|
+
- `"grant"` (`403`, `blockedByGrant: true`): outside what the user granted.
|
|
236
|
+
Do not retry it.
|
|
237
|
+
- `"plan_limit"` (`402`): a plan limit, not the grant. Wait and retry later.
|
|
238
|
+
Do not ask the user to reconnect.
|
|
239
|
+
- `"rate_limit"` (`429`): too many calls. Wait and retry.
|
|
240
|
+
- `"one_error"` (other, e.g. `5xx`): retry with a backoff.
|
|
241
|
+
- `null`: not One's refusal; the provider's own answer, or a success.
|
|
242
|
+
|
|
243
|
+
Only a `403` means "outside the grant". Never treat a `402`, `429` or `5xx`
|
|
244
|
+
as a missing permission.
|
|
226
245
|
|
|
227
246
|
Errors are `OneConnectError` with a `code`. Handle them where the app calls One:
|
|
228
247
|
|
package/src/button.ts
CHANGED
|
@@ -182,6 +182,8 @@ const flowOptions = (
|
|
|
182
182
|
): OneConnectFlowOptions => ({
|
|
183
183
|
authorizeUrl: props.authorizeUrl,
|
|
184
184
|
connectTheme: props.connectTheme,
|
|
185
|
+
connector: props.connector,
|
|
186
|
+
returnTo: props.returnTo,
|
|
185
187
|
...handlers,
|
|
186
188
|
});
|
|
187
189
|
|
|
@@ -382,6 +384,8 @@ const ATTRIBUTES = [
|
|
|
382
384
|
"full-width",
|
|
383
385
|
"theme",
|
|
384
386
|
"connect-theme",
|
|
387
|
+
"connector",
|
|
388
|
+
"return-to",
|
|
385
389
|
"label",
|
|
386
390
|
"connected-label",
|
|
387
391
|
"description",
|
|
@@ -454,6 +458,8 @@ export function registerConnectButton(): void {
|
|
|
454
458
|
fullWidth: flag(this, "full-width"),
|
|
455
459
|
theme: attributeAs(this, "theme", ["light", "dark", "auto"]),
|
|
456
460
|
connectTheme: attributeAs(this, "connect-theme", ["light", "dark"]),
|
|
461
|
+
connector: this.getAttribute("connector") ?? undefined,
|
|
462
|
+
returnTo: this.getAttribute("return-to") ?? undefined,
|
|
457
463
|
label: this.getAttribute("label") ?? undefined,
|
|
458
464
|
connectedLabel: this.getAttribute("connected-label") ?? undefined,
|
|
459
465
|
description: this.getAttribute("description") ?? undefined,
|
package/src/constants.ts
CHANGED
|
@@ -13,6 +13,16 @@ export const LEGACY_MESSAGE_PARAM = "one_connect_message";
|
|
|
13
13
|
* chain, so the app's backend forwards nothing. */
|
|
14
14
|
export const THEME_PARAM = "one_theme";
|
|
15
15
|
|
|
16
|
+
/** Fragment key naming the connector One's page opens first. Sign-in
|
|
17
|
+
* and the space step run as always; then that connector's connect
|
|
18
|
+
* screen opens instead of the list, and the list follows with
|
|
19
|
+
* everything the user has connected. */
|
|
20
|
+
export const CONNECTOR_PARAM = "one_connector";
|
|
21
|
+
|
|
22
|
+
/** Query param on the app's own authorize route naming where this one
|
|
23
|
+
* flow returns to. The route reads it; it never reaches One. */
|
|
24
|
+
export const RETURN_TO_PARAM = "one_return_to";
|
|
25
|
+
|
|
16
26
|
/** One's connector logos, by slug. */
|
|
17
27
|
export const CONNECTOR_ASSETS_URL = "https://assets.withone.ai/connectors";
|
|
18
28
|
|
package/src/flow.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { THEME_PARAM } from "./constants";
|
|
1
|
+
import { CONNECTOR_PARAM, RETURN_TO_PARAM, THEME_PARAM } from "./constants";
|
|
2
2
|
import { hasReturnParams, parseReturn, stripReturnParams } from "./return";
|
|
3
3
|
import type {
|
|
4
4
|
OneConnectFlow,
|
|
@@ -91,11 +91,19 @@ function bindPageshow(): void {
|
|
|
91
91
|
});
|
|
92
92
|
}
|
|
93
93
|
|
|
94
|
+
/** The theme and the connector ride the fragment: no server sees it,
|
|
95
|
+
* and it survives every redirect to One's page. The return path is a
|
|
96
|
+
* query param for the app's own route, which reads it and keeps it. */
|
|
94
97
|
const authorizeUrlFor = (options: OneConnectFlowOptions): string => {
|
|
95
|
-
const theme = options.connectTheme;
|
|
96
98
|
try {
|
|
97
99
|
const url = new URL(options.authorizeUrl, window.location.origin);
|
|
98
|
-
if (
|
|
100
|
+
if (options.returnTo) url.searchParams.set(RETURN_TO_PARAM, options.returnTo);
|
|
101
|
+
const fragment = new URLSearchParams();
|
|
102
|
+
if (options.connectTheme) fragment.set(THEME_PARAM, options.connectTheme);
|
|
103
|
+
const connector = options.connector?.trim().toLowerCase();
|
|
104
|
+
if (connector) fragment.set(CONNECTOR_PARAM, connector);
|
|
105
|
+
const hash = fragment.toString();
|
|
106
|
+
if (hash) url.hash = hash;
|
|
99
107
|
return url.toString();
|
|
100
108
|
} catch {
|
|
101
109
|
return options.authorizeUrl;
|
package/src/next.ts
CHANGED
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
* That serves /api/one/authorize and /api/one/callback. Register
|
|
15
15
|
* `https://yourapp.com/api/one/callback` as the app's redirect URI.
|
|
16
16
|
*/
|
|
17
|
+
import { RETURN_TO_PARAM } from "./constants";
|
|
17
18
|
import type { CompleteAuthorizationResult, OneConnectClient } from "./server";
|
|
18
19
|
|
|
19
20
|
export interface OneConnectRoutesOptions {
|
|
@@ -109,7 +110,11 @@ export function createOneConnectRoutes(
|
|
|
109
110
|
const user = await requireUser(request);
|
|
110
111
|
if (user instanceof Response) return user;
|
|
111
112
|
const loginHint = (await options.loginHintFor?.(request)) ?? undefined;
|
|
112
|
-
|
|
113
|
+
// The browser flow names where this one flow returns to; the client
|
|
114
|
+
// keeps only a path on the app, so a crafted link goes nowhere else.
|
|
115
|
+
const returnTo =
|
|
116
|
+
new URL(request.url).searchParams.get(RETURN_TO_PARAM) ?? undefined;
|
|
117
|
+
const { redirectUrl, cookie } = oneConnect.startAuthorization({ loginHint, returnTo });
|
|
113
118
|
return redirectWithCookies(redirectUrl, [
|
|
114
119
|
serializeCookie(cookie.name, cookie.value, cookie.options),
|
|
115
120
|
]);
|