@easy-cms/core 0.47.3 → 0.48.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/dist/index.d.ts +45 -5
- package/dist/index.js +180 -31
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -137,7 +137,7 @@ interface UserIdentity {
|
|
|
137
137
|
lastUsedAt: string | null;
|
|
138
138
|
}
|
|
139
139
|
/** Why a sign-in did not work, as `?sso=` on the page it ends on. */
|
|
140
|
-
type SsoOutcome = 'linked' | 'cancelled' | 'expired' | 'failed' | 'no-account' | 'unverified' | 'inactive' | 'taken';
|
|
140
|
+
type SsoOutcome = 'linked' | 'cancelled' | 'expired' | 'failed' | 'no-account' | 'link-first' | 'unverified' | 'inactive' | 'taken';
|
|
141
141
|
/** Single sign-on with `auth.providers`. Available as `cms.auth.sso`. */
|
|
142
142
|
declare class SingleSignOn {
|
|
143
143
|
private readonly cms;
|
|
@@ -195,6 +195,8 @@ declare class SingleSignOn {
|
|
|
195
195
|
private userByEmail;
|
|
196
196
|
/** A new account for people from `allowSignUp.domains`. */
|
|
197
197
|
private signUp;
|
|
198
|
+
/** A site member (`auth.members`): matched by email, like any shop or site account. */
|
|
199
|
+
private isMember;
|
|
198
200
|
private defaultRole;
|
|
199
201
|
}
|
|
200
202
|
|
|
@@ -287,6 +289,12 @@ declare class Auth {
|
|
|
287
289
|
*/
|
|
288
290
|
private memberLink;
|
|
289
291
|
hasUsers(): Promise<boolean>;
|
|
292
|
+
private warnedNoClientIp;
|
|
293
|
+
/**
|
|
294
|
+
* Without the client's address, failed logins count per email only, so anyone can lock an
|
|
295
|
+
* account for a while. Said once, in production.
|
|
296
|
+
*/
|
|
297
|
+
private warnNoClientIp;
|
|
290
298
|
/** Ends the session for a signed token. Unknown tokens are ignored. */
|
|
291
299
|
logout(signedToken: string): Promise<void>;
|
|
292
300
|
/**
|
|
@@ -1225,6 +1233,8 @@ declare class MediaFolders {
|
|
|
1225
1233
|
interface StoredFile {
|
|
1226
1234
|
readonly body: Uint8Array;
|
|
1227
1235
|
readonly size: number;
|
|
1236
|
+
/** The type the storage serves the file with, when it says (checked after direct uploads). */
|
|
1237
|
+
readonly contentType?: string;
|
|
1228
1238
|
}
|
|
1229
1239
|
/** Where uploaded files live. Keys are generated by Easy CMS: `<name>-<random>.<ext>`. */
|
|
1230
1240
|
interface StorageAdapter {
|
|
@@ -1633,6 +1643,11 @@ declare class EasyCMS<C extends Config = Config> {
|
|
|
1633
1643
|
/** The global's pending draft when it has one (see `withDrafts`), otherwise `saved`. */
|
|
1634
1644
|
private globalDraft;
|
|
1635
1645
|
private saveGlobal;
|
|
1646
|
+
/**
|
|
1647
|
+
* Users changing their own password or email say their current password first, so a session
|
|
1648
|
+
* someone else got hold of can't lock them out. Admins changing other users don't.
|
|
1649
|
+
*/
|
|
1650
|
+
private checkOwnCredentials;
|
|
1636
1651
|
/**
|
|
1637
1652
|
* What `user` may do with one document. Resolves `where`-style access
|
|
1638
1653
|
* against the document, so the admin UI can hide actions precisely.
|
|
@@ -2066,7 +2081,10 @@ interface BlocksField extends BaseField<'blocks', never> {
|
|
|
2066
2081
|
declare const uniqueWithinOf: (field: Field) => readonly string[];
|
|
2067
2082
|
/** Fields with `hasMany`: their value is a list of options, documents or files. */
|
|
2068
2083
|
declare const isHasMany: (field: Field) => boolean;
|
|
2069
|
-
/**
|
|
2084
|
+
/**
|
|
2085
|
+
* Whether a MIME type is one of `patterns` (`image/*` matches every image but SVG, which can
|
|
2086
|
+
* carry scripts and is allowed only when listed by name: `image/svg+xml`).
|
|
2087
|
+
*/
|
|
2070
2088
|
declare function mimeAllowedBy(mimeType: string, patterns: readonly string[]): boolean;
|
|
2071
2089
|
/** Fields whose value is a list of rows: `array` and `blocks`. */
|
|
2072
2090
|
declare const hasRows: (field: Field) => field is ArrayField | BlocksField;
|
|
@@ -2286,6 +2304,12 @@ interface AuthProvider {
|
|
|
2286
2304
|
}): Promise<AuthProviderProfile>;
|
|
2287
2305
|
/** For Settings → SSO: what it connects to, never secrets. */
|
|
2288
2306
|
describe?(): Readonly<Record<string, string>>;
|
|
2307
|
+
/**
|
|
2308
|
+
* Signs staff who have an account in by their verified email the first time, without linking
|
|
2309
|
+
* the provider from their account page. Only for a provider whose emails your organization
|
|
2310
|
+
* controls (e.g. its Google Workspace). Default `false`; site members are always matched.
|
|
2311
|
+
*/
|
|
2312
|
+
readonly linkByEmail?: boolean;
|
|
2289
2313
|
}
|
|
2290
2314
|
|
|
2291
2315
|
interface ConfigIssue {
|
|
@@ -2957,6 +2981,8 @@ interface EndpointRequest {
|
|
|
2957
2981
|
readonly cms: EasyCMS;
|
|
2958
2982
|
/** The request's JSON body, which must be an object (at most 1 MB). */
|
|
2959
2983
|
json(): Promise<Record<string, unknown>>;
|
|
2984
|
+
/** The request's body as text, as sent (at most 1 MB), e.g. to check a webhook's signature. */
|
|
2985
|
+
text(): Promise<string>;
|
|
2960
2986
|
}
|
|
2961
2987
|
/**
|
|
2962
2988
|
* Work that runs with the scheduled jobs: every minute in a server, or each time a cron calls
|
|
@@ -3373,9 +3399,20 @@ interface ApiHandlerOptions {
|
|
|
3373
3399
|
* Takes precedence over `trustProxy`.
|
|
3374
3400
|
*/
|
|
3375
3401
|
readonly getClientIp?: (request: Request) => string | undefined;
|
|
3376
|
-
/**
|
|
3402
|
+
/**
|
|
3403
|
+
* Behind a proxy that adds the client's address to `X-Forwarded-For`: the last address in it
|
|
3404
|
+
* (the one the proxy added; earlier ones come from the client and can say anything). Vercel
|
|
3405
|
+
* and Netlify are recognized without it.
|
|
3406
|
+
*/
|
|
3377
3407
|
readonly trustProxy?: boolean;
|
|
3378
3408
|
}
|
|
3409
|
+
/**
|
|
3410
|
+
* The client's address from `X-Forwarded-For` as the nearest proxy wrote it: its last entry.
|
|
3411
|
+
* Earlier entries were sent by the client, so they can't be trusted.
|
|
3412
|
+
*/
|
|
3413
|
+
declare function forwardedClientIp(value: string | null | undefined): string | undefined;
|
|
3414
|
+
/** The client's address on hosting platforms that set it themselves: Vercel, Netlify. */
|
|
3415
|
+
declare function platformClientIp(headers: Headers): string | undefined;
|
|
3379
3416
|
/**
|
|
3380
3417
|
* The REST API as a `(Request) => Response` handler for a framework's route, on the shared
|
|
3381
3418
|
* instance (`sharedEasyCMS`). Mount it at `routes.api` (default `/api/cms`), for every method.
|
|
@@ -3414,7 +3451,10 @@ declare function expandMimeTypes(patterns: readonly string[]): string[];
|
|
|
3414
3451
|
* tells CSV from plain text, which have the same contents.
|
|
3415
3452
|
*/
|
|
3416
3453
|
declare function sniffMimeType(data: Uint8Array, name?: string): string | undefined;
|
|
3417
|
-
/**
|
|
3454
|
+
/**
|
|
3455
|
+
* `image/*` style matching; group names (`documents`…) stand for their types. A wildcard leaves
|
|
3456
|
+
* out SVG, which can carry scripts: it is allowed only when listed as `image/svg+xml`.
|
|
3457
|
+
*/
|
|
3418
3458
|
declare function mimeAllowed(type: string, allowed: readonly string[]): boolean;
|
|
3419
3459
|
/** Width and height of PNG, GIF, JPEG and WebP images without decoding them. */
|
|
3420
3460
|
declare function imageDimensions(data: Uint8Array, type: string): {
|
|
@@ -3925,4 +3965,4 @@ declare function validateConfig(config: Config): ConfigIssue[];
|
|
|
3925
3965
|
/** The version of `@easy-cms/core`, set when the package is built. */
|
|
3926
3966
|
declare const VERSION: string;
|
|
3927
3967
|
|
|
3928
|
-
export { ADMIN_ICONS, API_KEYS, API_KEY_GLOBAL_OPERATIONS, API_KEY_OPERATIONS, type Access, type AccessArgs, type AccessOptions, type AdminAttention, type AdminBackup, type AdminBackups, type AdminBrand, type AdminCollection, type AdminCommand, type AdminComponent, type AdminComponentRef, type AdminConfig, type AdminDeliveries, type AdminDelivery, type AdminEmail, type AdminEmailDelivery, type AdminField, type AdminFieldArgs, type AdminGlobal, type AdminIcon, type AdminLayoutNode, type AdminLocale, type AdminNavGroup, type AdminNavItem, type AdminNavNode, type AdminPage, type AdminPageRef, type AdminRole, type AdminRoleChange, type AdminRoleField, type AdminRoleTarget, type AdminRoleView, type AdminRoles, type AdminSchema, type AdminSso, type AdminStatus, type AdminSwitcher, type AdminViewAccess, type AdminWebhookDelivery, type AdminWidgetRef, type AfterChangeHook, type AfterDeleteHook, type AfterReadHook, type ApiHandlerOptions, type ApiKeyContext, type ApiKeyGlobalOperation, type ApiKeyOperation, type ApiKeyPermissions, type ArrayField, type AuditChange, type AuditConfig, type AuditEntry, AuditLog, type AuditPage, type AuditVerification, Auth, type AuthConfig, type AuthProvider, type AuthProviderProfile, type AuthProviderRequest, type AuthUser, BUILTIN_COLLECTIONS, BUILTIN_NAV, type BackupState, type BackupsConfig, type BeforeChangeHook, type BeforeDeleteHook, type BeforeValidateHook, type Block, type BlocksField, type BooleanField, type BuiltinApiKeysCollection, type BuiltinMediaCollection, type BuiltinMediaFoldersCollection, type BuiltinUsersCollection, type BundleHints, CONFIG_FILE_NAMES, CSRF_COOKIE, CSRF_HEADER, type CliCommand, type CollectionAccess, type CollectionAdmin, type CollectionConfig, type CollectionDocument, type CollectionHooks, type CollectionListAdmin, type CollectionSlug, type Config, ConfigError, type ConfigIssue, type ContainerAdmin, type CopyProgress, type CopyResult, type CreateEasyCMSOptions, type CreateInput, type CreateMigrationResult, type CustomField, type CustomFieldTypes, DEFAULT_ADMIN_PATH, DEFAULT_API_PATH, DEFAULT_DEPTH, DEFAULT_MAX_FILE_SIZE, DEFAULT_MAX_VERSIONS, DEFAULT_PASSWORD_EMAILS, DEFAULT_TOKEN_EXPIRATION, type DashboardWidget, type Database, type DatabaseAdapter, type DatabaseInitArgs, type DatabaseTransfer, type DateField, type DeliveryKind, type DeliveryState, type DepthOptions, type DirectUpload, type DocumentOf, EXTENSIONS, EasyCMS, EasyCMSError, type EmailAdapter, type EmailCheck, type EmailField, type EmailMessage, type EmailSetting, type Endpoint, type EndpointRequest, FIELD_TYPES, FIELD_TYPE_BASES, FOLDER_LEVELS, type Field, type FieldAccess, type FieldAccessFn, type FieldAdmin, FieldCondition, type FieldError, type FieldRule, type FieldType, type FieldTypeBase, type FieldTypeDefinition, type FieldTypeValidateContext, type FieldValidate, type FieldValidateContext, type FieldValue, type FieldsInput, type FieldsValue, type FilterOptions, type FilterOptionsArgs, type FindArgs, type FindOptions, type FolderLevel, type FolderPermissions, ForbiddenError, type GlobalAccess, type GlobalConfig, type GlobalDocument, type GlobalHooks, type GlobalInput, type GlobalSlug, type GroupField, type ID, INTERNAL_COLLECTIONS, type ImageSize, type IncrementOptions, type InferCollection, type InferGlobal, type JobConfig, type JsonField, type Label, type LayoutNode, type LivePreview, type LoadConfigOptions, type LocalStorageOptions, type LocalizationConfig, type Logger, type LoginArgs, MAX_DEPTH, MEDIA, MEDIA_FOLDERS, MIME_GROUPS, MIN_PASSWORD_LENGTH, MIN_SECRET_LENGTH, type MediaDocument, MediaFolders, type MediaSize, type MembersConfig, type MembersSignup, type MigrationInfo, type NavGroup, NotFoundError, type NumberField, type OnRequest, type OnRequestArgs, type Operation, type PaginatedDocs, type PasswordEmail, type PasswordEmailArgs, type PasswordEmailFn, type PasswordLinkOptions, PayloadTooLargeError, type Plugin, type PluginInfo, type PluginTypes, type PreviewTarget, type PreviewURL, QueryError, ROLE_OPERATIONS, type RawDocument, type ReadOptions, type RelationshipField, type RequestContext, type ResolvedConfig, type RestHandler, type RestHandlerOptions, type RichTextDocument, type RichTextField, type Role, type RoleOperation, type RolePermissions, Roles, type RoutesConfig, SESSION_COOKIE, SSO_COOKIE, type ScheduledJob, SchemaError, type SchemaMode, type SearchHit, type SelectField, type SelectOption, type Session, type SidebarPanel, type SignupArgs, type SignupResult, type SlugField, type SlugOf, type SsoOutcome, type SsoProviderRef, type StorageAdapter, type StoredFile, type TextField, type TextareaField, TooManyRequestsError, type TypedPlugin, UnauthorizedError, type UpdateInput, type UpdateOptions, type UploadConfig, type UploadField, type UploadFromURLConfig, type UserIdentity, VERSION, ValidationError, type Version, type VersionSummary, type VersionsConfig, WEBHOOK_EVENTS, type WebhookConfig, type WebhookEvent, type WebhookPayload, type Where, type WhereOperators, anyone, applyDefaults, applyPlugins, checkFormToken, configSignature, consoleEmail, consoleLogger, copyDatabase, createApiHandler, createEasyCMS, createRestHandler, createRootEndpointHandler, defineConfig, defineFieldType, definePlugin, expandMimeTypes, fillMissing, findConfigFile, formToken, generateSlugs, generateTypes, hasRows, hashPassword, honeypotName, imageDimensions, importConfig, isAdmin, isHasMany, isLoggedIn, isSignedIn, isSystemAdmin, keyAllows, loadConfig, localStorage, mergeForUpdate, mimeAllowed, mimeAllowedBy, parseId, populate, rateKeys, readCookie, resolveAdminModule, resolveConfig, rowFields, sharedEasyCMS, silentLogger, singularize, slugify, sniffMimeType, uniqueWithinOf, validateConfig, validateFields, verifyPassword, verifyTurnstile, writeBackupFile };
|
|
3968
|
+
export { ADMIN_ICONS, API_KEYS, API_KEY_GLOBAL_OPERATIONS, API_KEY_OPERATIONS, type Access, type AccessArgs, type AccessOptions, type AdminAttention, type AdminBackup, type AdminBackups, type AdminBrand, type AdminCollection, type AdminCommand, type AdminComponent, type AdminComponentRef, type AdminConfig, type AdminDeliveries, type AdminDelivery, type AdminEmail, type AdminEmailDelivery, type AdminField, type AdminFieldArgs, type AdminGlobal, type AdminIcon, type AdminLayoutNode, type AdminLocale, type AdminNavGroup, type AdminNavItem, type AdminNavNode, type AdminPage, type AdminPageRef, type AdminRole, type AdminRoleChange, type AdminRoleField, type AdminRoleTarget, type AdminRoleView, type AdminRoles, type AdminSchema, type AdminSso, type AdminStatus, type AdminSwitcher, type AdminViewAccess, type AdminWebhookDelivery, type AdminWidgetRef, type AfterChangeHook, type AfterDeleteHook, type AfterReadHook, type ApiHandlerOptions, type ApiKeyContext, type ApiKeyGlobalOperation, type ApiKeyOperation, type ApiKeyPermissions, type ArrayField, type AuditChange, type AuditConfig, type AuditEntry, AuditLog, type AuditPage, type AuditVerification, Auth, type AuthConfig, type AuthProvider, type AuthProviderProfile, type AuthProviderRequest, type AuthUser, BUILTIN_COLLECTIONS, BUILTIN_NAV, type BackupState, type BackupsConfig, type BeforeChangeHook, type BeforeDeleteHook, type BeforeValidateHook, type Block, type BlocksField, type BooleanField, type BuiltinApiKeysCollection, type BuiltinMediaCollection, type BuiltinMediaFoldersCollection, type BuiltinUsersCollection, type BundleHints, CONFIG_FILE_NAMES, CSRF_COOKIE, CSRF_HEADER, type CliCommand, type CollectionAccess, type CollectionAdmin, type CollectionConfig, type CollectionDocument, type CollectionHooks, type CollectionListAdmin, type CollectionSlug, type Config, ConfigError, type ConfigIssue, type ContainerAdmin, type CopyProgress, type CopyResult, type CreateEasyCMSOptions, type CreateInput, type CreateMigrationResult, type CustomField, type CustomFieldTypes, DEFAULT_ADMIN_PATH, DEFAULT_API_PATH, DEFAULT_DEPTH, DEFAULT_MAX_FILE_SIZE, DEFAULT_MAX_VERSIONS, DEFAULT_PASSWORD_EMAILS, DEFAULT_TOKEN_EXPIRATION, type DashboardWidget, type Database, type DatabaseAdapter, type DatabaseInitArgs, type DatabaseTransfer, type DateField, type DeliveryKind, type DeliveryState, type DepthOptions, type DirectUpload, type DocumentOf, EXTENSIONS, EasyCMS, EasyCMSError, type EmailAdapter, type EmailCheck, type EmailField, type EmailMessage, type EmailSetting, type Endpoint, type EndpointRequest, FIELD_TYPES, FIELD_TYPE_BASES, FOLDER_LEVELS, type Field, type FieldAccess, type FieldAccessFn, type FieldAdmin, FieldCondition, type FieldError, type FieldRule, type FieldType, type FieldTypeBase, type FieldTypeDefinition, type FieldTypeValidateContext, type FieldValidate, type FieldValidateContext, type FieldValue, type FieldsInput, type FieldsValue, type FilterOptions, type FilterOptionsArgs, type FindArgs, type FindOptions, type FolderLevel, type FolderPermissions, ForbiddenError, type GlobalAccess, type GlobalConfig, type GlobalDocument, type GlobalHooks, type GlobalInput, type GlobalSlug, type GroupField, type ID, INTERNAL_COLLECTIONS, type ImageSize, type IncrementOptions, type InferCollection, type InferGlobal, type JobConfig, type JsonField, type Label, type LayoutNode, type LivePreview, type LoadConfigOptions, type LocalStorageOptions, type LocalizationConfig, type Logger, type LoginArgs, MAX_DEPTH, MEDIA, MEDIA_FOLDERS, MIME_GROUPS, MIN_PASSWORD_LENGTH, MIN_SECRET_LENGTH, type MediaDocument, MediaFolders, type MediaSize, type MembersConfig, type MembersSignup, type MigrationInfo, type NavGroup, NotFoundError, type NumberField, type OnRequest, type OnRequestArgs, type Operation, type PaginatedDocs, type PasswordEmail, type PasswordEmailArgs, type PasswordEmailFn, type PasswordLinkOptions, PayloadTooLargeError, type Plugin, type PluginInfo, type PluginTypes, type PreviewTarget, type PreviewURL, QueryError, ROLE_OPERATIONS, type RawDocument, type ReadOptions, type RelationshipField, type RequestContext, type ResolvedConfig, type RestHandler, type RestHandlerOptions, type RichTextDocument, type RichTextField, type Role, type RoleOperation, type RolePermissions, Roles, type RoutesConfig, SESSION_COOKIE, SSO_COOKIE, type ScheduledJob, SchemaError, type SchemaMode, type SearchHit, type SelectField, type SelectOption, type Session, type SidebarPanel, type SignupArgs, type SignupResult, type SlugField, type SlugOf, type SsoOutcome, type SsoProviderRef, type StorageAdapter, type StoredFile, type TextField, type TextareaField, TooManyRequestsError, type TypedPlugin, UnauthorizedError, type UpdateInput, type UpdateOptions, type UploadConfig, type UploadField, type UploadFromURLConfig, type UserIdentity, VERSION, ValidationError, type Version, type VersionSummary, type VersionsConfig, WEBHOOK_EVENTS, type WebhookConfig, type WebhookEvent, type WebhookPayload, type Where, type WhereOperators, anyone, applyDefaults, applyPlugins, checkFormToken, configSignature, consoleEmail, consoleLogger, copyDatabase, createApiHandler, createEasyCMS, createRestHandler, createRootEndpointHandler, defineConfig, defineFieldType, definePlugin, expandMimeTypes, fillMissing, findConfigFile, formToken, forwardedClientIp, generateSlugs, generateTypes, hasRows, hashPassword, honeypotName, imageDimensions, importConfig, isAdmin, isHasMany, isLoggedIn, isSignedIn, isSystemAdmin, keyAllows, loadConfig, localStorage, mergeForUpdate, mimeAllowed, mimeAllowedBy, parseId, platformClientIp, populate, rateKeys, readCookie, resolveAdminModule, resolveConfig, rowFields, sharedEasyCMS, silentLogger, singularize, slugify, sniffMimeType, uniqueWithinOf, validateConfig, validateFields, verifyPassword, verifyTurnstile, writeBackupFile };
|
package/dist/index.js
CHANGED
|
@@ -1112,7 +1112,7 @@ function sniffMimeType(data, name = "") {
|
|
|
1112
1112
|
}
|
|
1113
1113
|
function mimeAllowed(type, allowed2) {
|
|
1114
1114
|
return expandMimeTypes(allowed2).some(
|
|
1115
|
-
(pattern) => pattern.endsWith("/*") ? type.startsWith(pattern.slice(0, -1)) : pattern === type
|
|
1115
|
+
(pattern) => pattern.endsWith("/*") ? type.startsWith(pattern.slice(0, -1)) && type !== "image/svg+xml" : pattern === type
|
|
1116
1116
|
);
|
|
1117
1117
|
}
|
|
1118
1118
|
function imageDimensions(data, type) {
|
|
@@ -1190,9 +1190,10 @@ function mimeAllowedBy(mimeType, patterns) {
|
|
|
1190
1190
|
const type = mimeType.toLowerCase();
|
|
1191
1191
|
return expandMimeTypes(patterns).some((pattern) => {
|
|
1192
1192
|
const p = pattern.toLowerCase();
|
|
1193
|
-
return p.endsWith("/*") ? type.startsWith(p.slice(0, -1)) : type === p;
|
|
1193
|
+
return p.endsWith("/*") ? type.startsWith(p.slice(0, -1)) && type !== SVG : type === p;
|
|
1194
1194
|
});
|
|
1195
1195
|
}
|
|
1196
|
+
var SVG = "image/svg+xml";
|
|
1196
1197
|
var hasRows = (field) => field.type === "array" || field.type === "blocks";
|
|
1197
1198
|
function rowFields(field, row) {
|
|
1198
1199
|
if (field.type === "array") return field.fields;
|
|
@@ -1690,6 +1691,16 @@ var SingleSignOn = class {
|
|
|
1690
1691
|
if (!email2 || !profile.emailVerified)
|
|
1691
1692
|
return await back("login", email2 ? "unverified" : "no-account");
|
|
1692
1693
|
user = await this.userByEmail(email2);
|
|
1694
|
+
if (user && !this.isMember(user) && provider.linkByEmail !== true)
|
|
1695
|
+
return await back("login", "link-first");
|
|
1696
|
+
if (user && user.emailVerified === false) {
|
|
1697
|
+
const { id, ...rest } = user;
|
|
1698
|
+
user = await this.cms.db.update({
|
|
1699
|
+
collection: USERS,
|
|
1700
|
+
id,
|
|
1701
|
+
data: { ...rest, emailVerified: true, passwordHash: null }
|
|
1702
|
+
});
|
|
1703
|
+
}
|
|
1693
1704
|
if (!user) user = await this.signUp(email2, profile.name);
|
|
1694
1705
|
if (!user) return await back("login", "no-account");
|
|
1695
1706
|
await this.addIdentity(user.id, provider.id, subject, email2);
|
|
@@ -1742,7 +1753,7 @@ var SingleSignOn = class {
|
|
|
1742
1753
|
const row = await this.cms.db.findById({ collection: USER_IDENTITIES, id: identityId });
|
|
1743
1754
|
if (!row) throw new NotFoundError(USER_IDENTITIES, identityId);
|
|
1744
1755
|
const own = String(row.user) === String(actor.id);
|
|
1745
|
-
if (!own && (actor
|
|
1756
|
+
if (!own && (!isSystemAdmin(actor) || actor.apiKey)) throw new ForbiddenError();
|
|
1746
1757
|
const user = await this.cms.db.findById({ collection: USERS, id: row.user });
|
|
1747
1758
|
const others = (await this.identities(row.user)).length - 1;
|
|
1748
1759
|
const password = !!user?.passwordHash && this.passwordAllowed(user);
|
|
@@ -1834,6 +1845,10 @@ var SingleSignOn = class {
|
|
|
1834
1845
|
this.cms.logger.info(`${email2} signed up with a provider`);
|
|
1835
1846
|
return this.cms.db.findById({ collection: USERS, id: created.id });
|
|
1836
1847
|
}
|
|
1848
|
+
/** A site member (`auth.members`): matched by email, like any shop or site account. */
|
|
1849
|
+
isMember(user) {
|
|
1850
|
+
return (this.config.auth.members?.roles ?? []).includes(String(user.role));
|
|
1851
|
+
}
|
|
1837
1852
|
defaultRole() {
|
|
1838
1853
|
const roles = this.config.auth.roles;
|
|
1839
1854
|
return roles.includes("editor") ? "editor" : roles[roles.length - 1];
|
|
@@ -1872,6 +1887,7 @@ var Auth = class {
|
|
|
1872
1887
|
const email2 = typeof args.email === "string" ? args.email.trim().toLowerCase() : "";
|
|
1873
1888
|
const password = typeof args.password === "string" ? args.password : "";
|
|
1874
1889
|
const key2 = `${email2}|${args.ip ?? ""}`;
|
|
1890
|
+
if (args.ip === void 0) this.warnNoClientIp();
|
|
1875
1891
|
try {
|
|
1876
1892
|
await this.checkRateLimit(key2);
|
|
1877
1893
|
} catch (error) {
|
|
@@ -1994,8 +2010,16 @@ var Auth = class {
|
|
|
1994
2010
|
throw new QueryError("Signing up needs `email` in the config, and a site URL in production");
|
|
1995
2011
|
const existing = email2 ? await this.findByEmail(email2) : void 0;
|
|
1996
2012
|
if (existing && verify) {
|
|
1997
|
-
if (existing.emailVerified === false && existing.active !== false)
|
|
1998
|
-
await this.
|
|
2013
|
+
if (existing.emailVerified === false && existing.active !== false) {
|
|
2014
|
+
await this.cms.update(USERS, existing.id, { password: args.password }, {
|
|
2015
|
+
context: args.context ?? {}
|
|
2016
|
+
});
|
|
2017
|
+
const pending = await this.db.findById({
|
|
2018
|
+
collection: USERS,
|
|
2019
|
+
id: existing.id
|
|
2020
|
+
});
|
|
2021
|
+
await this.mailVerifyLink(pending, args);
|
|
2022
|
+
}
|
|
1999
2023
|
return { verify: true };
|
|
2000
2024
|
}
|
|
2001
2025
|
const created = await this.cms.create(
|
|
@@ -2030,14 +2054,15 @@ var Auth = class {
|
|
|
2030
2054
|
const claims = readPasswordToken(token);
|
|
2031
2055
|
const id = claims && (/^\d+$/.test(claims.userId) ? Number(claims.userId) : claims.userId);
|
|
2032
2056
|
const user = claims && claims.purpose === "verify" && claims.expiresAt > Date.now() && id !== null ? await this.db.findById({ collection: USERS, id }) : null;
|
|
2033
|
-
if (!user || user.active === false ||
|
|
2057
|
+
if (!user || user.active === false || // A link confirms once: it doesn't sign in again afterwards.
|
|
2058
|
+
user.emailVerified !== false || !passwordTokenMatches(this.config.secret, token, passwordFingerprint(user.passwordHash))) {
|
|
2034
2059
|
await this.recordFailure(key2);
|
|
2035
2060
|
throw new ValidationError(USERS, [
|
|
2036
2061
|
{ field: "token", message: "This link has expired or is not valid" }
|
|
2037
2062
|
]);
|
|
2038
2063
|
}
|
|
2039
2064
|
await this.clearFailures(key2);
|
|
2040
|
-
|
|
2065
|
+
{
|
|
2041
2066
|
const { id: _id, ...rest } = user;
|
|
2042
2067
|
await this.db.update({
|
|
2043
2068
|
collection: USERS,
|
|
@@ -2086,6 +2111,18 @@ var Auth = class {
|
|
|
2086
2111
|
async hasUsers() {
|
|
2087
2112
|
return await this.db.count({ collection: USERS }) > 0;
|
|
2088
2113
|
}
|
|
2114
|
+
warnedNoClientIp = false;
|
|
2115
|
+
/**
|
|
2116
|
+
* Without the client's address, failed logins count per email only, so anyone can lock an
|
|
2117
|
+
* account for a while. Said once, in production.
|
|
2118
|
+
*/
|
|
2119
|
+
warnNoClientIp() {
|
|
2120
|
+
if (this.warnedNoClientIp || process.env.NODE_ENV !== "production") return;
|
|
2121
|
+
this.warnedNoClientIp = true;
|
|
2122
|
+
this.cms.logger.warn(
|
|
2123
|
+
"Easy CMS doesn't know the client's IP address, so logins are rate limited per email only. Behind a proxy that sets X-Forwarded-For, set `trustProxy: true` in the adapter."
|
|
2124
|
+
);
|
|
2125
|
+
}
|
|
2089
2126
|
/** Ends the session for a signed token. Unknown tokens are ignored. */
|
|
2090
2127
|
async logout(signedToken) {
|
|
2091
2128
|
const token = unsignToken(this.config.secret, signedToken);
|
|
@@ -3988,12 +4025,41 @@ for (const [net, prefix] of [
|
|
|
3988
4025
|
["ff00::", 8]
|
|
3989
4026
|
])
|
|
3990
4027
|
PRIVATE.addSubnet(net, prefix, "ipv6");
|
|
4028
|
+
var EMBEDS_IPV4 = new BlockList();
|
|
4029
|
+
EMBEDS_IPV4.addSubnet("64:ff9b::", 96, "ipv6");
|
|
4030
|
+
EMBEDS_IPV4.addSubnet("::", 96, "ipv6");
|
|
4031
|
+
function embeddedIPv4(address) {
|
|
4032
|
+
if (address === "::" || address === "::1" || !EMBEDS_IPV4.check(address, "ipv6")) return void 0;
|
|
4033
|
+
const bytes = ipv6Bytes(address);
|
|
4034
|
+
return bytes ? bytes.slice(12).join(".") : void 0;
|
|
4035
|
+
}
|
|
4036
|
+
function ipv6Bytes(address) {
|
|
4037
|
+
let text2 = address;
|
|
4038
|
+
const tail = /(\d+)\.(\d+)\.(\d+)\.(\d+)$/.exec(text2);
|
|
4039
|
+
if (tail) {
|
|
4040
|
+
const [a, b, c, d] = tail.slice(1).map(Number);
|
|
4041
|
+
text2 = `${text2.slice(0, tail.index)}${(a << 8 | b).toString(16)}:${(c << 8 | d).toString(16)}`;
|
|
4042
|
+
}
|
|
4043
|
+
const [head = "", rest] = text2.split("::");
|
|
4044
|
+
const left = head ? head.split(":") : [];
|
|
4045
|
+
const right = rest === void 0 ? [] : rest ? rest.split(":") : [];
|
|
4046
|
+
const groups = rest === void 0 ? left : [...left, ...Array(8 - left.length - right.length).fill("0"), ...right];
|
|
4047
|
+
if (groups.length !== 8) return void 0;
|
|
4048
|
+
return groups.flatMap((g) => {
|
|
4049
|
+
const n = Number.parseInt(g, 16);
|
|
4050
|
+
return [n >> 8, n & 255];
|
|
4051
|
+
});
|
|
4052
|
+
}
|
|
3991
4053
|
function isPrivateAddress(address) {
|
|
3992
4054
|
const mapped = /^::ffff:(\d+\.\d+\.\d+\.\d+)$/i.exec(address);
|
|
3993
4055
|
if (mapped?.[1]) return PRIVATE.check(mapped[1], "ipv4");
|
|
3994
4056
|
const family = isIP(address);
|
|
3995
4057
|
if (family === 4) return PRIVATE.check(address, "ipv4");
|
|
3996
|
-
if (family === 6)
|
|
4058
|
+
if (family === 6) {
|
|
4059
|
+
const embedded = embeddedIPv4(address);
|
|
4060
|
+
if (embedded) return PRIVATE.check(embedded, "ipv4");
|
|
4061
|
+
return PRIVATE.check(address, "ipv6");
|
|
4062
|
+
}
|
|
3997
4063
|
return true;
|
|
3998
4064
|
}
|
|
3999
4065
|
function hostAllowed(host, allowedHosts) {
|
|
@@ -4363,6 +4429,7 @@ var Roles = class {
|
|
|
4363
4429
|
async canView(user, id) {
|
|
4364
4430
|
if (isSystemAdmin(user)) return true;
|
|
4365
4431
|
if (user.apiKey) return false;
|
|
4432
|
+
if (id === "deliveries" && user.scoped === true) return false;
|
|
4366
4433
|
if (id === "audit" && user.role === "admin" && user.scoped === true && this.config.audit !== false && typeof this.config.audit.scope === "function")
|
|
4367
4434
|
return true;
|
|
4368
4435
|
if (!this.enabled) return id.startsWith("page:") || id.startsWith("widget:");
|
|
@@ -5202,8 +5269,13 @@ function validateSecret(secret, add) {
|
|
|
5202
5269
|
);
|
|
5203
5270
|
} else if (secret.length < MIN_SECRET_LENGTH) {
|
|
5204
5271
|
add("secret", `must be at least ${MIN_SECRET_LENGTH} characters (got ${secret.length})`, hint);
|
|
5272
|
+
} else if (process.env.NODE_ENV === "production" && guessable(secret)) {
|
|
5273
|
+
add("secret", "looks like an example or a repeated pattern, not a random string", hint);
|
|
5205
5274
|
}
|
|
5206
5275
|
}
|
|
5276
|
+
function guessable(secret) {
|
|
5277
|
+
return /change[-_ ]?me|example|placeholder|secret-?key/i.test(secret) || new Set(secret).size < 8;
|
|
5278
|
+
}
|
|
5207
5279
|
function validateAdmin(config, add) {
|
|
5208
5280
|
const admin = config.admin;
|
|
5209
5281
|
if (admin === void 0) return;
|
|
@@ -6584,9 +6656,10 @@ var EasyCMS = class {
|
|
|
6584
6656
|
if (!Number.isInteger(limit) || limit < 0)
|
|
6585
6657
|
throw new QueryError("limit must be a non-negative integer");
|
|
6586
6658
|
if (!Number.isInteger(page) || page < 1) throw new QueryError("page must be a positive integer");
|
|
6659
|
+
const draft = readsDrafts(guard, options.draft);
|
|
6587
6660
|
const where = this.whereFor(
|
|
6588
6661
|
config,
|
|
6589
|
-
draftWhere(config,
|
|
6662
|
+
draftWhere(config, draft, await this.readWhere(config, guard, options.where)),
|
|
6590
6663
|
options
|
|
6591
6664
|
);
|
|
6592
6665
|
const requested = options.sort === void 0 ? ["-createdAt"] : [options.sort].flat();
|
|
@@ -6599,8 +6672,8 @@ var EasyCMS = class {
|
|
|
6599
6672
|
this.config.localization?.locales ?? []
|
|
6600
6673
|
);
|
|
6601
6674
|
const result = await this.db.find({ collection: collection2, where, sort, limit, page });
|
|
6602
|
-
const found =
|
|
6603
|
-
const docs = await this.output(config, found, guard, options);
|
|
6675
|
+
const found = draft ? await this.withDrafts(config, result.docs) : result.docs;
|
|
6676
|
+
const docs = await this.output(config, found, guard, { ...options, draft });
|
|
6604
6677
|
return { ...result, docs };
|
|
6605
6678
|
}
|
|
6606
6679
|
async findById(collection2, id, options = {}) {
|
|
@@ -6608,9 +6681,10 @@ var EasyCMS = class {
|
|
|
6608
6681
|
const guard = guardOf(options);
|
|
6609
6682
|
const parsed = parseId(id);
|
|
6610
6683
|
if (parsed === void 0) return null;
|
|
6684
|
+
const draft = readsDrafts(guard, options.draft);
|
|
6611
6685
|
const where = this.whereFor(
|
|
6612
6686
|
config,
|
|
6613
|
-
draftWhere(config,
|
|
6687
|
+
draftWhere(config, draft, await this.readWhere(config, guard, void 0)),
|
|
6614
6688
|
options
|
|
6615
6689
|
);
|
|
6616
6690
|
const doc = where ? (await this.db.find({
|
|
@@ -6621,8 +6695,8 @@ var EasyCMS = class {
|
|
|
6621
6695
|
page: 1
|
|
6622
6696
|
})).docs[0] : await this.db.findById({ collection: collection2, id: parsed });
|
|
6623
6697
|
if (!doc) return null;
|
|
6624
|
-
const [current] =
|
|
6625
|
-
const [out] = await this.output(config, [current], guard, options);
|
|
6698
|
+
const [current] = draft ? await this.withDrafts(config, [doc]) : [doc];
|
|
6699
|
+
const [out] = await this.output(config, [current], guard, { ...options, draft });
|
|
6626
6700
|
return out ?? null;
|
|
6627
6701
|
}
|
|
6628
6702
|
async count(collection2, options = {}) {
|
|
@@ -6631,7 +6705,7 @@ var EasyCMS = class {
|
|
|
6631
6705
|
config,
|
|
6632
6706
|
draftWhere(
|
|
6633
6707
|
config,
|
|
6634
|
-
options.draft,
|
|
6708
|
+
readsDrafts(guardOf(options), options.draft),
|
|
6635
6709
|
await this.readWhere(config, guardOf(options), options.where)
|
|
6636
6710
|
),
|
|
6637
6711
|
options
|
|
@@ -6782,6 +6856,8 @@ var EasyCMS = class {
|
|
|
6782
6856
|
const mimeType = sniffMimeType(start.body.subarray(0, SNIFF_BYTES), t.name);
|
|
6783
6857
|
if (mimeType !== t.type)
|
|
6784
6858
|
await refuse(`contains ${mimeType ?? "an unknown type"}, not ${t.type}`);
|
|
6859
|
+
const served = start.contentType?.split(";")[0]?.trim().toLowerCase();
|
|
6860
|
+
if (served && served !== t.type) await refuse(`is stored as ${served}, not ${t.type}`);
|
|
6785
6861
|
const stored = [];
|
|
6786
6862
|
try {
|
|
6787
6863
|
let dimensions;
|
|
@@ -7333,6 +7409,7 @@ var EasyCMS = class {
|
|
|
7333
7409
|
raw = rest;
|
|
7334
7410
|
}
|
|
7335
7411
|
const { input, password } = splitPassword(config, raw);
|
|
7412
|
+
if (config.slug === USERS) await this.checkOwnCredentials(guard, existing, raw, password);
|
|
7336
7413
|
const filtered = await filterInput(
|
|
7337
7414
|
config.fields,
|
|
7338
7415
|
input,
|
|
@@ -7492,7 +7569,7 @@ var EasyCMS = class {
|
|
|
7492
7569
|
/** A global as stored under `key` (see `globalKey`); empty when there is no key. */
|
|
7493
7570
|
async readGlobal(config, key2, guard, options) {
|
|
7494
7571
|
const saved = key2 ? await this.db.findGlobal({ slug: key2 }) : null;
|
|
7495
|
-
const stored = saved && options.draft ? await this.globalDraft(config, saved, key2) : saved;
|
|
7572
|
+
const stored = saved && readsDrafts(guard, options.draft) ? await this.globalDraft(config, saved, key2) : saved;
|
|
7496
7573
|
const data = fillMissing(
|
|
7497
7574
|
config.fields,
|
|
7498
7575
|
applyDefaults(config.fields, stored ?? {}, this.config.localization)
|
|
@@ -7652,6 +7729,33 @@ var EasyCMS = class {
|
|
|
7652
7729
|
await this.audited(config, "update", doc, current, guard, mode);
|
|
7653
7730
|
return this.readGlobal(config, key2, guard, { ...options, draft: true });
|
|
7654
7731
|
}
|
|
7732
|
+
/**
|
|
7733
|
+
* Users changing their own password or email say their current password first, so a session
|
|
7734
|
+
* someone else got hold of can't lock them out. Admins changing other users don't.
|
|
7735
|
+
*/
|
|
7736
|
+
async checkOwnCredentials(guard, existing, raw, password) {
|
|
7737
|
+
const user = guard.user;
|
|
7738
|
+
if (!guard.enforce || !user || user.apiKey || String(user.id) !== String(existing.id)) return;
|
|
7739
|
+
const email2 = typeof raw.email === "string" ? raw.email.trim().toLowerCase() : void 0;
|
|
7740
|
+
const emailChanges = email2 !== void 0 && email2 !== String(existing.email ?? "").toLowerCase();
|
|
7741
|
+
if (password === void 0 && !emailChanges) return;
|
|
7742
|
+
const hash = typeof existing.passwordHash === "string" ? existing.passwordHash : void 0;
|
|
7743
|
+
if (!hash) {
|
|
7744
|
+
if (emailChanges)
|
|
7745
|
+
throw new ValidationError(USERS, [
|
|
7746
|
+
{ field: "email", message: "can't be changed on an account without a password" }
|
|
7747
|
+
]);
|
|
7748
|
+
return;
|
|
7749
|
+
}
|
|
7750
|
+
const current = typeof raw.currentPassword === "string" ? raw.currentPassword : "";
|
|
7751
|
+
if (!current || !await verifyPassword(current, hash))
|
|
7752
|
+
throw new ValidationError(USERS, [
|
|
7753
|
+
{
|
|
7754
|
+
field: "currentPassword",
|
|
7755
|
+
message: current ? "is not right" : "is required to change your password or email"
|
|
7756
|
+
}
|
|
7757
|
+
]);
|
|
7758
|
+
}
|
|
7655
7759
|
/**
|
|
7656
7760
|
* What `user` may do with one document. Resolves `where`-style access
|
|
7657
7761
|
* against the document, so the admin UI can hide actions precisely.
|
|
@@ -8220,6 +8324,7 @@ var EasyCMS = class {
|
|
|
8220
8324
|
);
|
|
8221
8325
|
return await stripFields(target.fields, hooked, read2);
|
|
8222
8326
|
};
|
|
8327
|
+
const drafts = readsDrafts(guard, options.draft);
|
|
8223
8328
|
const load = async (target, ids) => {
|
|
8224
8329
|
const where = await this.readWhere(target, guard, { id: { in: [...ids] } }).catch((error) => {
|
|
8225
8330
|
if (error instanceof ForbiddenError || error instanceof UnauthorizedError) return null;
|
|
@@ -8228,12 +8333,12 @@ var EasyCMS = class {
|
|
|
8228
8333
|
if (where === null) return [];
|
|
8229
8334
|
const found = await this.db.find({
|
|
8230
8335
|
collection: target.slug,
|
|
8231
|
-
where: this.whereFor(target, draftWhere(target,
|
|
8336
|
+
where: this.whereFor(target, draftWhere(target, drafts, where), options),
|
|
8232
8337
|
sort: [],
|
|
8233
8338
|
limit: 0,
|
|
8234
8339
|
page: 1
|
|
8235
8340
|
});
|
|
8236
|
-
const docs2 =
|
|
8341
|
+
const docs2 = drafts ? await this.withDrafts(target, found.docs) : found.docs;
|
|
8237
8342
|
return Promise.all(
|
|
8238
8343
|
docs2.map((d) => finish(target, this.pick(target, d, options)))
|
|
8239
8344
|
);
|
|
@@ -8761,12 +8866,17 @@ function guardOf(options) {
|
|
|
8761
8866
|
context: options.context ?? NO_CONTEXT
|
|
8762
8867
|
};
|
|
8763
8868
|
}
|
|
8869
|
+
function readsDrafts(guard, draft) {
|
|
8870
|
+
return draft === true && (!guard.enforce || guard.user !== null && guard.user.member !== true);
|
|
8871
|
+
}
|
|
8764
8872
|
function deny(user) {
|
|
8765
8873
|
return user ? new ForbiddenError() : new UnauthorizedError();
|
|
8766
8874
|
}
|
|
8767
8875
|
function splitPassword(config, raw) {
|
|
8768
|
-
if (config.slug !== USERS
|
|
8769
|
-
const {
|
|
8876
|
+
if (config.slug !== USERS) return { input: raw };
|
|
8877
|
+
const { currentPassword: _current, ...rest } = raw;
|
|
8878
|
+
if (!Object.hasOwn(rest, "password")) return { input: rest };
|
|
8879
|
+
const { password, ...input } = rest;
|
|
8770
8880
|
if (password === void 0 || password === null || password === "") return { input };
|
|
8771
8881
|
if (typeof password !== "string" || password.length < MIN_PASSWORD_LENGTH) {
|
|
8772
8882
|
throw new ValidationError(USERS, [
|
|
@@ -9517,7 +9627,7 @@ async function adminSearch(cms, who, q, perCollection = 5) {
|
|
|
9517
9627
|
}
|
|
9518
9628
|
|
|
9519
9629
|
// src/version.ts
|
|
9520
|
-
var VERSION = true ? "0.
|
|
9630
|
+
var VERSION = true ? "0.48.0" : "0.0.0-dev";
|
|
9521
9631
|
|
|
9522
9632
|
// src/rest/admin-status.ts
|
|
9523
9633
|
var DAY2 = 864e5;
|
|
@@ -9860,7 +9970,8 @@ async function customEndpoint(cms, ctx, method, segments, root, options) {
|
|
|
9860
9970
|
context: ctx.context,
|
|
9861
9971
|
ip: options.getClientIp?.(ctx.request),
|
|
9862
9972
|
cms,
|
|
9863
|
-
json: () => readJson(ctx.request)
|
|
9973
|
+
json: () => readJson(ctx.request),
|
|
9974
|
+
text: async () => new TextDecoder().decode(await readBody(ctx.request, MAX_BODY_BYTES))
|
|
9864
9975
|
});
|
|
9865
9976
|
return { body: body === void 0 ? null : body };
|
|
9866
9977
|
}
|
|
@@ -10703,12 +10814,16 @@ async function readUpload(request, maxFileSize) {
|
|
|
10703
10814
|
415
|
|
10704
10815
|
);
|
|
10705
10816
|
}
|
|
10706
|
-
const
|
|
10707
|
-
|
|
10708
|
-
|
|
10817
|
+
const bytes = await readBody(request, maxFileSize + 64 * 1024).catch((error) => {
|
|
10818
|
+
throw error instanceof HttpError && error.status === 413 ? new PayloadTooLargeError(`File is larger than ${maxFileSize} bytes`) : error;
|
|
10819
|
+
});
|
|
10709
10820
|
let form;
|
|
10710
10821
|
try {
|
|
10711
|
-
form = await request.
|
|
10822
|
+
form = await new Request(request.url, {
|
|
10823
|
+
method: "POST",
|
|
10824
|
+
headers: { "content-type": request.headers.get("content-type") ?? "" },
|
|
10825
|
+
body: bytes
|
|
10826
|
+
}).formData();
|
|
10712
10827
|
} catch {
|
|
10713
10828
|
throw new HttpError("Malformed multipart body", 400);
|
|
10714
10829
|
}
|
|
@@ -10724,15 +10839,37 @@ async function readUpload(request, maxFileSize) {
|
|
|
10724
10839
|
data
|
|
10725
10840
|
};
|
|
10726
10841
|
}
|
|
10842
|
+
async function readBody(request, max) {
|
|
10843
|
+
const tooLarge = () => new HttpError("Request body too large", 413);
|
|
10844
|
+
if (Number(request.headers.get("content-length") ?? 0) > max) throw tooLarge();
|
|
10845
|
+
if (!request.body) return new Uint8Array();
|
|
10846
|
+
const reader = request.body.getReader();
|
|
10847
|
+
const chunks = [];
|
|
10848
|
+
let size = 0;
|
|
10849
|
+
for (; ; ) {
|
|
10850
|
+
const { done, value } = await reader.read();
|
|
10851
|
+
if (done) break;
|
|
10852
|
+
size += value.byteLength;
|
|
10853
|
+
if (size > max) {
|
|
10854
|
+
reader.releaseLock();
|
|
10855
|
+
throw tooLarge();
|
|
10856
|
+
}
|
|
10857
|
+
chunks.push(value);
|
|
10858
|
+
}
|
|
10859
|
+
const body = new Uint8Array(size);
|
|
10860
|
+
let at = 0;
|
|
10861
|
+
for (const chunk of chunks) {
|
|
10862
|
+
body.set(chunk, at);
|
|
10863
|
+
at += chunk.byteLength;
|
|
10864
|
+
}
|
|
10865
|
+
return body;
|
|
10866
|
+
}
|
|
10727
10867
|
async function readJson(request) {
|
|
10728
10868
|
const type = request.headers.get("content-type") ?? "";
|
|
10729
10869
|
if (!type.toLowerCase().startsWith("application/json")) {
|
|
10730
10870
|
throw new HttpError("Content-Type must be application/json", 415);
|
|
10731
10871
|
}
|
|
10732
|
-
const
|
|
10733
|
-
if (declared > MAX_BODY_BYTES) throw new HttpError("Request body too large", 413);
|
|
10734
|
-
const text2 = await request.text();
|
|
10735
|
-
if (Buffer.byteLength(text2) > MAX_BODY_BYTES) throw new HttpError("Request body too large", 413);
|
|
10872
|
+
const text2 = new TextDecoder().decode(await readBody(request, MAX_BODY_BYTES));
|
|
10736
10873
|
let body;
|
|
10737
10874
|
try {
|
|
10738
10875
|
body = JSON.parse(text2);
|
|
@@ -10860,9 +10997,19 @@ function sharedEasyCMS(config, options) {
|
|
|
10860
10997
|
});
|
|
10861
10998
|
return promise;
|
|
10862
10999
|
}
|
|
11000
|
+
function forwardedClientIp(value) {
|
|
11001
|
+
const last = value?.split(",").map((part) => part.trim()).filter(Boolean).at(-1);
|
|
11002
|
+
return last || void 0;
|
|
11003
|
+
}
|
|
11004
|
+
function platformClientIp(headers) {
|
|
11005
|
+
if (process.env.VERCEL)
|
|
11006
|
+
return headers.get("x-real-ip") ?? forwardedClientIp(headers.get("x-forwarded-for"));
|
|
11007
|
+
if (process.env.NETLIFY) return headers.get("x-nf-client-connection-ip") ?? void 0;
|
|
11008
|
+
return void 0;
|
|
11009
|
+
}
|
|
10863
11010
|
function createApiHandler(config, options = {}) {
|
|
10864
11011
|
const handlers = /* @__PURE__ */ new WeakMap();
|
|
10865
|
-
const getClientIp = options.getClientIp ?? (options.trustProxy ? (r) => r.headers.get("x-forwarded-for")
|
|
11012
|
+
const getClientIp = options.getClientIp ?? (options.trustProxy ? (r) => forwardedClientIp(r.headers.get("x-forwarded-for")) : (r) => platformClientIp(r.headers));
|
|
10866
11013
|
return async (request) => {
|
|
10867
11014
|
const cms = await sharedEasyCMS(config);
|
|
10868
11015
|
let handler = handlers.get(cms);
|
|
@@ -11158,6 +11305,7 @@ export {
|
|
|
11158
11305
|
fillMissing,
|
|
11159
11306
|
findConfigFile,
|
|
11160
11307
|
formToken,
|
|
11308
|
+
forwardedClientIp,
|
|
11161
11309
|
generateSlugs,
|
|
11162
11310
|
generateTypes,
|
|
11163
11311
|
hasRows,
|
|
@@ -11178,6 +11326,7 @@ export {
|
|
|
11178
11326
|
mimeAllowed,
|
|
11179
11327
|
mimeAllowedBy,
|
|
11180
11328
|
parseId,
|
|
11329
|
+
platformClientIp,
|
|
11181
11330
|
populate,
|
|
11182
11331
|
rateKeys,
|
|
11183
11332
|
readCookie,
|