@drawbridge/drawbridge-utils 0.0.105 → 0.0.107

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/dist/features.cjs CHANGED
@@ -99,11 +99,6 @@ var connection = {
99
99
  error: "Plan does not include Mailchimp connection",
100
100
  feature: "Mailchimp connection"
101
101
  },
102
- sender: {
103
- key: "organization:connection:sender",
104
- error: "Plan does not include a verified sending domain",
105
- feature: "Sending domain connection"
106
- },
107
102
  sendgrid: {
108
103
  key: "organization:connection:sendgrid",
109
104
  error: "Plan does not include SendGrid connection",
@@ -168,6 +163,19 @@ var organization = {
168
163
  error: "Your plan does not include brands",
169
164
  feature: "Organization brands management"
170
165
  },
166
+ // Gates the Networking section as a whole — a verified sending domain today,
167
+ // the SMS number and a custom page domain as they land. One key rather than
168
+ // one per type: they are the same capability to a merchant, and splitting
169
+ // them would mean a plan could grant half a section.
170
+ //
171
+ // It replaces `connection.sender`, which named a connection this stopped
172
+ // being. Free organizations cannot send lead-facing email at all, so a
173
+ // sending identity there is one they could never send from.
174
+ networking: {
175
+ key: "organization:networking",
176
+ error: "Your plan does not include a custom sending identity",
177
+ feature: "Organization networking"
178
+ },
171
179
  members: {
172
180
  key: "organization:members",
173
181
  error: "Your plan does not include team members",
@@ -73,11 +73,6 @@ const connection = {
73
73
  error : 'Plan does not include Mailchimp connection',
74
74
  feature : 'Mailchimp connection'
75
75
  },
76
- sender : {
77
- key : 'organization:connection:sender',
78
- error : 'Plan does not include a verified sending domain',
79
- feature : 'Sending domain connection'
80
- },
81
76
  sendgrid : {
82
77
  key : 'organization:connection:sendgrid',
83
78
  error : 'Plan does not include SendGrid connection',
@@ -144,6 +139,19 @@ const organization = {
144
139
  error : 'Your plan does not include brands',
145
140
  feature : 'Organization brands management',
146
141
  },
142
+ // Gates the Networking section as a whole — a verified sending domain today,
143
+ // the SMS number and a custom page domain as they land. One key rather than
144
+ // one per type: they are the same capability to a merchant, and splitting
145
+ // them would mean a plan could grant half a section.
146
+ //
147
+ // It replaces `connection.sender`, which named a connection this stopped
148
+ // being. Free organizations cannot send lead-facing email at all, so a
149
+ // sending identity there is one they could never send from.
150
+ networking : {
151
+ key : 'organization:networking',
152
+ error : 'Your plan does not include a custom sending identity',
153
+ feature : 'Organization networking'
154
+ },
147
155
  members : {
148
156
  key : 'organization:members',
149
157
  error : 'Your plan does not include team members',