@easy-cms/core 0.44.0 → 0.45.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 CHANGED
@@ -1087,9 +1087,15 @@ declare class MediaFolders {
1087
1087
  * The folder with this key (`folder: 'banners'` on upload fields), made at the top level when
1088
1088
  * there is none yet.
1089
1089
  */
1090
- keyed(key: string): Promise<ID>;
1090
+ /**
1091
+ * The folder with this key, made when first needed. `scope`: the values the key is unique
1092
+ * within (`uniqueWithin` of the `key` field, e.g. the tenant), given to a new folder too.
1093
+ * `null` when there is none and `create` is false.
1094
+ */
1095
+ keyed(key: string, scope?: Readonly<Record<string, unknown>>): Promise<ID>;
1096
+ keyed(key: string, scope: Readonly<Record<string, unknown>>, create: boolean): Promise<ID | null>;
1091
1097
  /** Whether a file in `folder` is in the folder with this key, or below it. */
1092
- inKeyed(key: string, folder: ID | null): Promise<boolean>;
1098
+ inKeyed(key: string, folder: ID | null, scope?: Readonly<Record<string, unknown>>): Promise<boolean>;
1093
1099
  /** The user's level in a folder (`null`: the top level). */
1094
1100
  level(user: AuthUser | null, id: ID | null): Promise<FolderLevel | 'none'>;
1095
1101
  /**
@@ -1673,6 +1679,23 @@ declare class EasyCMS<C extends Config = Config> {
1673
1679
  private makeSlugsUnique;
1674
1680
  private checkUnique;
1675
1681
  private isTaken;
1682
+ /**
1683
+ * What folder keys are unique within for a call (`uniqueWithin` of the `key` field of media
1684
+ * folders, e.g. the tenant): the value a folder made by this call would get from its hooks.
1685
+ */
1686
+ private folderScope;
1687
+ /**
1688
+ * Where a unique field's values must differ for a call (`uniqueWithin`, e.g. the tenant): the
1689
+ * value a document created by this call would get there from its hooks, as `{ [field]: value }`;
1690
+ * `{}` when the field is unique everywhere. For finding documents by such a field, e.g. a page
1691
+ * by its path in the current tenant.
1692
+ */
1693
+ uniqueScope(collection: string, field: string, options?: Pick<AccessOptions, 'user' | 'context'>): Promise<Record<string, unknown>>;
1694
+ /**
1695
+ * @internal The id of the media folder with this key (`folder` of upload fields), made when
1696
+ * needed; `null` when the call has no scope to make it in (e.g. all tenants) and none exists.
1697
+ */
1698
+ keyedFolder(key: string, options?: AccessOptions): Promise<ID | null>;
1676
1699
  /** Each reference is one its relationship's `filterOptions` allow. */
1677
1700
  private checkFilterOptions;
1678
1701
  private checkReferences;
@@ -1711,6 +1734,23 @@ interface FieldAdmin {
1711
1734
  readonly after?: readonly AdminComponent[];
1712
1735
  /** Shows the value in the admin's lists (e.g. a color swatch). Default: the plain value. */
1713
1736
  readonly cell?: AdminComponent;
1737
+ /**
1738
+ * A column of the list at first (each user can change it): `true`, or per request, e.g.
1739
+ * only when the admin shows every tenant.
1740
+ */
1741
+ readonly column?: boolean | ((args: AdminFieldArgs) => boolean | Promise<boolean>);
1742
+ /** Relationships: offer to create the related document in place. Default true. */
1743
+ readonly allowCreate?: boolean;
1744
+ /**
1745
+ * The value a new document's form starts with, worked out for each user, e.g. from the
1746
+ * request's context. The server's own default (`defaultValue`, hooks) still applies.
1747
+ */
1748
+ readonly defaultValue?: (args: AdminFieldArgs) => unknown;
1749
+ }
1750
+ /** What per-request admin options of a field receive. */
1751
+ interface AdminFieldArgs {
1752
+ readonly user: AuthUser;
1753
+ readonly context: RequestContext;
1714
1754
  }
1715
1755
  /**
1716
1756
  * Field types that packages add (`fieldTypes` in the config), for the types inferred from the
@@ -1741,11 +1781,11 @@ interface BaseField<TType extends string, TValue> {
1741
1781
  readonly required?: boolean;
1742
1782
  readonly unique?: boolean;
1743
1783
  /**
1744
- * With `unique` (or on a slug field): the name of a sibling field (e.g. `parent` or `tenant`)
1784
+ * With `unique` (or on a slug field): a sibling field (e.g. `parent` or `tenant`), or several,
1745
1785
  * whose documents only need to differ among themselves, so `/about/team` and `/careers/team`
1746
- * can both be `team`. A single, unlocalized relationship, select, text or number field.
1786
+ * can both be `team`. Single, unlocalized relationship, select, text or number fields.
1747
1787
  */
1748
- readonly uniqueWithin?: string;
1788
+ readonly uniqueWithin?: string | readonly string[];
1749
1789
  readonly index?: boolean;
1750
1790
  readonly defaultValue?: TValue;
1751
1791
  readonly validate?: FieldValidate<TValue>;
@@ -1825,6 +1865,8 @@ interface UploadField extends BaseField<'upload', never> {
1825
1865
  readonly folder?: string;
1826
1866
  /** With `folder`: only files in that folder (and its subfolders) may be chosen. */
1827
1867
  readonly folderOnly?: boolean;
1868
+ /** Which media documents may be chosen, as for relationships: offered and checked on save. */
1869
+ readonly filterOptions?: FilterOptions;
1828
1870
  }
1829
1871
  interface FilterOptionsArgs {
1830
1872
  /** The document being edited; `undefined` while it is being created. */
@@ -1876,6 +1918,8 @@ interface BlocksField extends BaseField<'blocks', never> {
1876
1918
  readonly minRows?: number;
1877
1919
  readonly maxRows?: number;
1878
1920
  }
1921
+ /** The fields `uniqueWithin` names, as a list. */
1922
+ declare const uniqueWithinOf: (field: Field) => readonly string[];
1879
1923
  /** Fields with `hasMany`: their value is a list of options, documents or files. */
1880
1924
  declare const isHasMany: (field: Field) => boolean;
1881
1925
  /** Whether a MIME type is one of `patterns` (`image/*` matches every image). */
@@ -1896,6 +1940,12 @@ interface AuditConfig {
1896
1940
  readonly values?: boolean;
1897
1941
  /** Failed logins within an hour that the dashboard warns about. Default 20. */
1898
1942
  readonly failedLogins?: number;
1943
+ /**
1944
+ * The part of the site an entry belongs to, from the request's context, e.g. its tenant
1945
+ * (usually set by a plugin). Users whose role holds in a part only (`scoped`) see that part's
1946
+ * entries; others see the chosen part's, or all.
1947
+ */
1948
+ readonly scope?: ((context: RequestContext) => string | null | undefined) | null;
1899
1949
  }
1900
1950
  /** A field that changed: values before and after, unless they are not kept. */
1901
1951
  interface AuditChange {
@@ -1943,6 +1993,8 @@ interface AuditFilter {
1943
1993
  /** ISO dates. */
1944
1994
  from?: string | null;
1945
1995
  to?: string | null;
1996
+ /** Only entries of this part of the site (`scope`). */
1997
+ scope?: string | null;
1946
1998
  }
1947
1999
  /** `POST <api>/admin/audit/verify`. */
1948
2000
  interface AuditVerification {
@@ -1979,12 +2031,14 @@ declare class AuditLog {
1979
2031
  email?: string | null;
1980
2032
  changes?: AuditChange[] | null;
1981
2033
  detail?: Record<string, unknown> | null;
2034
+ /** The call's context, when it is not the request's. */
2035
+ context?: RequestContext;
1982
2036
  }): Promise<void>;
1983
2037
  /**
1984
2038
  * A change to a document or global: what changed between `before` and `after`. Internal
1985
2039
  * collections (sessions, versions…) are not logged.
1986
2040
  */
1987
- content(config: CollectionConfig | GlobalConfig, global: boolean, action: string, after: Record<string, unknown> | undefined, before: Record<string, unknown> | undefined, user: AuthUser | null): Promise<void>;
2041
+ content(config: CollectionConfig | GlobalConfig, global: boolean, action: string, after: Record<string, unknown> | undefined, before: Record<string, unknown> | undefined, user: AuthUser | null, context?: RequestContext): Promise<void>;
1988
2042
  /** Fields whose stored value changed. Hidden ones never (the password only as "changed"). */
1989
2043
  diff(fields: readonly Field[], before: Record<string, unknown>, after: Record<string, unknown>): AuditChange[];
1990
2044
  list(filter: AuditFilter, page?: number): Promise<AuditPage>;
@@ -2009,6 +2063,8 @@ declare class AuditLog {
2009
2063
  invalid: number;
2010
2064
  at: string;
2011
2065
  })[]>;
2066
+ /** The part of the site a context is in (`scope`), or `null`. */
2067
+ scopeOf(context: RequestContext | undefined): string | null;
2012
2068
  private sign;
2013
2069
  private lastVerification;
2014
2070
  private where;
@@ -2299,6 +2355,16 @@ interface CollectionAdmin extends ContainerAdmin {
2299
2355
  * `author`, for roles given "own documents only". Default: who created it (`createdBy`).
2300
2356
  */
2301
2357
  readonly ownerField?: string;
2358
+ /**
2359
+ * Deleting asks more than "Are you sure?", for documents that take much with them (e.g. a
2360
+ * tenant): `typeTitle` has the user type the document's title; `impact` is a path under the
2361
+ * API, called with `?id=`, that returns `{ message }` (a label) saying what will be lost.
2362
+ * Documents are then deleted one at a time, not from a selection.
2363
+ */
2364
+ readonly confirmDelete?: {
2365
+ readonly typeTitle?: boolean;
2366
+ readonly impact?: string;
2367
+ };
2302
2368
  }
2303
2369
  interface CollectionListAdmin {
2304
2370
  /**
@@ -3199,6 +3265,10 @@ interface AdminField {
3199
3265
  after?: AdminComponentRef[];
3200
3266
  cell?: AdminComponentRef;
3201
3267
  };
3268
+ /** A column of the list at first (`admin.column`). */
3269
+ column?: boolean;
3270
+ /** A relationship that offers no "Create" in place (`admin.allowCreate: false`). */
3271
+ noCreate?: boolean;
3202
3272
  /** A field of an added type (`fieldTypes`), e.g. `color`; `type` is the base it's stored as. */
3203
3273
  customType?: string;
3204
3274
  }
@@ -3236,6 +3306,11 @@ interface AdminCollection {
3236
3306
  };
3237
3307
  /** With `auth.rbac`: the field naming a document's owner (`createdBy` or `admin.ownerField`). */
3238
3308
  owner?: string;
3309
+ /** Deleting asks to type the title, and says what goes too (`admin.confirmDelete`). */
3310
+ confirmDelete?: {
3311
+ typeTitle?: boolean;
3312
+ impact?: string;
3313
+ };
3239
3314
  }
3240
3315
  interface AdminGlobal {
3241
3316
  slug: string;
@@ -3437,4 +3512,4 @@ declare function validateConfig(config: Config): ConfigIssue[];
3437
3512
  /** The version of `@easy-cms/core`, set when the package is built. */
3438
3513
  declare const VERSION: string;
3439
3514
 
3440
- 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 AdminComponent, type AdminComponentRef, type AdminConfig, type AdminDeliveries, type AdminDelivery, type AdminEmail, type AdminEmailDelivery, type AdminField, type AdminGlobal, type AdminIcon, type AdminLocale, 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, 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, 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 InferCollection, type InferGlobal, type JsonField, type Label, 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 MigrationInfo, 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 SelectField, type SelectOption, type Session, 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, configSignature, consoleEmail, consoleLogger, copyDatabase, createApiHandler, createEasyCMS, createRestHandler, createRootEndpointHandler, defineConfig, defineFieldType, definePlugin, expandMimeTypes, fillMissing, findConfigFile, generateSlugs, generateTypes, hasRows, hashPassword, imageDimensions, importConfig, isAdmin, isHasMany, isLoggedIn, isSystemAdmin, keyAllows, loadConfig, localStorage, mergeForUpdate, mimeAllowed, mimeAllowedBy, parseId, populate, readCookie, resolveAdminModule, resolveConfig, rowFields, sharedEasyCMS, silentLogger, singularize, slugify, sniffMimeType, validateConfig, validateFields, verifyPassword, writeBackupFile };
3515
+ 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 AdminComponent, type AdminComponentRef, type AdminConfig, type AdminDeliveries, type AdminDelivery, type AdminEmail, type AdminEmailDelivery, type AdminField, type AdminFieldArgs, type AdminGlobal, type AdminIcon, type AdminLocale, 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, 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, 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 InferCollection, type InferGlobal, type JsonField, type Label, 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 MigrationInfo, 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 SelectField, type SelectOption, type Session, 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, configSignature, consoleEmail, consoleLogger, copyDatabase, createApiHandler, createEasyCMS, createRestHandler, createRootEndpointHandler, defineConfig, defineFieldType, definePlugin, expandMimeTypes, fillMissing, findConfigFile, generateSlugs, generateTypes, hasRows, hashPassword, imageDimensions, importConfig, isAdmin, isHasMany, isLoggedIn, isSystemAdmin, keyAllows, loadConfig, localStorage, mergeForUpdate, mimeAllowed, mimeAllowedBy, parseId, populate, readCookie, resolveAdminModule, resolveConfig, rowFields, sharedEasyCMS, silentLogger, singularize, slugify, sniffMimeType, uniqueWithinOf, validateConfig, validateFields, verifyPassword, writeBackupFile };
package/dist/index.js CHANGED
@@ -427,7 +427,16 @@ function withMedia(config) {
427
427
  const customFolders = config.collections?.find((c) => c.slug === MEDIA_FOLDERS);
428
428
  const mediaFolders = customFolders ? {
429
429
  ...mediaFoldersCollection,
430
- fields: [...mediaFoldersCollection.fields, ...customFolders.fields],
430
+ // A field named like a built-in one changes it (e.g. `key` unique per tenant).
431
+ fields: [
432
+ ...mediaFoldersCollection.fields.map((field) => {
433
+ const own = customFolders.fields.find((f) => f.name === field.name);
434
+ return own ? { ...field, ...own } : field;
435
+ }),
436
+ ...customFolders.fields.filter(
437
+ (f) => !mediaFoldersCollection.fields.some((b) => b.name === f.name)
438
+ )
439
+ ],
431
440
  access: { ...mediaFoldersCollection.access, ...customFolders.access },
432
441
  hooks: {
433
442
  ...customFolders.hooks,
@@ -562,7 +571,9 @@ var auditLogsCollection = {
562
571
  { name: "userAgent", type: "text" },
563
572
  { name: "changes", type: "json" },
564
573
  { name: "detail", type: "json" },
565
- { name: "signature", type: "text" }
574
+ { name: "signature", type: "text" },
575
+ // The part of the site, e.g. the tenant (`audit.scope`).
576
+ { name: "scope", type: "text", index: true }
566
577
  ]
567
578
  };
568
579
  var emailDeliveriesCollection = {
@@ -635,7 +646,8 @@ var AuditLog = class {
635
646
  return this.cms.config.audit || {
636
647
  keep: DEFAULT_AUDIT_KEEP,
637
648
  values: true,
638
- failedLogins: DEFAULT_FAILED_LOGINS
649
+ failedLogins: DEFAULT_FAILED_LOGINS,
650
+ scope: null
639
651
  };
640
652
  }
641
653
  /**
@@ -646,6 +658,7 @@ var AuditLog = class {
646
658
  if (!this.enabled) return;
647
659
  try {
648
660
  const context = auditContext.getStore();
661
+ const scope = this.scopeOf(entry.context ?? context?.context);
649
662
  const user = entry.user !== void 0 ? entry.user : context?.user ?? null;
650
663
  const apiKey = user?.apiKey;
651
664
  const row = {
@@ -660,7 +673,8 @@ var AuditLog = class {
660
673
  userAgent: context?.userAgent ? context.userAgent.slice(0, 200) : null,
661
674
  changes: entry.changes && entry.changes.length > 0 ? entry.changes : null,
662
675
  detail: apiKey ? { ...entry.detail, apiKey: apiKey.name ?? "" } : entry.detail ?? null,
663
- createdAt: (/* @__PURE__ */ new Date()).toISOString()
676
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
677
+ ...scope ? { scope } : {}
664
678
  };
665
679
  row.signature = this.sign(row);
666
680
  row.updatedAt = row.createdAt;
@@ -676,7 +690,7 @@ var AuditLog = class {
676
690
  * A change to a document or global: what changed between `before` and `after`. Internal
677
691
  * collections (sessions, versions…) are not logged.
678
692
  */
679
- async content(config, global2, action, after, before, user) {
693
+ async content(config, global2, action, after, before, user, context) {
680
694
  if (!this.enabled) return;
681
695
  if (!global2 && INTERNAL_COLLECTIONS.has(config.slug)) return;
682
696
  const doc = after ?? before;
@@ -692,7 +706,8 @@ var AuditLog = class {
692
706
  title,
693
707
  // A trusted call without a user keeps the request's (or is the system).
694
708
  ...user ? { user } : {},
695
- changes: action === "delete" ? null : this.diff(config.fields, before ?? {}, after ?? {})
709
+ changes: action === "delete" ? null : this.diff(config.fields, before ?? {}, after ?? {}),
710
+ ...context && Object.keys(context).length > 0 ? { context } : {}
696
711
  });
697
712
  }
698
713
  /** Fields whose stored value changed. Hidden ones never (the password only as "changed"). */
@@ -837,8 +852,15 @@ var AuditLog = class {
837
852
  return out;
838
853
  }
839
854
  // -------------------------------------------------------------------------
855
+ /** The part of the site a context is in (`scope`), or `null`. */
856
+ scopeOf(context) {
857
+ const scope = this.settings.scope;
858
+ return (scope && context ? scope(context) : null) ?? null;
859
+ }
840
860
  sign(row) {
841
- const payload = stable(SIGNED.map((key2) => row[key2] ?? null));
861
+ const values = SIGNED.map((key2) => row[key2] ?? null);
862
+ if (typeof row.scope === "string" && row.scope) values.push(row.scope);
863
+ const payload = stable(values);
842
864
  return createHmac("sha256", this.cms.config.secret).update(`audit:${payload}`).digest("base64url");
843
865
  }
844
866
  async lastVerification() {
@@ -866,6 +888,7 @@ var AuditLog = class {
866
888
  if (filter.actor) parts.push({ actorEmail: { like: filter.actor } });
867
889
  if (filter.from) parts.push({ createdAt: { gte: filter.from } });
868
890
  if (filter.to) parts.push({ createdAt: { lte: filter.to } });
891
+ if (filter.scope) parts.push({ scope: { equals: filter.scope } });
869
892
  return parts.length === 0 ? void 0 : parts.length === 1 ? parts[0] : { and: parts };
870
893
  }
871
894
  };
@@ -1137,6 +1160,7 @@ function sizeKey(filename, size) {
1137
1160
  var PRIVATE_KEY = /^[\p{L}\p{M}\p{N}-]+\.private(-[\p{L}\p{N}_-]+)?\.[a-z0-9]+$/u;
1138
1161
 
1139
1162
  // src/fields.ts
1163
+ var uniqueWithinOf = (field) => field.uniqueWithin === void 0 ? [] : [field.uniqueWithin].flat();
1140
1164
  var isHasMany = (field) => (field.type === "select" || field.type === "relationship" || field.type === "upload") && field.hasMany === true;
1141
1165
  function mimeAllowedBy(mimeType, patterns) {
1142
1166
  const type = mimeType.toLowerCase();
@@ -2890,7 +2914,7 @@ async function normalizeValue(field, raw, path, options, fail, errors, reference
2890
2914
  field: path,
2891
2915
  collection: target,
2892
2916
  id,
2893
- ...field.type === "relationship" && field.filterOptions ? { filterOptions: field.filterOptions } : {},
2917
+ ...field.filterOptions ? { filterOptions: field.filterOptions } : {},
2894
2918
  ...field.type === "upload" && field.mimeTypes ? { mimeTypes: field.mimeTypes } : {},
2895
2919
  ...field.type === "upload" && field.folder && field.folderOnly ? { folderOnly: field.folder } : {}
2896
2920
  });
@@ -3376,7 +3400,8 @@ var MediaFolders = class {
3376
3400
  name: String(doc.name ?? ""),
3377
3401
  key: typeof doc.key === "string" && doc.key ? doc.key : null,
3378
3402
  private: doc.private === true,
3379
- permissions: permissions && typeof permissions === "object" && !Array.isArray(permissions) ? permissions : null
3403
+ permissions: permissions && typeof permissions === "object" && !Array.isArray(permissions) ? permissions : null,
3404
+ doc
3380
3405
  });
3381
3406
  }
3382
3407
  this.cache = { at: Date.now(), rows };
@@ -3446,18 +3471,15 @@ var MediaFolders = class {
3446
3471
  });
