@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.
@@ -0,0 +1,438 @@
1
+ import { F as Field, C as CreateInput, U as UpdateInput, R as Resource, S as StoredField } from './define-D5CtCkOm.js';
2
+ export { B as BelongsToField, a as BooleanField, D as DEFAULT_FILE_MAX_BYTES, b as DateField, c as DateTimeField, E as EnumField, d as FILE_CATEGORIES, e as FieldKind, f as FieldValue, g as FileCategory, h as FileField, i as FloatField, H as HasManyField, I as IntField, j as RESERVED_FIELD_NAMES, k as RecordOf, l as ResourceConfig, m as ResourceDefinitionError, n as StringField, T as TextField, o as columnName, p as defaultFieldLabel, q as defineResource, r as field, s as fileMaxBytes, t as mimeTypesFor, u as storedFields } from './define-D5CtCkOm.js';
3
+ import { z } from 'zod';
4
+
5
+ /**
6
+ * Password hashing for Workers.
7
+ *
8
+ * Better Auth's default scrypt runs in pure JavaScript and costs ~250-300ms of
9
+ * CPU per hash, far over the Workers free-plan CPU limit. PBKDF2-SHA256 through
10
+ * WebCrypto runs natively and is several times cheaper. 100,000 iterations is
11
+ * the maximum Workers' WebCrypto accepts.
12
+ *
13
+ * Stored format: `pbkdf2-sha256$<iterations>$<base64 salt>$<base64 hash>`.
14
+ * The iteration count is read back from the stored hash, so it can be raised
15
+ * later without breaking existing passwords.
16
+ */
17
+ declare const PBKDF2_ITERATIONS = 100000;
18
+ declare function hashPassword(password: string): Promise<string>;
19
+ declare function verifyPassword({ hash, password }: {
20
+ hash: string;
21
+ password: string;
22
+ }): Promise<boolean>;
23
+
24
+ /**
25
+ * Import an HMAC key for one purpose. The purpose label is mixed in so the same
26
+ * app secret (e.g. BETTER_AUTH_SECRET) yields unrelated keys for unrelated uses.
27
+ */
28
+ declare function importSigningKey(secret: string, purpose: string): Promise<CryptoKey>;
29
+ declare function signToken(key: CryptoKey, payload: object): Promise<string>;
30
+ /** Returns the payload when the signature is valid, otherwise null. Does not check expiry. */
31
+ declare function verifyToken<T>(key: CryptoKey, token: string): Promise<T | null>;
32
+
33
+ /**
34
+ * Transactional email through Resend's REST API (plain fetch, no SDK).
35
+ *
36
+ * Like the official SDK, `send` never throws for API failures: it resolves to
37
+ * `{ data, error }`. Transient failures (429, 5xx, concurrent idempotent request)
38
+ * are retried with backoff, but only when an idempotency key is given, so a retry
39
+ * can never deliver a duplicate.
40
+ */
41
+ interface MailMessage {
42
+ to: string | string[];
43
+ subject: string;
44
+ html: string;
45
+ text?: string;
46
+ replyTo?: string | string[];
47
+ /** Overrides the mailer's default sender. */
48
+ from?: string;
49
+ /** e.g. `password-reset/<userId>/<tokenId>`. Required for retries; 1-256 chars; valid for 24h. */
50
+ idempotencyKey?: string;
51
+ }
52
+ type MailResult = {
53
+ data: {
54
+ id: string;
55
+ };
56
+ error: null;
57
+ } | {
58
+ data: null;
59
+ error: {
60
+ name: string;
61
+ message: string;
62
+ statusCode: number | null;
63
+ };
64
+ };
65
+ interface MailerOptions {
66
+ /** Resend API key. When empty, messages are logged instead of sent (local development). */
67
+ apiKey: string | undefined;
68
+ /** Default sender, e.g. `Acme <hello@acme.com>`. The domain must be verified in Resend. */
69
+ from: string;
70
+ /** Retries after the first attempt, for transient failures. Default 2. */
71
+ maxRetries?: number;
72
+ fetch?: typeof fetch;
73
+ /** Where logged (key-less) messages go. Default console.info. */
74
+ log?: (message: string) => void;
75
+ sleep?: (ms: number) => Promise<void>;
76
+ }
77
+ declare function createMailer(options: MailerOptions): {
78
+ send: (message: MailMessage) => Promise<MailResult>;
79
+ };
80
+ type Mailer = ReturnType<typeof createMailer>;
81
+ interface TransactionalEmail {
82
+ /** Product name shown above the heading. */
83
+ appName: string;
84
+ heading: string;
85
+ /** Plain-text paragraphs; HTML is escaped. */
86
+ paragraphs: string[];
87
+ action?: {
88
+ label: string;
89
+ url: string;
90
+ };
91
+ /** Small print under the content, e.g. "If you didn't request this, ignore this email." */
92
+ footer?: string;
93
+ /** Inbox preview text. */
94
+ preheader?: string;
95
+ }
96
+ /**
97
+ * A plain, monochrome transactional email (table layout with inline styles, which
98
+ * email clients need). Returns both HTML and text parts.
99
+ */
100
+ declare function renderTransactionalEmail(email: TransactionalEmail): {
101
+ html: string;
102
+ text: string;
103
+ };
104
+
105
+ /**
106
+ * Signed upload/read URLs for an R2 bucket binding.
107
+ *
108
+ * Bindings can't produce S3 presigned URLs, so Flare signs its own: a short-lived
109
+ * HMAC token scoped to one operation and one object key, redeemed by a Worker
110
+ * route (`app/api/storage/route.ts`) that streams through the binding. No R2 API
111
+ * credentials or bucket CORS are needed, and it behaves the same in local dev.
112
+ * Trade-off: bytes pass through the Worker, so uploads are capped by the Workers
113
+ * request body limit (100 MB on the free plan).
114
+ *
115
+ * Tokens authorize the holder, so only issue them after checking the current user
116
+ * may upload or read that key.
117
+ */
118
+ /** The subset of the R2Bucket binding this module uses (keeps core free of workers-types). */
119
+ interface StorageBucket {
120
+ get(key: string): Promise<StorageObjectBody | null>;
121
+ put(key: string, value: ReadableStream | ArrayBuffer | string | null, options?: {
122
+ httpMetadata?: {
123
+ contentType?: string;
124
+ contentDisposition?: string;
125
+ };
126
+ }): Promise<unknown>;
127
+ delete(keys: string | string[]): Promise<void>;
128
+ }
129
+ interface StorageObjectBody {
130
+ body: ReadableStream;
131
+ size: number;
132
+ httpEtag: string;
133
+ httpMetadata?: {
134
+ contentType?: string;
135
+ };
136
+ }
137
+ interface StorageOptions {
138
+ bucket: StorageBucket;
139
+ /** App secret the signing key is derived from (Flare apps pass BETTER_AUTH_SECRET). */
140
+ secret: string;
141
+ /** Path of the route that redeems tokens. Default "/api/storage". */
142
+ routePath?: string;
143
+ /** Prefix URLs with an origin (e.g. for emails). Default: relative URLs. */
144
+ baseURL?: string;
145
+ /** Default token lifetime in seconds. Default 900 (15 minutes). */
146
+ expiresIn?: number;
147
+ }
148
+ interface UploadUrlOptions {
149
+ key: string;
150
+ /** Allowed Content-Type values; "image/*" style wildcards are accepted. */
151
+ contentTypes: string[];
152
+ /** Maximum upload size in bytes. */
153
+ maxBytes: number;
154
+ expiresIn?: number;
155
+ }
156
+ interface ReadUrlOptions {
157
+ key: string;
158
+ expiresIn?: number;
159
+ /** Serve as an attachment with this filename instead of inline. */
160
+ downloadAs?: string;
161
+ }
162
+ declare function matchesContentType(contentType: string, allowed: string[]): boolean;
163
+ /** A collision-free object key, e.g. `uploads/2026/09/3f1c…-quarterly-report.pdf`. */
164
+ declare function createObjectKey(filename: string, prefix?: string): string;
165
+ declare function createStorage(options: StorageOptions): {
166
+ bucket: StorageBucket;
167
+ /** A URL the browser can PUT the file body to, with the `Content-Type` header set. */
168
+ createUploadUrl(upload: UploadUrlOptions): Promise<{
169
+ url: string;
170
+ method: "PUT";
171
+ key: string;
172
+ expiresAt: Date;
173
+ }>;
174
+ /** A temporary URL that serves the object. */
175
+ createReadUrl(read: ReadUrlOptions): Promise<string>;
176
+ delete: (key: string) => Promise<void>;
177
+ /** Handles `PUT <routePath>?token=` (upload) and `GET <routePath>?token=` (read). */
178
+ handleRequest(request: Request): Promise<Response>;
179
+ };
180
+ type Storage = ReturnType<typeof createStorage>;
181
+
182
+ /**
183
+ * Content sniffing for uploads: does a file's leading bytes match the type it claims?
184
+ *
185
+ * A browser (or anyone with a signed URL) chooses the Content-Type it sends, so a
186
+ * `file:[image]` field can't rely on the label alone. These checks look at the
187
+ * signature every real file of the type starts with. Used on the server as the
188
+ * gate, and in the admin's file widget for instant feedback.
189
+ */
190
+ /** Bytes needed from the start of a file for every check below. */
191
+ declare const SNIFF_BYTES = 32;
192
+ /**
193
+ * Whether `head` (the first {@link SNIFF_BYTES} bytes, or the whole file when it's
194
+ * shorter) looks like `contentType`.
195
+ *
196
+ * Returns `undefined` for types with no known signature, so custom types passed to
197
+ * `createUploadUrl` keep working; `false` means the bytes contradict the label.
198
+ */
199
+ declare function sniffMatches(contentType: string, head: Uint8Array): boolean | undefined;
200
+
201
+ /**
202
+ * Billing decisions shared by the code `flare gen billing` writes into an app.
203
+ *
204
+ * Kept here, free of the Stripe SDK and the database, so the rules that decide
205
+ * what a customer has paid for are unit-tested once instead of living only in
206
+ * generated templates. The shapes below are the minimal subset of Stripe's
207
+ * objects the rules read.
208
+ */
209
+ /** Subscription states a Customer row can hold. */
210
+ declare const SUBSCRIPTION_STATUSES: readonly ["none", "incomplete", "trialing", "active", "past_due", "paused", "canceled", "unpaid"];
211
+ type SubscriptionStatus = (typeof SUBSCRIPTION_STATUSES)[number];
212
+ /** Whether a status still counts as "has a subscription" (so a second one must not be started). */
213
+ declare const isLiveSubscription: (status: string | null | undefined) => boolean;
214
+ /** Whether a status grants access to paid features. past_due keeps access while Stripe retries. */
215
+ declare const grantsAccess: (status: string | null | undefined) => boolean;
216
+ /** Map a Stripe subscription status onto the app's statuses. */
217
+ declare function toSubscriptionStatus(stripeStatus: string): SubscriptionStatus;
218
+ /** The fields of a Stripe Subscription these rules read. */
219
+ interface SubscriptionLike {
220
+ id: string;
221
+ status: string;
222
+ created: number;
223
+ cancel_at_period_end?: boolean | null;
224
+ cancel_at?: number | null;
225
+ ended_at?: number | null;
226
+ items?: {
227
+ data?: Array<{
228
+ current_period_end?: number | null;
229
+ price?: {
230
+ id: string;
231
+ } | null;
232
+ }>;
233
+ } | null;
234
+ }
235
+ /**
236
+ * When the current paid period ends, in ms. On API versions from 2025-03-31
237
+ * (basil) onward the period lives on subscription items, not the subscription.
238
+ */
239
+ declare function currentPeriodEnd(subscription: SubscriptionLike): number | null;
240
+ /** The subscription's (first) price, which maps it to a Plan row. */
241
+ declare const subscriptionPriceId: (subscription: SubscriptionLike) => string | null;
242
+ /** The customer-facing subscription state, derived from a freshly fetched Subscription. */
243
+ interface SubscriptionState {
244
+ stripeSubscriptionId: string;
245
+ subscriptionStatus: SubscriptionStatus;
246
+ /** Renewal date while active; the date access ends when cancelling or ended. */
247
+ subscriptionEndsAt: Date | null;
248
+ cancelAtPeriodEnd: boolean;
249
+ priceId: string | null;
250
+ }
251
+ declare function subscriptionState(subscription: SubscriptionLike): SubscriptionState;
252
+ /**
253
+ * Whether a subscription's state may overwrite what a Customer row holds.
254
+ *
255
+ * Handlers always re-fetch the subscription from Stripe, so the state they write
256
+ * is current and order no longer matters for a single subscription. What remains
257
+ * is a customer with more than one: a late event about an old, ended subscription
258
+ * must not replace a newer live one. So a different subscription only takes over
259
+ * when it is live itself, or when the stored one no longer is.
260
+ */
261
+ declare function shouldApplySubscription(stored: {
262
+ stripeSubscriptionId: string | null;
263
+ subscriptionStatus: string | null;
264
+ }, incoming: SubscriptionLike): boolean;
265
+ /** A Checkout Session's payment is settled (card now, or a delayed method that has succeeded). */
266
+ declare const checkoutPaid: (session: {
267
+ payment_status: string;
268
+ }) => boolean;
269
+ /** `8 random lowercase letters`, the suffix Stripe asks for on `integration_identifier`. */
270
+ declare function randomLetters(length?: number): string;
271
+
272
+ /**
273
+ * Zod schemas derived from a descriptor at runtime. Unknown keys are rejected,
274
+ * so clients can't set `id`, timestamps or undeclared columns (no mass assignment).
275
+ * Messages are written for people (they're shown next to form fields and returned by
276
+ * the REST API).
277
+ */
278
+ interface ResourceValidators<Fields extends Record<string, Field>> {
279
+ create: z.ZodType<CreateInput<Fields>>;
280
+ update: z.ZodType<UpdateInput<Fields>>;
281
+ }
282
+ declare function fieldSchema(def: StoredField): z.ZodType;
283
+ /**
284
+ * Where a file field's uploads live: `<table>/<field>/`. The admin issues upload URLs
285
+ * under this prefix, and a record only accepts keys under it — otherwise anyone who can
286
+ * edit a record could attach another resource's private file and read it through theirs.
287
+ */
288
+ declare const fileKeyPrefix: (resource: Pick<Resource, "table">, fieldKey: string) => string;
289
+ declare function createValidators<Fields extends Record<string, Field>>(resource: Resource<Fields>): ResourceValidators<Fields>;
290
+
291
+ /** Naming helpers shared by descriptors and the generator. */
292
+ /** "OrderItem" / "order item" / "order-item" → ["order", "item"] */
293
+ declare function words(value: string): string[];
294
+ declare const pascalCase: (value: string) => string;
295
+ declare const camelCase: (value: string) => string;
296
+ declare const snakeCase: (value: string) => string;
297
+ declare const kebabCase: (value: string) => string;
298
+ /** "orderItem" → "Order item" */
299
+ declare const humanize: (value: string) => string;
300
+ /** English plural of the last word: "category" → "categories", "OrderItem" → "OrderItems". */
301
+ declare function pluralize(value: string): string;
302
+
303
+ interface BelongsToRelation {
304
+ /** Field key on this resource, e.g. "companyId". */
305
+ key: string;
306
+ /** Relation accessor name, e.g. "company". */
307
+ name: string;
308
+ target: string;
309
+ required: boolean;
310
+ onDelete: "cascade" | "restrict" | "set null";
311
+ /** Stable name shared by both sides (Drizzle `relationName`). */
312
+ relationName: string;
313
+ }
314
+ interface HasManyRelation {
315
+ key: string;
316
+ target: string;
317
+ /** belongsTo key on the target pointing back here. */
318
+ foreignKey: string;
319
+ relationName: string;
320
+ }
321
+ interface ResourceRelations {
322
+ belongsTo: BelongsToRelation[];
323
+ hasMany: HasManyRelation[];
324
+ }
325
+ interface RelationGraph {
326
+ byResource: Record<string, ResourceRelations>;
327
+ /** hasMany declarations whose target resource doesn't exist yet. */
328
+ pending: {
329
+ resource: string;
330
+ key: string;
331
+ target: string;
332
+ }[];
333
+ }
334
+ declare class RelationError extends Error {
335
+ constructor(message: string);
336
+ }
337
+ /**
338
+ * Resolve belongsTo/hasMany across all resources. Throws for belongsTo targets that
339
+ * don't exist and for hasMany declarations without a matching belongsTo on the target.
340
+ */
341
+ declare function relationGraph(resources: readonly Resource[]): RelationGraph;
342
+
343
+ /** Semantic tone for a status-like enum value (style-guide status colors). */
344
+ type Tone = "success" | "warning" | "danger" | "neutral";
345
+ declare function statusTone(value: unknown): Tone;
346
+ /** Display label for an enum option: the descriptor's optionLabels, else "on_hold" → "On hold". */
347
+ declare function optionLabel(def: StoredField, value: string): string;
348
+ interface FormatOptions {
349
+ locale?: string;
350
+ timeZone?: string;
351
+ }
352
+ /**
353
+ * A field value as short display text (tables, detail views). Relations and files are
354
+ * returned as-is (their ids/keys); callers resolve those to titles or links.
355
+ */
356
+ declare function formatValue(def: StoredField | {
357
+ kind: "timestamp";
358
+ }, value: unknown, options?: FormatOptions): string;
359
+
360
+ /**
361
+ * Form state for a resource: strings for text-like inputs, booleans for toggles.
362
+ * These helpers convert between records, form state, and API input so every admin
363
+ * form handles empty values, numbers, and defaults the same way.
364
+ */
365
+ type FormValues = Record<string, string | boolean>;
366
+ /** Initial form state: the record's values (edit) or the descriptor's defaults (create). */
367
+ declare function initialFormValues(resource: Resource, record?: Record<string, unknown> | null): FormValues;
368
+ /**
369
+ * API input from form state. Empty optional fields become null (clearing them);
370
+ * empty fields that have a default are omitted on create so the default applies;
371
+ * empty required fields are left for validation to report as "Required".
372
+ */
373
+ declare function formValuesToInput(resource: Resource, values: FormValues, mode: "create" | "edit"): Record<string, unknown>;
374
+ /** Field errors keyed by field name, from validation or API issues (first message wins). */
375
+ declare function issuesByField(issues: {
376
+ path: string | PropertyKey[];
377
+ message: string;
378
+ }[]): Record<string, string>;
379
+
380
+ /**
381
+ * Resource-level policies: which roles may read, create, update, or delete a resource.
382
+ *
383
+ * Policies are plain data, like descriptors, so the API layer and the admin UI read the
384
+ * same rules at runtime. Field-level rules and per-record ownership are deliberately out
385
+ * of scope for v1.
386
+ */
387
+ type PolicyAction = "list" | "read" | "create" | "update" | "delete";
388
+ /** Roles allowed per action; `"*"` means any signed-in user. */
389
+ interface Policy {
390
+ resource: string;
391
+ read: string[];
392
+ create: string[];
393
+ update: string[];
394
+ delete: string[];
395
+ }
396
+ interface PolicyConfig {
397
+ resource: string;
398
+ read?: string[];
399
+ create?: string[];
400
+ update?: string[];
401
+ delete?: string[];
402
+ }
403
+ declare class PolicyError extends Error {
404
+ constructor(message: string);
405
+ }
406
+ declare function definePolicy(config: PolicyConfig): Policy;
407
+ /**
408
+ * Whether `role` may perform `action`. Without a policy any signed-in user is allowed
409
+ * (callers must already have required a session); generate one with
410
+ * `flare gen policy <Resource> --roles …` to restrict a resource.
411
+ */
412
+ declare function can(policy: Policy | undefined | null, role: string | null | undefined, action: PolicyAction): boolean;
413
+ /** Actions `role` may perform, for hiding UI it can't use. */
414
+ declare function allowedActions(policy: Policy | undefined | null, role: string | null | undefined): Record<Exclude<PolicyAction, "list">, boolean>;
415
+
416
+ /**
417
+ * Seed files: `seeds/<name>.seed.ts`, run by `flare seed` against the local D1 database.
418
+ *
419
+ * import { defineSeed } from "@flaredev/core";
420
+ * import { contacts } from "@/db/schema";
421
+ *
422
+ * export default defineSeed(async ({ db }) => {
423
+ * await db.insert(contacts).values([{ name: "Ada", email: "ada@example.com" }]);
424
+ * });
425
+ */
426
+ interface SeedContext {
427
+ /** Drizzle client over the app's schema, bound to the local D1 database. */
428
+ db: any;
429
+ /** All local bindings and variables (D1, R2, .dev.vars). */
430
+ env: Record<string, unknown>;
431
+ log: (message: string) => void;
432
+ }
433
+ type Seed = (context: SeedContext) => void | Promise<void>;
434
+ declare function defineSeed(seed: Seed): Seed;
435
+
436
+ declare const FLARE_VERSION = "0.1.0";
437
+
438
+ export { type BelongsToRelation, CreateInput, FLARE_VERSION, Field, type FormValues, type FormatOptions, type HasManyRelation, type MailMessage, type MailResult, type Mailer, type MailerOptions, PBKDF2_ITERATIONS, type Policy, type PolicyAction, type PolicyConfig, PolicyError, type ReadUrlOptions, RelationError, type RelationGraph, Resource, type ResourceRelations, type ResourceValidators, SNIFF_BYTES, SUBSCRIPTION_STATUSES, type Seed, type SeedContext, type Storage, type StorageBucket, type StorageOptions, StoredField, type SubscriptionLike, type SubscriptionState, type SubscriptionStatus, type Tone, type TransactionalEmail, UpdateInput, type UploadUrlOptions, allowedActions, camelCase, can, checkoutPaid, createMailer, createObjectKey, createStorage, createValidators, currentPeriodEnd, definePolicy, defineSeed, fieldSchema, fileKeyPrefix, formValuesToInput, formatValue, grantsAccess, hashPassword, humanize, importSigningKey, initialFormValues, isLiveSubscription, issuesByField, kebabCase, matchesContentType, optionLabel, pascalCase, pluralize, randomLetters, relationGraph, renderTransactionalEmail, shouldApplySubscription, signToken, snakeCase, sniffMatches, statusTone, subscriptionPriceId, subscriptionState, toSubscriptionStatus, verifyPassword, verifyToken, words };