@drawbridge/drawbridge-utils 0.0.5 → 0.0.7

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/dist/axios.cjs ADDED
@@ -0,0 +1,126 @@
1
+ var __create = Object.create;
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __getProtoOf = Object.getPrototypeOf;
6
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
7
+ var __export = (target, all) => {
8
+ for (var name in all)
9
+ __defProp(target, name, { get: all[name], enumerable: true });
10
+ };
11
+ var __copyProps = (to, from, except, desc) => {
12
+ if (from && typeof from === "object" || typeof from === "function") {
13
+ for (let key of __getOwnPropNames(from))
14
+ if (!__hasOwnProp.call(to, key) && key !== except)
15
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
16
+ }
17
+ return to;
18
+ };
19
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
20
+ // If the importer is in node compatibility mode or this is not an ESM
21
+ // file that has been converted to a CommonJS file using a Babel-
22
+ // compatible transform (i.e. "__esModule" has not been set), then set
23
+ // "default" to the CommonJS "module.exports" for node compatibility.
24
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
25
+ mod
26
+ ));
27
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
28
+
29
+ // axios.js
30
+ var axios_exports = {};
31
+ __export(axios_exports, {
32
+ axios: () => axios,
33
+ isBlockedIP: () => isBlockedIP,
34
+ safe: () => safe
35
+ });
36
+ module.exports = __toCommonJS(axios_exports);
37
+ var import_axios = __toESM(require("axios"), 1);
38
+ var import_dns = __toESM(require("dns"), 1);
39
+ var import_https = __toESM(require("https"), 1);
40
+ var import_net = __toESM(require("net"), 1);
41
+ var dnsLookup = import_dns.default.promises.lookup;
42
+ var isBlockedIPv4 = (ip) => {
43
+ const parts = ip.split(".").map(Number);
44
+ if (parts.length !== 4 || parts.some((n) => Number.isNaN(n) || n < 0 || n > 255)) return true;
45
+ const [a, b] = parts;
46
+ if (a === 0) return true;
47
+ if (a === 10) return true;
48
+ if (a === 127) return true;
49
+ if (a === 169 && b === 254) return true;
50
+ if (a === 172 && b >= 16 && b <= 31) return true;
51
+ if (a === 192 && b === 168) return true;
52
+ if (a === 100 && b >= 64 && b <= 127) return true;
53
+ if (a === 192 && b === 0) return true;
54
+ if (a === 198 && (b === 18 || b === 19)) return true;
55
+ if (a === 198 && b === 51) return true;
56
+ if (a === 203 && b === 0) return true;
57
+ if (a >= 224) return true;
58
+ return false;
59
+ };
60
+ var isBlockedIPv6 = (ip) => {
61
+ const lower = ip.toLowerCase();
62
+ if (lower === "::1" || lower === "::") return true;
63
+ if (lower.startsWith("fc") || lower.startsWith("fd")) return true;
64
+ if (/^fe[89ab]/.test(lower)) return true;
65
+ if (lower.startsWith("ff")) return true;
66
+ if (lower.startsWith("::ffff:")) {
67
+ const v4 = lower.slice(7);
68
+ return isBlockedIPv4(v4);
69
+ }
70
+ ;
71
+ return false;
72
+ };
73
+ var isBlockedIP = (ip) => {
74
+ const version = import_net.default.isIP(ip);
75
+ if (version === 4) return isBlockedIPv4(ip);
76
+ if (version === 6) return isBlockedIPv6(ip);
77
+ return true;
78
+ };
79
+ var safeGet = async (url, axiosOptions = {}) => {
80
+ let parsed;
81
+ try {
82
+ parsed = new URL(url);
83
+ } catch {
84
+ throw new Error("Invalid URL");
85
+ }
86
+ ;
87
+ if (parsed.protocol !== "https:") {
88
+ throw new Error("Only https URLs are allowed");
89
+ }
90
+ ;
91
+ const records = await dnsLookup(parsed.hostname, { all: true });
92
+ if (!records || records.length === 0) {
93
+ throw new Error("Host could not be resolved");
94
+ }
95
+ ;
96
+ for (const record of records) {
97
+ if (isBlockedIP(record.address)) {
98
+ throw new Error("Host resolves to a blocked IP range");
99
+ }
100
+ ;
101
+ }
102
+ ;
103
+ const pinned = records[0];
104
+ const agent = new import_https.default.Agent({
105
+ lookup: (hostname, options, callback) => {
106
+ callback(null, pinned.address, pinned.family);
107
+ }
108
+ });
109
+ return import_axios.default.get(url, {
110
+ ...axiosOptions,
111
+ httpsAgent: agent,
112
+ maxRedirects: 0,
113
+ maxContentLength: 5 * 1024 * 1024,
114
+ maxBodyLength: 5 * 1024 * 1024
115
+ });
116
+ };
117
+ var safe = {
118
+ get: safeGet
119
+ };
120
+ var axios = import_axios.default;
121
+ // Annotate the CommonJS export names for ESM import in node:
122
+ 0 && (module.exports = {
123
+ axios,
124
+ isBlockedIP,
125
+ safe
126
+ });
@@ -0,0 +1,142 @@
1
+ import axiosLib from 'axios';
2
+ import dns from 'dns';
3
+ import https from 'https';
4
+ import net from 'net';
5
+
6
+ const dnsLookup = dns.promises.lookup;
7
+
8
+ // Reject any IPv4 in the standard private/loopback/link-local/CGN/multicast/reserved
9
+ // ranges. Matches RFC 1918, 5735, 5737, 6598, 6890.
10
+
11
+ const isBlockedIPv4 = ( ip ) => {
12
+
13
+ const parts = ip.split( '.' ).map( Number );
14
+
15
+ if( parts.length !== 4 || parts.some( ( n ) => Number.isNaN( n ) || n < 0 || n > 255 ) ) return true;
16
+
17
+ const [ a, b ] = parts;
18
+
19
+ if( a === 0 ) return true;
20
+ if( a === 10 ) return true;
21
+ if( a === 127 ) return true;
22
+ if( a === 169 && b === 254 ) return true;
23
+ if( a === 172 && b >= 16 && b <= 31 ) return true;
24
+ if( a === 192 && b === 168 ) return true;
25
+ if( a === 100 && b >= 64 && b <= 127 ) return true;
26
+ if( a === 192 && b === 0 ) return true;
27
+ if( a === 198 && ( b === 18 || b === 19 ) ) return true;
28
+ if( a === 198 && b === 51 ) return true;
29
+ if( a === 203 && b === 0 ) return true;
30
+ if( a >= 224 ) return true;
31
+
32
+ return false;
33
+
34
+ };
35
+
36
+ const isBlockedIPv6 = ( ip ) => {
37
+
38
+ const lower = ip.toLowerCase();
39
+
40
+ if( lower === '::1' || lower === '::' ) return true;
41
+
42
+ // fc00::/7 — unique local addresses
43
+ if( lower.startsWith( 'fc' ) || lower.startsWith( 'fd' ) ) return true;
44
+
45
+ // fe80::/10 — link-local
46
+ if( /^fe[89ab]/.test( lower ) ) return true;
47
+
48
+ // ff00::/8 — multicast
49
+ if( lower.startsWith( 'ff' ) ) return true;
50
+
51
+ // ::ffff:0:0/96 — IPv4-mapped; check the embedded v4
52
+ if( lower.startsWith( '::ffff:' ) ){
53
+
54
+ const v4 = lower.slice( 7 );
55
+
56
+ return isBlockedIPv4( v4 );
57
+
58
+ }
59
+ return false;
60
+
61
+ };
62
+
63
+ const isBlockedIP = ( ip ) => {
64
+
65
+ const version = net.isIP( ip );
66
+
67
+ if( version === 4 ) return isBlockedIPv4( ip );
68
+ if( version === 6 ) return isBlockedIPv6( ip );
69
+
70
+ return true;
71
+
72
+ };
73
+
74
+ // SSRF-protected GET for user-supplied URLs.
75
+ // Requires https://, resolves all A/AAAA records and rejects if any is in a blocked
76
+ // range, then pins the axios lookup to the validated IP (closes DNS rebinding) and
77
+ // caps response size + disables redirects.
78
+ const safeGet = async ( url, axiosOptions = {} ) => {
79
+
80
+ let parsed;
81
+
82
+ try {
83
+
84
+ parsed = new URL( url );
85
+
86
+ } catch {
87
+
88
+ throw new Error( 'Invalid URL' );
89
+
90
+ }
91
+ if( parsed.protocol !== 'https:' ){
92
+
93
+ throw new Error( 'Only https URLs are allowed' );
94
+
95
+ }
96
+ const records = await dnsLookup( parsed.hostname, { all : true } );
97
+
98
+ if( ! records || records.length === 0 ){
99
+
100
+ throw new Error( 'Host could not be resolved' );
101
+
102
+ }
103
+ for( const record of records ){
104
+
105
+ if( isBlockedIP( record.address ) ){
106
+
107
+ throw new Error( 'Host resolves to a blocked IP range' );
108
+
109
+ }
110
+ }
111
+ const pinned = records[ 0 ];
112
+
113
+ const agent = new https.Agent({
114
+ lookup : ( hostname, options, callback ) => {
115
+
116
+ callback( null, pinned.address, pinned.family );
117
+
118
+ }
119
+ });
120
+
121
+ return axiosLib.get( url, {
122
+ ...axiosOptions,
123
+ httpsAgent : agent,
124
+ maxRedirects : 0,
125
+ maxContentLength : 5 * 1024 * 1024,
126
+ maxBodyLength : 5 * 1024 * 1024
127
+ });
128
+
129
+ };
130
+
131
+ // SSRF-protected namespace. Use for ANY URL that comes from request input.
132
+ // await safe.get( userUrl, options );
133
+ const safe = {
134
+ get : safeGet
135
+ };
136
+
137
+ // Raw axios passthrough. Use for trusted/hardcoded URLs (Google APIs, Stripe, etc.).
138
+ // await axios.get( 'https://api.stripe.com/...' );
139
+ // await axios.post( ... );
140
+ const axios = axiosLib;
141
+
142
+ export { axios, isBlockedIP, safe };
@@ -0,0 +1,142 @@
1
+ import axiosLib from 'axios';
2
+ import dns from 'dns';
3
+ import https from 'https';
4
+ import net from 'net';
5
+
6
+ const dnsLookup = dns.promises.lookup;
7
+
8
+ // Reject any IPv4 in the standard private/loopback/link-local/CGN/multicast/reserved
9
+ // ranges. Matches RFC 1918, 5735, 5737, 6598, 6890.
10
+
11
+ const isBlockedIPv4 = ( ip ) => {
12
+
13
+ const parts = ip.split( '.' ).map( Number );
14
+
15
+ if( parts.length !== 4 || parts.some( ( n ) => Number.isNaN( n ) || n < 0 || n > 255 ) ) return true;
16
+
17
+ const [ a, b ] = parts;
18
+
19
+ if( a === 0 ) return true;
20
+ if( a === 10 ) return true;
21
+ if( a === 127 ) return true;
22
+ if( a === 169 && b === 254 ) return true;
23
+ if( a === 172 && b >= 16 && b <= 31 ) return true;
24
+ if( a === 192 && b === 168 ) return true;
25
+ if( a === 100 && b >= 64 && b <= 127 ) return true;
26
+ if( a === 192 && b === 0 ) return true;
27
+ if( a === 198 && ( b === 18 || b === 19 ) ) return true;
28
+ if( a === 198 && b === 51 ) return true;
29
+ if( a === 203 && b === 0 ) return true;
30
+ if( a >= 224 ) return true;
31
+
32
+ return false;
33
+
34
+ };
35
+
36
+ const isBlockedIPv6 = ( ip ) => {
37
+
38
+ const lower = ip.toLowerCase();
39
+
40
+ if( lower === '::1' || lower === '::' ) return true;
41
+
42
+ // fc00::/7 — unique local addresses
43
+ if( lower.startsWith( 'fc' ) || lower.startsWith( 'fd' ) ) return true;
44
+
45
+ // fe80::/10 — link-local
46
+ if( /^fe[89ab]/.test( lower ) ) return true;
47
+
48
+ // ff00::/8 — multicast
49
+ if( lower.startsWith( 'ff' ) ) return true;
50
+
51
+ // ::ffff:0:0/96 — IPv4-mapped; check the embedded v4
52
+ if( lower.startsWith( '::ffff:' ) ){
53
+
54
+ const v4 = lower.slice( 7 );
55
+
56
+ return isBlockedIPv4( v4 );
57
+
58
+ }
59
+ return false;
60
+
61
+ };
62
+
63
+ const isBlockedIP = ( ip ) => {
64
+
65
+ const version = net.isIP( ip );
66
+
67
+ if( version === 4 ) return isBlockedIPv4( ip );
68
+ if( version === 6 ) return isBlockedIPv6( ip );
69
+
70
+ return true;
71
+
72
+ };
73
+
74
+ // SSRF-protected GET for user-supplied URLs.
75
+ // Requires https://, resolves all A/AAAA records and rejects if any is in a blocked
76
+ // range, then pins the axios lookup to the validated IP (closes DNS rebinding) and
77
+ // caps response size + disables redirects.
78
+ const safeGet = async ( url, axiosOptions = {} ) => {
79
+
80
+ let parsed;
81
+
82
+ try {
83
+
84
+ parsed = new URL( url );
85
+
86
+ } catch {
87
+
88
+ throw new Error( 'Invalid URL' );
89
+
90
+ }
91
+ if( parsed.protocol !== 'https:' ){
92
+
93
+ throw new Error( 'Only https URLs are allowed' );
94
+
95
+ }
96
+ const records = await dnsLookup( parsed.hostname, { all : true } );
97
+
98
+ if( ! records || records.length === 0 ){
99
+
100
+ throw new Error( 'Host could not be resolved' );
101
+
102
+ }
103
+ for( const record of records ){
104
+
105
+ if( isBlockedIP( record.address ) ){
106
+
107
+ throw new Error( 'Host resolves to a blocked IP range' );
108
+
109
+ }
110
+ }
111
+ const pinned = records[ 0 ];
112
+
113
+ const agent = new https.Agent({
114
+ lookup : ( hostname, options, callback ) => {
115
+
116
+ callback( null, pinned.address, pinned.family );
117
+
118
+ }
119
+ });
120
+
121
+ return axiosLib.get( url, {
122
+ ...axiosOptions,
123
+ httpsAgent : agent,
124
+ maxRedirects : 0,
125
+ maxContentLength : 5 * 1024 * 1024,
126
+ maxBodyLength : 5 * 1024 * 1024
127
+ });
128
+
129
+ };
130
+
131
+ // SSRF-protected namespace. Use for ANY URL that comes from request input.
132
+ // await safe.get( userUrl, options );
133
+ const safe = {
134
+ get : safeGet
135
+ };
136
+
137
+ // Raw axios passthrough. Use for trusted/hardcoded URLs (Google APIs, Stripe, etc.).
138
+ // await axios.get( 'https://api.stripe.com/...' );
139
+ // await axios.post( ... );
140
+ const axios = axiosLib;
141
+
142
+ export { axios, isBlockedIP, safe };
package/dist/axios.js ADDED
@@ -0,0 +1,90 @@
1
+ // axios.js
2
+ import axiosLib from "axios";
3
+ import dns from "dns";
4
+ import https from "https";
5
+ import net from "net";
6
+ var dnsLookup = dns.promises.lookup;
7
+ var isBlockedIPv4 = (ip) => {
8
+ const parts = ip.split(".").map(Number);
9
+ if (parts.length !== 4 || parts.some((n) => Number.isNaN(n) || n < 0 || n > 255)) return true;
10
+ const [a, b] = parts;
11
+ if (a === 0) return true;
12
+ if (a === 10) return true;
13
+ if (a === 127) return true;
14
+ if (a === 169 && b === 254) return true;
15
+ if (a === 172 && b >= 16 && b <= 31) return true;
16
+ if (a === 192 && b === 168) return true;
17
+ if (a === 100 && b >= 64 && b <= 127) return true;
18
+ if (a === 192 && b === 0) return true;
19
+ if (a === 198 && (b === 18 || b === 19)) return true;
20
+ if (a === 198 && b === 51) return true;
21
+ if (a === 203 && b === 0) return true;
22
+ if (a >= 224) return true;
23
+ return false;
24
+ };
25
+ var isBlockedIPv6 = (ip) => {
26
+ const lower = ip.toLowerCase();
27
+ if (lower === "::1" || lower === "::") return true;
28
+ if (lower.startsWith("fc") || lower.startsWith("fd")) return true;
29
+ if (/^fe[89ab]/.test(lower)) return true;
30
+ if (lower.startsWith("ff")) return true;
31
+ if (lower.startsWith("::ffff:")) {
32
+ const v4 = lower.slice(7);
33
+ return isBlockedIPv4(v4);
34
+ }
35
+ ;
36
+ return false;
37
+ };
38
+ var isBlockedIP = (ip) => {
39
+ const version = net.isIP(ip);
40
+ if (version === 4) return isBlockedIPv4(ip);
41
+ if (version === 6) return isBlockedIPv6(ip);
42
+ return true;
43
+ };
44
+ var safeGet = async (url, axiosOptions = {}) => {
45
+ let parsed;
46
+ try {
47
+ parsed = new URL(url);
48
+ } catch {
49
+ throw new Error("Invalid URL");
50
+ }
51
+ ;
52
+ if (parsed.protocol !== "https:") {
53
+ throw new Error("Only https URLs are allowed");
54
+ }
55
+ ;
56
+ const records = await dnsLookup(parsed.hostname, { all: true });
57
+ if (!records || records.length === 0) {
58
+ throw new Error("Host could not be resolved");
59
+ }
60
+ ;
61
+ for (const record of records) {
62
+ if (isBlockedIP(record.address)) {
63
+ throw new Error("Host resolves to a blocked IP range");
64
+ }
65
+ ;
66
+ }
67
+ ;
68
+ const pinned = records[0];
69
+ const agent = new https.Agent({
70
+ lookup: (hostname, options, callback) => {
71
+ callback(null, pinned.address, pinned.family);
72
+ }
73
+ });
74
+ return axiosLib.get(url, {
75
+ ...axiosOptions,
76
+ httpsAgent: agent,
77
+ maxRedirects: 0,
78
+ maxContentLength: 5 * 1024 * 1024,
79
+ maxBodyLength: 5 * 1024 * 1024
80
+ });
81
+ };
82
+ var safe = {
83
+ get: safeGet
84
+ };
85
+ var axios = axiosLib;
86
+ export {
87
+ axios,
88
+ isBlockedIP,
89
+ safe
90
+ };
@@ -0,0 +1,77 @@
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
+ // circuit.js
20
+ var circuit_exports = {};
21
+ __export(circuit_exports, {
22
+ circuit: () => circuit
23
+ });
24
+ module.exports = __toCommonJS(circuit_exports);
25
+ var CLOSED = "CLOSED";
26
+ var OPEN = "OPEN";
27
+ var HALF_OPEN = "HALF_OPEN";
28
+ var circuit = ({
29
+ name,
30
+ threshold = 5,
31
+ timeout = 3e4
32
+ }) => {
33
+ let state = CLOSED;
34
+ let failures = 0;
35
+ let openedAt = null;
36
+ const trip = () => {
37
+ state = OPEN;
38
+ openedAt = Date.now();
39
+ console.error(`Circuit breaker OPEN: ${name}`);
40
+ };
41
+ const reset = () => {
42
+ state = CLOSED;
43
+ failures = 0;
44
+ openedAt = null;
45
+ console.log(`Circuit breaker CLOSED: ${name}`);
46
+ };
47
+ return async (fn) => {
48
+ if (state === OPEN) {
49
+ if (Date.now() - openedAt >= timeout) {
50
+ state = HALF_OPEN;
51
+ } else {
52
+ const error = new Error(`${name} is temporarily unavailable`);
53
+ error.status = 503;
54
+ throw error;
55
+ }
56
+ }
57
+ try {
58
+ const result = await fn();
59
+ if (state === HALF_OPEN) {
60
+ reset();
61
+ } else {
62
+ failures = 0;
63
+ }
64
+ return result;
65
+ } catch (error) {
66
+ failures++;
67
+ if (state === HALF_OPEN || failures >= threshold) {
68
+ trip();
69
+ }
70
+ throw error;
71
+ }
72
+ };
73
+ };
74
+ // Annotate the CommonJS export names for ESM import in node:
75
+ 0 && (module.exports = {
76
+ circuit
77
+ });
@@ -0,0 +1,96 @@
1
+ // Simple circuit breaker for wrapped async operations.
2
+ //
3
+ // const breaker = circuit({ name : 'kinde-auth', threshold : 5, timeout : 30000 });
4
+ // const result = await breaker( () => validateToken( ... ) );
5
+ //
6
+ // States: CLOSED → OPEN (after `threshold` consecutive failures) → HALF_OPEN
7
+ // (after `timeout` ms; next call probes recovery) → CLOSED (on success).
8
+ //
9
+ // IMPORTANT: state is per-process (per closure). With multiple replicas, each has
10
+ // its own counters — you can get N × threshold failures globally before any replica
11
+ // trips. For higher-stakes calls, back this with Redis.
12
+
13
+ const CLOSED = 'CLOSED';
14
+ const OPEN = 'OPEN';
15
+ const HALF_OPEN = 'HALF_OPEN';
16
+
17
+ const circuit = ({
18
+ name,
19
+ threshold = 5,
20
+ timeout = 30000
21
+ }) => {
22
+
23
+ let state = CLOSED;
24
+ let failures = 0;
25
+ let openedAt = null;
26
+
27
+ const trip = () => {
28
+
29
+ state = OPEN;
30
+ openedAt = Date.now();
31
+ console.error( `Circuit breaker OPEN: ${ name }` );
32
+
33
+ };
34
+
35
+ const reset = () => {
36
+
37
+ state = CLOSED;
38
+ failures = 0;
39
+ openedAt = null;
40
+ console.log( `Circuit breaker CLOSED: ${ name }` );
41
+
42
+ };
43
+
44
+ return async ( fn ) => {
45
+
46
+ if( state === OPEN ){
47
+
48
+ if( Date.now() - openedAt >= timeout ){
49
+
50
+ state = HALF_OPEN;
51
+
52
+ } else {
53
+
54
+ const error = new Error( `${ name } is temporarily unavailable` );
55
+ error.status = 503;
56
+ throw error;
57
+
58
+ }
59
+
60
+ }
61
+
62
+ try {
63
+
64
+ const result = await fn();
65
+
66
+ if( state === HALF_OPEN ){
67
+
68
+ reset();
69
+
70
+ } else {
71
+
72
+ failures = 0;
73
+
74
+ }
75
+
76
+ return result;
77
+
78
+ } catch ( error ) {
79
+
80
+ failures++;
81
+
82
+ if( state === HALF_OPEN || failures >= threshold ){
83
+
84
+ trip();
85
+
86
+ }
87
+
88
+ throw error;
89
+
90
+ }
91
+
92
+ };
93
+
94
+ };
95
+
96
+ export { circuit };
@@ -0,0 +1,96 @@
1
+ // Simple circuit breaker for wrapped async operations.
2
+ //
3
+ // const breaker = circuit({ name : 'kinde-auth', threshold : 5, timeout : 30000 });
4
+ // const result = await breaker( () => validateToken( ... ) );
5
+ //
6
+ // States: CLOSED → OPEN (after `threshold` consecutive failures) → HALF_OPEN
7
+ // (after `timeout` ms; next call probes recovery) → CLOSED (on success).
8
+ //
9
+ // IMPORTANT: state is per-process (per closure). With multiple replicas, each has
10
+ // its own counters — you can get N × threshold failures globally before any replica
11
+ // trips. For higher-stakes calls, back this with Redis.
12
+
13
+ const CLOSED = 'CLOSED';
14
+ const OPEN = 'OPEN';
15
+ const HALF_OPEN = 'HALF_OPEN';
16
+
17
+ const circuit = ({
18
+ name,
19
+ threshold = 5,
20
+ timeout = 30000
21
+ }) => {
22
+
23
+ let state = CLOSED;
24
+ let failures = 0;
25
+ let openedAt = null;
26
+
27
+ const trip = () => {
28
+
29
+ state = OPEN;
30
+ openedAt = Date.now();
31
+ console.error( `Circuit breaker OPEN: ${ name }` );
32
+
33
+ };
34
+
35
+ const reset = () => {
36
+
37
+ state = CLOSED;
38
+ failures = 0;
39
+ openedAt = null;
40
+ console.log( `Circuit breaker CLOSED: ${ name }` );
41
+
42
+ };
43
+
44
+ return async ( fn ) => {
45
+
46
+ if( state === OPEN ){
47
+
48
+ if( Date.now() - openedAt >= timeout ){
49
+
50
+ state = HALF_OPEN;
51
+
52
+ } else {
53
+
54
+ const error = new Error( `${ name } is temporarily unavailable` );
55
+ error.status = 503;
56
+ throw error;
57
+
58
+ }
59
+
60
+ }
61
+
62
+ try {
63
+
64
+ const result = await fn();
65
+
66
+ if( state === HALF_OPEN ){
67
+
68
+ reset();
69
+
70
+ } else {
71
+
72
+ failures = 0;
73
+
74
+ }
75
+
76
+ return result;
77
+
78
+ } catch ( error ) {
79
+
80
+ failures++;
81
+
82
+ if( state === HALF_OPEN || failures >= threshold ){
83
+
84
+ trip();
85
+
86
+ }
87
+
88
+ throw error;
89
+
90
+ }
91
+
92
+ };
93
+
94
+ };
95
+
96
+ export { circuit };
@@ -0,0 +1,53 @@
1
+ // circuit.js
2
+ var CLOSED = "CLOSED";
3
+ var OPEN = "OPEN";
4
+ var HALF_OPEN = "HALF_OPEN";
5
+ var circuit = ({
6
+ name,
7
+ threshold = 5,
8
+ timeout = 3e4
9
+ }) => {
10
+ let state = CLOSED;
11
+ let failures = 0;
12
+ let openedAt = null;
13
+ const trip = () => {
14
+ state = OPEN;
15
+ openedAt = Date.now();
16
+ console.error(`Circuit breaker OPEN: ${name}`);
17
+ };
18
+ const reset = () => {
19
+ state = CLOSED;
20
+ failures = 0;
21
+ openedAt = null;
22
+ console.log(`Circuit breaker CLOSED: ${name}`);
23
+ };
24
+ return async (fn) => {
25
+ if (state === OPEN) {
26
+ if (Date.now() - openedAt >= timeout) {
27
+ state = HALF_OPEN;
28
+ } else {
29
+ const error = new Error(`${name} is temporarily unavailable`);
30
+ error.status = 503;
31
+ throw error;
32
+ }
33
+ }
34
+ try {
35
+ const result = await fn();
36
+ if (state === HALF_OPEN) {
37
+ reset();
38
+ } else {
39
+ failures = 0;
40
+ }
41
+ return result;
42
+ } catch (error) {
43
+ failures++;
44
+ if (state === HALF_OPEN || failures >= threshold) {
45
+ trip();
46
+ }
47
+ throw error;
48
+ }
49
+ };
50
+ };
51
+ export {
52
+ circuit
53
+ };
package/dist/kinde.cjs ADDED
@@ -0,0 +1,56 @@
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
+ // kinde.js
20
+ var kinde_exports = {};
21
+ __export(kinde_exports, {
22
+ verifyToken: () => verifyToken
23
+ });
24
+ module.exports = __toCommonJS(kinde_exports);
25
+ var import_jwt_decoder = require("@kinde/jwt-decoder");
26
+ var import_jwt_validator = require("@kinde/jwt-validator");
27
+ var normalizeDomain = (raw) => {
28
+ const trimmed = (raw || "").trim().replace(/\/+$/, "");
29
+ if (!trimmed) return trimmed;
30
+ return /^https?:\/\//.test(trimmed) ? trimmed : "https://" + trimmed;
31
+ };
32
+ var verifyToken = async (rawToken, { domain } = {}) => {
33
+ if (!rawToken) {
34
+ throw new Error("No token");
35
+ }
36
+ ;
37
+ const token = rawToken.startsWith("Bearer ") ? rawToken.slice(7) : rawToken;
38
+ const validation = await (0, import_jwt_validator.validateToken)({
39
+ domain: normalizeDomain(domain),
40
+ token
41
+ });
42
+ if (!(validation == null ? void 0 : validation.valid)) {
43
+ throw new Error((validation == null ? void 0 : validation.message) || "Invalid token");
44
+ }
45
+ ;
46
+ const decoded = (0, import_jwt_decoder.jwtDecoder)(token);
47
+ if (!(decoded == null ? void 0 : decoded.sub)) {
48
+ throw new Error("Invalid token payload");
49
+ }
50
+ ;
51
+ return decoded;
52
+ };
53
+ // Annotate the CommonJS export names for ESM import in node:
54
+ 0 && (module.exports = {
55
+ verifyToken
56
+ });
@@ -0,0 +1,52 @@
1
+ import { jwtDecoder } from '@kinde/jwt-decoder';
2
+ import { validateToken } from '@kinde/jwt-validator';
3
+
4
+ const normalizeDomain = ( raw ) => {
5
+
6
+ const trimmed = ( raw || '' ).trim().replace( /\/+$/, '' );
7
+
8
+ if( ! trimmed ) return trimmed;
9
+
10
+ return /^https?:\/\//.test( trimmed ) ? trimmed : 'https://' + trimmed;
11
+
12
+ };
13
+
14
+ // Verifies a Kinde access token cryptographically via JWKS and returns the decoded
15
+ // payload. Throws on any failure (missing/invalid signature, missing sub claim, etc.).
16
+ // Caller does user lookup + service-specific glue around this.
17
+ //
18
+ // const decoded = await verifyToken( token, { domain : process.env.KINDE_DOMAIN } );
19
+ // decoded.sub // Kinde user id
20
+ // decoded.org_code // org code
21
+ // decoded.permissions // permissions array
22
+ const verifyToken = async ( rawToken, { domain } = {} ) => {
23
+
24
+ if( ! rawToken ){
25
+
26
+ throw new Error( 'No token' );
27
+
28
+ }
29
+ const token = rawToken.startsWith( 'Bearer ' ) ? rawToken.slice( 7 ) : rawToken;
30
+
31
+ const validation = await validateToken({
32
+ domain : normalizeDomain( domain ),
33
+ token
34
+ });
35
+
36
+ if( ! validation?.valid ){
37
+
38
+ throw new Error( validation?.message || 'Invalid token' );
39
+
40
+ }
41
+ const decoded = jwtDecoder( token );
42
+
43
+ if( ! decoded?.sub ){
44
+
45
+ throw new Error( 'Invalid token payload' );
46
+
47
+ }
48
+ return decoded;
49
+
50
+ };
51
+
52
+ export { verifyToken };
@@ -0,0 +1,52 @@
1
+ import { jwtDecoder } from '@kinde/jwt-decoder';
2
+ import { validateToken } from '@kinde/jwt-validator';
3
+
4
+ const normalizeDomain = ( raw ) => {
5
+
6
+ const trimmed = ( raw || '' ).trim().replace( /\/+$/, '' );
7
+
8
+ if( ! trimmed ) return trimmed;
9
+
10
+ return /^https?:\/\//.test( trimmed ) ? trimmed : 'https://' + trimmed;
11
+
12
+ };
13
+
14
+ // Verifies a Kinde access token cryptographically via JWKS and returns the decoded
15
+ // payload. Throws on any failure (missing/invalid signature, missing sub claim, etc.).
16
+ // Caller does user lookup + service-specific glue around this.
17
+ //
18
+ // const decoded = await verifyToken( token, { domain : process.env.KINDE_DOMAIN } );
19
+ // decoded.sub // Kinde user id
20
+ // decoded.org_code // org code
21
+ // decoded.permissions // permissions array
22
+ const verifyToken = async ( rawToken, { domain } = {} ) => {
23
+
24
+ if( ! rawToken ){
25
+
26
+ throw new Error( 'No token' );
27
+
28
+ }
29
+ const token = rawToken.startsWith( 'Bearer ' ) ? rawToken.slice( 7 ) : rawToken;
30
+
31
+ const validation = await validateToken({
32
+ domain : normalizeDomain( domain ),
33
+ token
34
+ });
35
+
36
+ if( ! validation?.valid ){
37
+
38
+ throw new Error( validation?.message || 'Invalid token' );
39
+
40
+ }
41
+ const decoded = jwtDecoder( token );
42
+
43
+ if( ! decoded?.sub ){
44
+
45
+ throw new Error( 'Invalid token payload' );
46
+
47
+ }
48
+ return decoded;
49
+
50
+ };
51
+
52
+ export { verifyToken };
package/dist/kinde.js ADDED
@@ -0,0 +1,32 @@
1
+ // kinde.js
2
+ import { jwtDecoder } from "@kinde/jwt-decoder";
3
+ import { validateToken } from "@kinde/jwt-validator";
4
+ var normalizeDomain = (raw) => {
5
+ const trimmed = (raw || "").trim().replace(/\/+$/, "");
6
+ if (!trimmed) return trimmed;
7
+ return /^https?:\/\//.test(trimmed) ? trimmed : "https://" + trimmed;
8
+ };
9
+ var verifyToken = async (rawToken, { domain } = {}) => {
10
+ if (!rawToken) {
11
+ throw new Error("No token");
12
+ }
13
+ ;
14
+ const token = rawToken.startsWith("Bearer ") ? rawToken.slice(7) : rawToken;
15
+ const validation = await validateToken({
16
+ domain: normalizeDomain(domain),
17
+ token
18
+ });
19
+ if (!(validation == null ? void 0 : validation.valid)) {
20
+ throw new Error((validation == null ? void 0 : validation.message) || "Invalid token");
21
+ }
22
+ ;
23
+ const decoded = jwtDecoder(token);
24
+ if (!(decoded == null ? void 0 : decoded.sub)) {
25
+ throw new Error("Invalid token payload");
26
+ }
27
+ ;
28
+ return decoded;
29
+ };
30
+ export {
31
+ verifyToken
32
+ };
@@ -0,0 +1,65 @@
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
+ // upload.js
20
+ var upload_exports = {};
21
+ __export(upload_exports, {
22
+ allowedMimes: () => allowedMimes,
23
+ allowedUploadTypes: () => allowedUploadTypes,
24
+ isAllowedMime: () => isAllowedMime,
25
+ resolveUploadType: () => resolveUploadType
26
+ });
27
+ module.exports = __toCommonJS(upload_exports);
28
+ var allowedUploadTypes = {
29
+ application: {
30
+ pdf: "application/pdf"
31
+ },
32
+ image: {
33
+ jpeg: "image/jpeg",
34
+ jpg: "image/jpeg",
35
+ png: "image/png",
36
+ webp: "image/webp"
37
+ },
38
+ video: {
39
+ mp4: "video/mp4"
40
+ }
41
+ };
42
+ var allowedMimes = new Set(
43
+ Object.values(allowedUploadTypes).flatMap(
44
+ (extensions) => Object.values(extensions)
45
+ )
46
+ );
47
+ var resolveUploadType = (type, extension) => {
48
+ var _a;
49
+ const mime = (_a = allowedUploadTypes == null ? void 0 : allowedUploadTypes[type]) == null ? void 0 : _a[extension];
50
+ if (!mime) {
51
+ const error = new Error("Unsupported upload type or extension");
52
+ error.status = 400;
53
+ throw error;
54
+ }
55
+ ;
56
+ return mime;
57
+ };
58
+ var isAllowedMime = (mime) => allowedMimes.has(mime);
59
+ // Annotate the CommonJS export names for ESM import in node:
60
+ 0 && (module.exports = {
61
+ allowedMimes,
62
+ allowedUploadTypes,
63
+ isAllowedMime,
64
+ resolveUploadType
65
+ });
@@ -0,0 +1,58 @@
1
+ // Source-of-truth allowlist for file upload types accepted across drawbridge.
2
+ //
3
+ // Two consumer shapes:
4
+ //
5
+ // 1. The API needs `(type, extension) → mime` when serving an upload route
6
+ // where the URL carries those params. Use `resolveUploadType( type, extension )`.
7
+ // Throws { status: 400 } on anything not in the allowlist.
8
+ //
9
+ // 2. The sync worker downstream needs to validate that a `mimetype` flowing
10
+ // from the DB / job payload is one of the canonical values before passing
11
+ // it to S3 as a ContentType header. Use `isAllowedMime( mime )`.
12
+ //
13
+ // Adding a new accepted upload format means updating `allowedUploadTypes` here
14
+ // AND publishing a new utils version. Both api and sync inherit the change.
15
+ //
16
+ // Why both helpers and not just one: api routes know (type, extension) but not
17
+ // the canonical mime; sync workers have the mime but not the original type tuple.
18
+
19
+ const allowedUploadTypes = {
20
+ application : {
21
+ pdf : 'application/pdf'
22
+ },
23
+ image : {
24
+ jpeg : 'image/jpeg',
25
+ jpg : 'image/jpeg',
26
+ png : 'image/png',
27
+ webp : 'image/webp'
28
+ },
29
+ video : {
30
+ mp4 : 'video/mp4'
31
+ }
32
+ };
33
+
34
+ const allowedMimes = new Set(
35
+ Object.values( allowedUploadTypes ).flatMap(
36
+ ( extensions ) => Object.values( extensions )
37
+ )
38
+ );
39
+
40
+ const resolveUploadType = ( type, extension ) => {
41
+
42
+ const mime = allowedUploadTypes?.[ type ]?.[ extension ];
43
+
44
+ if( ! mime ){
45
+
46
+ const error = new Error( 'Unsupported upload type or extension' );
47
+ error.status = 400;
48
+
49
+ throw error;
50
+
51
+ }
52
+ return mime;
53
+
54
+ };
55
+
56
+ const isAllowedMime = ( mime ) => allowedMimes.has( mime );
57
+
58
+ export { allowedMimes, allowedUploadTypes, isAllowedMime, resolveUploadType };
@@ -0,0 +1,58 @@
1
+ // Source-of-truth allowlist for file upload types accepted across drawbridge.
2
+ //
3
+ // Two consumer shapes:
4
+ //
5
+ // 1. The API needs `(type, extension) → mime` when serving an upload route
6
+ // where the URL carries those params. Use `resolveUploadType( type, extension )`.
7
+ // Throws { status: 400 } on anything not in the allowlist.
8
+ //
9
+ // 2. The sync worker downstream needs to validate that a `mimetype` flowing
10
+ // from the DB / job payload is one of the canonical values before passing
11
+ // it to S3 as a ContentType header. Use `isAllowedMime( mime )`.
12
+ //
13
+ // Adding a new accepted upload format means updating `allowedUploadTypes` here
14
+ // AND publishing a new utils version. Both api and sync inherit the change.
15
+ //
16
+ // Why both helpers and not just one: api routes know (type, extension) but not
17
+ // the canonical mime; sync workers have the mime but not the original type tuple.
18
+
19
+ const allowedUploadTypes = {
20
+ application : {
21
+ pdf : 'application/pdf'
22
+ },
23
+ image : {
24
+ jpeg : 'image/jpeg',
25
+ jpg : 'image/jpeg',
26
+ png : 'image/png',
27
+ webp : 'image/webp'
28
+ },
29
+ video : {
30
+ mp4 : 'video/mp4'
31
+ }
32
+ };
33
+
34
+ const allowedMimes = new Set(
35
+ Object.values( allowedUploadTypes ).flatMap(
36
+ ( extensions ) => Object.values( extensions )
37
+ )
38
+ );
39
+
40
+ const resolveUploadType = ( type, extension ) => {
41
+
42
+ const mime = allowedUploadTypes?.[ type ]?.[ extension ];
43
+
44
+ if( ! mime ){
45
+
46
+ const error = new Error( 'Unsupported upload type or extension' );
47
+ error.status = 400;
48
+
49
+ throw error;
50
+
51
+ }
52
+ return mime;
53
+
54
+ };
55
+
56
+ const isAllowedMime = ( mime ) => allowedMimes.has( mime );
57
+
58
+ export { allowedMimes, allowedUploadTypes, isAllowedMime, resolveUploadType };
package/dist/upload.js ADDED
@@ -0,0 +1,38 @@
1
+ // upload.js
2
+ var allowedUploadTypes = {
3
+ application: {
4
+ pdf: "application/pdf"
5
+ },
6
+ image: {
7
+ jpeg: "image/jpeg",
8
+ jpg: "image/jpeg",
9
+ png: "image/png",
10
+ webp: "image/webp"
11
+ },
12
+ video: {
13
+ mp4: "video/mp4"
14
+ }
15
+ };
16
+ var allowedMimes = new Set(
17
+ Object.values(allowedUploadTypes).flatMap(
18
+ (extensions) => Object.values(extensions)
19
+ )
20
+ );
21
+ var resolveUploadType = (type, extension) => {
22
+ var _a;
23
+ const mime = (_a = allowedUploadTypes == null ? void 0 : allowedUploadTypes[type]) == null ? void 0 : _a[extension];
24
+ if (!mime) {
25
+ const error = new Error("Unsupported upload type or extension");
26
+ error.status = 400;
27
+ throw error;
28
+ }
29
+ ;
30
+ return mime;
31
+ };
32
+ var isAllowedMime = (mime) => allowedMimes.has(mime);
33
+ export {
34
+ allowedMimes,
35
+ allowedUploadTypes,
36
+ isAllowedMime,
37
+ resolveUploadType
38
+ };
package/package.json CHANGED
@@ -1,6 +1,9 @@
1
1
  {
2
2
  "type": "module",
3
3
  "dependencies": {
4
+ "@kinde/jwt-decoder": "0.2.1",
5
+ "@kinde/jwt-validator": "0.4.1",
6
+ "axios": "1.16.0",
4
7
  "currency-codes": "2.2.0",
5
8
  "nanoid": "3.3.8",
6
9
  "tsup": "8.5.1",
@@ -16,6 +19,26 @@
16
19
  "types": "./dist/encrypt.d.ts",
17
20
  "import": "./dist/encrypt.js",
18
21
  "require": "./dist/encrypt.cjs"
22
+ },
23
+ "./kinde": {
24
+ "types": "./dist/kinde.d.ts",
25
+ "import": "./dist/kinde.js",
26
+ "require": "./dist/kinde.cjs"
27
+ },
28
+ "./axios": {
29
+ "types": "./dist/axios.d.ts",
30
+ "import": "./dist/axios.js",
31
+ "require": "./dist/axios.cjs"
32
+ },
33
+ "./circuit": {
34
+ "types": "./dist/circuit.d.ts",
35
+ "import": "./dist/circuit.js",
36
+ "require": "./dist/circuit.cjs"
37
+ },
38
+ "./upload": {
39
+ "types": "./dist/upload.d.ts",
40
+ "import": "./dist/upload.js",
41
+ "require": "./dist/upload.cjs"
19
42
  }
20
43
  },
21
44
  "files": [
@@ -32,5 +55,5 @@
32
55
  "build": "tsup && npm publish"
33
56
  },
34
57
  "types": "dist/index.d.ts",
35
- "version": "0.0.5"
58
+ "version": "0.0.7"
36
59
  }