@flaredev/core 0.11.0 → 0.12.1
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/chunk-7NQIVWGG.js +68 -0
- package/dist/{chunk-62JI5DCR.js → chunk-BTVEH5AU.js} +342 -8
- package/dist/chunk-I2HSIPCF.js +65 -0
- package/dist/index.d.ts +121 -5
- package/dist/index.js +71 -217
- package/dist/{policy-DaK3dWQ_.d.ts → policy-b4FXN3ZJ.d.ts} +147 -2
- package/dist/rate-limit/server.d.ts +44 -0
- package/dist/rate-limit/server.js +45 -0
- package/dist/rate-limit-ClbdZioJ.d.ts +97 -0
- package/dist/security/worker.js +13 -5
- package/dist/server.d.ts +69 -62
- package/dist/server.js +116 -37
- package/package.json +5 -1
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
export { R as Rate, b as RateLimitConfig, a as RateLimitDecision, c as RateLimitStore, d as RateLimitedAction, e as RateSyntaxError, f as fixedWindow, p as parseRate, r as rateFor, g as rateLimitKey, h as ratesIn } from './rate-limit-ClbdZioJ.js';
|
|
2
|
+
import { F as Field, C as CreateInput, U as UpdateInput, R as Resource, S as StoredField } from './policy-b4FXN3ZJ.js';
|
|
3
|
+
export { B as BelongsToField, f as BooleanField, g as ClientResource, D as DEFAULT_FILE_MAX_BYTES, h as DateField, i as DateTimeField, E as EnumField, j as FILE_CATEGORIES, k as FieldCondition, l as FieldKind, m as FieldValue, n as FileCategory, o as FileField, q as FloatField, H as HasManyField, I as ISSUE_CODES, r as IntField, c as IssueCode, J as JsonField, s as JsonOptions, M as MultiSelectField, N as NUMBER_FORMATS, t as NumberFormat, d as PROBLEM_CODES, b as Policy, u as PolicyAction, v as PolicyConfig, x as PolicyError, y as PolicyOwnership, e as ProblemBody, P as ProblemCode, a as ProblemIssue, z as RESERVED_FIELD_NAMES, A as RecordOf, G as ResourceConfig, K as ResourceDefinitionError, L as STRING_FORMATS, O as StringField, Q as StringFormat, T as TEXT_FORMATS, V as TagsField, W as TagsOptions, X as TextField, Y as TextFormat, Z as allowedActions, _ as can, $ as clientResource, a0 as columnName, a1 as conditionHolds, a2 as defaultFieldLabel, a3 as definePolicy, a4 as defineResource, a5 as describeCondition, a6 as field, a7 as fileMaxBytes, a8 as mimeTypesFor, a9 as ownershipFilter, p as problemResponse, aa as storedFields, w as wantsJson } from './policy-b4FXN3ZJ.js';
|
|
3
4
|
import { z } from 'zod';
|
|
4
5
|
|
|
5
6
|
/**
|
|
@@ -141,6 +142,19 @@ interface StorageBucket {
|
|
|
141
142
|
truncated: boolean;
|
|
142
143
|
cursor?: string;
|
|
143
144
|
}>;
|
|
145
|
+
/**
|
|
146
|
+
* A URL that serves this object directly, for a provider that is already a CDN.
|
|
147
|
+
*
|
|
148
|
+
* Returning one makes `createReadUrl` hand it back instead of a signed, expiring URL
|
|
149
|
+
* through this app — which is the point of Cloudinary or UploadThing, and the reason
|
|
150
|
+
* to pay for them. It also means the file is readable by anyone who has the link, for
|
|
151
|
+
* as long as it exists: there is no token to expire and no policy consulted on the way
|
|
152
|
+
* in. Buckets that hold anything private leave this undefined, and R2 and disk do.
|
|
153
|
+
*
|
|
154
|
+
* Return `undefined` for a key this bucket cannot address publicly, and the signed
|
|
155
|
+
* path is used for it instead.
|
|
156
|
+
*/
|
|
157
|
+
publicUrl?(key: string): string | undefined;
|
|
144
158
|
}
|
|
145
159
|
interface StorageObjectBody {
|
|
146
160
|
body: ReadableStream;
|
|
@@ -187,7 +201,13 @@ declare function createStorage(options: StorageOptions): {
|
|
|
187
201
|
key: string;
|
|
188
202
|
expiresAt: Date;
|
|
189
203
|
}>;
|
|
190
|
-
/**
|
|
204
|
+
/**
|
|
205
|
+
* A temporary URL that serves the object.
|
|
206
|
+
*
|
|
207
|
+
* On a bucket that is its own CDN, this is that provider's URL and is neither
|
|
208
|
+
* temporary nor private — see {@link StorageBucket.publicUrl}. `downloadAs` still
|
|
209
|
+
* takes the signed path, because forcing a filename is this app's header to set.
|
|
210
|
+
*/
|
|
191
211
|
createReadUrl(read: ReadUrlOptions): Promise<string>;
|
|
192
212
|
delete: (key: string) => Promise<void>;
|
|
193
213
|
/** Handles `PUT <routePath>?token=` (upload) and `GET <routePath>?token=` (read). */
|
|
@@ -331,6 +351,79 @@ declare function fromMinorUnits(minor: number, currency?: string): number;
|
|
|
331
351
|
*/
|
|
332
352
|
declare function hasTooManyPlaces(amount: number, currency?: string): boolean;
|
|
333
353
|
|
|
354
|
+
/**
|
|
355
|
+
* Turning an enum option into a Prisma enum member name, and back.
|
|
356
|
+
*
|
|
357
|
+
* In core rather than in the CLI because three places need the same rule and they must
|
|
358
|
+
* not drift: the Prisma schema generator writes the members, the seed writer writes
|
|
359
|
+
* values through them, and the app's own Prisma store has to translate in both
|
|
360
|
+
* directions at runtime.
|
|
361
|
+
*
|
|
362
|
+
* A Prisma enum member has to be an identifier, so an option that is not one is renamed
|
|
363
|
+
* and the real value is kept with `@map`. That mapping applies to the database column,
|
|
364
|
+
* not to the client: Prisma's generated client speaks member names, so `A+` stored in
|
|
365
|
+
* Postgres arrives in JavaScript as `A_2b`. Translating at the store boundary is what
|
|
366
|
+
* keeps that out of the rest of the app — the point of the exercise is that the user
|
|
367
|
+
* sees `A+`.
|
|
368
|
+
*/
|
|
369
|
+
/**
|
|
370
|
+
* The identifier a Prisma enum member gets. `in-progress` can't be one, so it becomes
|
|
371
|
+
* `in_progress`, and `A+` becomes `A_2b`, each with the real value in `@map`.
|
|
372
|
+
*
|
|
373
|
+
* Anything going through the Prisma client — `createMany` in a seed, a `where` clause —
|
|
374
|
+
* has to use this name rather than the value stored in the column, so it is exported
|
|
375
|
+
* and shared rather than written twice.
|
|
376
|
+
*
|
|
377
|
+
* `-` and a space become `_` rather than `_2d` and `_20`. That is not the tidiest rule,
|
|
378
|
+
* but it is the right one: both were reachable long before the rest of the punctuation
|
|
379
|
+
* was — `-` through the field grammar, a space through a hand-written descriptor — and
|
|
380
|
+
* escaping them now would quietly rename `in_progress` in every schema already
|
|
381
|
+
* generated, breaking every `where` clause written against it.
|
|
382
|
+
*
|
|
383
|
+
* Everything else is escaped by code point, which is what stops two options flattening
|
|
384
|
+
* onto one member: `A+` and `A-` both became `A_` before this, and a Prisma schema that
|
|
385
|
+
* declares the same member twice does not generate at all. The separators can still
|
|
386
|
+
* collide with each other (`in progress` against `in-progress`), so callers check with
|
|
387
|
+
* `enumMemberCollision` rather than trusting this to be injective.
|
|
388
|
+
*/
|
|
389
|
+
declare function prismaEnumMember(option: string): string;
|
|
390
|
+
/**
|
|
391
|
+
* The first pair of options that would end up as the same member name, if there is one.
|
|
392
|
+
*
|
|
393
|
+
* Escaping by code point rules out the collisions that come up in practice, but not all
|
|
394
|
+
* of them: `in progress` and `in-progress` both fold to `in_progress`, an option spelled
|
|
395
|
+
* literally `A_2b` collides with `A+`, and `A_` with `A-`. Rare, and worth catching
|
|
396
|
+
* anyway — the alternative is a Prisma schema that declares one member twice and fails to
|
|
397
|
+
* generate, with an error that says nothing about which two options caused it.
|
|
398
|
+
*/
|
|
399
|
+
declare function enumMemberCollision(options: readonly string[]): {
|
|
400
|
+
a: string;
|
|
401
|
+
b: string;
|
|
402
|
+
member: string;
|
|
403
|
+
} | undefined;
|
|
404
|
+
/**
|
|
405
|
+
* The two lookups a store needs to translate a resource's enum values, by field.
|
|
406
|
+
*
|
|
407
|
+
* Built once per resource rather than per row. `toMember` is used on the way in — to
|
|
408
|
+
* Prisma — and `toValue` on the way out; a field whose options are all identifiers
|
|
409
|
+
* already is left out of both, so the common case costs nothing.
|
|
410
|
+
*/
|
|
411
|
+
interface EnumTranslation {
|
|
412
|
+
toMember: Record<string, string>;
|
|
413
|
+
toValue: Record<string, string>;
|
|
414
|
+
}
|
|
415
|
+
/**
|
|
416
|
+
* Translation tables for every enum field whose options are not already identifiers.
|
|
417
|
+
*
|
|
418
|
+
* `fields` is the resource's stored fields, as `storedFields()` gives them. Taking that
|
|
419
|
+
* rather than a `Resource` keeps this usable from a store that was handed a descriptor
|
|
420
|
+
* and from the generator, which works field by field.
|
|
421
|
+
*/
|
|
422
|
+
declare function enumTranslations(fields: Iterable<readonly [string, {
|
|
423
|
+
kind: string;
|
|
424
|
+
options?: readonly string[];
|
|
425
|
+
}]>): Record<string, EnumTranslation>;
|
|
426
|
+
|
|
334
427
|
/**
|
|
335
428
|
* Zod schemas derived from a descriptor at runtime. Unknown keys are rejected,
|
|
336
429
|
* so clients can't set `id`, timestamps or undeclared columns (no mass assignment).
|
|
@@ -396,6 +489,29 @@ interface RelationGraph {
|
|
|
396
489
|
declare class RelationError extends Error {
|
|
397
490
|
constructor(message: string);
|
|
398
491
|
}
|
|
492
|
+
/**
|
|
493
|
+
* The one relation target that is not a generated resource.
|
|
494
|
+
*
|
|
495
|
+
* Better Auth owns the account table — `user` in Drizzle, `User` in Prisma — and
|
|
496
|
+
* generating a `User` resource to point at would create a second, competing user table.
|
|
497
|
+
* So `belongsTo("User")` resolves to the auth table instead, which is what makes "who
|
|
498
|
+
* reported it" a relation like any other rather than a bare string column.
|
|
499
|
+
*/
|
|
500
|
+
declare const AUTH_USER = "User";
|
|
501
|
+
/** Whether a belongsTo target is Better Auth's account table rather than a resource. */
|
|
502
|
+
declare const isAuthUserTarget: (target: string) => target is "User";
|
|
503
|
+
/**
|
|
504
|
+
* What `onDelete` a relation to the auth user gets when the descriptor doesn't say.
|
|
505
|
+
*
|
|
506
|
+
* `set null` when the field is optional, which is the case worth defaulting for: a
|
|
507
|
+
* person who closes their account should not be held open by a lost-property report
|
|
508
|
+
* from two years ago, and the report itself usually wants to outlive them.
|
|
509
|
+
*
|
|
510
|
+
* `restrict` when the field is required, because there is nothing else it could be — a
|
|
511
|
+
* required column cannot be set to null, and silently deleting the record instead
|
|
512
|
+
* (`cascade`) would destroy data the user never asked to lose.
|
|
513
|
+
*/
|
|
514
|
+
declare const defaultAuthUserOnDelete: (required: boolean) => "restrict" | "set null";
|
|
399
515
|
/**
|
|
400
516
|
* Resolve belongsTo/hasMany across all resources. Throws for belongsTo targets that
|
|
401
517
|
* don't exist and for hasMany declarations without a matching belongsTo on the target.
|
|
@@ -626,6 +742,6 @@ type SeedUser = (user: SeedUserInput) => Promise<CredentialUserRows["user"]>;
|
|
|
626
742
|
type Seed = (context: SeedContext) => void | Promise<void>;
|
|
627
743
|
declare function defineSeed(seed: Seed): Seed;
|
|
628
744
|
|
|
629
|
-
declare const FLARE_VERSION = "0.
|
|
745
|
+
declare const FLARE_VERSION = "0.12.1";
|
|
630
746
|
|
|
631
|
-
export { type BelongsToRelation, type Country, CreateInput, type CredentialUserRows, DEFAULT_MONEY_EXPONENT, FLARE_VERSION, type Fake, Field, type FormValues, type FormatOptions, type HasManyRelation, type InsertMany, type MailMessage, type MailResult, type Mailer, type MailerOptions, PBKDF2_ITERATIONS, type ReadUrlOptions, RelationError, type RelationGraph, Resource, type ResourceRelations, type ResourceValidators, SNIFF_BYTES, SUBSCRIPTION_STATUSES, type Seed, type SeedContext, type SeedUser, type SeedUserInput, type Storage, type StorageBucket, type StorageOptions, StoredField, type SubscriptionLike, type SubscriptionState, type SubscriptionStatus, type Tone, type TransactionalEmail, UpdateInput, type UploadUrlOptions, camelCase, checkoutPaid, countries, countryName, createFake, createMailer, createObjectKey, createStorage, createValidators, credentialUserRows, currentPeriodEnd, defineSeed, fake, fieldSchema, fileKeyPrefix, flagEmoji, formValuesToInput, formatPhone, formatValue, fromMinorUnits, grantsAccess, hasTooManyPlaces, hashPassword, humanize, importSigningKey, initialFormValues, isColor, isCountryCode, isDomain, isLiveSubscription, isPhoneNumber, isSlug, issuesByField, kebabCase, matchesContentType, moneyExponent, normalizeDomain, optionLabel, parseMultiValue, pascalCase, pluralize, randomLetters, relationGraph, renderTransactionalEmail, shouldApplySubscription, signToken, slugify, snakeCase, sniffMatches, splitPhone, statusTone, subscriptionPriceId, subscriptionState, toE164, toMinorUnits, toSubscriptionStatus, verifyPassword, verifyToken, words };
|
|
747
|
+
export { AUTH_USER, type BelongsToRelation, type Country, CreateInput, type CredentialUserRows, DEFAULT_MONEY_EXPONENT, type EnumTranslation, FLARE_VERSION, type Fake, Field, type FormValues, type FormatOptions, type HasManyRelation, type InsertMany, type MailMessage, type MailResult, type Mailer, type MailerOptions, PBKDF2_ITERATIONS, type ReadUrlOptions, RelationError, type RelationGraph, Resource, type ResourceRelations, type ResourceValidators, SNIFF_BYTES, SUBSCRIPTION_STATUSES, type Seed, type SeedContext, type SeedUser, type SeedUserInput, type Storage, type StorageBucket, type StorageObjectBody, type StorageOptions, StoredField, type SubscriptionLike, type SubscriptionState, type SubscriptionStatus, type Tone, type TransactionalEmail, UpdateInput, type UploadUrlOptions, camelCase, checkoutPaid, countries, countryName, createFake, createMailer, createObjectKey, createStorage, createValidators, credentialUserRows, currentPeriodEnd, defaultAuthUserOnDelete, defineSeed, enumMemberCollision, enumTranslations, fake, fieldSchema, fileKeyPrefix, flagEmoji, formValuesToInput, formatPhone, formatValue, fromMinorUnits, grantsAccess, hasTooManyPlaces, hashPassword, humanize, importSigningKey, initialFormValues, isAuthUserTarget, isColor, isCountryCode, isDomain, isLiveSubscription, isPhoneNumber, isSlug, issuesByField, kebabCase, matchesContentType, moneyExponent, normalizeDomain, optionLabel, parseMultiValue, pascalCase, pluralize, prismaEnumMember, randomLetters, relationGraph, renderTransactionalEmail, shouldApplySubscription, signToken, slugify, snakeCase, sniffMatches, splitPhone, statusTone, subscriptionPriceId, subscriptionState, toE164, toMinorUnits, toSubscriptionStatus, verifyPassword, verifyToken, words };
|
package/dist/index.js
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import {
|
|
2
|
+
AUTH_USER,
|
|
2
3
|
DEFAULT_FILE_MAX_BYTES,
|
|
3
4
|
DEFAULT_MONEY_EXPONENT,
|
|
4
5
|
FILE_CATEGORIES,
|
|
5
6
|
NUMBER_FORMATS,
|
|
6
7
|
PolicyError,
|
|
7
8
|
RESERVED_FIELD_NAMES,
|
|
9
|
+
RelationError,
|
|
8
10
|
ResourceDefinitionError,
|
|
9
11
|
STRING_FORMATS,
|
|
10
12
|
TEXT_FORMATS,
|
|
@@ -17,39 +19,66 @@ import {
|
|
|
17
19
|
countries,
|
|
18
20
|
countryName,
|
|
19
21
|
createValidators,
|
|
22
|
+
defaultAuthUserOnDelete,
|
|
20
23
|
defaultFieldLabel,
|
|
21
24
|
definePolicy,
|
|
22
25
|
defineResource,
|
|
23
26
|
describeCondition,
|
|
27
|
+
enumMemberCollision,
|
|
28
|
+
enumTranslations,
|
|
24
29
|
field,
|
|
25
30
|
fieldSchema,
|
|
26
31
|
fileKeyPrefix,
|
|
27
32
|
fileMaxBytes,
|
|
28
33
|
flagEmoji,
|
|
34
|
+
formValuesToInput,
|
|
29
35
|
formatPhone,
|
|
36
|
+
formatValue,
|
|
30
37
|
fromMinorUnits,
|
|
31
38
|
hasTooManyPlaces,
|
|
32
39
|
humanize,
|
|
40
|
+
initialFormValues,
|
|
41
|
+
isAuthUserTarget,
|
|
33
42
|
isColor,
|
|
34
43
|
isCountryCode,
|
|
35
44
|
isDomain,
|
|
36
45
|
isPhoneNumber,
|
|
37
46
|
isSlug,
|
|
47
|
+
issuesByField,
|
|
38
48
|
kebabCase,
|
|
39
49
|
mimeTypesFor,
|
|
40
50
|
moneyExponent,
|
|
41
51
|
normalizeDomain,
|
|
52
|
+
optionLabel,
|
|
42
53
|
ownershipFilter,
|
|
54
|
+
parseMultiValue,
|
|
43
55
|
pascalCase,
|
|
44
56
|
pluralize,
|
|
57
|
+
prismaEnumMember,
|
|
58
|
+
relationGraph,
|
|
45
59
|
slugify,
|
|
46
60
|
snakeCase,
|
|
47
61
|
splitPhone,
|
|
62
|
+
statusTone,
|
|
48
63
|
storedFields,
|
|
49
64
|
toE164,
|
|
50
65
|
toMinorUnits,
|
|
51
66
|
words
|
|
52
|
-
} from "./chunk-
|
|
67
|
+
} from "./chunk-BTVEH5AU.js";
|
|
68
|
+
import {
|
|
69
|
+
ISSUE_CODES,
|
|
70
|
+
PROBLEM_CODES,
|
|
71
|
+
problemResponse,
|
|
72
|
+
wantsJson
|
|
73
|
+
} from "./chunk-7NQIVWGG.js";
|
|
74
|
+
import {
|
|
75
|
+
RateSyntaxError,
|
|
76
|
+
fixedWindow,
|
|
77
|
+
parseRate,
|
|
78
|
+
rateFor,
|
|
79
|
+
rateLimitKey,
|
|
80
|
+
ratesIn
|
|
81
|
+
} from "./chunk-I2HSIPCF.js";
|
|
53
82
|
|
|
54
83
|
// src/password.ts
|
|
55
84
|
var ALGORITHM = "pbkdf2-sha256";
|
|
@@ -369,8 +398,8 @@ function replay(chunks, reader, length) {
|
|
|
369
398
|
});
|
|
370
399
|
return fixed.readable;
|
|
371
400
|
}
|
|
372
|
-
function error(status, message) {
|
|
373
|
-
return
|
|
401
|
+
function error(status, message, code) {
|
|
402
|
+
return problemResponse(status, message, code);
|
|
374
403
|
}
|
|
375
404
|
function createStorage(options) {
|
|
376
405
|
const routePath = options.routePath ?? "/api/storage";
|
|
@@ -381,36 +410,36 @@ function createStorage(options) {
|
|
|
381
410
|
const urlFor = (token) => `${options.baseURL?.replace(/\/+$/, "") ?? ""}${routePath}?token=${token}`;
|
|
382
411
|
async function handle(request) {
|
|
383
412
|
const token = new URL(request.url).searchParams.get("token");
|
|
384
|
-
if (!token) return error(400, "Missing token.");
|
|
413
|
+
if (!token) return error(400, "Missing token.", PROBLEM_CODES.invalidToken);
|
|
385
414
|
const payload = await verifyToken(await signingKey(), token);
|
|
386
415
|
if (!payload || typeof payload.exp !== "number" || !isValidKey(payload.key)) {
|
|
387
|
-
return error(403, "Invalid token.");
|
|
416
|
+
return error(403, "Invalid token.", PROBLEM_CODES.invalidToken);
|
|
388
417
|
}
|
|
389
|
-
if (payload.exp < Date.now() / 1e3) return error(403, "Token expired.");
|
|
418
|
+
if (payload.exp < Date.now() / 1e3) return error(403, "Token expired.", PROBLEM_CODES.invalidToken);
|
|
390
419
|
if (request.method === "PUT") {
|
|
391
|
-
if (payload.op !== "put") return error(403, "Token does not allow uploads.");
|
|
420
|
+
if (payload.op !== "put") return error(403, "Token does not allow uploads.", PROBLEM_CODES.invalidToken);
|
|
392
421
|
const contentType = request.headers.get("content-type") ?? "";
|
|
393
422
|
if (!matchesContentType(contentType, payload.types)) {
|
|
394
|
-
return error(415, `Content-Type must be one of: ${payload.types.join(", ")}
|
|
423
|
+
return error(415, `Content-Type must be one of: ${payload.types.join(", ")}.`, PROBLEM_CODES.unsupportedMediaType);
|
|
395
424
|
}
|
|
396
425
|
const length = Number(request.headers.get("content-length"));
|
|
397
426
|
if (!request.headers.has("content-length") || !Number.isInteger(length) || length < 0) {
|
|
398
|
-
return error(411, "Content-Length is required.");
|
|
427
|
+
return error(411, "Content-Length is required.", PROBLEM_CODES.lengthRequired);
|
|
399
428
|
}
|
|
400
|
-
if (length > payload.max) return error(413, `File exceeds the ${payload.max}-byte limit
|
|
401
|
-
if (!request.body) return error(400, "Missing request body.");
|
|
429
|
+
if (length > payload.max) return error(413, `File exceeds the ${payload.max}-byte limit.`, PROBLEM_CODES.payloadTooLarge);
|
|
430
|
+
if (!request.body) return error(400, "Missing request body.", PROBLEM_CODES.badRequest);
|
|
402
431
|
const { head, chunks, reader } = await readHead(request.body, SNIFF_BYTES);
|
|
403
432
|
if (sniffMatches(contentType, head) === false) {
|
|
404
433
|
await drain(reader);
|
|
405
|
-
return error(415, `The file's contents don't match its type (${contentType.split(";")[0].trim()})
|
|
434
|
+
return error(415, `The file's contents don't match its type (${contentType.split(";")[0].trim()}).`, PROBLEM_CODES.unsupportedMediaType);
|
|
406
435
|
}
|
|
407
436
|
await options.bucket.put(payload.key, replay(chunks, reader, length), { httpMetadata: { contentType } });
|
|
408
437
|
return Response.json({ key: payload.key }, { status: 201 });
|
|
409
438
|
}
|
|
410
439
|
if (request.method === "GET" || request.method === "HEAD") {
|
|
411
|
-
if (payload.op !== "get") return error(403, "Token does not allow reads.");
|
|
440
|
+
if (payload.op !== "get") return error(403, "Token does not allow reads.", PROBLEM_CODES.invalidToken);
|
|
412
441
|
const object = await options.bucket.get(payload.key);
|
|
413
|
-
if (!object) return error(404, "Not found.");
|
|
442
|
+
if (!object) return error(404, "Not found.", PROBLEM_CODES.notFound);
|
|
414
443
|
const headers = new Headers({
|
|
415
444
|
"content-type": object.httpMetadata?.contentType ?? "application/octet-stream",
|
|
416
445
|
"content-length": String(object.size),
|
|
@@ -447,9 +476,19 @@ function createStorage(options) {
|
|
|
447
476
|
});
|
|
448
477
|
return { url: urlFor(token), method: "PUT", key: upload.key, expiresAt: new Date(exp * 1e3) };
|
|
449
478
|
},
|
|
450
|
-
/**
|
|
479
|
+
/**
|
|
480
|
+
* A temporary URL that serves the object.
|
|
481
|
+
*
|
|
482
|
+
* On a bucket that is its own CDN, this is that provider's URL and is neither
|
|
483
|
+
* temporary nor private — see {@link StorageBucket.publicUrl}. `downloadAs` still
|
|
484
|
+
* takes the signed path, because forcing a filename is this app's header to set.
|
|
485
|
+
*/
|
|
451
486
|
async createReadUrl(read) {
|
|
452
487
|
if (!isValidKey(read.key)) throw new Error(`Invalid object key: ${read.key}`);
|
|
488
|
+
if (!read.downloadAs) {
|
|
489
|
+
const direct = options.bucket.publicUrl?.(read.key);
|
|
490
|
+
if (direct) return direct;
|
|
491
|
+
}
|
|
453
492
|
const payload = { op: "get", key: read.key, exp: expiry(read.expiresIn) };
|
|
454
493
|
if (read.downloadAs) payload.name = read.downloadAs;
|
|
455
494
|
return urlFor(await signToken(await signingKey(), payload));
|
|
@@ -510,207 +549,6 @@ function randomLetters(length = 8) {
|
|
|
510
549
|
return Array.from(bytes, (byte) => String.fromCharCode(97 + byte % 26)).join("");
|
|
511
550
|
}
|
|
512
551
|
|
|
513
|
-
// src/resource/relations.ts
|
|
514
|
-
var RelationError = class extends Error {
|
|
515
|
-
constructor(message) {
|
|
516
|
-
super(message);
|
|
517
|
-
this.name = "RelationError";
|
|
518
|
-
}
|
|
519
|
-
};
|
|
520
|
-
var relationNameFor = (table, key) => `${table}_${columnName(key)}`;
|
|
521
|
-
function relationGraph(resources) {
|
|
522
|
-
const byName = new Map(resources.map((resource) => [resource.name, resource]));
|
|
523
|
-
const graph = { byResource: {}, pending: [] };
|
|
524
|
-
for (const resource of resources) {
|
|
525
|
-
const relations = { belongsTo: [], hasMany: [] };
|
|
526
|
-
for (const [key, def] of storedFields(resource)) {
|
|
527
|
-
if (def.kind !== "belongsTo") continue;
|
|
528
|
-
if (!byName.has(def.target)) {
|
|
529
|
-
throw new RelationError(`${resource.name}.${key} belongs to "${def.target}", but there is no ${def.target} resource.`);
|
|
530
|
-
}
|
|
531
|
-
relations.belongsTo.push({
|
|
532
|
-
key,
|
|
533
|
-
name: key.replace(/Id$/, ""),
|
|
534
|
-
target: def.target,
|
|
535
|
-
required: def.required,
|
|
536
|
-
onDelete: def.onDelete ?? "restrict",
|
|
537
|
-
relationName: relationNameFor(resource.table, key)
|
|
538
|
-
});
|
|
539
|
-
}
|
|
540
|
-
graph.byResource[resource.name] = relations;
|
|
541
|
-
}
|
|
542
|
-
for (const resource of resources) {
|
|
543
|
-
for (const [key, def] of Object.entries(resource.fields)) {
|
|
544
|
-
if (def.kind !== "hasMany") continue;
|
|
545
|
-
const target = byName.get(def.target);
|
|
546
|
-
if (!target) {
|
|
547
|
-
graph.pending.push({ resource: resource.name, key, target: def.target });
|
|
548
|
-
continue;
|
|
549
|
-
}
|
|
550
|
-
const foreignKey = def.foreignKey ?? `${camelCase(resource.name)}Id`;
|
|
551
|
-
const back = graph.byResource[target.name].belongsTo.find((relation) => relation.key === foreignKey);
|
|
552
|
-
if (!back || back.target !== resource.name) {
|
|
553
|
-
throw new RelationError(
|
|
554
|
-
`${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.`
|
|
555
|
-
);
|
|
556
|
-
}
|
|
557
|
-
graph.byResource[resource.name].hasMany.push({ key, target: target.name, foreignKey, relationName: back.relationName });
|
|
558
|
-
}
|
|
559
|
-
}
|
|
560
|
-
for (const resource of resources) {
|
|
561
|
-
const { belongsTo, hasMany } = graph.byResource[resource.name];
|
|
562
|
-
const taken = new Set(Object.keys(resource.fields));
|
|
563
|
-
for (const { key, name } of belongsTo) {
|
|
564
|
-
if (taken.has(name)) {
|
|
565
|
-
throw new RelationError(`${resource.name}.${key} exposes relation "${name}", which collides with the ${resource.name}.${name} field. Rename one of them.`);
|
|
566
|
-
}
|
|
567
|
-
taken.add(name);
|
|
568
|
-
}
|
|
569
|
-
for (const { key } of hasMany) {
|
|
570
|
-
if (belongsTo.some((relation) => relation.name === key)) {
|
|
571
|
-
throw new RelationError(`${resource.name}.${key} collides with a belongsTo relation of the same name.`);
|
|
572
|
-
}
|
|
573
|
-
}
|
|
574
|
-
}
|
|
575
|
-
return graph;
|
|
576
|
-
}
|
|
577
|
-
|
|
578
|
-
// src/resource/display.ts
|
|
579
|
-
var TONES = [
|
|
580
|
-
["success", /^(paid|fulfilled|active|approved|completed?|delivered|done|published|resolved|shipped|succeeded|success|verified|won)$/],
|
|
581
|
-
["danger", /^(banned|blocked|cancel+ed|churned|declined|error|expired|failed|lost|overdue|refunded|rejected|suspended)$/],
|
|
582
|
-
["warning", /^(awaiting|draft|in[-_ ]?review|on[-_ ]?hold|pending|processing|review|trial|unpaid|unverified)$/]
|
|
583
|
-
];
|
|
584
|
-
function statusTone(value) {
|
|
585
|
-
if (typeof value !== "string") return "neutral";
|
|
586
|
-
const normalized = value.trim().toLowerCase();
|
|
587
|
-
return TONES.find(([, pattern]) => pattern.test(normalized))?.[0] ?? "neutral";
|
|
588
|
-
}
|
|
589
|
-
function optionLabel(def, value) {
|
|
590
|
-
if ((def.kind === "enum" || def.kind === "multiselect") && def.optionLabels?.[value]) return def.optionLabels[value];
|
|
591
|
-
const text2 = value.replace(/[_-]+/g, " ").trim();
|
|
592
|
-
return text2 ? text2[0].toUpperCase() + text2.slice(1) : value;
|
|
593
|
-
}
|
|
594
|
-
function formatValue(def, value, options = {}) {
|
|
595
|
-
if (value === null || value === void 0 || value === "") return "\u2014";
|
|
596
|
-
const locale = options.locale ?? "en-US";
|
|
597
|
-
switch (def.kind) {
|
|
598
|
-
case "boolean":
|
|
599
|
-
return value ? "Yes" : "No";
|
|
600
|
-
case "int":
|
|
601
|
-
return new Intl.NumberFormat(locale, { maximumFractionDigits: 0 }).format(Number(value));
|
|
602
|
-
case "float":
|
|
603
|
-
return new Intl.NumberFormat(locale, { maximumFractionDigits: 2 }).format(Number(value));
|
|
604
|
-
case "date": {
|
|
605
|
-
const date = /* @__PURE__ */ new Date(`${String(value)}T00:00:00Z`);
|
|
606
|
-
return Number.isNaN(date.getTime()) ? String(value) : new Intl.DateTimeFormat(locale, { dateStyle: "medium", timeZone: "UTC" }).format(date);
|
|
607
|
-
}
|
|
608
|
-
case "datetime":
|
|
609
|
-
case "timestamp": {
|
|
610
|
-
const date = value instanceof Date ? value : new Date(value);
|
|
611
|
-
return Number.isNaN(date.getTime()) ? String(value) : new Intl.DateTimeFormat(locale, { dateStyle: "medium", timeStyle: "short", timeZone: options.timeZone ?? "UTC" }).format(date);
|
|
612
|
-
}
|
|
613
|
-
case "enum":
|
|
614
|
-
return optionLabel(def, String(value));
|
|
615
|
-
case "multiselect":
|
|
616
|
-
return Array.isArray(value) && value.length ? value.map((item) => optionLabel(def, String(item))).join(", ") : "\u2014";
|
|
617
|
-
case "tags":
|
|
618
|
-
return Array.isArray(value) && value.length ? value.map(String).join(", ") : "\u2014";
|
|
619
|
-
case "json": {
|
|
620
|
-
if (value === null || value === void 0) return "\u2014";
|
|
621
|
-
const text2 = typeof value === "string" ? value : JSON.stringify(value);
|
|
622
|
-
return text2.length > 60 ? `${text2.slice(0, 59)}\u2026` : text2;
|
|
623
|
-
}
|
|
624
|
-
case "string":
|
|
625
|
-
if (def.format === "tel") return formatPhone(String(value));
|
|
626
|
-
if (def.format === "country") return `${flagEmoji(String(value))} ${countryName(String(value), locale.split("-")[0])}`.trim();
|
|
627
|
-
return String(value);
|
|
628
|
-
case "file":
|
|
629
|
-
return String(value).split("/").pop().replace(/^[0-9a-f-]{36}-/, "");
|
|
630
|
-
default:
|
|
631
|
-
return String(value);
|
|
632
|
-
}
|
|
633
|
-
}
|
|
634
|
-
|
|
635
|
-
// src/resource/form.ts
|
|
636
|
-
function initialFormValues(resource, record) {
|
|
637
|
-
const values = {};
|
|
638
|
-
for (const [key, def] of storedFields(resource)) {
|
|
639
|
-
const source = record ? record[key] : "default" in def ? def.default : void 0;
|
|
640
|
-
if (def.kind === "boolean") values[key] = source === true || source === 1;
|
|
641
|
-
else if (def.kind === "multiselect" || def.kind === "tags") values[key] = JSON.stringify(Array.isArray(source) ? source : []);
|
|
642
|
-
else if (def.kind === "json") values[key] = source === null || source === void 0 ? "" : JSON.stringify(source, null, 2);
|
|
643
|
-
else if (source === null || source === void 0) values[key] = "";
|
|
644
|
-
else if (source instanceof Date) values[key] = source.toISOString();
|
|
645
|
-
else values[key] = String(source);
|
|
646
|
-
}
|
|
647
|
-
return values;
|
|
648
|
-
}
|
|
649
|
-
function formValuesToInput(resource, values, mode) {
|
|
650
|
-
const input = {};
|
|
651
|
-
for (const [key, def] of storedFields(resource)) {
|
|
652
|
-
const raw = values[key];
|
|
653
|
-
if (def.kind === "boolean") {
|
|
654
|
-
input[key] = raw === true;
|
|
655
|
-
continue;
|
|
656
|
-
}
|
|
657
|
-
if (def.kind === "multiselect" || def.kind === "tags") {
|
|
658
|
-
const picked = parseMultiValue(raw);
|
|
659
|
-
if (picked.length === 0 && mode === "create" && "default" in def && def.default !== void 0) continue;
|
|
660
|
-
input[key] = picked.length === 0 && !def.required ? null : picked;
|
|
661
|
-
continue;
|
|
662
|
-
}
|
|
663
|
-
if (def.kind === "json") {
|
|
664
|
-
const source = typeof raw === "string" ? raw.trim() : "";
|
|
665
|
-
if (source === "") {
|
|
666
|
-
if (mode === "create" && "default" in def && def.default !== void 0) continue;
|
|
667
|
-
if (!def.required) input[key] = null;
|
|
668
|
-
continue;
|
|
669
|
-
}
|
|
670
|
-
try {
|
|
671
|
-
input[key] = JSON.parse(source);
|
|
672
|
-
} catch {
|
|
673
|
-
input[key] = Number.NaN;
|
|
674
|
-
}
|
|
675
|
-
continue;
|
|
676
|
-
}
|
|
677
|
-
const text2 = typeof raw === "string" ? raw : "";
|
|
678
|
-
const empty = text2.trim() === "";
|
|
679
|
-
if (empty) {
|
|
680
|
-
const hasDefault = "default" in def && def.default !== void 0;
|
|
681
|
-
if (mode === "create" && hasDefault) continue;
|
|
682
|
-
if (!def.required) input[key] = null;
|
|
683
|
-
else if (def.kind === "string" || def.kind === "text") input[key] = text2;
|
|
684
|
-
continue;
|
|
685
|
-
}
|
|
686
|
-
if (def.kind === "int" || def.kind === "float") {
|
|
687
|
-
const number = Number(text2.trim());
|
|
688
|
-
input[key] = Number.isFinite(number) ? number : text2;
|
|
689
|
-
continue;
|
|
690
|
-
}
|
|
691
|
-
input[key] = text2;
|
|
692
|
-
}
|
|
693
|
-
return input;
|
|
694
|
-
}
|
|
695
|
-
function parseMultiValue(raw) {
|
|
696
|
-
if (typeof raw !== "string" || raw === "") return [];
|
|
697
|
-
try {
|
|
698
|
-
const parsed = JSON.parse(raw);
|
|
699
|
-
return Array.isArray(parsed) ? parsed.filter((value) => typeof value === "string") : [];
|
|
700
|
-
} catch {
|
|
701
|
-
return [];
|
|
702
|
-
}
|
|
703
|
-
}
|
|
704
|
-
function issuesByField(issues) {
|
|
705
|
-
const errors = {};
|
|
706
|
-
for (const issue of issues) {
|
|
707
|
-
const path = Array.isArray(issue.path) ? issue.path.map(String).join(".") : issue.path;
|
|
708
|
-
const key = path.split(".")[0] || "_form";
|
|
709
|
-
errors[key] ??= issue.message;
|
|
710
|
-
}
|
|
711
|
-
return errors;
|
|
712
|
-
}
|
|
713
|
-
|
|
714
552
|
// src/seed.ts
|
|
715
553
|
async function credentialUserRows(input) {
|
|
716
554
|
const email = input.email.trim().toLowerCase();
|
|
@@ -1054,16 +892,20 @@ function createFake(seed = 20260101) {
|
|
|
1054
892
|
var fake = createFake();
|
|
1055
893
|
|
|
1056
894
|
// src/index.ts
|
|
1057
|
-
var FLARE_VERSION = "0.
|
|
895
|
+
var FLARE_VERSION = "0.12.1";
|
|
1058
896
|
export {
|
|
897
|
+
AUTH_USER,
|
|
1059
898
|
DEFAULT_FILE_MAX_BYTES,
|
|
1060
899
|
DEFAULT_MONEY_EXPONENT,
|
|
1061
900
|
FILE_CATEGORIES,
|
|
1062
901
|
FLARE_VERSION,
|
|
902
|
+
ISSUE_CODES,
|
|
1063
903
|
NUMBER_FORMATS,
|
|
1064
904
|
PBKDF2_ITERATIONS,
|
|
905
|
+
PROBLEM_CODES,
|
|
1065
906
|
PolicyError,
|
|
1066
907
|
RESERVED_FIELD_NAMES,
|
|
908
|
+
RateSyntaxError,
|
|
1067
909
|
RelationError,
|
|
1068
910
|
ResourceDefinitionError,
|
|
1069
911
|
SNIFF_BYTES,
|
|
@@ -1086,16 +928,20 @@ export {
|
|
|
1086
928
|
createValidators,
|
|
1087
929
|
credentialUserRows,
|
|
1088
930
|
currentPeriodEnd,
|
|
931
|
+
defaultAuthUserOnDelete,
|
|
1089
932
|
defaultFieldLabel,
|
|
1090
933
|
definePolicy,
|
|
1091
934
|
defineResource,
|
|
1092
935
|
defineSeed,
|
|
1093
936
|
describeCondition,
|
|
937
|
+
enumMemberCollision,
|
|
938
|
+
enumTranslations,
|
|
1094
939
|
fake,
|
|
1095
940
|
field,
|
|
1096
941
|
fieldSchema,
|
|
1097
942
|
fileKeyPrefix,
|
|
1098
943
|
fileMaxBytes,
|
|
944
|
+
fixedWindow,
|
|
1099
945
|
flagEmoji,
|
|
1100
946
|
formValuesToInput,
|
|
1101
947
|
formatPhone,
|
|
@@ -1107,6 +953,7 @@ export {
|
|
|
1107
953
|
humanize,
|
|
1108
954
|
importSigningKey,
|
|
1109
955
|
initialFormValues,
|
|
956
|
+
isAuthUserTarget,
|
|
1110
957
|
isColor,
|
|
1111
958
|
isCountryCode,
|
|
1112
959
|
isDomain,
|
|
@@ -1122,9 +969,15 @@ export {
|
|
|
1122
969
|
optionLabel,
|
|
1123
970
|
ownershipFilter,
|
|
1124
971
|
parseMultiValue,
|
|
972
|
+
parseRate,
|
|
1125
973
|
pascalCase,
|
|
1126
974
|
pluralize,
|
|
975
|
+
prismaEnumMember,
|
|
976
|
+
problemResponse,
|
|
1127
977
|
randomLetters,
|
|
978
|
+
rateFor,
|
|
979
|
+
rateLimitKey,
|
|
980
|
+
ratesIn,
|
|
1128
981
|
relationGraph,
|
|
1129
982
|
renderTransactionalEmail,
|
|
1130
983
|
shouldApplySubscription,
|
|
@@ -1142,5 +995,6 @@ export {
|
|
|
1142
995
|
toSubscriptionStatus,
|
|
1143
996
|
verifyPassword,
|
|
1144
997
|
verifyToken,
|
|
998
|
+
wantsJson,
|
|
1145
999
|
words
|
|
1146
1000
|
};
|