3447
3472
  return this.within(rows, [id]).map((f) => ({ id: f, private: this.privateIn(rows, f) }));
3448
3473
  }
3449
- /**
3450
- * The folder with this key (`folder: 'banners'` on upload fields), made at the top level when
3451
- * there is none yet.
3452
- */
3453
- async keyed(key2) {
3454
- const find = async () => [...(await this.rows()).values()].find((r) => r.key === key2);
3474
+ async keyed(key2, scope = {}, create = true) {
3475
+ const find = async () => [...(await this.rows()).values()].find((r) => r.key === key2 && inScope(r.doc, scope));
3455
3476
  let found = await find();
3456
3477
  if (!found) {
3457
3478
  this.invalidate();
3458
3479
  found = await find();
3459
3480
  }
3460
3481
  if (found) return found.id;
3482
+ if (!create) return null;
3461
3483
  const now = (/* @__PURE__ */ new Date()).toISOString();
3462
3484
  try {
3463
3485
  const doc = await this.db.create({
@@ -3468,6 +3490,7 @@ var MediaFolders = class {
3468
3490
  parent: null,
3469
3491
  permissions: null,
3470
3492
  private: false,
3493
+ ...scope,
3471
3494
  createdAt: now,
3472
3495
  updatedAt: now
3473
3496
  }
@@ -3482,10 +3505,10 @@ var MediaFolders = class {
3482
3505
  }
3483
3506
  }
3484
3507
  /** Whether a file in `folder` is in the folder with this key, or below it. */
3485
- async inKeyed(key2, folder) {
3508
+ async inKeyed(key2, folder, scope = {}) {
3486
3509
  if (folder === null) return false;
3487
3510
  const rows = await this.rows();
3488
- const root = [...rows.values()].find((r) => r.key === key2);
3511
+ const root = [...rows.values()].find((r) => r.key === key2 && inScope(r.doc, scope));
3489
3512
  return !!root && this.within(rows, [root.id]).some((id) => String(id) === String(folder));
3490
3513
  }
3491
3514
  /** The user's level in a folder (`null`: the top level). */
@@ -3610,6 +3633,11 @@ var MediaFolders = class {
3610
3633
  return this.rows();
3611
3634
  }
3612
3635
  };
3636
+ function inScope(row, scope) {
3637
+ return Object.entries(scope).every(
3638
+ ([name, value]) => value === null || value === void 0 ? row[name] === null || row[name] === void 0 : String(row[name]) === String(value)
3639
+ );
3640
+ }
3613
3641
 
3614
3642
  // src/populate.ts
3615
3643
  var DEFAULT_DEPTH = 1;
@@ -4099,6 +4127,8 @@ var Roles = class {
4099
4127
  async canView(user, id) {
4100
4128
  if (isSystemAdmin(user)) return true;
4101
4129
  if (user.apiKey) return false;
4130
+ if (id === "audit" && user.role === "admin" && user.scoped === true && this.config.audit !== false && typeof this.config.audit.scope === "function")
4131
+ return true;
4102
4132
  if (!this.enabled) return id.startsWith("page:") || id.startsWith("widget:");
4103
4133
  return (await this.permissionsOf(user.role)).admin?.includes(id) ?? false;
4104
4134
  }
@@ -5528,17 +5558,20 @@ function validateField(field, path, siblings, collectionSlugs, add) {
5528
5558
  }
5529
5559
  }
5530
5560
  if (field.uniqueWithin !== void 0) {
5531
- const scope = siblings.get(field.uniqueWithin);
5532
- if (!field.unique && field.type !== "slug") {
5561
+ if (!field.unique && field.type !== "slug")
5533
5562
  add(`${path}.uniqueWithin`, "needs `unique: true` (or a slug field)");
5534
- } else if (!scope || scope === field) {
5535
- add(`${path}.uniqueWithin`, `no sibling field named "${field.uniqueWithin}"`);
5536
- } else if (!["relationship", "select", "text", "number"].includes(scope.type) || "hasMany" in scope && scope.hasMany || scope.localized) {
5537
- add(
5538
- `${path}.uniqueWithin`,
5539
- `"${field.uniqueWithin}" must be a single, unlocalized relationship, select, text or number field`
5540
- );
5541
- }
5563
+ else
5564
+ for (const name of [field.uniqueWithin].flat()) {
5565
+ const scope = siblings.get(name);
5566
+ if (!scope || scope === field) {
5567
+ add(`${path}.uniqueWithin`, `no sibling field named "${name}"`);
5568
+ } else if (!["relationship", "select", "text", "number"].includes(scope.type) || "hasMany" in scope && scope.hasMany || scope.localized) {
5569
+ add(
5570
+ `${path}.uniqueWithin`,
5571
+ `"${name}" must be a single, unlocalized relationship, select, text or number field`
5572
+ );
5573
+ }
5574
+ }
5542
5575
  }
5543
5576
  switch (field.type) {
5544
5577
  case "text":
@@ -5914,7 +5947,8 @@ function resolveAudit(audit) {
5914
5947
  return {
5915
5948
  keep: audit.keep ?? DEFAULT_AUDIT_KEEP,
5916
5949
  values: audit.values !== false,
5917
- failedLogins: audit.failedLogins ?? DEFAULT_FAILED_LOGINS
5950
+ failedLogins: audit.failedLogins ?? DEFAULT_FAILED_LOGINS,
5951
+ scope: audit.scope ?? null
5918
5952
  };
5919
5953
  }
5920
5954
 
@@ -7809,7 +7843,8 @@ var EasyCMS = class {
7809
7843
  action,
7810
7844
  event === "delete" ? void 0 : doc,
7811
7845
  event === "create" ? void 0 : previous,
7812
- guard.user
7846
+ guard.user,
7847
+ guard.context
7813
7848
  );
7814
7849
  }
7815
7850
  /** Records a version after a save, when the collection or global keeps versions. */
@@ -7919,7 +7954,7 @@ var EasyCMS = class {
7919
7954
  await this.makeSlugsUnique(config, clean2, selfId);
7920
7955
  errors.push(...await this.checkUnique(config, clean2, selfId));
7921
7956
  }
7922
- errors.push(...await this.checkReferences(result.references));
7957
+ errors.push(...await this.checkReferences(result.references, guard));
7923
7958
  if (errors.length === 0)
7924
7959
  errors.push(...await this.checkFilterOptions(result.references, selfId, guard));
7925
7960
  if (errors.length > 0) throw new ValidationError(config.slug, errors);
@@ -7949,7 +7984,11 @@ var EasyCMS = class {
7949
7984
  async makeSlugsUnique(config, data, selfId) {
7950
7985
  for (const field of config.fields) {
7951
7986
  if (field.type !== "slug") continue;
7952
- const scope = field.uniqueWithin === void 0 ? void 0 : { [field.uniqueWithin]: { equals: data[field.uniqueWithin] ?? null } };
7987
+ const scope = field.uniqueWithin === void 0 ? void 0 : {
7988
+ and: uniqueWithinOf(field).map((name) => ({
7989
+ [name]: { equals: data[name] ?? null }
7990
+ }))
7991
+ };
7953
7992
  const unique = async (path, base) => {
7954
7993
  let candidate = base;
7955
7994
  for (let n = 2; await this.isTaken(config.slug, path, candidate, selfId, scope); n++) {
@@ -7980,7 +8019,11 @@ var EasyCMS = class {
7980
8019
  locale === this.config.localization?.defaultLocale ? field.name : `${field.name}.${locale}`,
7981
8020
  v
7982
8021
  ]) : [[field.name, field.name, value]];
7983
- const scope = field.uniqueWithin === void 0 ? void 0 : { [field.uniqueWithin]: { equals: data[field.uniqueWithin] ?? null } };
8022
+ const scope = field.uniqueWithin === void 0 ? void 0 : {
8023
+ and: uniqueWithinOf(field).map((name) => ({
8024
+ [name]: { equals: data[name] ?? null }
8025
+ }))
8026
+ };
7984
8027
  for (const [path, errorField, v] of checks) {
7985
8028
  if (v === null || v === void 0) continue;
7986
8029
  if (await this.isTaken(config.slug, path, v, selfId, scope)) {
@@ -8000,6 +8043,46 @@ var EasyCMS = class {
8000
8043
  };
8001
8044
  return await this.db.count({ collection: collection2, where }) > 0;
8002
8045
  }
8046
+ /**
8047
+ * What folder keys are unique within for a call (`uniqueWithin` of the `key` field of media
8048
+ * folders, e.g. the tenant): the value a folder made by this call would get from its hooks.
8049
+ */
8050
+ async folderScope(guard) {
8051
+ return this.uniqueScope(MEDIA_FOLDERS, "key", guard);
8052
+ }
8053
+ /**
8054
+ * Where a unique field's values must differ for a call (`uniqueWithin`, e.g. the tenant): the
8055
+ * value a document created by this call would get there from its hooks, as `{ [field]: value }`;
8056
+ * `{}` when the field is unique everywhere. For finding documents by such a field, e.g. a page
8057
+ * by its path in the current tenant.
8058
+ */
8059
+ async uniqueScope(collection2, field, options = {}) {
8060
+ const config = this.config.collections.find((c) => c.slug === collection2);
8061
+ const target = config?.fields.find((f) => f.name === field);
8062
+ const within = target ? uniqueWithinOf(target) : [];
8063
+ if (!config || within.length === 0) return {};
8064
+ const guard = {
8065
+ enforce: false,
8066
+ user: options.user ?? null,
8067
+ context: options.context ?? NO_CONTEXT
8068
+ };
8069
+ const data = await this.transform(
8070
+ config.hooks?.beforeValidate,
8071
+ "data",
8072
+ { ...this.hookArgs(config, guard), operation: "create" },
8073
+ {}
8074
+ ).catch(() => ({}));
8075
+ return Object.fromEntries(within.map((name) => [name, data[name] ?? null]));
8076
+ }
8077
+ /**
8078
+ * @internal The id of the media folder with this key (`folder` of upload fields), made when
8079
+ * needed; `null` when the call has no scope to make it in (e.g. all tenants) and none exists.
8080
+ */
8081
+ async keyedFolder(key2, options = {}) {
8082
+ const scope = await this.folderScope(guardOf(options));
8083
+ const complete = Object.values(scope).every((v) => v !== null && v !== void 0);
8084
+ return this.folders.keyed(key2, scope, complete);
8085
+ }
8003
8086
  /** Each reference is one its relationship's `filterOptions` allow. */
8004
8087
  async checkFilterOptions(references2, selfId, guard) {
8005
8088
  const errors = [];
@@ -8025,7 +8108,7 @@ var EasyCMS = class {
8025
8108
  }
8026
8109
  return errors;
8027
8110
  }
8028
- async checkReferences(references2) {
8111
+ async checkReferences(references2, guard) {
8029
8112
  const byCollection = /* @__PURE__ */ new Map();
8030
8113
  for (const ref of references2) {
8031
8114
  if (!this.config.collections.some((c) => c.slug === ref.collection)) continue;
@@ -8054,7 +8137,8 @@ var EasyCMS = class {
8054
8137
  }
8055
8138
  if (ref.folderOnly && this.folders.enabled && !await this.folders.inKeyed(
8056
8139
  ref.folderOnly,
8057
- doc.folder ?? null
8140
+ doc.folder ?? null,
8141
+ await this.folderScope(guard)
8058
8142
  ))
8059
8143
  errors.push({
8060
8144
  field: ref.field,
@@ -8400,7 +8484,7 @@ function componentRef(component) {
8400
8484
  async function allowed(access, user, context) {
8401
8485
  return await evaluateAccess(access, { user, context }) !== false;
8402
8486
  }
8403
- async function serializeFields(fields, checks, localized, folderId) {
8487
+ async function serializeFields(fields, checks, localized, folderId, who) {
8404
8488
  const { update: update2 } = checks;
8405
8489
  const out = [];
8406
8490
  for (const field of fields) {
@@ -8410,6 +8494,13 @@ async function serializeFields(fields, checks, localized, folderId) {
8410
8494
  if (field.required) f.required = true;
8411
8495
  if (field.unique) f.unique = true;
8412
8496
  if (field.defaultValue !== void 0) f.defaultValue = field.defaultValue;
8497
+ if (field.admin?.defaultValue) {
8498
+ const value = await field.admin.defaultValue(who);
8499
+ if (value !== void 0) f.defaultValue = value;
8500
+ }
8501
+ const column = field.admin?.column;
8502
+ if (column === true || typeof column === "function" && await column(who)) f.column = true;
8503
+ if (field.admin?.allowCreate === false) f.noCreate = true;
8413
8504
  if (!await update2.allows(field)) f.readOnly = true;
8414
8505
  if (field.localized && localized) f.localized = true;
8415
8506
  if (field.position === "sidebar") f.position = "sidebar";
@@ -8450,16 +8541,19 @@ async function serializeFields(fields, checks, localized, folderId) {
8450
8541
  case "upload":
8451
8542
  f.to = "media";
8452
8543
  if (field.hasMany) f.hasMany = true;
8544
+ if (field.filterOptions) f.filtered = true;
8453
8545
  if (field.minRows !== void 0) f.minRows = field.minRows;
8454
8546
  if (field.maxRows !== void 0) f.maxRows = field.maxRows;
8455
8547
  if (field.mimeTypes) f.mimeTypes = expandMimeTypes(field.mimeTypes);
8456
- if (field.folder && folderId)
8457
- f.folder = { id: await folderId(field.folder), only: field.folderOnly === true };
8548
+ if (field.folder && folderId) {
8549
+ const id = await folderId(field.folder);
8550
+ if (id !== null) f.folder = { id, only: field.folderOnly === true };
8551
+ }
8458
8552
  break;
8459
8553
  case "array":
8460
8554
  if (field.minRows !== void 0) f.minRows = field.minRows;
8461
8555
  if (field.maxRows !== void 0) f.maxRows = field.maxRows;
8462
- f.fields = await serializeFields(field.fields, checks, localized, folderId);
8556
+ f.fields = await serializeFields(field.fields, checks, localized, folderId, who);
8463
8557
  break;
8464
8558
  case "blocks":
8465
8559
  if (field.minRows !== void 0) f.minRows = field.minRows;
@@ -8468,12 +8562,12 @@ async function serializeFields(fields, checks, localized, folderId) {
8468
8562
  field.blocks.map(async (block) => ({
8469
8563
  slug: block.slug,
8470
8564
  ...block.labels ? { labels: block.labels } : {},
8471
- fields: await serializeFields(block.fields, checks, localized, folderId)
8565
+ fields: await serializeFields(block.fields, checks, localized, folderId, who)
8472
8566
  }))
8473
8567
  );
8474
8568
  break;
8475
8569
  case "group":
8476
- f.fields = await serializeFields(field.fields, checks, localized, folderId);
8570
+ f.fields = await serializeFields(field.fields, checks, localized, folderId, who);
8477
8571
  break;
8478
8572
  }
8479
8573
  out.push(f);
@@ -8496,7 +8590,8 @@ async function collection(cms, config, user, localized, roles, context) {
8496
8590
  config.fields,
8497
8591
  await checkers(cms, user, context),
8498
8592
  localized,
8499
- folderIds(cms)
8593
+ folderIds(cms, user, context),
8594
+ { user, context }
8500
8595
  );
8501
8596
  if (config.slug === USERS && cms.roles.enabled) {
8502
8597
  const role = fields.find((f) => f.name === "role");
@@ -8527,6 +8622,7 @@ async function collection(cms, config, user, localized, roles, context) {
8527
8622
  if (config.admin?.group === "settings" || SETTINGS.has(config.slug)) result.group = "settings";
8528
8623
  if (config.admin?.list?.tree) result.tree = config.admin.list.tree;
8529
8624
  if (config.admin?.list?.sort) result.defaultSort = config.admin.list.sort;
8625
+ if (config.admin?.confirmDelete) result.confirmDelete = { ...config.admin.confirmDelete };
8530
8626
  const owner = cms.roles.ownerOf(config.slug);
8531
8627
  if (owner && fields.some((f) => f.name === owner)) result.owner = owner;
8532
8628
  if (config.editIn === "drawer" && !result.drafts && !result.versions && !result.preview)
@@ -8542,7 +8638,10 @@ async function global(cms, config, user, localized, context) {
8542
8638
  versions: Boolean(config.versions),
8543
8639
  preview: typeof config.preview === "function",
8544
8640
  schedule: config.schedule === true,
8545
- fields: await serializeFields(config.fields, checks, localized, folderIds(cms)),
8641
+ fields: await serializeFields(config.fields, checks, localized, folderIds(cms, user, context), {
8642
+ user,
8643
+ context
8644
+ }),
8546
8645
  permissions: {
8547
8646
  read: await may(cms, config.access?.read, user, target, "read", context),
8548
8647
  update: await may(cms, config.access?.update, user, target, "update", context),
@@ -8643,12 +8742,12 @@ async function widgets(cms, user) {
8643
8742
  }
8644
8743
  return out;
8645
8744
  }
8646
- function folderIds(cms) {
8647
- return cms.folders.enabled ? (key2) => cms.folders.keyed(key2) : void 0;
8745
+ function folderIds(cms, user, context) {
8746
+ return cms.folders.enabled ? (key2) => cms.keyedFolder(key2, { user, context }) : void 0;
8648
8747
  }
8649
8748
 
8650
8749
  // src/version.ts
8651
- var VERSION = true ? "0.44.0" : "0.0.0-dev";
8750
+ var VERSION = true ? "0.45.0" : "0.0.0-dev";
8652
8751
 
8653
8752
  // src/rest/admin-status.ts
8654
8753
  var DAY2 = 864e5;
@@ -8745,7 +8844,8 @@ async function pickerFilter(cms, url, target, user, context = {}) {
8745
8844
  const slug2 = dot > 0 ? spec.slice(0, dot) : "";
8746
8845
  const owner = forCollection !== null ? cms.config.collections.find((c) => c.slug === slug2) : cms.config.globals.find((g) => g.slug === slug2);
8747
8846
  const field = owner ? fieldAt(owner.fields, spec.slice(dot + 1)) : void 0;
8748
- if (field?.type !== "relationship" || field.to !== target)
8847
+ const to = field?.type === "upload" ? "media" : field?.type === "relationship" ? field.to : null;
8848
+ if (!field || field.type !== "relationship" && field.type !== "upload" || to !== target)
8749
8849
  throw new QueryError(`filterFor: no relationship to "${target}" at "${spec}"`);
8750
8850
  if (!field.filterOptions) return void 0;
8751
8851
  const rawId = url.searchParams.get("filterId");
@@ -8927,6 +9027,7 @@ async function respond(cms, request, url, path, root, options, production) {
8927
9027
  if (method !== "GET" && method !== "HEAD") checkCsrf(cms, ctx);
8928
9028
  const audit = {
8929
9029
  user,
9030
+ context,
8930
9031
  ip: options.getClientIp?.(request),
8931
9032
  userAgent: request.headers.get("user-agent")
8932
9033
  };
@@ -9199,8 +9300,11 @@ async function route(cms, ctx, method, segments, options) {
9199
9300
  doc: q.get("doc"),
9200
9301
  actor: q.get("actor"),
9201
9302
  from: q.get("from"),
9202
- to: q.get("to")
9303
+ to: q.get("to"),
9304
+ // Users of a part of the site (e.g. one tenant) see its entries; others the chosen part's.
9305
+ scope: cms.audit.scopeOf(ctx.context)
9203
9306
  };
9307
+ if (ctx.user.scoped && !filter.scope) throw new ForbiddenError();
9204
9308
  if (second === "audit.csv" && third === void 0) {
9205
9309
  if (method !== "GET") throw methodNotAllowed(ctx, "GET");
9206
9310
  return {
@@ -10255,6 +10359,7 @@ export {
10255
10359
  singularize,
10256
10360
  slugify,
10257
10361
  sniffMimeType,
10362
+ uniqueWithinOf,
10258
10363
  validateConfig,
10259
10364
  validateFields,
10260
10365
  verifyPassword,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@easy-cms/core",
3
- "version": "0.44.0",
3
+ "version": "0.45.0",
4
4
  "description": "Embedded, code-first headless CMS for Nuxt and Next.js",
5
5
  "license": "MIT",
6
6
  "type": "module",