@drawbridge/drawbridge-utils 0.0.106 → 0.0.108

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.
@@ -0,0 +1,123 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
3
+ var __getOwnPropNames = Object.getOwnPropertyNames;
4
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
5
+ var __export = (target, all) => {
6
+ for (var name in all)
7
+ __defProp(target, name, { get: all[name], enumerable: true });
8
+ };
9
+ var __copyProps = (to, from, except, desc) => {
10
+ if (from && typeof from === "object" || typeof from === "function") {
11
+ for (let key of __getOwnPropNames(from))
12
+ if (!__hasOwnProp.call(to, key) && key !== except)
13
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
14
+ }
15
+ return to;
16
+ };
17
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
18
+
19
+ // lib/connections/oauth.js
20
+ var oauth_exports = {};
21
+ __export(oauth_exports, {
22
+ consentUrl: () => consentUrl,
23
+ exchange: () => exchange,
24
+ pkcePair: () => pkcePair,
25
+ refresh: () => refresh
26
+ });
27
+ module.exports = __toCommonJS(oauth_exports);
28
+ var import_node_crypto = require("crypto");
29
+ var credentials = ({ clientId, clientSecret, descriptor }) => (descriptor == null ? void 0 : descriptor.clientAuth) === "basic" ? {
30
+ headers: { authorization: "Basic " + Buffer.from(clientId + ":" + clientSecret).toString("base64") },
31
+ body: {}
32
+ } : {
33
+ headers: {},
34
+ body: { client_id: clientId, client_secret: clientSecret }
35
+ };
36
+ var pkcePair = () => {
37
+ const verifier = (0, import_node_crypto.randomBytes)(32).toString("base64url");
38
+ return {
39
+ challenge: (0, import_node_crypto.createHash)("sha256").update(verifier).digest("base64url"),
40
+ method: "S256",
41
+ verifier
42
+ };
43
+ };
44
+ var consentUrl = ({ challenge, clientId, descriptor, redirect, state } = {}) => {
45
+ if (!clientId) throw new Error("This deployment has no OAuth client configured, so there is nothing to consent through");
46
+ if (!(descriptor == null ? void 0 : descriptor.authorize)) throw new Error("This connection declares no authorize url");
47
+ if (descriptor.pkce && !challenge) throw new Error("This connection requires PKCE, so a code challenge is not optional");
48
+ return descriptor.authorize + "?" + new URLSearchParams({
49
+ // The descriptor's own params go FIRST, so a vendor quirk cannot quietly
50
+ // overwrite one of the fields below that every consent carries.
51
+ ...descriptor.params || {},
52
+ client_id: clientId,
53
+ redirect_uri: redirect,
54
+ response_type: "code",
55
+ ...descriptor.scopes && { scope: descriptor.scopes },
56
+ ...descriptor.pkce && {
57
+ code_challenge: challenge,
58
+ code_challenge_method: "S256"
59
+ },
60
+ state
61
+ });
62
+ };
63
+ var exchange = async ({ clientId, clientSecret, code, descriptor, fetcher = fetch, redirect, verifier } = {}) => {
64
+ if (!(descriptor == null ? void 0 : descriptor.token)) throw new Error("This connection declares no token url");
65
+ if (descriptor.pkce && !verifier) throw new Error("This connection requires PKCE, so the code verifier is not optional");
66
+ const client = credentials({ clientId, clientSecret, descriptor });
67
+ const response = await fetcher(descriptor.token, {
68
+ body: new URLSearchParams({
69
+ ...client.body,
70
+ code: decodeURIComponent(String(code || "").trim()),
71
+ grant_type: "authorization_code",
72
+ redirect_uri: redirect,
73
+ ...descriptor.pkce && { code_verifier: verifier }
74
+ }),
75
+ headers: { "content-type": "application/x-www-form-urlencoded", ...client.headers },
76
+ method: "POST",
77
+ signal: AbortSignal.timeout(15e3)
78
+ });
79
+ const body = await response.json().catch(() => ({}));
80
+ if (!response.ok) {
81
+ throw new Error("The vendor refused the exchange (" + response.status + ")" + ((body == null ? void 0 : body.error) ? ": " + body.error : ""));
82
+ }
83
+ if (!body.access_token) throw new Error("The vendor returned no access token");
84
+ return {
85
+ accessToken: body.access_token,
86
+ expiresIn: body.expires_in || null,
87
+ refreshToken: body.refresh_token || null,
88
+ scope: body.scope || null
89
+ };
90
+ };
91
+ var refresh = async ({ clientId, clientSecret, descriptor, fetcher = fetch, refreshToken } = {}) => {
92
+ if (!clientId || !clientSecret) throw new Error("This deployment has no OAuth client configured, so no token can be minted");
93
+ if (!refreshToken) throw new Error("Nothing has been consented to yet, so there is no refresh token to spend");
94
+ if (!(descriptor == null ? void 0 : descriptor.token)) throw new Error("This connection declares no token url");
95
+ const client = credentials({ clientId, clientSecret, descriptor });
96
+ const response = await fetcher(descriptor.token, {
97
+ body: new URLSearchParams({
98
+ ...client.body,
99
+ grant_type: "refresh_token",
100
+ refresh_token: refreshToken
101
+ }),
102
+ headers: { "content-type": "application/x-www-form-urlencoded", ...client.headers },
103
+ method: "POST",
104
+ signal: AbortSignal.timeout(15e3)
105
+ });
106
+ if (!response.ok) throw new Error("The vendor refused the refresh token (" + response.status + ") \u2014 reconnect the connection");
107
+ const body = await response.json();
108
+ return {
109
+ accessToken: body.access_token,
110
+ expiresIn: body.expires_in || null,
111
+ // A vendor that rotates its refresh token returns a new one, and dropping
112
+ // it silently invalidates the stored grant on the NEXT refresh rather
113
+ // than this one — a failure a day late and nowhere near its cause.
114
+ refreshToken: body.refresh_token || null
115
+ };
116
+ };
117
+ // Annotate the CommonJS export names for ESM import in node:
118
+ 0 && (module.exports = {
119
+ consentUrl,
120
+ exchange,
121
+ pkcePair,
122
+ refresh
123
+ });
@@ -0,0 +1,189 @@
1
+ import { randomBytes, createHash } from 'node:crypto';
2
+
3
+ // HOW A VENDOR WANTS TO BE TOLD WHO WE ARE. Two conventions, both standard, and
4
+ // vendors genuinely differ: Google takes client_id and client_secret as form
5
+ // fields; Klaviyo requires HTTP Basic and rejects the body form. Declared per
6
+ // vendor rather than branched on a slug, because the second Basic vendor would
7
+ // otherwise add a second branch.
8
+ const credentials = ({ clientId, clientSecret, descriptor }) => (
9
+
10
+ descriptor?.clientAuth === 'basic'
11
+ ? {
12
+ headers : { authorization : 'Basic ' + Buffer.from( clientId + ':' + clientSecret ).toString( 'base64' ) },
13
+ body : {}
14
+ }
15
+ : {
16
+ headers : {},
17
+ body : { client_id : clientId, client_secret : clientSecret }
18
+ }
19
+
20
+ );
21
+
22
+ // The OAuth flow, once, for every vendor that uses one.
23
+ //
24
+ // A connection declares what it needs and this runs it. Adding an OAuth vendor
25
+ // is a manifest, not a module — which is the whole point: the alternative is
26
+ // what Shopify already is, a vendor whose consent, exchange and callback are
27
+ // hand-written, so a second one copies every line.
28
+ //
29
+ // auth : {
30
+ // type : 'oauth',
31
+ // oauth : {
32
+ // authorize : 'https://login.mailchimp.com/oauth2/authorize',
33
+ // client : { id : 'MAILCHIMP_OAUTH_CLIENT_ID', secret : 'MAILCHIMP_OAUTH_CLIENT_SECRET' },
34
+ // redirect : '/api/connection/mailchimp/callback',
35
+ // token : 'https://login.mailchimp.com/oauth2/token',
36
+ // params : { ... }, optional vendor quirks
37
+ // pkce : true, Klaviyo's OAuth 2.1 requires it
38
+ // scopes : '...' optional; Mailchimp has none
39
+ // }
40
+ // }
41
+ //
42
+ // NO CREDENTIAL LIVES HERE OR IN A MANIFEST. `client` NAMES the environment
43
+ // variables holding OUR application's client — one identity, every merchant,
44
+ // which is what an OAuth client is. The token belongs to the merchant and
45
+ // arrives from their own consent, and that distinction is what stops one
46
+ // organization reading another's data.
47
+ //
48
+ // `fetcher` is injected on every call so tests never open a socket. Production
49
+ // passes nothing and gets global fetch.
50
+
51
+ // PKCE, for the vendors that demand it. Klaviyo's OAuth 2.1 refuses an exchange
52
+ // without it: a random verifier is held by us, its SHA-256 challenge travels
53
+ // with the consent, and the vendor checks they match — which is what stops an
54
+ // intercepted code being redeemed by somebody else.
55
+ //
56
+ // 43-128 characters of unreserved alphabet, per RFC 7636. base64url of 32 bytes
57
+ // lands at 43 and needs no padding stripped beyond the two substitutions.
58
+ const pkcePair = () => {
59
+
60
+ const verifier = randomBytes( 32 ).toString( 'base64url' );
61
+
62
+ return {
63
+ challenge : createHash( 'sha256' ).update( verifier ).digest( 'base64url' ),
64
+ method : 'S256',
65
+ verifier
66
+ };
67
+
68
+ };
69
+
70
+ // The url a merchant follows. Side-effect free by construction — building it
71
+ // writes nothing, which is what lets a card render it as a link rather than a
72
+ // button that mints a consent on click.
73
+ const consentUrl = ({ challenge, clientId, descriptor, redirect, state } = {}) => {
74
+
75
+ // A url naming no application is not a consent, and it reports a deployment
76
+ // fault nobody can fix from a connection page.
77
+ if( ! clientId ) throw new Error( 'This deployment has no OAuth client configured, so there is nothing to consent through' );
78
+
79
+ if( ! descriptor?.authorize ) throw new Error( 'This connection declares no authorize url' );
80
+
81
+ if( descriptor.pkce && ! challenge ) throw new Error( 'This connection requires PKCE, so a code challenge is not optional' );
82
+
83
+ return descriptor.authorize + '?' + new URLSearchParams({
84
+ // The descriptor's own params go FIRST, so a vendor quirk cannot quietly
85
+ // overwrite one of the fields below that every consent carries.
86
+ ...( descriptor.params || {} ),
87
+ client_id : clientId,
88
+ redirect_uri : redirect,
89
+ response_type : 'code',
90
+ ...( descriptor.scopes && { scope : descriptor.scopes }),
91
+ ...( descriptor.pkce && {
92
+ code_challenge : challenge,
93
+ code_challenge_method : 'S256'
94
+ }),
95
+ state
96
+ });
97
+
98
+ };
99
+
100
+ // The code-for-token trade.
101
+ //
102
+ // The redirect must byte-match what the consent carried — it is registered in
103
+ // the vendor's console — so the caller passes the declared callback and it is
104
+ // used verbatim, never rebuilt from a slug.
105
+ const exchange = async ({ clientId, clientSecret, code, descriptor, fetcher = fetch, redirect, verifier } = {}) => {
106
+
107
+ if( ! descriptor?.token ) throw new Error( 'This connection declares no token url' );
108
+
109
+ if( descriptor.pkce && ! verifier ) throw new Error( 'This connection requires PKCE, so the code verifier is not optional' );
110
+
111
+ const client = credentials({ clientId, clientSecret, descriptor });
112
+
113
+ const response = await fetcher( descriptor.token, {
114
+ body : new URLSearchParams({
115
+ ...client.body,
116
+ code : decodeURIComponent( String( code || '' ).trim() ),
117
+ grant_type : 'authorization_code',
118
+ redirect_uri : redirect,
119
+ ...( descriptor.pkce && { code_verifier : verifier })
120
+ }),
121
+ headers : { 'content-type' : 'application/x-www-form-urlencoded', ...client.headers },
122
+ method : 'POST',
123
+ signal : AbortSignal.timeout( 15000 )
124
+ });
125
+
126
+ const body = await response.json().catch( () => ({}) );
127
+
128
+ if( ! response.ok ){
129
+
130
+ throw new Error( 'The vendor refused the exchange (' + response.status + ')' + ( body?.error ? ': ' + body.error : '' ) );
131
+
132
+ }
133
+
134
+ if( ! body.access_token ) throw new Error( 'The vendor returned no access token' );
135
+
136
+ // A REFRESH TOKEN IS NOT UNIVERSAL. Google returns one only with
137
+ // access_type=offline; Mailchimp's tokens do not expire and it returns none
138
+ // at all. So its absence cannot be an error here the way it is in a
139
+ // refresh-only world — the manifest says whether the vendor issues one by
140
+ // whether it declares the params that ask for it.
141
+ return {
142
+ accessToken : body.access_token,
143
+ expiresIn : body.expires_in || null,
144
+ refreshToken : body.refresh_token || null,
145
+ scope : body.scope || null
146
+ };
147
+
148
+ };
149
+
150
+ // Short-lived access, minted from a stored refresh token.
151
+ //
152
+ // THE MINT IS THE PROBE. A revoked, rotated or wrong-account grant fails here in
153
+ // the vendor's own words, rather than as an empty result three steps later that
154
+ // reads like "this account has no data".
155
+ const refresh = async ({ clientId, clientSecret, descriptor, fetcher = fetch, refreshToken } = {}) => {
156
+
157
+ if( ! clientId || ! clientSecret ) throw new Error( 'This deployment has no OAuth client configured, so no token can be minted' );
158
+ if( ! refreshToken ) throw new Error( 'Nothing has been consented to yet, so there is no refresh token to spend' );
159
+ if( ! descriptor?.token ) throw new Error( 'This connection declares no token url' );
160
+
161
+ const client = credentials({ clientId, clientSecret, descriptor });
162
+
163
+ const response = await fetcher( descriptor.token, {
164
+ body : new URLSearchParams({
165
+ ...client.body,
166
+ grant_type : 'refresh_token',
167
+ refresh_token : refreshToken
168
+ }),
169
+ headers : { 'content-type' : 'application/x-www-form-urlencoded', ...client.headers },
170
+ method : 'POST',
171
+ signal : AbortSignal.timeout( 15000 )
172
+ });
173
+
174
+ if( ! response.ok ) throw new Error( 'The vendor refused the refresh token (' + response.status + ') — reconnect the connection' );
175
+
176
+ const body = await response.json();
177
+
178
+ return {
179
+ accessToken : body.access_token,
180
+ expiresIn : body.expires_in || null,
181
+ // A vendor that rotates its refresh token returns a new one, and dropping
182
+ // it silently invalidates the stored grant on the NEXT refresh rather
183
+ // than this one — a failure a day late and nowhere near its cause.
184
+ refreshToken : body.refresh_token || null
185
+ };
186
+
187
+ };
188
+
189
+ export { consentUrl, exchange, pkcePair, refresh };
@@ -0,0 +1,189 @@
1
+ import { randomBytes, createHash } from 'node:crypto';
2
+
3
+ // HOW A VENDOR WANTS TO BE TOLD WHO WE ARE. Two conventions, both standard, and
4
+ // vendors genuinely differ: Google takes client_id and client_secret as form
5
+ // fields; Klaviyo requires HTTP Basic and rejects the body form. Declared per
6
+ // vendor rather than branched on a slug, because the second Basic vendor would
7
+ // otherwise add a second branch.
8
+ const credentials = ({ clientId, clientSecret, descriptor }) => (
9
+
10
+ descriptor?.clientAuth === 'basic'
11
+ ? {
12
+ headers : { authorization : 'Basic ' + Buffer.from( clientId + ':' + clientSecret ).toString( 'base64' ) },
13
+ body : {}
14
+ }
15
+ : {
16
+ headers : {},
17
+ body : { client_id : clientId, client_secret : clientSecret }
18
+ }
19
+
20
+ );
21
+
22
+ // The OAuth flow, once, for every vendor that uses one.
23
+ //
24
+ // A connection declares what it needs and this runs it. Adding an OAuth vendor
25
+ // is a manifest, not a module — which is the whole point: the alternative is
26
+ // what Shopify already is, a vendor whose consent, exchange and callback are
27
+ // hand-written, so a second one copies every line.
28
+ //
29
+ // auth : {
30
+ // type : 'oauth',
31
+ // oauth : {
32
+ // authorize : 'https://login.mailchimp.com/oauth2/authorize',
33
+ // client : { id : 'MAILCHIMP_OAUTH_CLIENT_ID', secret : 'MAILCHIMP_OAUTH_CLIENT_SECRET' },
34
+ // redirect : '/api/connection/mailchimp/callback',
35
+ // token : 'https://login.mailchimp.com/oauth2/token',
36
+ // params : { ... }, optional vendor quirks
37
+ // pkce : true, Klaviyo's OAuth 2.1 requires it
38
+ // scopes : '...' optional; Mailchimp has none
39
+ // }
40
+ // }
41
+ //
42
+ // NO CREDENTIAL LIVES HERE OR IN A MANIFEST. `client` NAMES the environment
43
+ // variables holding OUR application's client — one identity, every merchant,
44
+ // which is what an OAuth client is. The token belongs to the merchant and
45
+ // arrives from their own consent, and that distinction is what stops one
46
+ // organization reading another's data.
47
+ //
48
+ // `fetcher` is injected on every call so tests never open a socket. Production
49
+ // passes nothing and gets global fetch.
50
+
51
+ // PKCE, for the vendors that demand it. Klaviyo's OAuth 2.1 refuses an exchange
52
+ // without it: a random verifier is held by us, its SHA-256 challenge travels
53
+ // with the consent, and the vendor checks they match — which is what stops an
54
+ // intercepted code being redeemed by somebody else.
55
+ //
56
+ // 43-128 characters of unreserved alphabet, per RFC 7636. base64url of 32 bytes
57
+ // lands at 43 and needs no padding stripped beyond the two substitutions.
58
+ const pkcePair = () => {
59
+
60
+ const verifier = randomBytes( 32 ).toString( 'base64url' );
61
+
62
+ return {
63
+ challenge : createHash( 'sha256' ).update( verifier ).digest( 'base64url' ),
64
+ method : 'S256',
65
+ verifier
66
+ };
67
+
68
+ };
69
+
70
+ // The url a merchant follows. Side-effect free by construction — building it
71
+ // writes nothing, which is what lets a card render it as a link rather than a
72
+ // button that mints a consent on click.
73
+ const consentUrl = ({ challenge, clientId, descriptor, redirect, state } = {}) => {
74
+
75
+ // A url naming no application is not a consent, and it reports a deployment
76
+ // fault nobody can fix from a connection page.
77
+ if( ! clientId ) throw new Error( 'This deployment has no OAuth client configured, so there is nothing to consent through' );
78
+
79
+ if( ! descriptor?.authorize ) throw new Error( 'This connection declares no authorize url' );
80
+
81
+ if( descriptor.pkce && ! challenge ) throw new Error( 'This connection requires PKCE, so a code challenge is not optional' );
82
+
83
+ return descriptor.authorize + '?' + new URLSearchParams({
84
+ // The descriptor's own params go FIRST, so a vendor quirk cannot quietly
85
+ // overwrite one of the fields below that every consent carries.
86
+ ...( descriptor.params || {} ),
87
+ client_id : clientId,
88
+ redirect_uri : redirect,
89
+ response_type : 'code',
90
+ ...( descriptor.scopes && { scope : descriptor.scopes }),
91
+ ...( descriptor.pkce && {
92
+ code_challenge : challenge,
93
+ code_challenge_method : 'S256'
94
+ }),
95
+ state
96
+ });
97
+
98
+ };
99
+
100
+ // The code-for-token trade.
101
+ //
102
+ // The redirect must byte-match what the consent carried — it is registered in
103
+ // the vendor's console — so the caller passes the declared callback and it is
104
+ // used verbatim, never rebuilt from a slug.
105
+ const exchange = async ({ clientId, clientSecret, code, descriptor, fetcher = fetch, redirect, verifier } = {}) => {
106
+
107
+ if( ! descriptor?.token ) throw new Error( 'This connection declares no token url' );
108
+
109
+ if( descriptor.pkce && ! verifier ) throw new Error( 'This connection requires PKCE, so the code verifier is not optional' );
110
+
111
+ const client = credentials({ clientId, clientSecret, descriptor });
112
+
113
+ const response = await fetcher( descriptor.token, {
114
+ body : new URLSearchParams({
115
+ ...client.body,
116
+ code : decodeURIComponent( String( code || '' ).trim() ),
117
+ grant_type : 'authorization_code',
118
+ redirect_uri : redirect,
119
+ ...( descriptor.pkce && { code_verifier : verifier })
120
+ }),
121
+ headers : { 'content-type' : 'application/x-www-form-urlencoded', ...client.headers },
122
+ method : 'POST',
123
+ signal : AbortSignal.timeout( 15000 )
124
+ });
125
+
126
+ const body = await response.json().catch( () => ({}) );
127
+
128
+ if( ! response.ok ){
129
+
130
+ throw new Error( 'The vendor refused the exchange (' + response.status + ')' + ( body?.error ? ': ' + body.error : '' ) );
131
+
132
+ }
133
+
134
+ if( ! body.access_token ) throw new Error( 'The vendor returned no access token' );
135
+
136
+ // A REFRESH TOKEN IS NOT UNIVERSAL. Google returns one only with
137
+ // access_type=offline; Mailchimp's tokens do not expire and it returns none
138
+ // at all. So its absence cannot be an error here the way it is in a
139
+ // refresh-only world — the manifest says whether the vendor issues one by
140
+ // whether it declares the params that ask for it.
141
+ return {
142
+ accessToken : body.access_token,
143
+ expiresIn : body.expires_in || null,
144
+ refreshToken : body.refresh_token || null,
145
+ scope : body.scope || null
146
+ };
147
+
148
+ };
149
+
150
+ // Short-lived access, minted from a stored refresh token.
151
+ //
152
+ // THE MINT IS THE PROBE. A revoked, rotated or wrong-account grant fails here in
153
+ // the vendor's own words, rather than as an empty result three steps later that
154
+ // reads like "this account has no data".
155
+ const refresh = async ({ clientId, clientSecret, descriptor, fetcher = fetch, refreshToken } = {}) => {
156
+
157
+ if( ! clientId || ! clientSecret ) throw new Error( 'This deployment has no OAuth client configured, so no token can be minted' );
158
+ if( ! refreshToken ) throw new Error( 'Nothing has been consented to yet, so there is no refresh token to spend' );
159
+ if( ! descriptor?.token ) throw new Error( 'This connection declares no token url' );
160
+
161
+ const client = credentials({ clientId, clientSecret, descriptor });
162
+
163
+ const response = await fetcher( descriptor.token, {
164
+ body : new URLSearchParams({
165
+ ...client.body,
166
+ grant_type : 'refresh_token',
167
+ refresh_token : refreshToken
168
+ }),
169
+ headers : { 'content-type' : 'application/x-www-form-urlencoded', ...client.headers },
170
+ method : 'POST',
171
+ signal : AbortSignal.timeout( 15000 )
172
+ });
173
+
174
+ if( ! response.ok ) throw new Error( 'The vendor refused the refresh token (' + response.status + ') — reconnect the connection' );
175
+
176
+ const body = await response.json();
177
+
178
+ return {
179
+ accessToken : body.access_token,
180
+ expiresIn : body.expires_in || null,
181
+ // A vendor that rotates its refresh token returns a new one, and dropping
182
+ // it silently invalidates the stored grant on the NEXT refresh rather
183
+ // than this one — a failure a day late and nowhere near its cause.
184
+ refreshToken : body.refresh_token || null
185
+ };
186
+
187
+ };
188
+
189
+ export { consentUrl, exchange, pkcePair, refresh };
@@ -0,0 +1,96 @@
1
+ // lib/connections/oauth.js
2
+ import { createHash, randomBytes } from "crypto";
3
+ var credentials = ({ clientId, clientSecret, descriptor }) => (descriptor == null ? void 0 : descriptor.clientAuth) === "basic" ? {
4
+ headers: { authorization: "Basic " + Buffer.from(clientId + ":" + clientSecret).toString("base64") },
5
+ body: {}
6
+ } : {
7
+ headers: {},
8
+ body: { client_id: clientId, client_secret: clientSecret }
9
+ };
10
+ var pkcePair = () => {
11
+ const verifier = randomBytes(32).toString("base64url");
12
+ return {
13
+ challenge: createHash("sha256").update(verifier).digest("base64url"),
14
+ method: "S256",
15
+ verifier
16
+ };
17
+ };
18
+ var consentUrl = ({ challenge, clientId, descriptor, redirect, state } = {}) => {
19
+ if (!clientId) throw new Error("This deployment has no OAuth client configured, so there is nothing to consent through");
20
+ if (!(descriptor == null ? void 0 : descriptor.authorize)) throw new Error("This connection declares no authorize url");
21
+ if (descriptor.pkce && !challenge) throw new Error("This connection requires PKCE, so a code challenge is not optional");
22
+ return descriptor.authorize + "?" + new URLSearchParams({
23
+ // The descriptor's own params go FIRST, so a vendor quirk cannot quietly
24
+ // overwrite one of the fields below that every consent carries.
25
+ ...descriptor.params || {},
26
+ client_id: clientId,
27
+ redirect_uri: redirect,
28
+ response_type: "code",
29
+ ...descriptor.scopes && { scope: descriptor.scopes },
30
+ ...descriptor.pkce && {
31
+ code_challenge: challenge,
32
+ code_challenge_method: "S256"
33
+ },
34
+ state
35
+ });
36
+ };
37
+ var exchange = async ({ clientId, clientSecret, code, descriptor, fetcher = fetch, redirect, verifier } = {}) => {
38
+ if (!(descriptor == null ? void 0 : descriptor.token)) throw new Error("This connection declares no token url");
39
+ if (descriptor.pkce && !verifier) throw new Error("This connection requires PKCE, so the code verifier is not optional");
40
+ const client = credentials({ clientId, clientSecret, descriptor });
41
+ const response = await fetcher(descriptor.token, {
42
+ body: new URLSearchParams({
43
+ ...client.body,
44
+ code: decodeURIComponent(String(code || "").trim()),
45
+ grant_type: "authorization_code",
46
+ redirect_uri: redirect,
47
+ ...descriptor.pkce && { code_verifier: verifier }
48
+ }),
49
+ headers: { "content-type": "application/x-www-form-urlencoded", ...client.headers },
50
+ method: "POST",
51
+ signal: AbortSignal.timeout(15e3)
52
+ });
53
+ const body = await response.json().catch(() => ({}));
54
+ if (!response.ok) {
55
+ throw new Error("The vendor refused the exchange (" + response.status + ")" + ((body == null ? void 0 : body.error) ? ": " + body.error : ""));
56
+ }
57
+ if (!body.access_token) throw new Error("The vendor returned no access token");
58
+ return {
59
+ accessToken: body.access_token,
60
+ expiresIn: body.expires_in || null,
61
+ refreshToken: body.refresh_token || null,
62
+ scope: body.scope || null
63
+ };
64
+ };
65
+ var refresh = async ({ clientId, clientSecret, descriptor, fetcher = fetch, refreshToken } = {}) => {
66
+ if (!clientId || !clientSecret) throw new Error("This deployment has no OAuth client configured, so no token can be minted");
67
+ if (!refreshToken) throw new Error("Nothing has been consented to yet, so there is no refresh token to spend");
68
+ if (!(descriptor == null ? void 0 : descriptor.token)) throw new Error("This connection declares no token url");
69
+ const client = credentials({ clientId, clientSecret, descriptor });
70
+ const response = await fetcher(descriptor.token, {
71
+ body: new URLSearchParams({
72
+ ...client.body,
73
+ grant_type: "refresh_token",
74
+ refresh_token: refreshToken
75
+ }),
76
+ headers: { "content-type": "application/x-www-form-urlencoded", ...client.headers },
77
+ method: "POST",
78
+ signal: AbortSignal.timeout(15e3)
79
+ });
80
+ if (!response.ok) throw new Error("The vendor refused the refresh token (" + response.status + ") \u2014 reconnect the connection");
81
+ const body = await response.json();
82
+ return {
83
+ accessToken: body.access_token,
84
+ expiresIn: body.expires_in || null,
85
+ // A vendor that rotates its refresh token returns a new one, and dropping
86
+ // it silently invalidates the stored grant on the NEXT refresh rather
87
+ // than this one — a failure a day late and nowhere near its cause.
88
+ refreshToken: body.refresh_token || null
89
+ };
90
+ };
91
+ export {
92
+ consentUrl,
93
+ exchange,
94
+ pkcePair,
95
+ refresh
96
+ };
package/package.json CHANGED
@@ -172,6 +172,16 @@
172
172
  "types": "./dist/fields-validate.d.ts",
173
173
  "import": "./dist/fields-validate.js",
174
174
  "require": "./dist/fields-validate.cjs"
175
+ },
176
+ "./connections": {
177
+ "types": "./dist/connections/index.d.ts",
178
+ "import": "./dist/connections/index.js",
179
+ "require": "./dist/connections/index.cjs"
180
+ },
181
+ "./connections/oauth": {
182
+ "types": "./dist/connections/oauth.d.ts",
183
+ "import": "./dist/connections/oauth.js",
184
+ "require": "./dist/connections/oauth.cjs"
175
185
  }
176
186
  },
177
187
  "files": [
@@ -190,5 +200,5 @@
190
200
  "test": ". \"$HOME/.nvm/nvm.sh\" && nvm use && node --test"
191
201
  },
192
202
  "types": "dist/index.d.ts",
193
- "version": "0.0.106"
203
+ "version": "0.0.108"
194
204
  }