@drawbridge/drawbridge-utils 0.0.87 → 0.0.88

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 CHANGED
@@ -36,7 +36,8 @@ __export(axios_exports, {
36
36
  module.exports = __toCommonJS(axios_exports);
37
37
  var import_axios = __toESM(require("axios"), 1);
38
38
  var import_dns = __toESM(require("dns"), 1);
39
- var import_https = __toESM(require("https"), 1);
39
+ var http = __toESM(require("http"), 1);
40
+ var https = __toESM(require("https"), 1);
40
41
  var import_net = __toESM(require("net"), 1);
41
42
  var dnsLookup = import_dns.default.promises.lookup;
42
43
  var isBlockedIPv4 = (ip) => {
@@ -101,7 +102,7 @@ var safeGet = async (url, axiosOptions = {}) => {
101
102
  }
102
103
  ;
103
104
  const pinned = records[0];
104
- const agent = new import_https.default.Agent({
105
+ const agent = new https.Agent({
105
106
  lookup: (hostname, options, callback) => {
106
107
  if (options == null ? void 0 : options.all) {
107
108
  callback(null, [{ address: pinned.address, family: pinned.family }]);
@@ -112,6 +113,9 @@ var safeGet = async (url, axiosOptions = {}) => {
112
113
  }
113
114
  });
114
115
  return import_axios.default.get(url, {
116
+ // Default bound for user-supplied URLs; callers override via
117
+ // axiosOptions.timeout (listed first so the spread wins).
118
+ timeout: 15e3,
115
119
  ...axiosOptions,
116
120
  httpsAgent: agent,
117
121
  maxRedirects: 0,
@@ -122,7 +126,11 @@ var safeGet = async (url, axiosOptions = {}) => {
122
126
  var safe = {
123
127
  get: safeGet
124
128
  };
125
- var axios = import_axios.default;
129
+ var axios = import_axios.default.create({
130
+ timeout: 3e4,
131
+ httpAgent: new http.Agent({ keepAlive: true, maxSockets: 128 }),
132
+ httpsAgent: new https.Agent({ keepAlive: true, maxSockets: 128 })
133
+ });
126
134
  // Annotate the CommonJS export names for ESM import in node:
127
135
  0 && (module.exports = {
128
136
  axios,
package/dist/axios.d.cts CHANGED
@@ -1,6 +1,7 @@
1
1
  import axiosLib from 'axios';
2
2
  import dns from 'dns';
3
- import https from 'https';
3
+ import * as http from 'node:http';
4
+ import * as https from 'node:https';
4
5
  import net from 'net';
5
6
 
6
7
  const dnsLookup = dns.promises.lookup;
@@ -130,6 +131,9 @@ const safeGet = async ( url, axiosOptions = {} ) => {
130
131
  });
131
132
 
132
133
  return axiosLib.get( url, {
134
+ // Default bound for user-supplied URLs; callers override via
135
+ // axiosOptions.timeout (listed first so the spread wins).
136
+ timeout : 15000,
133
137
  ...axiosOptions,
134
138
  httpsAgent : agent,
135
139
  maxRedirects : 0,
@@ -145,9 +149,21 @@ const safe = {
145
149
  get : safeGet
146
150
  };
147
151
 
148
- // Raw axios passthrough. Use for trusted/hardcoded URLs (Google APIs, Stripe, etc.).
152
+ // Raw axios instance for trusted/hardcoded URLs (Google APIs, Stripe, etc.).
149
153
  // await axios.get( 'https://api.stripe.com/...' );
150
154
  // await axios.post( ... );
151
- const axios = axiosLib;
155
+ //
156
+ // Bounded by default so a forgotten per-call override can never hang a request
157
+ // handler or queue worker indefinitely (the classic unguarded-await failure
158
+ // under load). Every call site can still override: per-request `timeout`,
159
+ // `signal` (used by streaming downloads whose 30s would be wrong), and
160
+ // per-request `httpAgent`/`httpsAgent` (used by the SSRF-pinned agents) all
161
+ // take precedence over these defaults. Audited 2026-07-24: every existing
162
+ // call site already sets its own bound, so this changes no current behavior.
163
+ const axios = axiosLib.create({
164
+ timeout : 30000,
165
+ httpAgent : new http.Agent({ keepAlive : true, maxSockets : 128 }),
166
+ httpsAgent : new https.Agent({ keepAlive : true, maxSockets : 128 })
167
+ });
152
168
 
153
169
  export { axios, isBlockedIP, safe };
package/dist/axios.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import axiosLib from 'axios';
2
2
  import dns from 'dns';
3
- import https from 'https';
3
+ import * as http from 'node:http';
4
+ import * as https from 'node:https';
4
5
  import net from 'net';
5
6
 
6
7
  const dnsLookup = dns.promises.lookup;
@@ -130,6 +131,9 @@ const safeGet = async ( url, axiosOptions = {} ) => {
130
131
  });
131
132
 
132
133
  return axiosLib.get( url, {
134
+ // Default bound for user-supplied URLs; callers override via
135
+ // axiosOptions.timeout (listed first so the spread wins).
136
+ timeout : 15000,
133
137
  ...axiosOptions,
134
138
  httpsAgent : agent,
135
139
  maxRedirects : 0,
@@ -145,9 +149,21 @@ const safe = {
145
149
  get : safeGet
146
150
  };
147
151
 
148
- // Raw axios passthrough. Use for trusted/hardcoded URLs (Google APIs, Stripe, etc.).
152
+ // Raw axios instance for trusted/hardcoded URLs (Google APIs, Stripe, etc.).
149
153
  // await axios.get( 'https://api.stripe.com/...' );
150
154
  // await axios.post( ... );
151
- const axios = axiosLib;
155
+ //
156
+ // Bounded by default so a forgotten per-call override can never hang a request
157
+ // handler or queue worker indefinitely (the classic unguarded-await failure
158
+ // under load). Every call site can still override: per-request `timeout`,
159
+ // `signal` (used by streaming downloads whose 30s would be wrong), and
160
+ // per-request `httpAgent`/`httpsAgent` (used by the SSRF-pinned agents) all
161
+ // take precedence over these defaults. Audited 2026-07-24: every existing
162
+ // call site already sets its own bound, so this changes no current behavior.
163
+ const axios = axiosLib.create({
164
+ timeout : 30000,
165
+ httpAgent : new http.Agent({ keepAlive : true, maxSockets : 128 }),
166
+ httpsAgent : new https.Agent({ keepAlive : true, maxSockets : 128 })
167
+ });
152
168
 
153
169
  export { axios, isBlockedIP, safe };
package/dist/axios.js CHANGED
@@ -1,7 +1,8 @@
1
1
  // lib/axios.js
2
2
  import axiosLib from "axios";
3
3
  import dns from "dns";
4
- import https from "https";
4
+ import * as http from "http";
5
+ import * as https from "https";
5
6
  import net from "net";
6
7
  var dnsLookup = dns.promises.lookup;
7
8
  var isBlockedIPv4 = (ip) => {
@@ -77,6 +78,9 @@ var safeGet = async (url, axiosOptions = {}) => {
77
78
  }
78
79
  });
79
80
  return axiosLib.get(url, {
81
+ // Default bound for user-supplied URLs; callers override via
82
+ // axiosOptions.timeout (listed first so the spread wins).
83
+ timeout: 15e3,
80
84
  ...axiosOptions,
81
85
  httpsAgent: agent,
82
86
  maxRedirects: 0,
@@ -87,7 +91,11 @@ var safeGet = async (url, axiosOptions = {}) => {
87
91
  var safe = {
88
92
  get: safeGet
89
93
  };
90
- var axios = axiosLib;
94
+ var axios = axiosLib.create({
95
+ timeout: 3e4,
96
+ httpAgent: new http.Agent({ keepAlive: true, maxSockets: 128 }),
97
+ httpsAgent: new https.Agent({ keepAlive: true, maxSockets: 128 })
98
+ });
91
99
  export {
92
100
  axios,
93
101
  isBlockedIP,
@@ -0,0 +1,292 @@
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
+ // lib/cloudflare.js
30
+ var cloudflare_exports = {};
31
+ __export(cloudflare_exports, {
32
+ pageEdgeUrls: () => pageEdgeUrls,
33
+ purge: () => purge
34
+ });
35
+ module.exports = __toCommonJS(cloudflare_exports);
36
+
37
+ // lib/http.js
38
+ var DEFAULT_TIMEOUT_MS = 15e3;
39
+ var request = async ({
40
+ body,
41
+ headers = {},
42
+ method = "GET",
43
+ query,
44
+ timeout = DEFAULT_TIMEOUT_MS,
45
+ type = "json",
46
+ url
47
+ }) => {
48
+ const fullUrl = new URL(url);
49
+ if (query) {
50
+ Object.entries(query).forEach(([k, v]) => fullUrl.searchParams.set(k, v));
51
+ }
52
+ ;
53
+ const isForm = type === "form";
54
+ const response = await fetch(fullUrl.toString(), {
55
+ method,
56
+ headers: {
57
+ "Content-Type": isForm ? "application/x-www-form-urlencoded" : "application/json",
58
+ ...headers
59
+ },
60
+ signal: AbortSignal.timeout(timeout),
61
+ ...body !== void 0 && {
62
+ body: isForm ? new URLSearchParams(body).toString() : JSON.stringify(body)
63
+ }
64
+ });
65
+ if (!response.ok) {
66
+ const text2 = await response.text().catch(() => "");
67
+ const error = new Error(text2 || response.statusText);
68
+ error.status = response.status;
69
+ throw error;
70
+ }
71
+ ;
72
+ const text = await response.text();
73
+ try {
74
+ return text ? JSON.parse(text) : null;
75
+ } catch {
76
+ return null;
77
+ }
78
+ };
79
+
80
+ // index.js
81
+ var import_currency_codes = require("currency-codes");
82
+ var import_nanoid = require("nanoid");
83
+
84
+ // lib/color.js
85
+ var import_tinycolor2 = __toESM(require("tinycolor2"), 1);
86
+ var colorFormatted = (value) => {
87
+ const color = (0, import_tinycolor2.default)(value);
88
+ const attributes = {
89
+ brightness: color.getBrightness(),
90
+ dark: color.isDark(),
91
+ light: color.isLight(),
92
+ luminance: color.getLuminance()
93
+ };
94
+ return {
95
+ attributes,
96
+ hex: color.toHexString(),
97
+ hsl: color.toHsl(),
98
+ hsv: color.toHsv(),
99
+ rgb: color.toRgbString()
100
+ };
101
+ };
102
+ var colorAccessible = (background2) => {
103
+ const white = "#ffffff";
104
+ const black = "#000000";
105
+ return import_tinycolor2.default.isReadable(
106
+ background2,
107
+ white,
108
+ {
109
+ level: "AA",
110
+ size: "normal"
111
+ }
112
+ ) ? white : black;
113
+ };
114
+
115
+ // lib/constants.js
116
+ var font = {
117
+ family: "Roboto Flex",
118
+ transform: "none",
119
+ weight: "regular"
120
+ };
121
+ var background = "#ffffff";
122
+ var style = {
123
+ background: {
124
+ color: colorFormatted(background)
125
+ },
126
+ body: font,
127
+ button: {
128
+ background: {
129
+ color: colorFormatted(background)
130
+ },
131
+ radius: 0,
132
+ text: {
133
+ color: colorFormatted(colorAccessible(background))
134
+ }
135
+ },
136
+ heading: font,
137
+ input: {
138
+ radius: 0
139
+ },
140
+ text: {
141
+ color: colorFormatted(colorAccessible(background))
142
+ }
143
+ };
144
+
145
+ // index.js
146
+ var nanoid = (0, import_nanoid.customAlphabet)("0123456789abcdefghijklmnopqrstuvwxyz", 8);
147
+ var megabyte = 1024 * 1024;
148
+ var gigabyte = megabyte * 1024;
149
+ var urlRoot = (string) => (string == null ? void 0 : string.includes("https")) ? "https://" : "http://";
150
+ var shareUrls = ({
151
+ formUri,
152
+ organization,
153
+ page
154
+ }) => {
155
+ var _a;
156
+ const resolvedFormUri = formUri || (typeof process !== "undefined" ? (_a = process.env) == null ? void 0 : _a.APP_CLIENT_FORM_URI : void 0);
157
+ const preface = urlRoot(resolvedFormUri);
158
+ const base = resolvedFormUri;
159
+ const qr = preface + (base == null ? void 0 : base.replace(preface, "")) + "/" + (page == null ? void 0 : page.shortId) + "?qr=1";
160
+ const short = preface + (base == null ? void 0 : base.replace(preface, "")) + "/" + (page == null ? void 0 : page.shortId);
161
+ const url = preface + (organization == null ? void 0 : organization.subdomain) + "." + (base == null ? void 0 : base.replace(preface, "")) + "/" + (page == null ? void 0 : page.slug);
162
+ const clients = [
163
+ base,
164
+ qr,
165
+ short,
166
+ url
167
+ ];
168
+ const urls = {
169
+ qr,
170
+ short,
171
+ url
172
+ };
173
+ if ((organization == null ? void 0 : organization.subdomain) && (page == null ? void 0 : page.shortId)) {
174
+ clients.push(preface + (base == null ? void 0 : base.replace(preface, "")) + "/" + (page == null ? void 0 : page.shortId));
175
+ clients.push(preface + (base == null ? void 0 : base.replace(preface, "")) + "/" + (page == null ? void 0 : page.shortId) + "?qr=1");
176
+ clients.push(preface + organization.subdomain + "." + (base == null ? void 0 : base.replace(preface, "")));
177
+ clients.push(preface + organization.subdomain + "." + (base == null ? void 0 : base.replace(preface, "")) + "/" + (page == null ? void 0 : page.shortId));
178
+ }
179
+ ;
180
+ if ((organization == null ? void 0 : organization.shortId) && (page == null ? void 0 : page.slug) && (page == null ? void 0 : page.shortId)) {
181
+ clients.push(preface + organization.shortId + "." + (base == null ? void 0 : base.replace(preface, "")));
182
+ clients.push(preface + organization.shortId + "." + (base == null ? void 0 : base.replace(preface, "")) + "/" + (page == null ? void 0 : page.slug));
183
+ clients.push(preface + organization.shortId + "." + (base == null ? void 0 : base.replace(preface, "")) + "/" + (page == null ? void 0 : page.shortId));
184
+ }
185
+ ;
186
+ return {
187
+ clients,
188
+ urls
189
+ };
190
+ };
191
+ var currencies = import_currency_codes.data.map((item) => ({
192
+ ...item,
193
+ key: item.currency,
194
+ value: item.code
195
+ }));
196
+
197
+ // lib/cloudflare.js
198
+ var PURGE_BATCH = 30;
199
+ var endpoint = (zoneId) => "https://api.cloudflare.com/client/v4/zones/" + zoneId + "/purge_cache";
200
+ var pageEdgeUrls = ({
201
+ formUri,
202
+ organization,
203
+ page
204
+ }) => {
205
+ if (!page) return [];
206
+ const { clients } = shareUrls({
207
+ formUri,
208
+ organization,
209
+ page
210
+ });
211
+ const set = /* @__PURE__ */ new Set();
212
+ for (const client of clients || []) {
213
+ try {
214
+ const url = new URL(client);
215
+ if (url.pathname && url.pathname !== "/") {
216
+ set.add(url.toString());
217
+ }
218
+ ;
219
+ } catch {
220
+ continue;
221
+ }
222
+ ;
223
+ }
224
+ ;
225
+ if (page == null ? void 0 : page.slug) {
226
+ for (const item of [...set]) {
227
+ const url = new URL(item);
228
+ if (url.pathname === "/" + page.slug && !url.search) {
229
+ set.add(url.origin + "/" + page.slug + "/embed");
230
+ }
231
+ ;
232
+ }
233
+ ;
234
+ }
235
+ ;
236
+ return [...set];
237
+ };
238
+ var purge = async ({
239
+ files = [],
240
+ logger,
241
+ transport = request
242
+ } = {}) => {
243
+ var _a, _b;
244
+ const apiToken = typeof process !== "undefined" ? (_a = process.env) == null ? void 0 : _a.CLOUDFLARE_API_TOKEN : void 0;
245
+ const zoneId = typeof process !== "undefined" ? (_b = process.env) == null ? void 0 : _b.CLOUDFLARE_ZONE_ID : void 0;
246
+ if (!apiToken || !zoneId) {
247
+ return {
248
+ purged: 0,
249
+ skipped: true
250
+ };
251
+ }
252
+ ;
253
+ const unique = [...new Set(files.filter(Boolean))];
254
+ let purged = 0;
255
+ for (let index = 0; index < unique.length; index += PURGE_BATCH) {
256
+ const batch = unique.slice(index, index + PURGE_BATCH);
257
+ try {
258
+ await transport({
259
+ method: "POST",
260
+ url: endpoint(zoneId),
261
+ headers: {
262
+ Authorization: "Bearer " + apiToken
263
+ },
264
+ body: {
265
+ files: batch
266
+ }
267
+ });
268
+ purged += batch.length;
269
+ } catch (error) {
270
+ if (logger == null ? void 0 : logger.error) {
271
+ logger.error(error, {
272
+ extra: {
273
+ batch: batch.length,
274
+ files: batch.slice(0, 5)
275
+ }
276
+ });
277
+ }
278
+ ;
279
+ }
280
+ ;
281
+ }
282
+ ;
283
+ return {
284
+ purged,
285
+ skipped: false
286
+ };
287
+ };
288
+ // Annotate the CommonJS export names for ESM import in node:
289
+ 0 && (module.exports = {
290
+ pageEdgeUrls,
291
+ purge
292
+ });
@@ -0,0 +1,153 @@
1
+ import { request } from './http.cjs';
2
+ import { shareUrls } from './index.cjs';
3
+ import 'currency-codes';
4
+ import 'nanoid';
5
+ import './color.cjs';
6
+ import 'tinycolor2';
7
+ import './usage.cjs';
8
+
9
+ // Cloudflare edge-cache helpers (spike-readiness A1). Server-only — the token
10
+ // is read from plain env (never NEXT_PUBLIC_*), so Next never bundles it.
11
+ //
12
+ // purge() is deliberately safe to wire BEFORE Cloudflare exists in front of
13
+ // the domain: with CLOUDFLARE_API_TOKEN / CLOUDFLARE_ZONE_ID unset it is a
14
+ // no-op, so the sync invalidation paths can carry edge purges today and they
15
+ // simply start working the day the zone goes live. Errors are swallowed and
16
+ // logged — a failed edge purge must never break a stream handler or write
17
+ // path (same contract as drawbridge-redis cache.delete).
18
+ //
19
+ // pageEdgeUrls() enumerates every public HTML URL a page is reachable at —
20
+ // the subdomain + org-shortId hosts with the slug and shortId paths, the
21
+ // shortId redirect on the bare domain (plus its ?qr=1 twin, a distinct edge
22
+ // cache key), and the /embed twin of each full-page URL — derived from the
23
+ // same shareUrls() the rest of the family uses, so the purge set can never
24
+ // drift from the real URL scheme.
25
+
26
+
27
+ const PURGE_BATCH = 30; // Cloudflare's per-request purge-by-URL cap
28
+
29
+ const endpoint = ( zoneId ) =>
30
+ 'https://api.cloudflare.com/client/v4/zones/' + zoneId + '/purge_cache';
31
+
32
+ const pageEdgeUrls = ({
33
+ formUri,
34
+ organization,
35
+ page
36
+ }) => {
37
+
38
+ if( ! page ) return [];
39
+
40
+ const { clients } = shareUrls({
41
+ formUri,
42
+ organization,
43
+ page
44
+ });
45
+
46
+ const set = new Set();
47
+
48
+ for( const client of clients || [] ){
49
+
50
+ // Only page-specific URLs — bare hosts (the org/domain roots) are not
51
+ // this page's cache entries and purging them here would be collateral.
52
+ try {
53
+
54
+ const url = new URL( client );
55
+
56
+ if( url.pathname && url.pathname !== '/' ){
57
+
58
+ set.add( url.toString() );
59
+
60
+ };
61
+
62
+ } catch {
63
+
64
+ continue;
65
+
66
+ }
67
+ }
68
+ // The form-only embed renders at <host>/<slug>/embed for each full-page
69
+ // host — a second cached HTML surface that must die with the page.
70
+ if( page?.slug ){
71
+
72
+ for( const item of [ ...set ] ){
73
+
74
+ const url = new URL( item );
75
+
76
+ if( url.pathname === '/' + page.slug && ! url.search ){
77
+
78
+ set.add( url.origin + '/' + page.slug + '/embed' );
79
+
80
+ }
81
+ }
82
+ }
83
+ return [ ...set ];
84
+
85
+ };
86
+
87
+ // transport is injectable for tests (mirrors how stripe injects the shared
88
+ // http request); production callers never pass it.
89
+ const purge = async ({
90
+ files = [],
91
+ logger,
92
+ transport = request
93
+ } = {}) => {
94
+
95
+ const apiToken = typeof process !== 'undefined' ? process.env?.CLOUDFLARE_API_TOKEN : undefined;
96
+ const zoneId = typeof process !== 'undefined' ? process.env?.CLOUDFLARE_ZONE_ID : undefined;
97
+
98
+ // Not configured = deliberate no-op (ships before Cloudflare is live).
99
+ if( ! apiToken || ! zoneId ){
100
+
101
+ return {
102
+ purged : 0,
103
+ skipped : true
104
+ };
105
+
106
+ }
107
+ const unique = [ ...new Set( files.filter( Boolean ) ) ];
108
+
109
+ let purged = 0;
110
+
111
+ for( let index = 0; index < unique.length; index += PURGE_BATCH ){
112
+
113
+ const batch = unique.slice( index, index + PURGE_BATCH );
114
+
115
+ try {
116
+
117
+ await transport({
118
+ method : 'POST',
119
+ url : endpoint( zoneId ),
120
+ headers : {
121
+ Authorization : 'Bearer ' + apiToken
122
+ },
123
+ body : {
124
+ files : batch
125
+ }
126
+ });
127
+
128
+ purged += batch.length;
129
+
130
+ } catch ( error ) {
131
+
132
+ // Swallow — the edge self-heals via TTL; the caller's write path
133
+ // must not fail because a purge did.
134
+ if( logger?.error ){
135
+
136
+ logger.error( error, {
137
+ extra : {
138
+ batch : batch.length,
139
+ files : batch.slice( 0, 5 )
140
+ }
141
+ });
142
+
143
+ }
144
+ }
145
+ }
146
+ return {
147
+ purged,
148
+ skipped : false
149
+ };
150
+
151
+ };
152
+
153
+ export { pageEdgeUrls, purge };
@@ -0,0 +1,153 @@
1
+ import { request } from './http.js';
2
+ import { shareUrls } from './index.js';
3
+ import 'currency-codes';
4
+ import 'nanoid';
5
+ import './color.js';
6
+ import 'tinycolor2';
7
+ import './usage.js';
8
+
9
+ // Cloudflare edge-cache helpers (spike-readiness A1). Server-only — the token
10
+ // is read from plain env (never NEXT_PUBLIC_*), so Next never bundles it.
11
+ //
12
+ // purge() is deliberately safe to wire BEFORE Cloudflare exists in front of
13
+ // the domain: with CLOUDFLARE_API_TOKEN / CLOUDFLARE_ZONE_ID unset it is a
14
+ // no-op, so the sync invalidation paths can carry edge purges today and they
15
+ // simply start working the day the zone goes live. Errors are swallowed and
16
+ // logged — a failed edge purge must never break a stream handler or write
17
+ // path (same contract as drawbridge-redis cache.delete).
18
+ //
19
+ // pageEdgeUrls() enumerates every public HTML URL a page is reachable at —
20
+ // the subdomain + org-shortId hosts with the slug and shortId paths, the
21
+ // shortId redirect on the bare domain (plus its ?qr=1 twin, a distinct edge
22
+ // cache key), and the /embed twin of each full-page URL — derived from the
23
+ // same shareUrls() the rest of the family uses, so the purge set can never
24
+ // drift from the real URL scheme.
25
+
26
+
27
+ const PURGE_BATCH = 30; // Cloudflare's per-request purge-by-URL cap
28
+
29
+ const endpoint = ( zoneId ) =>
30
+ 'https://api.cloudflare.com/client/v4/zones/' + zoneId + '/purge_cache';
31
+
32
+ const pageEdgeUrls = ({
33
+ formUri,
34
+ organization,
35
+ page
36
+ }) => {
37
+
38
+ if( ! page ) return [];
39
+
40
+ const { clients } = shareUrls({
41
+ formUri,
42
+ organization,
43
+ page
44
+ });
45
+
46
+ const set = new Set();
47
+
48
+ for( const client of clients || [] ){
49
+
50
+ // Only page-specific URLs — bare hosts (the org/domain roots) are not
51
+ // this page's cache entries and purging them here would be collateral.
52
+ try {
53
+
54
+ const url = new URL( client );
55
+
56
+ if( url.pathname && url.pathname !== '/' ){
57
+
58
+ set.add( url.toString() );
59
+
60
+ };
61
+
62
+ } catch {
63
+
64
+ continue;
65
+
66
+ }
67
+ }
68
+ // The form-only embed renders at <host>/<slug>/embed for each full-page
69
+ // host — a second cached HTML surface that must die with the page.
70
+ if( page?.slug ){
71
+
72
+ for( const item of [ ...set ] ){
73
+
74
+ const url = new URL( item );
75
+
76
+ if( url.pathname === '/' + page.slug && ! url.search ){
77
+
78
+ set.add( url.origin + '/' + page.slug + '/embed' );
79
+
80
+ }
81
+ }
82
+ }
83
+ return [ ...set ];
84
+
85
+ };
86
+
87
+ // transport is injectable for tests (mirrors how stripe injects the shared
88
+ // http request); production callers never pass it.
89
+ const purge = async ({
90
+ files = [],
91
+ logger,
92
+ transport = request
93
+ } = {}) => {
94
+
95
+ const apiToken = typeof process !== 'undefined' ? process.env?.CLOUDFLARE_API_TOKEN : undefined;
96
+ const zoneId = typeof process !== 'undefined' ? process.env?.CLOUDFLARE_ZONE_ID : undefined;
97
+
98
+ // Not configured = deliberate no-op (ships before Cloudflare is live).
99
+ if( ! apiToken || ! zoneId ){
100
+
101
+ return {
102
+ purged : 0,
103
+ skipped : true
104
+ };
105
+
106
+ }
107
+ const unique = [ ...new Set( files.filter( Boolean ) ) ];
108
+
109
+ let purged = 0;
110
+
111
+ for( let index = 0; index < unique.length; index += PURGE_BATCH ){
112
+
113
+ const batch = unique.slice( index, index + PURGE_BATCH );
114
+
115
+ try {
116
+
117
+ await transport({
118
+ method : 'POST',
119
+ url : endpoint( zoneId ),
120
+ headers : {
121
+ Authorization : 'Bearer ' + apiToken
122
+ },
123
+ body : {
124
+ files : batch
125
+ }
126
+ });
127
+
128
+ purged += batch.length;
129
+
130
+ } catch ( error ) {
131
+
132
+ // Swallow — the edge self-heals via TTL; the caller's write path
133
+ // must not fail because a purge did.
134
+ if( logger?.error ){
135
+
136
+ logger.error( error, {
137
+ extra : {
138
+ batch : batch.length,
139
+ files : batch.slice( 0, 5 )
140
+ }
141
+ });
142
+
143
+ }
144
+ }
145
+ }
146
+ return {
147
+ purged,
148
+ skipped : false
149
+ };
150
+
151
+ };
152
+
153
+ export { pageEdgeUrls, purge };
@@ -0,0 +1,255 @@
1
+ // lib/http.js
2
+ var DEFAULT_TIMEOUT_MS = 15e3;
3
+ var request = async ({
4
+ body,
5
+ headers = {},
6
+ method = "GET",
7
+ query,
8
+ timeout = DEFAULT_TIMEOUT_MS,
9
+ type = "json",
10
+ url
11
+ }) => {
12
+ const fullUrl = new URL(url);
13
+ if (query) {
14
+ Object.entries(query).forEach(([k, v]) => fullUrl.searchParams.set(k, v));
15
+ }
16
+ ;
17
+ const isForm = type === "form";
18
+ const response = await fetch(fullUrl.toString(), {
19
+ method,
20
+ headers: {
21
+ "Content-Type": isForm ? "application/x-www-form-urlencoded" : "application/json",
22
+ ...headers
23
+ },
24
+ signal: AbortSignal.timeout(timeout),
25
+ ...body !== void 0 && {
26
+ body: isForm ? new URLSearchParams(body).toString() : JSON.stringify(body)
27
+ }
28
+ });
29
+ if (!response.ok) {
30
+ const text2 = await response.text().catch(() => "");
31
+ const error = new Error(text2 || response.statusText);
32
+ error.status = response.status;
33
+ throw error;
34
+ }
35
+ ;
36
+ const text = await response.text();
37
+ try {
38
+ return text ? JSON.parse(text) : null;
39
+ } catch {
40
+ return null;
41
+ }
42
+ };
43
+
44
+ // index.js
45
+ import { code, data } from "currency-codes";
46
+ import { customAlphabet } from "nanoid";
47
+
48
+ // lib/color.js
49
+ import tinycolor from "tinycolor2";
50
+ var colorFormatted = (value) => {
51
+ const color = tinycolor(value);
52
+ const attributes = {
53
+ brightness: color.getBrightness(),
54
+ dark: color.isDark(),
55
+ light: color.isLight(),
56
+ luminance: color.getLuminance()
57
+ };
58
+ return {
59
+ attributes,
60
+ hex: color.toHexString(),
61
+ hsl: color.toHsl(),
62
+ hsv: color.toHsv(),
63
+ rgb: color.toRgbString()
64
+ };
65
+ };
66
+ var colorAccessible = (background2) => {
67
+ const white = "#ffffff";
68
+ const black = "#000000";
69
+ return tinycolor.isReadable(
70
+ background2,
71
+ white,
72
+ {
73
+ level: "AA",
74
+ size: "normal"
75
+ }
76
+ ) ? white : black;
77
+ };
78
+
79
+ // lib/constants.js
80
+ var font = {
81
+ family: "Roboto Flex",
82
+ transform: "none",
83
+ weight: "regular"
84
+ };
85
+ var background = "#ffffff";
86
+ var style = {
87
+ background: {
88
+ color: colorFormatted(background)
89
+ },
90
+ body: font,
91
+ button: {
92
+ background: {
93
+ color: colorFormatted(background)
94
+ },
95
+ radius: 0,
96
+ text: {
97
+ color: colorFormatted(colorAccessible(background))
98
+ }
99
+ },
100
+ heading: font,
101
+ input: {
102
+ radius: 0
103
+ },
104
+ text: {
105
+ color: colorFormatted(colorAccessible(background))
106
+ }
107
+ };
108
+
109
+ // index.js
110
+ var nanoid = customAlphabet("0123456789abcdefghijklmnopqrstuvwxyz", 8);
111
+ var megabyte = 1024 * 1024;
112
+ var gigabyte = megabyte * 1024;
113
+ var urlRoot = (string) => (string == null ? void 0 : string.includes("https")) ? "https://" : "http://";
114
+ var shareUrls = ({
115
+ formUri,
116
+ organization,
117
+ page
118
+ }) => {
119
+ var _a;
120
+ const resolvedFormUri = formUri || (typeof process !== "undefined" ? (_a = process.env) == null ? void 0 : _a.APP_CLIENT_FORM_URI : void 0);
121
+ const preface = urlRoot(resolvedFormUri);
122
+ const base = resolvedFormUri;
123
+ const qr = preface + (base == null ? void 0 : base.replace(preface, "")) + "/" + (page == null ? void 0 : page.shortId) + "?qr=1";
124
+ const short = preface + (base == null ? void 0 : base.replace(preface, "")) + "/" + (page == null ? void 0 : page.shortId);
125
+ const url = preface + (organization == null ? void 0 : organization.subdomain) + "." + (base == null ? void 0 : base.replace(preface, "")) + "/" + (page == null ? void 0 : page.slug);
126
+ const clients = [
127
+ base,
128
+ qr,
129
+ short,
130
+ url
131
+ ];
132
+ const urls = {
133
+ qr,
134
+ short,
135
+ url
136
+ };
137
+ if ((organization == null ? void 0 : organization.subdomain) && (page == null ? void 0 : page.shortId)) {
138
+ clients.push(preface + (base == null ? void 0 : base.replace(preface, "")) + "/" + (page == null ? void 0 : page.shortId));
139
+ clients.push(preface + (base == null ? void 0 : base.replace(preface, "")) + "/" + (page == null ? void 0 : page.shortId) + "?qr=1");
140
+ clients.push(preface + organization.subdomain + "." + (base == null ? void 0 : base.replace(preface, "")));
141
+ clients.push(preface + organization.subdomain + "." + (base == null ? void 0 : base.replace(preface, "")) + "/" + (page == null ? void 0 : page.shortId));
142
+ }
143
+ ;
144
+ if ((organization == null ? void 0 : organization.shortId) && (page == null ? void 0 : page.slug) && (page == null ? void 0 : page.shortId)) {
145
+ clients.push(preface + organization.shortId + "." + (base == null ? void 0 : base.replace(preface, "")));
146
+ clients.push(preface + organization.shortId + "." + (base == null ? void 0 : base.replace(preface, "")) + "/" + (page == null ? void 0 : page.slug));
147
+ clients.push(preface + organization.shortId + "." + (base == null ? void 0 : base.replace(preface, "")) + "/" + (page == null ? void 0 : page.shortId));
148
+ }
149
+ ;
150
+ return {
151
+ clients,
152
+ urls
153
+ };
154
+ };
155
+ var currencies = data.map((item) => ({
156
+ ...item,
157
+ key: item.currency,
158
+ value: item.code
159
+ }));
160
+
161
+ // lib/cloudflare.js
162
+ var PURGE_BATCH = 30;
163
+ var endpoint = (zoneId) => "https://api.cloudflare.com/client/v4/zones/" + zoneId + "/purge_cache";
164
+ var pageEdgeUrls = ({
165
+ formUri,
166
+ organization,
167
+ page
168
+ }) => {
169
+ if (!page) return [];
170
+ const { clients } = shareUrls({
171
+ formUri,
172
+ organization,
173
+ page
174
+ });
175
+ const set = /* @__PURE__ */ new Set();
176
+ for (const client of clients || []) {
177
+ try {
178
+ const url = new URL(client);
179
+ if (url.pathname && url.pathname !== "/") {
180
+ set.add(url.toString());
181
+ }
182
+ ;
183
+ } catch {
184
+ continue;
185
+ }
186
+ ;
187
+ }
188
+ ;
189
+ if (page == null ? void 0 : page.slug) {
190
+ for (const item of [...set]) {
191
+ const url = new URL(item);
192
+ if (url.pathname === "/" + page.slug && !url.search) {
193
+ set.add(url.origin + "/" + page.slug + "/embed");
194
+ }
195
+ ;
196
+ }
197
+ ;
198
+ }
199
+ ;
200
+ return [...set];
201
+ };
202
+ var purge = async ({
203
+ files = [],
204
+ logger,
205
+ transport = request
206
+ } = {}) => {
207
+ var _a, _b;
208
+ const apiToken = typeof process !== "undefined" ? (_a = process.env) == null ? void 0 : _a.CLOUDFLARE_API_TOKEN : void 0;
209
+ const zoneId = typeof process !== "undefined" ? (_b = process.env) == null ? void 0 : _b.CLOUDFLARE_ZONE_ID : void 0;
210
+ if (!apiToken || !zoneId) {
211
+ return {
212
+ purged: 0,
213
+ skipped: true
214
+ };
215
+ }
216
+ ;
217
+ const unique = [...new Set(files.filter(Boolean))];
218
+ let purged = 0;
219
+ for (let index = 0; index < unique.length; index += PURGE_BATCH) {
220
+ const batch = unique.slice(index, index + PURGE_BATCH);
221
+ try {
222
+ await transport({
223
+ method: "POST",
224
+ url: endpoint(zoneId),
225
+ headers: {
226
+ Authorization: "Bearer " + apiToken
227
+ },
228
+ body: {
229
+ files: batch
230
+ }
231
+ });
232
+ purged += batch.length;
233
+ } catch (error) {
234
+ if (logger == null ? void 0 : logger.error) {
235
+ logger.error(error, {
236
+ extra: {
237
+ batch: batch.length,
238
+ files: batch.slice(0, 5)
239
+ }
240
+ });
241
+ }
242
+ ;
243
+ }
244
+ ;
245
+ }
246
+ ;
247
+ return {
248
+ purged,
249
+ skipped: false
250
+ };
251
+ };
252
+ export {
253
+ pageEdgeUrls,
254
+ purge
255
+ };
package/package.json CHANGED
@@ -146,6 +146,11 @@
146
146
  "types": "./dist/plans.d.ts",
147
147
  "import": "./dist/plans.js",
148
148
  "require": "./dist/plans.cjs"
149
+ },
150
+ "./cloudflare": {
151
+ "types": "./dist/cloudflare.d.ts",
152
+ "import": "./dist/cloudflare.js",
153
+ "require": "./dist/cloudflare.cjs"
149
154
  }
150
155
  },
151
156
  "files": [
@@ -164,5 +169,5 @@
164
169
  "test": ". \"$HOME/.nvm/nvm.sh\" && nvm use && node --test"
165
170
  },
166
171
  "types": "dist/index.d.ts",
167
- "version": "0.0.87"
172
+ "version": "0.0.88"
168
173
  }