@flaredev/core 0.1.0
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/LICENSE +21 -0
- package/README.md +18 -0
- package/dist/chunk-DARDHZPX.js +192 -0
- package/dist/chunk-KSVLQLJD.js +91 -0
- package/dist/chunk-LYQT4S47.js +248 -0
- package/dist/chunk-ZJPJGME5.js +40 -0
- package/dist/client.d.ts +66 -0
- package/dist/client.js +52 -0
- package/dist/define-D5CtCkOm.d.ts +232 -0
- package/dist/index.d.ts +438 -0
- package/dist/index.js +770 -0
- package/dist/react.d.ts +34 -0
- package/dist/react.js +95 -0
- package/dist/realtime/client.d.ts +82 -0
- package/dist/realtime/client.js +7 -0
- package/dist/realtime/server.d.ts +109 -0
- package/dist/realtime/server.js +200 -0
- package/dist/security/index.d.ts +159 -0
- package/dist/security/index.js +147 -0
- package/dist/security/worker.d.ts +104 -0
- package/dist/security/worker.js +116 -0
- package/dist/server.d.ts +156 -0
- package/dist/server.js +319 -0
- package/dist/types-B6Y73BgH.d.ts +54 -0
- package/package.json +100 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,770 @@
|
|
|
1
|
+
import {
|
|
2
|
+
RESERVED_FIELD_NAMES,
|
|
3
|
+
ResourceDefinitionError,
|
|
4
|
+
camelCase,
|
|
5
|
+
columnName,
|
|
6
|
+
createValidators,
|
|
7
|
+
defaultFieldLabel,
|
|
8
|
+
defineResource,
|
|
9
|
+
fieldSchema,
|
|
10
|
+
fileKeyPrefix,
|
|
11
|
+
humanize,
|
|
12
|
+
kebabCase,
|
|
13
|
+
pascalCase,
|
|
14
|
+
pluralize,
|
|
15
|
+
snakeCase,
|
|
16
|
+
storedFields,
|
|
17
|
+
words
|
|
18
|
+
} from "./chunk-DARDHZPX.js";
|
|
19
|
+
|
|
20
|
+
// src/password.ts
|
|
21
|
+
var ALGORITHM = "pbkdf2-sha256";
|
|
22
|
+
var PBKDF2_ITERATIONS = 1e5;
|
|
23
|
+
var SALT_BYTES = 16;
|
|
24
|
+
var HASH_BITS = 256;
|
|
25
|
+
var encoder = new TextEncoder();
|
|
26
|
+
async function derive(password, salt, iterations) {
|
|
27
|
+
const key = await crypto.subtle.importKey("raw", encoder.encode(password.normalize("NFKC")), "PBKDF2", false, [
|
|
28
|
+
"deriveBits"
|
|
29
|
+
]);
|
|
30
|
+
const bits = await crypto.subtle.deriveBits(
|
|
31
|
+
{ name: "PBKDF2", hash: "SHA-256", salt, iterations },
|
|
32
|
+
key,
|
|
33
|
+
HASH_BITS
|
|
34
|
+
);
|
|
35
|
+
return new Uint8Array(bits);
|
|
36
|
+
}
|
|
37
|
+
function toBase64(bytes) {
|
|
38
|
+
let binary = "";
|
|
39
|
+
for (const byte of bytes) binary += String.fromCharCode(byte);
|
|
40
|
+
return btoa(binary);
|
|
41
|
+
}
|
|
42
|
+
function fromBase64(value) {
|
|
43
|
+
return Uint8Array.from(atob(value), (char) => char.charCodeAt(0));
|
|
44
|
+
}
|
|
45
|
+
function timingSafeEqual(a, b) {
|
|
46
|
+
if (a.length !== b.length) return false;
|
|
47
|
+
let diff = 0;
|
|
48
|
+
for (let i = 0; i < a.length; i++) diff |= a[i] ^ b[i];
|
|
49
|
+
return diff === 0;
|
|
50
|
+
}
|
|
51
|
+
async function hashPassword(password) {
|
|
52
|
+
const salt = crypto.getRandomValues(new Uint8Array(SALT_BYTES));
|
|
53
|
+
const hash = await derive(password, salt, PBKDF2_ITERATIONS);
|
|
54
|
+
return `${ALGORITHM}$${PBKDF2_ITERATIONS}$${toBase64(salt)}$${toBase64(hash)}`;
|
|
55
|
+
}
|
|
56
|
+
async function verifyPassword({ hash, password }) {
|
|
57
|
+
const [algorithm, iterationsText, saltText, hashText] = hash.split("$");
|
|
58
|
+
const iterations = Number(iterationsText);
|
|
59
|
+
if (algorithm !== ALGORITHM || !Number.isInteger(iterations) || iterations < 1 || !saltText || !hashText) {
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
try {
|
|
63
|
+
const expected = fromBase64(hashText);
|
|
64
|
+
const actual = await derive(password, fromBase64(saltText), iterations);
|
|
65
|
+
return timingSafeEqual(actual, expected);
|
|
66
|
+
} catch {
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// src/signing.ts
|
|
72
|
+
var encoder2 = new TextEncoder();
|
|
73
|
+
var decoder = new TextDecoder();
|
|
74
|
+
function base64UrlEncode(bytes) {
|
|
75
|
+
let binary = "";
|
|
76
|
+
for (const byte of bytes) binary += String.fromCharCode(byte);
|
|
77
|
+
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
78
|
+
}
|
|
79
|
+
function base64UrlDecode(value) {
|
|
80
|
+
if (!/^[A-Za-z0-9_-]*$/.test(value)) throw new Error("Invalid base64url");
|
|
81
|
+
const base64 = value.replace(/-/g, "+").replace(/_/g, "/") + "=".repeat((4 - value.length % 4) % 4);
|
|
82
|
+
return Uint8Array.from(atob(base64), (char) => char.charCodeAt(0));
|
|
83
|
+
}
|
|
84
|
+
async function importSigningKey(secret, purpose) {
|
|
85
|
+
if (!secret) throw new Error("A signing secret is required.");
|
|
86
|
+
const root = await crypto.subtle.importKey("raw", encoder2.encode(secret), { name: "HMAC", hash: "SHA-256" }, false, [
|
|
87
|
+
"sign"
|
|
88
|
+
]);
|
|
89
|
+
const derived = await crypto.subtle.sign("HMAC", root, encoder2.encode(`flare:${purpose}`));
|
|
90
|
+
return crypto.subtle.importKey("raw", derived, { name: "HMAC", hash: "SHA-256" }, false, ["sign", "verify"]);
|
|
91
|
+
}
|
|
92
|
+
async function signToken(key, payload) {
|
|
93
|
+
const body = base64UrlEncode(encoder2.encode(JSON.stringify(payload)));
|
|
94
|
+
const signature = new Uint8Array(await crypto.subtle.sign("HMAC", key, encoder2.encode(body)));
|
|
95
|
+
return `${body}.${base64UrlEncode(signature)}`;
|
|
96
|
+
}
|
|
97
|
+
async function verifyToken(key, token) {
|
|
98
|
+
const parts = token.split(".");
|
|
99
|
+
if (parts.length !== 2) return null;
|
|
100
|
+
const [body, signature] = parts;
|
|
101
|
+
try {
|
|
102
|
+
const valid = await crypto.subtle.verify(
|
|
103
|
+
"HMAC",
|
|
104
|
+
key,
|
|
105
|
+
base64UrlDecode(signature),
|
|
106
|
+
encoder2.encode(body)
|
|
107
|
+
);
|
|
108
|
+
if (!valid) return null;
|
|
109
|
+
return JSON.parse(decoder.decode(base64UrlDecode(body)));
|
|
110
|
+
} catch {
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// src/mail.ts
|
|
116
|
+
var RESEND_ENDPOINT = "https://api.resend.com/emails";
|
|
117
|
+
var RETRYABLE = /* @__PURE__ */ new Set([429, 500, 502, 503, 504]);
|
|
118
|
+
function createMailer(options) {
|
|
119
|
+
const doFetch = options.fetch ?? fetch;
|
|
120
|
+
const maxRetries = options.maxRetries ?? 2;
|
|
121
|
+
const sleep = options.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
|
|
122
|
+
const log = options.log ?? ((message) => console.info(message));
|
|
123
|
+
async function send(message) {
|
|
124
|
+
const from = message.from ?? options.from;
|
|
125
|
+
if (!options.apiKey) {
|
|
126
|
+
log(
|
|
127
|
+
[
|
|
128
|
+
"[flare/mail] RESEND_API_KEY is not set; email logged instead of sent:",
|
|
129
|
+
` From: ${from}`,
|
|
130
|
+
` To: ${[message.to].flat().join(", ")}`,
|
|
131
|
+
` Subject: ${message.subject}`,
|
|
132
|
+
"",
|
|
133
|
+
message.text ?? message.html
|
|
134
|
+
].join("\n")
|
|
135
|
+
);
|
|
136
|
+
return { data: { id: "logged" }, error: null };
|
|
137
|
+
}
|
|
138
|
+
const headers = {
|
|
139
|
+
Authorization: `Bearer ${options.apiKey}`,
|
|
140
|
+
"Content-Type": "application/json"
|
|
141
|
+
};
|
|
142
|
+
if (message.idempotencyKey) headers["Idempotency-Key"] = message.idempotencyKey;
|
|
143
|
+
const body = JSON.stringify({
|
|
144
|
+
from,
|
|
145
|
+
to: [message.to].flat(),
|
|
146
|
+
subject: message.subject,
|
|
147
|
+
html: message.html,
|
|
148
|
+
...message.text !== void 0 && { text: message.text },
|
|
149
|
+
...message.replyTo !== void 0 && { reply_to: [message.replyTo].flat() }
|
|
150
|
+
});
|
|
151
|
+
for (let attempt = 0; ; attempt++) {
|
|
152
|
+
let response;
|
|
153
|
+
try {
|
|
154
|
+
response = await doFetch(RESEND_ENDPOINT, { method: "POST", headers, body });
|
|
155
|
+
} catch (cause) {
|
|
156
|
+
if (message.idempotencyKey && attempt < maxRetries) {
|
|
157
|
+
await sleep(backoff(attempt));
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
return failure("network_error", cause instanceof Error ? cause.message : String(cause), null);
|
|
161
|
+
}
|
|
162
|
+
if (response.ok) {
|
|
163
|
+
const { id } = await response.json();
|
|
164
|
+
return { data: { id }, error: null };
|
|
165
|
+
}
|
|
166
|
+
const payload = await response.json().catch(() => ({}));
|
|
167
|
+
const name = payload.name ?? "application_error";
|
|
168
|
+
const retryable = RETRYABLE.has(response.status) || name === "concurrent_idempotent_requests";
|
|
169
|
+
if (retryable && message.idempotencyKey && attempt < maxRetries) {
|
|
170
|
+
const retryAfter = Number(response.headers.get("retry-after"));
|
|
171
|
+
await sleep(Number.isFinite(retryAfter) && retryAfter > 0 ? retryAfter * 1e3 : backoff(attempt));
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
return failure(name, payload.message ?? response.statusText, response.status);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
return { send };
|
|
178
|
+
}
|
|
179
|
+
function backoff(attempt) {
|
|
180
|
+
return 500 * 2 ** attempt + Math.floor(Math.random() * 250);
|
|
181
|
+
}
|
|
182
|
+
function failure(name, message, statusCode) {
|
|
183
|
+
return { data: null, error: { name, message, statusCode } };
|
|
184
|
+
}
|
|
185
|
+
var escapeHtml = (value) => value.replace(/[&<>"']/g, (char) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[char]);
|
|
186
|
+
function renderTransactionalEmail(email) {
|
|
187
|
+
if (email.action && !/^https?:\/\//i.test(email.action.url)) {
|
|
188
|
+
throw new Error("Email action URLs must be absolute http(s) URLs.");
|
|
189
|
+
}
|
|
190
|
+
const font = `font-family:ui-sans-serif,-apple-system,"Segoe UI",Helvetica,Arial,sans-serif`;
|
|
191
|
+
const paragraphs = email.paragraphs.map((p) => `<p style="margin:0 0 16px;font-size:14px;line-height:22px;color:#0a0a0a">${escapeHtml(p)}</p>`).join("");
|
|
192
|
+
const action = email.action ? `<table role="presentation" cellpadding="0" cellspacing="0" style="margin:8px 0 24px"><tr><td style="border-radius:6px;background:#0a0a0a"><a href="${escapeHtml(email.action.url)}" style="display:inline-block;padding:10px 18px;font-size:14px;font-weight:600;color:#ffffff;text-decoration:none">${escapeHtml(email.action.label)}</a></td></tr></table><p style="margin:0 0 16px;font-size:12px;line-height:18px;color:#6b7280">Or paste this link into your browser:<br><a href="${escapeHtml(email.action.url)}" style="color:#6b7280;word-break:break-all">${escapeHtml(email.action.url)}</a></p>` : "";
|
|
193
|
+
const footer = email.footer ? `<p style="margin:24px 0 0;padding-top:16px;border-top:1px solid #e5e5e7;font-size:12px;line-height:18px;color:#6b7280">${escapeHtml(email.footer)}</p>` : "";
|
|
194
|
+
const preheader = email.preheader ? `<div style="display:none;max-height:0;overflow:hidden;opacity:0">${escapeHtml(email.preheader)}</div>` : "";
|
|
195
|
+
const html = `<!doctype html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>${escapeHtml(email.heading)}</title></head><body style="margin:0;padding:0;background:#f7f7f8;${font}">${preheader}<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="background:#f7f7f8;padding:32px 16px"><tr><td align="center"><table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="max-width:480px;background:#ffffff;border:1px solid #e5e5e7;border-radius:8px"><tr><td style="padding:32px;${font}"><p style="margin:0 0 24px;font-size:14px;font-weight:600;color:#6b7280">${escapeHtml(email.appName)}</p><h1 style="margin:0 0 16px;font-size:20px;line-height:28px;font-weight:600;color:#0a0a0a">${escapeHtml(email.heading)}</h1>${paragraphs}${action}${footer}</td></tr></table></td></tr></table></body></html>`;
|
|
196
|
+
const text2 = [
|
|
197
|
+
email.heading,
|
|
198
|
+
"",
|
|
199
|
+
...email.paragraphs.flatMap((p) => [p, ""]),
|
|
200
|
+
...email.action ? [`${email.action.label}: ${email.action.url}`, ""] : [],
|
|
201
|
+
...email.footer ? ["--", email.footer] : []
|
|
202
|
+
].join("\n").trimEnd();
|
|
203
|
+
return { html, text: text2 };
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// src/sniff.ts
|
|
207
|
+
var SNIFF_BYTES = 32;
|
|
208
|
+
var ascii = (text2) => Array.from(text2, (char) => char.charCodeAt(0));
|
|
209
|
+
function startsWith(head, signature, offset = 0) {
|
|
210
|
+
if (head.length < offset + signature.length) return false;
|
|
211
|
+
return signature.every((byte, index) => head[offset + index] === byte);
|
|
212
|
+
}
|
|
213
|
+
var text = (head, offset, length) => String.fromCharCode(...head.subarray(offset, offset + length));
|
|
214
|
+
var isoBrand = (head) => startsWith(head, ascii("ftyp"), 4) ? text(head, 8, 4) : null;
|
|
215
|
+
var riffType = (head) => startsWith(head, ascii("RIFF")) ? text(head, 8, 4) : null;
|
|
216
|
+
var ZIP = [80, 75, 3, 4];
|
|
217
|
+
var EMPTY_ZIP = [80, 75, 5, 6];
|
|
218
|
+
var OLE2 = [208, 207, 17, 224, 161, 177, 26, 225];
|
|
219
|
+
var EBML = [26, 69, 223, 163];
|
|
220
|
+
var isZip = (head) => startsWith(head, ZIP) || startsWith(head, EMPTY_ZIP);
|
|
221
|
+
var EXACT = {
|
|
222
|
+
"image/png": (head) => startsWith(head, [137, 80, 78, 71, 13, 10, 26, 10]),
|
|
223
|
+
"image/jpeg": (head) => startsWith(head, [255, 216, 255]),
|
|
224
|
+
"image/gif": (head) => startsWith(head, ascii("GIF87a")) || startsWith(head, ascii("GIF89a")),
|
|
225
|
+
"image/webp": (head) => riffType(head) === "WEBP",
|
|
226
|
+
"image/avif": (head) => ["avif", "avis", "mif1", "msf1"].includes(isoBrand(head) ?? ""),
|
|
227
|
+
"application/pdf": (head) => startsWith(head, ascii("%PDF-")),
|
|
228
|
+
"application/zip": isZip,
|
|
229
|
+
// Office Open XML and OpenDocument files are zip archives.
|
|
230
|
+
"application/vnd.openxmlformats-officedocument.wordprocessingml.document": isZip,
|
|
231
|
+
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": isZip,
|
|
232
|
+
"application/vnd.oasis.opendocument.text": isZip,
|
|
233
|
+
"application/vnd.oasis.opendocument.spreadsheet": isZip,
|
|
234
|
+
// Legacy .doc / .xls are OLE2 compound files.
|
|
235
|
+
"application/msword": (head) => startsWith(head, OLE2),
|
|
236
|
+
"application/vnd.ms-excel": (head) => startsWith(head, OLE2)
|
|
237
|
+
};
|
|
238
|
+
function isMediaContainer(head) {
|
|
239
|
+
return isoBrand(head) !== null || startsWith(head, EBML) || startsWith(head, ascii("OggS")) || ["AVI ", "WAVE"].includes(riffType(head) ?? "") || startsWith(head, ascii("fLaC")) || startsWith(head, ascii("ID3")) || // MPEG audio frame sync (MP3/AAC without an ID3 tag).
|
|
240
|
+
head.length >= 2 && head[0] === 255 && (head[1] & 224) === 224 || // MPEG program stream.
|
|
241
|
+
startsWith(head, [0, 0, 1, 186]);
|
|
242
|
+
}
|
|
243
|
+
var isText = (head) => head.every((byte) => byte >= 32 || byte === 9 || byte === 10 || byte === 12 || byte === 13);
|
|
244
|
+
function sniffMatches(contentType, head) {
|
|
245
|
+
const type = contentType.split(";")[0].trim().toLowerCase();
|
|
246
|
+
const exact = EXACT[type];
|
|
247
|
+
if (exact) return exact(head);
|
|
248
|
+
if (type.startsWith("video/") || type.startsWith("audio/")) return isMediaContainer(head);
|
|
249
|
+
if (type === "text/plain" || type === "text/csv") return isText(head);
|
|
250
|
+
return void 0;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// src/storage.ts
|
|
254
|
+
var SIGNING_PURPOSE = "storage";
|
|
255
|
+
var DEFAULT_EXPIRES_IN = 15 * 60;
|
|
256
|
+
function matchesContentType(contentType, allowed) {
|
|
257
|
+
const type = contentType.split(";")[0].trim().toLowerCase();
|
|
258
|
+
if (!/^[a-z0-9!#$&^_.+-]+\/[a-z0-9!#$&^_.+-]+$/.test(type)) return false;
|
|
259
|
+
return allowed.some((pattern) => {
|
|
260
|
+
const p = pattern.trim().toLowerCase();
|
|
261
|
+
return p.endsWith("/*") ? type.startsWith(p.slice(0, -1)) : type === p;
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
function createObjectKey(filename, prefix = "uploads") {
|
|
265
|
+
const now = /* @__PURE__ */ new Date();
|
|
266
|
+
const month = String(now.getUTCMonth() + 1).padStart(2, "0");
|
|
267
|
+
const safe = filename.normalize("NFKD").replace(/[^\w.-]+/g, "-").replace(/-+/g, "-").replace(/^[-.]+|[-.]+$/g, "").slice(-80) || "file";
|
|
268
|
+
return `${prefix.replace(/\/+$/, "")}/${now.getUTCFullYear()}/${month}/${crypto.randomUUID()}-${safe}`;
|
|
269
|
+
}
|
|
270
|
+
var INLINE_SAFE = /^(image\/(png|jpeg|gif|webp|avif)|application\/pdf|text\/plain|video\/[\w.+-]+|audio\/[\w.+-]+)$/;
|
|
271
|
+
function isInlineSafe(contentType) {
|
|
272
|
+
return INLINE_SAFE.test(contentType.split(";")[0].trim().toLowerCase());
|
|
273
|
+
}
|
|
274
|
+
function isValidKey(key) {
|
|
275
|
+
return key.length > 0 && key.length <= 1024 && !key.startsWith("/") && !key.split("/").includes("..");
|
|
276
|
+
}
|
|
277
|
+
async function discard(body) {
|
|
278
|
+
try {
|
|
279
|
+
await drain(body.getReader());
|
|
280
|
+
} catch {
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
async function drain(reader) {
|
|
284
|
+
try {
|
|
285
|
+
while (!(await reader.read()).done) ;
|
|
286
|
+
} catch {
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
async function readHead(body, bytes) {
|
|
290
|
+
const reader = body.getReader();
|
|
291
|
+
const chunks = [];
|
|
292
|
+
let size = 0;
|
|
293
|
+
while (size < bytes) {
|
|
294
|
+
const { done, value } = await reader.read();
|
|
295
|
+
if (done) break;
|
|
296
|
+
chunks.push(value);
|
|
297
|
+
size += value.byteLength;
|
|
298
|
+
}
|
|
299
|
+
const head = new Uint8Array(Math.min(size, bytes));
|
|
300
|
+
let offset = 0;
|
|
301
|
+
for (const chunk of chunks) {
|
|
302
|
+
if (offset >= head.length) break;
|
|
303
|
+
const part = chunk.subarray(0, head.length - offset);
|
|
304
|
+
head.set(part, offset);
|
|
305
|
+
offset += part.length;
|
|
306
|
+
}
|
|
307
|
+
return { head, chunks, reader };
|
|
308
|
+
}
|
|
309
|
+
function replay(chunks, reader, length) {
|
|
310
|
+
const source = new ReadableStream({
|
|
311
|
+
start(controller) {
|
|
312
|
+
for (const chunk of chunks) controller.enqueue(chunk);
|
|
313
|
+
},
|
|
314
|
+
async pull(controller) {
|
|
315
|
+
const { done, value } = await reader.read();
|
|
316
|
+
if (done) controller.close();
|
|
317
|
+
else controller.enqueue(value);
|
|
318
|
+
},
|
|
319
|
+
cancel: (reason) => reader.cancel(reason)
|
|
320
|
+
});
|
|
321
|
+
const FixedLength = globalThis.FixedLengthStream;
|
|
322
|
+
if (!FixedLength) return source;
|
|
323
|
+
const fixed = new FixedLength(length);
|
|
324
|
+
source.pipeTo(fixed.writable).catch(() => {
|
|
325
|
+
});
|
|
326
|
+
return fixed.readable;
|
|
327
|
+
}
|
|
328
|
+
function error(status, message) {
|
|
329
|
+
return Response.json({ error: message }, { status });
|
|
330
|
+
}
|
|
331
|
+
function createStorage(options) {
|
|
332
|
+
const routePath = options.routePath ?? "/api/storage";
|
|
333
|
+
const defaultExpiresIn = options.expiresIn ?? DEFAULT_EXPIRES_IN;
|
|
334
|
+
let keyPromise;
|
|
335
|
+
const signingKey = () => keyPromise ??= importSigningKey(options.secret, SIGNING_PURPOSE);
|
|
336
|
+
const expiry = (seconds = defaultExpiresIn) => Math.floor(Date.now() / 1e3) + seconds;
|
|
337
|
+
const urlFor = (token) => `${options.baseURL?.replace(/\/+$/, "") ?? ""}${routePath}?token=${token}`;
|
|
338
|
+
async function handle(request) {
|
|
339
|
+
const token = new URL(request.url).searchParams.get("token");
|
|
340
|
+
if (!token) return error(400, "Missing token.");
|
|
341
|
+
const payload = await verifyToken(await signingKey(), token);
|
|
342
|
+
if (!payload || typeof payload.exp !== "number" || !isValidKey(payload.key)) {
|
|
343
|
+
return error(403, "Invalid token.");
|
|
344
|
+
}
|
|
345
|
+
if (payload.exp < Date.now() / 1e3) return error(403, "Token expired.");
|
|
346
|
+
if (request.method === "PUT") {
|
|
347
|
+
if (payload.op !== "put") return error(403, "Token does not allow uploads.");
|
|
348
|
+
const contentType = request.headers.get("content-type") ?? "";
|
|
349
|
+
if (!matchesContentType(contentType, payload.types)) {
|
|
350
|
+
return error(415, `Content-Type must be one of: ${payload.types.join(", ")}.`);
|
|
351
|
+
}
|
|
352
|
+
const length = Number(request.headers.get("content-length"));
|
|
353
|
+
if (!request.headers.has("content-length") || !Number.isInteger(length) || length < 0) {
|
|
354
|
+
return error(411, "Content-Length is required.");
|
|
355
|
+
}
|
|
356
|
+
if (length > payload.max) return error(413, `File exceeds the ${payload.max}-byte limit.`);
|
|
357
|
+
if (!request.body) return error(400, "Missing request body.");
|
|
358
|
+
const { head, chunks, reader } = await readHead(request.body, SNIFF_BYTES);
|
|
359
|
+
if (sniffMatches(contentType, head) === false) {
|
|
360
|
+
await drain(reader);
|
|
361
|
+
return error(415, `The file's contents don't match its type (${contentType.split(";")[0].trim()}).`);
|
|
362
|
+
}
|
|
363
|
+
await options.bucket.put(payload.key, replay(chunks, reader, length), { httpMetadata: { contentType } });
|
|
364
|
+
return Response.json({ key: payload.key }, { status: 201 });
|
|
365
|
+
}
|
|
366
|
+
if (request.method === "GET" || request.method === "HEAD") {
|
|
367
|
+
if (payload.op !== "get") return error(403, "Token does not allow reads.");
|
|
368
|
+
const object = await options.bucket.get(payload.key);
|
|
369
|
+
if (!object) return error(404, "Not found.");
|
|
370
|
+
const headers = new Headers({
|
|
371
|
+
"content-type": object.httpMetadata?.contentType ?? "application/octet-stream",
|
|
372
|
+
"content-length": String(object.size),
|
|
373
|
+
etag: object.httpEtag,
|
|
374
|
+
"cache-control": "private, max-age=300",
|
|
375
|
+
"x-content-type-options": "nosniff"
|
|
376
|
+
});
|
|
377
|
+
const inline = !payload.name && isInlineSafe(headers.get("content-type"));
|
|
378
|
+
if (!inline) {
|
|
379
|
+
headers.set("content-security-policy", "default-src 'none'; sandbox");
|
|
380
|
+
}
|
|
381
|
+
const filename = payload.name ?? payload.key.split("/").pop();
|
|
382
|
+
headers.set(
|
|
383
|
+
"content-disposition",
|
|
384
|
+
`${inline ? "inline" : "attachment"}; filename*=UTF-8''${encodeURIComponent(filename)}`
|
|
385
|
+
);
|
|
386
|
+
return new Response(request.method === "HEAD" ? null : object.body, { headers });
|
|
387
|
+
}
|
|
388
|
+
return new Response(null, { status: 405, headers: { allow: "GET, HEAD, PUT" } });
|
|
389
|
+
}
|
|
390
|
+
return {
|
|
391
|
+
bucket: options.bucket,
|
|
392
|
+
/** A URL the browser can PUT the file body to, with the `Content-Type` header set. */
|
|
393
|
+
async createUploadUrl(upload) {
|
|
394
|
+
if (!isValidKey(upload.key)) throw new Error(`Invalid object key: ${upload.key}`);
|
|
395
|
+
if (upload.contentTypes.length === 0) throw new Error("contentTypes must list at least one type.");
|
|
396
|
+
const exp = expiry(upload.expiresIn);
|
|
397
|
+
const token = await signToken(await signingKey(), {
|
|
398
|
+
op: "put",
|
|
399
|
+
key: upload.key,
|
|
400
|
+
types: upload.contentTypes,
|
|
401
|
+
max: upload.maxBytes,
|
|
402
|
+
exp
|
|
403
|
+
});
|
|
404
|
+
return { url: urlFor(token), method: "PUT", key: upload.key, expiresAt: new Date(exp * 1e3) };
|
|
405
|
+
},
|
|
406
|
+
/** A temporary URL that serves the object. */
|
|
407
|
+
async createReadUrl(read) {
|
|
408
|
+
if (!isValidKey(read.key)) throw new Error(`Invalid object key: ${read.key}`);
|
|
409
|
+
const payload = { op: "get", key: read.key, exp: expiry(read.expiresIn) };
|
|
410
|
+
if (read.downloadAs) payload.name = read.downloadAs;
|
|
411
|
+
return urlFor(await signToken(await signingKey(), payload));
|
|
412
|
+
},
|
|
413
|
+
delete: (key) => options.bucket.delete(key),
|
|
414
|
+
/** Handles `PUT <routePath>?token=` (upload) and `GET <routePath>?token=` (read). */
|
|
415
|
+
async handleRequest(request) {
|
|
416
|
+
const response = await handle(request);
|
|
417
|
+
if (request.body && !request.bodyUsed) await discard(request.body);
|
|
418
|
+
return response;
|
|
419
|
+
}
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// src/billing.ts
|
|
424
|
+
var SUBSCRIPTION_STATUSES = [
|
|
425
|
+
"none",
|
|
426
|
+
"incomplete",
|
|
427
|
+
"trialing",
|
|
428
|
+
"active",
|
|
429
|
+
"past_due",
|
|
430
|
+
"paused",
|
|
431
|
+
"canceled",
|
|
432
|
+
"unpaid"
|
|
433
|
+
];
|
|
434
|
+
var LIVE = /* @__PURE__ */ new Set(["incomplete", "trialing", "active", "past_due", "paused"]);
|
|
435
|
+
var isLiveSubscription = (status) => LIVE.has(status ?? "none");
|
|
436
|
+
var grantsAccess = (status) => status === "active" || status === "trialing" || status === "past_due";
|
|
437
|
+
function toSubscriptionStatus(stripeStatus) {
|
|
438
|
+
if (stripeStatus === "incomplete_expired") return "canceled";
|
|
439
|
+
return SUBSCRIPTION_STATUSES.includes(stripeStatus) ? stripeStatus : "none";
|
|
440
|
+
}
|
|
441
|
+
function currentPeriodEnd(subscription) {
|
|
442
|
+
const ends = (subscription.items?.data ?? []).map((item) => item.current_period_end).filter((value) => typeof value === "number");
|
|
443
|
+
return ends.length ? Math.max(...ends) * 1e3 : null;
|
|
444
|
+
}
|
|
445
|
+
var subscriptionPriceId = (subscription) => subscription.items?.data?.[0]?.price?.id ?? null;
|
|
446
|
+
function subscriptionState(subscription) {
|
|
447
|
+
const status = toSubscriptionStatus(subscription.status);
|
|
448
|
+
const ended = subscription.ended_at ? subscription.ended_at * 1e3 : null;
|
|
449
|
+
const scheduled = subscription.cancel_at ? subscription.cancel_at * 1e3 : null;
|
|
450
|
+
const end = status === "canceled" ? ended ?? currentPeriodEnd(subscription) : scheduled ?? currentPeriodEnd(subscription);
|
|
451
|
+
return {
|
|
452
|
+
stripeSubscriptionId: subscription.id,
|
|
453
|
+
subscriptionStatus: status,
|
|
454
|
+
subscriptionEndsAt: end ? new Date(end) : null,
|
|
455
|
+
cancelAtPeriodEnd: Boolean(subscription.cancel_at_period_end) || scheduled !== null,
|
|
456
|
+
priceId: subscriptionPriceId(subscription)
|
|
457
|
+
};
|
|
458
|
+
}
|
|
459
|
+
function shouldApplySubscription(stored, incoming) {
|
|
460
|
+
if (!stored.stripeSubscriptionId || stored.stripeSubscriptionId === incoming.id) return true;
|
|
461
|
+
return isLiveSubscription(toSubscriptionStatus(incoming.status)) || !isLiveSubscription(stored.subscriptionStatus);
|
|
462
|
+
}
|
|
463
|
+
var checkoutPaid = (session) => session.payment_status !== "unpaid";
|
|
464
|
+
function randomLetters(length = 8) {
|
|
465
|
+
const bytes = crypto.getRandomValues(new Uint8Array(length));
|
|
466
|
+
return Array.from(bytes, (byte) => String.fromCharCode(97 + byte % 26)).join("");
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
// src/resource/fields.ts
|
|
470
|
+
var FILE_CATEGORIES = {
|
|
471
|
+
image: ["image/png", "image/jpeg", "image/gif", "image/webp", "image/avif"],
|
|
472
|
+
pdf: ["application/pdf"],
|
|
473
|
+
video: ["video/*"],
|
|
474
|
+
audio: ["audio/*"],
|
|
475
|
+
text: ["text/plain"],
|
|
476
|
+
csv: ["text/csv"],
|
|
477
|
+
document: [
|
|
478
|
+
"application/msword",
|
|
479
|
+
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
480
|
+
"application/vnd.oasis.opendocument.text"
|
|
481
|
+
],
|
|
482
|
+
spreadsheet: [
|
|
483
|
+
"application/vnd.ms-excel",
|
|
484
|
+
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
485
|
+
"application/vnd.oasis.opendocument.spreadsheet"
|
|
486
|
+
],
|
|
487
|
+
archive: ["application/zip"]
|
|
488
|
+
};
|
|
489
|
+
function mimeTypesFor(accept) {
|
|
490
|
+
return [...new Set(accept.flatMap((category) => FILE_CATEGORIES[category]))];
|
|
491
|
+
}
|
|
492
|
+
var DEFAULT_FILE_MAX_BYTES = 10 * 1024 * 1024;
|
|
493
|
+
var fileMaxBytes = (field2) => field2.maxBytes ?? DEFAULT_FILE_MAX_BYTES;
|
|
494
|
+
function build(kind, options, extra = {}) {
|
|
495
|
+
return { kind, ...extra, ...options, required: options?.required !== false };
|
|
496
|
+
}
|
|
497
|
+
var field = {
|
|
498
|
+
string: (options) => build("string", options),
|
|
499
|
+
text: (options) => build("text", options, { list: false, searchable: false }),
|
|
500
|
+
int: (options) => build("int", options),
|
|
501
|
+
float: (options) => build("float", options),
|
|
502
|
+
boolean: (options) => build("boolean", options),
|
|
503
|
+
date: (options) => build("date", options),
|
|
504
|
+
datetime: (options) => build("datetime", options),
|
|
505
|
+
enum: (values, options) => build("enum", options, { options: values }),
|
|
506
|
+
file: (accept, options) => build("file", options, { accept, list: false, sortable: false }),
|
|
507
|
+
belongsTo: (target, options) => build("belongsTo", options, { target }),
|
|
508
|
+
hasMany: (target, options) => ({ kind: "hasMany", target, ...options, required: false })
|
|
509
|
+
};
|
|
510
|
+
|
|
511
|
+
// src/resource/relations.ts
|
|
512
|
+
var RelationError = class extends Error {
|
|
513
|
+
constructor(message) {
|
|
514
|
+
super(message);
|
|
515
|
+
this.name = "RelationError";
|
|
516
|
+
}
|
|
517
|
+
};
|
|
518
|
+
var relationNameFor = (table, key) => `${table}_${columnName(key)}`;
|
|
519
|
+
function relationGraph(resources) {
|
|
520
|
+
const byName = new Map(resources.map((resource) => [resource.name, resource]));
|
|
521
|
+
const graph = { byResource: {}, pending: [] };
|
|
522
|
+
for (const resource of resources) {
|
|
523
|
+
const relations = { belongsTo: [], hasMany: [] };
|
|
524
|
+
for (const [key, def] of storedFields(resource)) {
|
|
525
|
+
if (def.kind !== "belongsTo") continue;
|
|
526
|
+
if (!byName.has(def.target)) {
|
|
527
|
+
throw new RelationError(`${resource.name}.${key} belongs to "${def.target}", but there is no ${def.target} resource.`);
|
|
528
|
+
}
|
|
529
|
+
relations.belongsTo.push({
|
|
530
|
+
key,
|
|
531
|
+
name: key.replace(/Id$/, ""),
|
|
532
|
+
target: def.target,
|
|
533
|
+
required: def.required,
|
|
534
|
+
onDelete: def.onDelete ?? "restrict",
|
|
535
|
+
relationName: relationNameFor(resource.table, key)
|
|
536
|
+
});
|
|
537
|
+
}
|
|
538
|
+
graph.byResource[resource.name] = relations;
|
|
539
|
+
}
|
|
540
|
+
for (const resource of resources) {
|
|
541
|
+
for (const [key, def] of Object.entries(resource.fields)) {
|
|
542
|
+
if (def.kind !== "hasMany") continue;
|
|
543
|
+
const target = byName.get(def.target);
|
|
544
|
+
if (!target) {
|
|
545
|
+
graph.pending.push({ resource: resource.name, key, target: def.target });
|
|
546
|
+
continue;
|
|
547
|
+
}
|
|
548
|
+
const foreignKey = def.foreignKey ?? `${camelCase(resource.name)}Id`;
|
|
549
|
+
const back = graph.byResource[target.name].belongsTo.find((relation) => relation.key === foreignKey);
|
|
550
|
+
if (!back || back.target !== resource.name) {
|
|
551
|
+
throw new RelationError(
|
|
552
|
+
`${resource.name}.${key} is hasMany(${target.name}), but ${target.name} has no ${foreignKey}: belongsTo(${resource.name}) field. Add it to ${target.name}, or set foreignKey to the field that points back.`
|
|
553
|
+
);
|
|
554
|
+
}
|
|
555
|
+
graph.byResource[resource.name].hasMany.push({ key, target: target.name, foreignKey, relationName: back.relationName });
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
for (const resource of resources) {
|
|
559
|
+
const { belongsTo, hasMany } = graph.byResource[resource.name];
|
|
560
|
+
const taken = new Set(Object.keys(resource.fields));
|
|
561
|
+
for (const { key, name } of belongsTo) {
|
|
562
|
+
if (taken.has(name)) {
|
|
563
|
+
throw new RelationError(`${resource.name}.${key} exposes relation "${name}", which collides with the ${resource.name}.${name} field. Rename one of them.`);
|
|
564
|
+
}
|
|
565
|
+
taken.add(name);
|
|
566
|
+
}
|
|
567
|
+
for (const { key } of hasMany) {
|
|
568
|
+
if (belongsTo.some((relation) => relation.name === key)) {
|
|
569
|
+
throw new RelationError(`${resource.name}.${key} collides with a belongsTo relation of the same name.`);
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
return graph;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
// src/resource/display.ts
|
|
577
|
+
var TONES = [
|
|
578
|
+
["success", /^(paid|fulfilled|active|approved|completed?|delivered|done|published|resolved|shipped|succeeded|success|verified|won)$/],
|
|
579
|
+
["danger", /^(banned|blocked|cancel+ed|churned|declined|error|expired|failed|lost|overdue|refunded|rejected|suspended)$/],
|
|
580
|
+
["warning", /^(awaiting|draft|in[-_ ]?review|on[-_ ]?hold|pending|processing|review|trial|unpaid|unverified)$/]
|
|
581
|
+
];
|
|
582
|
+
function statusTone(value) {
|
|
583
|
+
if (typeof value !== "string") return "neutral";
|
|
584
|
+
const normalized = value.trim().toLowerCase();
|
|
585
|
+
return TONES.find(([, pattern]) => pattern.test(normalized))?.[0] ?? "neutral";
|
|
586
|
+
}
|
|
587
|
+
function optionLabel(def, value) {
|
|
588
|
+
if (def.kind === "enum" && def.optionLabels?.[value]) return def.optionLabels[value];
|
|
589
|
+
const text2 = value.replace(/[_-]+/g, " ").trim();
|
|
590
|
+
return text2 ? text2[0].toUpperCase() + text2.slice(1) : value;
|
|
591
|
+
}
|
|
592
|
+
function formatValue(def, value, options = {}) {
|
|
593
|
+
if (value === null || value === void 0 || value === "") return "\u2014";
|
|
594
|
+
const locale = options.locale ?? "en-US";
|
|
595
|
+
switch (def.kind) {
|
|
596
|
+
case "boolean":
|
|
597
|
+
return value ? "Yes" : "No";
|
|
598
|
+
case "int":
|
|
599
|
+
return new Intl.NumberFormat(locale, { maximumFractionDigits: 0 }).format(Number(value));
|
|
600
|
+
case "float":
|
|
601
|
+
return new Intl.NumberFormat(locale, { maximumFractionDigits: 2 }).format(Number(value));
|
|
602
|
+
case "date": {
|
|
603
|
+
const date = /* @__PURE__ */ new Date(`${String(value)}T00:00:00Z`);
|
|
604
|
+
return Number.isNaN(date.getTime()) ? String(value) : new Intl.DateTimeFormat(locale, { dateStyle: "medium", timeZone: "UTC" }).format(date);
|
|
605
|
+
}
|
|
606
|
+
case "datetime":
|
|
607
|
+
case "timestamp": {
|
|
608
|
+
const date = value instanceof Date ? value : new Date(value);
|
|
609
|
+
return Number.isNaN(date.getTime()) ? String(value) : new Intl.DateTimeFormat(locale, { dateStyle: "medium", timeStyle: "short", timeZone: options.timeZone ?? "UTC" }).format(date);
|
|
610
|
+
}
|
|
611
|
+
case "enum":
|
|
612
|
+
return optionLabel(def, String(value));
|
|
613
|
+
case "file":
|
|
614
|
+
return String(value).split("/").pop().replace(/^[0-9a-f-]{36}-/, "");
|
|
615
|
+
default:
|
|
616
|
+
return String(value);
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
// src/resource/form.ts
|
|
621
|
+
function initialFormValues(resource, record) {
|
|
622
|
+
const values = {};
|
|
623
|
+
for (const [key, def] of storedFields(resource)) {
|
|
624
|
+
const source = record ? record[key] : "default" in def ? def.default : void 0;
|
|
625
|
+
if (def.kind === "boolean") values[key] = source === true || source === 1;
|
|
626
|
+
else if (source === null || source === void 0) values[key] = "";
|
|
627
|
+
else if (source instanceof Date) values[key] = source.toISOString();
|
|
628
|
+
else values[key] = String(source);
|
|
629
|
+
}
|
|
630
|
+
return values;
|
|
631
|
+
}
|
|
632
|
+
function formValuesToInput(resource, values, mode) {
|
|
633
|
+
const input = {};
|
|
634
|
+
for (const [key, def] of storedFields(resource)) {
|
|
635
|
+
const raw = values[key];
|
|
636
|
+
if (def.kind === "boolean") {
|
|
637
|
+
input[key] = raw === true;
|
|
638
|
+
continue;
|
|
639
|
+
}
|
|
640
|
+
const text2 = typeof raw === "string" ? raw : "";
|
|
641
|
+
const empty = text2.trim() === "";
|
|
642
|
+
if (empty) {
|
|
643
|
+
const hasDefault = "default" in def && def.default !== void 0;
|
|
644
|
+
if (mode === "create" && hasDefault) continue;
|
|
645
|
+
if (!def.required) input[key] = null;
|
|
646
|
+
else if (def.kind === "string" || def.kind === "text") input[key] = text2;
|
|
647
|
+
continue;
|
|
648
|
+
}
|
|
649
|
+
if (def.kind === "int" || def.kind === "float") {
|
|
650
|
+
const number = Number(text2.trim());
|
|
651
|
+
input[key] = Number.isFinite(number) ? number : text2;
|
|
652
|
+
continue;
|
|
653
|
+
}
|
|
654
|
+
input[key] = text2;
|
|
655
|
+
}
|
|
656
|
+
return input;
|
|
657
|
+
}
|
|
658
|
+
function issuesByField(issues) {
|
|
659
|
+
const errors = {};
|
|
660
|
+
for (const issue of issues) {
|
|
661
|
+
const path = Array.isArray(issue.path) ? issue.path.map(String).join(".") : issue.path;
|
|
662
|
+
const key = path.split(".")[0] || "_form";
|
|
663
|
+
errors[key] ??= issue.message;
|
|
664
|
+
}
|
|
665
|
+
return errors;
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
// src/resource/policy.ts
|
|
669
|
+
var ROLE = /^(\*|[a-z][a-z0-9_-]{0,31})$/;
|
|
670
|
+
var PolicyError = class extends Error {
|
|
671
|
+
constructor(message) {
|
|
672
|
+
super(message);
|
|
673
|
+
this.name = "PolicyError";
|
|
674
|
+
}
|
|
675
|
+
};
|
|
676
|
+
function definePolicy(config) {
|
|
677
|
+
if (!/^[A-Z][A-Za-z0-9]*$/.test(config.resource)) {
|
|
678
|
+
throw new PolicyError(`Policy resource "${config.resource}" must be a PascalCase resource name.`);
|
|
679
|
+
}
|
|
680
|
+
const clean = (action) => {
|
|
681
|
+
const roles = config[action] ?? [];
|
|
682
|
+
for (const role of roles) {
|
|
683
|
+
if (!ROLE.test(role)) throw new PolicyError(`Policy ${config.resource}.${action}: invalid role "${role}" (lowercase letters, digits, _ or -, or "*").`);
|
|
684
|
+
}
|
|
685
|
+
return [...new Set(roles)];
|
|
686
|
+
};
|
|
687
|
+
return { resource: config.resource, read: clean("read"), create: clean("create"), update: clean("update"), delete: clean("delete") };
|
|
688
|
+
}
|
|
689
|
+
function can(policy, role, action) {
|
|
690
|
+
if (!policy) return true;
|
|
691
|
+
const allowed = policy[action === "list" ? "read" : action];
|
|
692
|
+
if (allowed.includes("*")) return Boolean(role);
|
|
693
|
+
return Boolean(role) && allowed.includes(role);
|
|
694
|
+
}
|
|
695
|
+
function allowedActions(policy, role) {
|
|
696
|
+
return {
|
|
697
|
+
read: can(policy, role, "read"),
|
|
698
|
+
create: can(policy, role, "create"),
|
|
699
|
+
update: can(policy, role, "update"),
|
|
700
|
+
delete: can(policy, role, "delete")
|
|
701
|
+
};
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
// src/seed.ts
|
|
705
|
+
function defineSeed(seed) {
|
|
706
|
+
return seed;
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
// src/index.ts
|
|
710
|
+
var FLARE_VERSION = "0.1.0";
|
|
711
|
+
export {
|
|
712
|
+
DEFAULT_FILE_MAX_BYTES,
|
|
713
|
+
FILE_CATEGORIES,
|
|
714
|
+
FLARE_VERSION,
|
|
715
|
+
PBKDF2_ITERATIONS,
|
|
716
|
+
PolicyError,
|
|
717
|
+
RESERVED_FIELD_NAMES,
|
|
718
|
+
RelationError,
|
|
719
|
+
ResourceDefinitionError,
|
|
720
|
+
SNIFF_BYTES,
|
|
721
|
+
SUBSCRIPTION_STATUSES,
|
|
722
|
+
allowedActions,
|
|
723
|
+
camelCase,
|
|
724
|
+
can,
|
|
725
|
+
checkoutPaid,
|
|
726
|
+
columnName,
|
|
727
|
+
createMailer,
|
|
728
|
+
createObjectKey,
|
|
729
|
+
createStorage,
|
|
730
|
+
createValidators,
|
|
731
|
+
currentPeriodEnd,
|
|
732
|
+
defaultFieldLabel,
|
|
733
|
+
definePolicy,
|
|
734
|
+
defineResource,
|
|
735
|
+
defineSeed,
|
|
736
|
+
field,
|
|
737
|
+
fieldSchema,
|
|
738
|
+
fileKeyPrefix,
|
|
739
|
+
fileMaxBytes,
|
|
740
|
+
formValuesToInput,
|
|
741
|
+
formatValue,
|
|
742
|
+
grantsAccess,
|
|
743
|
+
hashPassword,
|
|
744
|
+
humanize,
|
|
745
|
+
importSigningKey,
|
|
746
|
+
initialFormValues,
|
|
747
|
+
isLiveSubscription,
|
|
748
|
+
issuesByField,
|
|
749
|
+
kebabCase,
|
|
750
|
+
matchesContentType,
|
|
751
|
+
mimeTypesFor,
|
|
752
|
+
optionLabel,
|
|
753
|
+
pascalCase,
|
|
754
|
+
pluralize,
|
|
755
|
+
randomLetters,
|
|
756
|
+
relationGraph,
|
|
757
|
+
renderTransactionalEmail,
|
|
758
|
+
shouldApplySubscription,
|
|
759
|
+
signToken,
|
|
760
|
+
snakeCase,
|
|
761
|
+
sniffMatches,
|
|
762
|
+
statusTone,
|
|
763
|
+
storedFields,
|
|
764
|
+
subscriptionPriceId,
|
|
765
|
+
subscriptionState,
|
|
766
|
+
toSubscriptionStatus,
|
|
767
|
+
verifyPassword,
|
|
768
|
+
verifyToken,
|
|
769
|
+
words
|
|
770
|
+
};
|